Browse Source

feat(autofix): Add indexing and repo access to autofix project setup endpoint (#70073)

Adds a repo access check and codebase index start to the Autofix setup
modal. This finally decouples the codebase indexing from the actual
autofix run and runs it after setup instead.

![Screenshot 2024-04-30 at 10 32
45 AM](https://github.com/getsentry/sentry/assets/30991498/5da671fd-ae29-4753-8c4a-dccdf9848478)

Adds supports the loading state to the autofix banner when a codebase
index creation is running
Jenn Mueng 10 months ago
parent
commit
8373465934

+ 49 - 0
src/sentry/api/endpoints/group_autofix_setup_check.py

@@ -2,6 +2,8 @@ from __future__ import annotations
 
 import logging
 
+import requests
+from django.conf import settings
 from rest_framework.response import Response
 
 from sentry import features
@@ -10,11 +12,18 @@ from sentry.api.api_publish_status import ApiPublishStatus
 from sentry.api.base import region_silo_endpoint
 from sentry.api.bases.group import GroupEndpoint
 from sentry.api.endpoints.event_ai_suggested_fix import get_openai_policy
+from sentry.api.helpers.autofix import (
+    AutofixCodebaseIndexingStatus,
+    get_project_codebase_indexing_status,
+)
+from sentry.api.helpers.repos import get_repos_from_project_code_mappings
 from sentry.constants import ObjectStatus
 from sentry.models.group import Group
 from sentry.models.integrations.repository_project_path_config import RepositoryProjectPathConfig
 from sentry.models.organization import Organization
+from sentry.models.project import Project
 from sentry.services.hybrid_cloud.integration import integration_service
+from sentry.utils import json
 
 logger = logging.getLogger(__name__)
 
@@ -48,6 +57,33 @@ def get_autofix_integration_setup_problems(organization: Organization) -> str |
     return None
 
 
+def get_repos_and_access(project: Project) -> list[dict]:
+    """
+    Gets the repos that would be indexed for the given project from the code mappings, and checks if we have write access to them.
+
+    Returns a list of repos with the "ok" key set to True if we have write access, False otherwise.
+    """
+    repos = get_repos_from_project_code_mappings(project)
+
+    repos_and_access: list[dict] = []
+    for repo in repos:
+        response = requests.post(
+            f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/repo/check-access",
+            data=json.dumps(
+                {
+                    "repo": repo,
+                }
+            ),
+            headers={"content-type": "application/json;charset=utf-8"},
+        )
+
+        response.raise_for_status()
+
+        repos_and_access.append({**repo, "ok": response.json().get("has_access", False)})
+
+    return repos_and_access
+
+
 @region_silo_endpoint
 class GroupAutofixSetupCheck(GroupEndpoint):
     publish_status = {
@@ -76,6 +112,11 @@ class GroupAutofixSetupCheck(GroupEndpoint):
 
         integration_check = get_autofix_integration_setup_problems(organization=org)
 
+        repos = get_repos_and_access(group.project)
+        write_access_ok = all(repo["ok"] for repo in repos)
+
+        codebase_indexing_status = get_project_codebase_indexing_status(group.project)
+
         return Response(
             {
                 "subprocessorConsent": {
@@ -90,5 +131,13 @@ class GroupAutofixSetupCheck(GroupEndpoint):
                     "ok": integration_check is None,
                     "reason": integration_check,
                 },
+                "githubWriteIntegration": {
+                    "ok": write_access_ok,
+                    "repos": repos,
+                },
+                "codebaseIndexing": {
+                    "ok": codebase_indexing_status == AutofixCodebaseIndexingStatus.UP_TO_DATE
+                    or codebase_indexing_status == AutofixCodebaseIndexingStatus.INDEXING,
+                },
             }
         )

+ 3 - 40
src/sentry/api/endpoints/project_autofix_codebase_index_status.py

@@ -1,31 +1,21 @@
 from __future__ import annotations
 
-import enum
 import logging
 
-import requests
-from django.conf import settings
 from rest_framework.response import Response
 
 from sentry.api.api_owners import ApiOwner
 from sentry.api.api_publish_status import ApiPublishStatus
 from sentry.api.base import region_silo_endpoint
 from sentry.api.bases.project import ProjectEndpoint
-from sentry.api.helpers.repos import get_repos_from_project_code_mappings
+from sentry.api.helpers.autofix import get_project_codebase_indexing_status
 from sentry.models.project import Project
-from sentry.utils import json
 
 logger = logging.getLogger(__name__)
 
 from rest_framework.request import Request
 
 
-class CodebaseIndexStatus(enum.StrEnum):
-    UP_TO_DATE = "up_to_date"
-    INDEXING = "indexing"
-    NOT_INDEXED = "not_indexed"
-
-
 @region_silo_endpoint
 class ProjectAutofixCodebaseIndexStatusEndpoint(ProjectEndpoint):
     publish_status = {
@@ -38,33 +28,6 @@ class ProjectAutofixCodebaseIndexStatusEndpoint(ProjectEndpoint):
         """
         Create a codebase index for for a project's repositories, uses the code mapping to determine which repositories to index
         """
-        repos = get_repos_from_project_code_mappings(project)
-
-        if not repos:
-            return Response({"status": None}, status=200)
-
-        statuses = []
-        for repo in repos:
-            response = requests.post(
-                f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
-                data=json.dumps(
-                    {
-                        "organization_id": project.organization.id,
-                        "project_id": project.id,
-                        "repo": repo,
-                    }
-                ),
-                headers={"content-type": "application/json;charset=utf-8"},
-            )
-
-            response.raise_for_status()
-
-            statuses.append(response.json()["status"])
-
-        if any(status == CodebaseIndexStatus.NOT_INDEXED for status in statuses):
-            return Response({"status": CodebaseIndexStatus.NOT_INDEXED}, status=200)
-
-        if any(status == CodebaseIndexStatus.INDEXING for status in statuses):
-            return Response({"status": CodebaseIndexStatus.INDEXING}, status=200)
+        status = get_project_codebase_indexing_status(project)
 
-        return Response({"status": CodebaseIndexStatus.UP_TO_DATE}, status=200)
+        return Response({"status": status}, status=200)

+ 46 - 0
src/sentry/api/helpers/autofix.py

@@ -0,0 +1,46 @@
+import enum
+
+import requests
+from django.conf import settings
+
+from sentry.api.helpers.repos import get_repos_from_project_code_mappings
+from sentry.utils import json
+
+
+class AutofixCodebaseIndexingStatus(str, enum.Enum):
+    UP_TO_DATE = "up_to_date"
+    INDEXING = "indexing"
+    NOT_INDEXED = "not_indexed"
+
+
+def get_project_codebase_indexing_status(project):
+    repos = get_repos_from_project_code_mappings(project)
+
+    if not repos:
+        return None
+
+    statuses = []
+    for repo in repos:
+        response = requests.post(
+            f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
+            data=json.dumps(
+                {
+                    "organization_id": project.organization.id,
+                    "project_id": project.id,
+                    "repo": repo,
+                }
+            ),
+            headers={"content-type": "application/json;charset=utf-8"},
+        )
+
+        response.raise_for_status()
+
+        statuses.append(response.json()["status"])
+
+    if any(status == AutofixCodebaseIndexingStatus.NOT_INDEXED for status in statuses):
+        return AutofixCodebaseIndexingStatus.NOT_INDEXED
+
+    if any(status == AutofixCodebaseIndexingStatus.INDEXING for status in statuses):
+        return AutofixCodebaseIndexingStatus.INDEXING
+
+    return AutofixCodebaseIndexingStatus.UP_TO_DATE

+ 94 - 1
tests/sentry/api/endpoints/test_group_autofix_setup_check.py

@@ -1,5 +1,6 @@
 from unittest.mock import patch
 
+from sentry.api.helpers.autofix import AutofixCodebaseIndexingStatus
 from sentry.constants import ObjectStatus
 from sentry.models.integrations.repository_project_path_config import RepositoryProjectPathConfig
 from sentry.models.repository import Repository
@@ -32,7 +33,23 @@ class GroupAIAutofixEndpointSuccessTest(APITestCase, SnubaTestCase):
         )
         self.organization.update_option("sentry:gen_ai_consent", True)
 
-    def test_successful_setup(self):
+    @patch(
+        "sentry.api.endpoints.group_autofix_setup_check.get_repos_and_access",
+        return_value=[
+            {
+                "provider": "github",
+                "owner": "getsentry",
+                "name": "seer",
+                "external_id": "123",
+                "ok": True,
+            }
+        ],
+    )
+    @patch(
+        "sentry.api.endpoints.group_autofix_setup_check.get_project_codebase_indexing_status",
+        return_value=AutofixCodebaseIndexingStatus.UP_TO_DATE,
+    )
+    def test_successful_setup(self, mock_update_codebase_index, mock_get_repos_and_access):
         """
         Everything is set up correctly, should respond with OKs.
         """
@@ -55,6 +72,21 @@ class GroupAIAutofixEndpointSuccessTest(APITestCase, SnubaTestCase):
                 "ok": True,
                 "reason": None,
             },
+            "githubWriteIntegration": {
+                "ok": True,
+                "repos": [
+                    {
+                        "provider": "github",
+                        "owner": "getsentry",
+                        "name": "seer",
+                        "external_id": "123",
+                        "ok": True,
+                    }
+                ],
+            },
+            "codebaseIndexing": {
+                "ok": True,
+            },
         }
 
 
@@ -136,3 +168,64 @@ class GroupAIAutofixEndpointFailureTest(APITestCase, SnubaTestCase):
             "ok": False,
             "reason": "integration_missing",
         }
+
+    @patch(
+        "sentry.api.endpoints.group_autofix_setup_check.get_repos_and_access",
+        return_value=[
+            {
+                "provider": "github",
+                "owner": "getsentry",
+                "name": "seer",
+                "external_id": "123",
+                "ok": False,
+            },
+            {
+                "provider": "github",
+                "owner": "getsentry",
+                "name": "sentry",
+                "external_id": "234",
+                "ok": True,
+            },
+        ],
+    )
+    def test_repo_write_access_not_ready(self, mock_get_repos_and_access):
+        group = self.create_group()
+        self.login_as(user=self.user)
+        url = f"/api/0/issues/{group.id}/autofix/setup/"
+        response = self.client.get(url, format="json")
+
+        assert response.status_code == 200
+        assert response.data["githubWriteIntegration"] == {
+            "ok": False,
+            "repos": [
+                {
+                    "provider": "github",
+                    "owner": "getsentry",
+                    "name": "seer",
+                    "external_id": "123",
+                    "ok": False,
+                },
+                {
+                    "provider": "github",
+                    "owner": "getsentry",
+                    "name": "sentry",
+                    "external_id": "234",
+                    "ok": True,
+                },
+            ],
+        }
+
+    @patch(
+        "sentry.api.endpoints.group_autofix_setup_check.get_project_codebase_indexing_status",
+        return_value=AutofixCodebaseIndexingStatus.NOT_INDEXED,
+    )
+    def test_codebase_indexing_not_done(self, mock_get_project_codebase_indexing_status):
+        group = self.create_group()
+        self.login_as(user=self.user)
+        url = f"/api/0/issues/{group.id}/autofix/setup/"
+        response = self.client.get(url, format="json")
+
+        assert response.status_code == 200
+        assert response.data["codebaseIndexing"] == {
+            "ok": False,
+        }

+ 11 - 221
tests/sentry/api/endpoints/test_project_autofix_codebase_index_status.py

@@ -1,12 +1,9 @@
-from unittest import mock
-from unittest.mock import call, patch
+from unittest.mock import patch
 
-from django.conf import settings
 from django.urls import reverse
 from rest_framework import status
 
 from sentry.testutils.cases import APITestCase
-from sentry.utils import json
 
 
 class TestProjectAutofixCodebaseIndexStatus(APITestCase):
@@ -15,7 +12,6 @@ class TestProjectAutofixCodebaseIndexStatus(APITestCase):
     def setUp(self):
         super().setUp()
         self.login_as(user=self.user)
-        self.project = self.create_project()
         self.url = reverse(
             self.endpoint,
             kwargs={
@@ -24,16 +20,11 @@ class TestProjectAutofixCodebaseIndexStatus(APITestCase):
             },
         )
 
-    @patch("sentry.api.endpoints.project_autofix_codebase_index_status.requests.post")
-    def test_autofix_codebase_status_successful(self, mock_post):
-        mock_post.return_value.status_code = 200
-        mock_post.return_value.json.return_value = {"status": "up_to_date"}
-
-        repo = self.create_repo(
-            name="getsentry/sentry", provider="integrations:github", external_id="123"
-        )
-        self.create_code_mapping(project=self.project, repo=repo)
-
+    @patch(
+        "sentry.api.endpoints.project_autofix_codebase_index_status.get_project_codebase_indexing_status",
+        return_value="up_to_date",
+    )
+    def test_autofix_codebase_status_up_to_date(self, mock_get_project_codebase_indexing_status):
         response = self.client.get(
             self.url,
             format="json",
@@ -41,217 +32,16 @@ class TestProjectAutofixCodebaseIndexStatus(APITestCase):
 
         assert response.status_code == status.HTTP_200_OK
         assert response.data == {"status": "up_to_date"}
-        mock_post.assert_called_once_with(
-            f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
-            data=json.dumps(
-                {
-                    "organization_id": self.project.organization.id,
-                    "project_id": self.project.id,
-                    "repo": {
-                        "provider": "integrations:github",
-                        "owner": "getsentry",
-                        "name": "sentry",
-                        "external_id": "123",
-                    },
-                }
-            ),
-            headers={"content-type": "application/json;charset=utf-8"},
-        )
-
-    @patch("sentry.api.endpoints.project_autofix_codebase_index_status.requests.post")
-    def test_autofix_codebase_status_multiple_repos_one_in_progress(self, mock_post):
-        # Setup multiple repositories
-        repo1 = self.create_repo(
-            name="getsentry/sentry", provider="integrations:github", external_id="123"
-        )
-        repo2 = self.create_repo(
-            name="getsentry/relay", provider="integrations:github", external_id="234"
-        )
-        self.create_code_mapping(project=self.project, repo=repo1, stack_root="/path1")
-        self.create_code_mapping(project=self.project, repo=repo2, stack_root="/path2")
-
-        # Mock the POST request to return successful status
-        mock_post.side_effect = [
-            mock.Mock(status_code=200, json=mock.Mock(return_value={"status": "up_to_date"})),
-            mock.Mock(status_code=200, json=mock.Mock(return_value={"status": "indexing"})),
-        ]
 
-        # Perform the POST request
+    @patch(
+        "sentry.api.endpoints.project_autofix_codebase_index_status.get_project_codebase_indexing_status",
+        return_value="indexing",
+    )
+    def test_autofix_codebase_status_indexing(self, mock_get_project_codebase_indexing_status):
         response = self.client.get(
             self.url,
             format="json",
         )
 
-        # Assertions
         assert response.status_code == status.HTTP_200_OK
-        assert (
-            mock_post.call_count == 2
-        )  # Ensure that the endpoint was called twice, once for each repo
         assert response.data == {"status": "indexing"}
-        calls = [
-            call(
-                f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
-                data=json.dumps(
-                    {
-                        "organization_id": self.project.organization.id,
-                        "project_id": self.project.id,
-                        "repo": {
-                            "provider": "integrations:github",
-                            "owner": "getsentry",
-                            "name": "sentry",
-                            "external_id": "123",
-                        },
-                    }
-                ),
-                headers={"content-type": "application/json;charset=utf-8"},
-            ),
-            call(
-                f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
-                data=json.dumps(
-                    {
-                        "organization_id": self.project.organization.id,
-                        "project_id": self.project.id,
-                        "repo": {
-                            "provider": "integrations:github",
-                            "owner": "getsentry",
-                            "name": "relay",
-                            "external_id": "234",
-                        },
-                    }
-                ),
-                headers={"content-type": "application/json;charset=utf-8"},
-            ),
-        ]
-        mock_post.assert_has_calls(calls, any_order=True)
-
-    @patch("sentry.api.endpoints.project_autofix_codebase_index_status.requests.post")
-    def test_autofix_codebase_status_multiple_repos_both_done(self, mock_post):
-        # Setup multiple repositories
-        repo1 = self.create_repo(
-            name="getsentry/sentry", provider="integrations:github", external_id="123"
-        )
-        repo2 = self.create_repo(
-            name="getsentry/relay", provider="integrations:github", external_id="234"
-        )
-        self.create_code_mapping(project=self.project, repo=repo1, stack_root="/path1")
-        self.create_code_mapping(project=self.project, repo=repo2, stack_root="/path2")
-
-        # Mock the POST request to return successful status
-        mock_post.side_effect = [
-            mock.Mock(status_code=200, json=mock.Mock(return_value={"status": "up_to_date"})),
-            mock.Mock(status_code=200, json=mock.Mock(return_value={"status": "up_to_date"})),
-        ]
-
-        # Perform the POST request
-        response = self.client.get(
-            self.url,
-            format="json",
-        )
-
-        # Assertions
-        assert response.status_code == status.HTTP_200_OK
-        assert (
-            mock_post.call_count == 2
-        )  # Ensure that the endpoint was called twice, once for each repo
-        assert response.data == {"status": "up_to_date"}
-        calls = [
-            call(
-                f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
-                data=json.dumps(
-                    {
-                        "organization_id": self.project.organization.id,
-                        "project_id": self.project.id,
-                        "repo": {
-                            "provider": "integrations:github",
-                            "owner": "getsentry",
-                            "name": "sentry",
-                            "external_id": "123",
-                        },
-                    }
-                ),
-                headers={"content-type": "application/json;charset=utf-8"},
-            ),
-            call(
-                f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
-                data=json.dumps(
-                    {
-                        "organization_id": self.project.organization.id,
-                        "project_id": self.project.id,
-                        "repo": {
-                            "provider": "integrations:github",
-                            "owner": "getsentry",
-                            "name": "relay",
-                            "external_id": "234",
-                        },
-                    }
-                ),
-                headers={"content-type": "application/json;charset=utf-8"},
-            ),
-        ]
-        mock_post.assert_has_calls(calls, any_order=True)
-
-    @patch("sentry.api.endpoints.project_autofix_codebase_index_status.requests.post")
-    def test_autofix_codebase_status_multiple_repos_one_not_indexed(self, mock_post):
-        # Setup multiple repositories
-        repo1 = self.create_repo(
-            name="getsentry/sentry", provider="integrations:github", external_id="123"
-        )
-        repo2 = self.create_repo(
-            name="getsentry/relay", provider="integrations:github", external_id="234"
-        )
-        self.create_code_mapping(project=self.project, repo=repo1, stack_root="/path1")
-        self.create_code_mapping(project=self.project, repo=repo2, stack_root="/path2")
-
-        # Mock the POST request to return successful status
-        mock_post.side_effect = [
-            mock.Mock(status_code=200, json=mock.Mock(return_value={"status": "up_to_date"})),
-            mock.Mock(status_code=200, json=mock.Mock(return_value={"status": "not_indexed"})),
-        ]
-
-        # Perform the POST request
-        response = self.client.get(
-            self.url,
-            format="json",
-        )
-
-        # Assertions
-        assert response.status_code == status.HTTP_200_OK
-        assert (
-            mock_post.call_count == 2
-        )  # Ensure that the endpoint was called twice, once for each repo
-        assert response.data == {"status": "not_indexed"}
-        calls = [
-            call(
-                f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
-                data=json.dumps(
-                    {
-                        "organization_id": self.project.organization.id,
-                        "project_id": self.project.id,
-                        "repo": {
-                            "provider": "integrations:github",
-                            "owner": "getsentry",
-                            "name": "sentry",
-                            "external_id": "123",
-                        },
-                    }
-                ),
-                headers={"content-type": "application/json;charset=utf-8"},
-            ),
-            call(
-                f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
-                data=json.dumps(
-                    {
-                        "organization_id": self.project.organization.id,
-                        "project_id": self.project.id,
-                        "repo": {
-                            "provider": "integrations:github",
-                            "owner": "getsentry",
-                            "name": "relay",
-                            "external_id": "234",
-                        },
-                    }
-                ),
-                headers={"content-type": "application/json;charset=utf-8"},
-            ),
-        ]
-        mock_post.assert_has_calls(calls, any_order=True)

+ 228 - 0
tests/sentry/api/helpers/test_autofix.py

@@ -0,0 +1,228 @@
+from unittest import mock
+from unittest.mock import call, patch
+
+from django.conf import settings
+
+from sentry.api.helpers.autofix import get_project_codebase_indexing_status
+from sentry.testutils.cases import TestCase
+from sentry.utils import json
+
+
+class TestGetProjectCodebaseIndexingStatus(TestCase):
+    def setUp(self):
+        super().setUp()
+        self.project = self.create_project()
+
+    @patch("sentry.api.helpers.autofix.requests.post")
+    def test_autofix_codebase_status_successful(self, mock_post):
+        mock_post.return_value.status_code = 200
+        mock_post.return_value.json.return_value = {"status": "up_to_date"}
+
+        repo = self.create_repo(
+            name="getsentry/sentry", provider="integrations:github", external_id="123"
+        )
+        self.create_code_mapping(project=self.project, repo=repo)
+
+        status = get_project_codebase_indexing_status(self.project)
+
+        assert status == "up_to_date"
+        mock_post.assert_called_once_with(
+            f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
+            data=json.dumps(
+                {
+                    "organization_id": self.project.organization.id,
+                    "project_id": self.project.id,
+                    "repo": {
+                        "provider": "integrations:github",
+                        "owner": "getsentry",
+                        "name": "sentry",
+                        "external_id": "123",
+                    },
+                }
+            ),
+            headers={"content-type": "application/json;charset=utf-8"},
+        )
+
+    @patch("sentry.api.helpers.autofix.requests.post")
+    def test_autofix_codebase_status_multiple_repos_one_in_progress(self, mock_post):
+        # Setup multiple repositories
+        repo1 = self.create_repo(
+            name="getsentry/sentry", provider="integrations:github", external_id="123"
+        )
+        repo2 = self.create_repo(
+            name="getsentry/relay", provider="integrations:github", external_id="234"
+        )
+        self.create_code_mapping(project=self.project, repo=repo1, stack_root="/path1")
+        self.create_code_mapping(project=self.project, repo=repo2, stack_root="/path2")
+
+        # Mock the POST request to return successful status
+        mock_post.side_effect = [
+            mock.Mock(status_code=200, json=mock.Mock(return_value={"status": "up_to_date"})),
+            mock.Mock(status_code=200, json=mock.Mock(return_value={"status": "indexing"})),
+        ]
+
+        status = get_project_codebase_indexing_status(self.project)
+
+        # Assertions
+        assert (
+            mock_post.call_count == 2
+        )  # Ensure that the endpoint was called twice, once for each repo
+        assert status == "indexing"
+        calls = [
+            call(
+                f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
+                data=json.dumps(
+                    {
+                        "organization_id": self.project.organization.id,
+                        "project_id": self.project.id,
+                        "repo": {
+                            "provider": "integrations:github",
+                            "owner": "getsentry",
+                            "name": "sentry",
+                            "external_id": "123",
+                        },
+                    }
+                ),
+                headers={"content-type": "application/json;charset=utf-8"},
+            ),
+            call(
+                f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
+                data=json.dumps(
+                    {
+                        "organization_id": self.project.organization.id,
+                        "project_id": self.project.id,
+                        "repo": {
+                            "provider": "integrations:github",
+                            "owner": "getsentry",
+                            "name": "relay",
+                            "external_id": "234",
+                        },
+                    }
+                ),
+                headers={"content-type": "application/json;charset=utf-8"},
+            ),
+        ]
+        mock_post.assert_has_calls(calls, any_order=True)
+
+    @patch("sentry.api.helpers.autofix.requests.post")
+    def test_autofix_codebase_status_multiple_repos_both_done(self, mock_post):
+        # Setup multiple repositories
+        repo1 = self.create_repo(
+            name="getsentry/sentry", provider="integrations:github", external_id="123"
+        )
+        repo2 = self.create_repo(
+            name="getsentry/relay", provider="integrations:github", external_id="234"
+        )
+        self.create_code_mapping(project=self.project, repo=repo1, stack_root="/path1")
+        self.create_code_mapping(project=self.project, repo=repo2, stack_root="/path2")
+
+        # Mock the POST request to return successful status
+        mock_post.side_effect = [
+            mock.Mock(status_code=200, json=mock.Mock(return_value={"status": "up_to_date"})),
+            mock.Mock(status_code=200, json=mock.Mock(return_value={"status": "up_to_date"})),
+        ]
+
+        status = get_project_codebase_indexing_status(self.project)
+
+        # Assertions
+        assert (
+            mock_post.call_count == 2
+        )  # Ensure that the endpoint was called twice, once for each repo
+        assert status == "up_to_date"
+        calls = [
+            call(
+                f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
+                data=json.dumps(
+                    {
+                        "organization_id": self.project.organization.id,
+                        "project_id": self.project.id,
+                        "repo": {
+                            "provider": "integrations:github",
+                            "owner": "getsentry",
+                            "name": "sentry",
+                            "external_id": "123",
+                        },
+                    }
+                ),
+                headers={"content-type": "application/json;charset=utf-8"},
+            ),
+            call(
+                f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
+                data=json.dumps(
+                    {
+                        "organization_id": self.project.organization.id,
+                        "project_id": self.project.id,
+                        "repo": {
+                            "provider": "integrations:github",
+                            "owner": "getsentry",
+                            "name": "relay",
+                            "external_id": "234",
+                        },
+                    }
+                ),
+                headers={"content-type": "application/json;charset=utf-8"},
+            ),
+        ]
+        mock_post.assert_has_calls(calls, any_order=True)
+
+    @patch("sentry.api.helpers.autofix.requests.post")
+    def test_autofix_codebase_status_multiple_repos_one_not_indexed(self, mock_post):
+        # Setup multiple repositories
+        repo1 = self.create_repo(
+            name="getsentry/sentry", provider="integrations:github", external_id="123"
+        )
+        repo2 = self.create_repo(
+            name="getsentry/relay", provider="integrations:github", external_id="234"
+        )
+        self.create_code_mapping(project=self.project, repo=repo1, stack_root="/path1")
+        self.create_code_mapping(project=self.project, repo=repo2, stack_root="/path2")
+
+        # Mock the POST request to return successful status
+        mock_post.side_effect = [
+            mock.Mock(status_code=200, json=mock.Mock(return_value={"status": "up_to_date"})),
+            mock.Mock(status_code=200, json=mock.Mock(return_value={"status": "not_indexed"})),
+        ]
+
+        # Perform the POST request
+        status = get_project_codebase_indexing_status(self.project)
+
+        # Assertions
+        assert (
+            mock_post.call_count == 2
+        )  # Ensure that the endpoint was called twice, once for each repo
+        assert status == "not_indexed"
+        calls = [
+            call(
+                f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
+                data=json.dumps(
+                    {
+                        "organization_id": self.project.organization.id,
+                        "project_id": self.project.id,
+                        "repo": {
+                            "provider": "integrations:github",
+                            "owner": "getsentry",
+                            "name": "sentry",
+                            "external_id": "123",
+                        },
+                    }
+                ),
+                headers={"content-type": "application/json;charset=utf-8"},
+            ),
+            call(
+                f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
+                data=json.dumps(
+                    {
+                        "organization_id": self.project.organization.id,
+                        "project_id": self.project.id,
+                        "repo": {
+                            "provider": "integrations:github",
+                            "owner": "getsentry",
+                            "name": "relay",
+                            "external_id": "234",
+                        },
+                    }
+                ),
+                headers={"content-type": "application/json;charset=utf-8"},
+            ),
+        ]
+        mock_post.assert_has_calls(calls, any_order=True)