From c33ccd66621388d52de7de29c2d7f3b90c387686 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 8 Apr 2026 13:54:16 -0700 Subject: [PATCH 1/2] feat(integrations): Return `externalId` from OrganizationIntegrationReposEndpoint As requested in https://github.com/getsentry/sentry/pull/112327#issuecomment-4207871278 --- .../organization_integration_repos.py | 2 + .../test_organization_integration_repos.py | 91 ++++++++++++++++--- 2 files changed, 80 insertions(+), 13 deletions(-) diff --git a/src/sentry/integrations/api/endpoints/organization_integration_repos.py b/src/sentry/integrations/api/endpoints/organization_integration_repos.py index a633deca9c809f..0053c495313de9 100644 --- a/src/sentry/integrations/api/endpoints/organization_integration_repos.py +++ b/src/sentry/integrations/api/endpoints/organization_integration_repos.py @@ -22,6 +22,7 @@ class IntegrationRepository(TypedDict): identifier: str isInstalled: bool defaultBranch: str | None + externalId: str @cell_silo_endpoint @@ -85,6 +86,7 @@ def get( identifier=repo["identifier"], defaultBranch=repo.get("default_branch"), isInstalled=repo["identifier"] in installed_repo_names, + externalId=repo["external_id"], ) for repo in repositories if not installable_only or repo["identifier"] not in installed_repo_names diff --git a/tests/sentry/integrations/api/endpoints/test_organization_integration_repos.py b/tests/sentry/integrations/api/endpoints/test_organization_integration_repos.py index 53a2d06b5694c4..e7caacc02cf588 100644 --- a/tests/sentry/integrations/api/endpoints/test_organization_integration_repos.py +++ b/tests/sentry/integrations/api/endpoints/test_organization_integration_repos.py @@ -22,8 +22,13 @@ def setUp(self) -> None: ) def test_simple(self, get_repositories: MagicMock) -> None: get_repositories.return_value = [ - {"name": "rad-repo", "identifier": "Example/rad-repo", "default_branch": "main"}, - {"name": "cool-repo", "identifier": "Example/cool-repo"}, + { + "name": "rad-repo", + "identifier": "Example/rad-repo", + "default_branch": "main", + "external_id": "rad-repo", + }, + {"name": "cool-repo", "identifier": "Example/cool-repo", "external_id": "cool-repo"}, ] response = self.client.get(self.path, format="json") @@ -35,12 +40,14 @@ def test_simple(self, get_repositories: MagicMock) -> None: "identifier": "Example/rad-repo", "defaultBranch": "main", "isInstalled": False, + "externalId": "rad-repo", }, { "name": "cool-repo", "identifier": "Example/cool-repo", "defaultBranch": None, "isInstalled": False, + "externalId": "cool-repo", }, ], "searchable": True, @@ -55,8 +62,9 @@ def test_hide_hidden_repos(self, get_repositories: MagicMock) -> None: "name": "rad-repo", "identifier": "Example/rad-repo", "default_branch": "main", + "external_id": "rad-repo", }, - {"name": "cool-repo", "identifier": "Example/cool-repo"}, + {"name": "cool-repo", "identifier": "Example/cool-repo", "external_id": "cool-repo"}, ] self.create_repo( @@ -75,6 +83,7 @@ def test_hide_hidden_repos(self, get_repositories: MagicMock) -> None: "identifier": "Example/cool-repo", "defaultBranch": None, "isInstalled": False, + "externalId": "cool-repo", }, ], "searchable": True, @@ -85,9 +94,23 @@ def test_hide_hidden_repos(self, get_repositories: MagicMock) -> None: ) def test_installable_only(self, get_repositories: MagicMock) -> None: get_repositories.return_value = [ - {"name": "rad-repo", "identifier": "Example/rad-repo", "default_branch": "main"}, - {"name": "cool-repo", "identifier": "Example/cool-repo", "default_branch": "dev"}, - {"name": "awesome-repo", "identifier": "Example/awesome-repo"}, + { + "name": "rad-repo", + "identifier": "Example/rad-repo", + "default_branch": "main", + "external_id": "rad-repo", + }, + { + "name": "cool-repo", + "identifier": "Example/cool-repo", + "default_branch": "dev", + "external_id": "cool-repo", + }, + { + "name": "awesome-repo", + "identifier": "Example/awesome-repo", + "external_id": "awesome-repo", + }, ] self.create_repo( @@ -105,12 +128,14 @@ def test_installable_only(self, get_repositories: MagicMock) -> None: "identifier": "Example/cool-repo", "defaultBranch": "dev", "isInstalled": False, + "externalId": "cool-repo", }, { "name": "awesome-repo", "identifier": "Example/awesome-repo", "defaultBranch": None, "isInstalled": False, + "externalId": "awesome-repo", }, ], "searchable": True, @@ -121,8 +146,18 @@ def test_installable_only(self, get_repositories: MagicMock) -> None: ) def test_is_installed_field(self, get_repositories: MagicMock) -> None: get_repositories.return_value = [ - {"name": "rad-repo", "identifier": "Example/rad-repo", "default_branch": "main"}, - {"name": "rad-repo", "identifier": "Example2/rad-repo", "default_branch": "dev"}, + { + "name": "rad-repo", + "identifier": "Example/rad-repo", + "default_branch": "main", + "external_id": "rad-repo", + }, + { + "name": "rad-repo", + "identifier": "Example2/rad-repo", + "default_branch": "dev", + "external_id": "rad-repo", + }, ] self.create_repo( @@ -141,11 +176,13 @@ def test_is_installed_field(self, get_repositories: MagicMock) -> None: "identifier": "Example/rad-repo", "defaultBranch": "main", "isInstalled": True, + "externalId": "rad-repo", }, { "name": "rad-repo", "identifier": "Example2/rad-repo", "defaultBranch": "dev", + "externalId": "rad-repo", "isInstalled": False, }, ], @@ -161,7 +198,12 @@ def test_repo_installed_by_other_org_not_excluded(self, get_repositories: MagicM one organization should not affect the available repos for the other. """ get_repositories.return_value = [ - {"name": "shared-repo", "identifier": "Example/shared-repo", "default_branch": "main"}, + { + "name": "shared-repo", + "identifier": "Example/shared-repo", + "default_branch": "main", + "external_id": "shared-repo", + }, ] other_org = self.create_organization(owner=self.user, name="other-org") @@ -182,6 +224,7 @@ def test_repo_installed_by_other_org_not_excluded(self, get_repositories: MagicM "identifier": "Example/shared-repo", "defaultBranch": "main", "isInstalled": False, + "externalId": "shared-repo", }, ], "searchable": True, @@ -193,7 +236,12 @@ def test_repo_installed_by_other_org_not_excluded(self, get_repositories: MagicM def test_accessible_only_passes_param(self, get_repositories: MagicMock) -> None: """When accessibleOnly=true, passes accessible_only to get_repositories.""" get_repositories.return_value = [ - {"name": "rad-repo", "identifier": "Example/rad-repo", "default_branch": "main"}, + { + "name": "rad-repo", + "identifier": "Example/rad-repo", + "default_branch": "main", + "external_id": "rad-repo", + }, ] response = self.client.get( self.path, format="json", data={"search": "rad", "accessibleOnly": "true"} @@ -208,6 +256,7 @@ def test_accessible_only_passes_param(self, get_repositories: MagicMock) -> None "identifier": "Example/rad-repo", "defaultBranch": "main", "isInstalled": False, + "externalId": "rad-repo", }, ], "searchable": True, @@ -219,7 +268,12 @@ def test_accessible_only_passes_param(self, get_repositories: MagicMock) -> None def test_accessible_only_without_search(self, get_repositories: MagicMock) -> None: """When accessibleOnly=true but no search, passes both params through.""" get_repositories.return_value = [ - {"name": "rad-repo", "identifier": "Example/rad-repo", "default_branch": "main"}, + { + "name": "rad-repo", + "identifier": "Example/rad-repo", + "default_branch": "main", + "external_id": "rad-repo", + }, ] response = self.client.get(self.path, format="json", data={"accessibleOnly": "true"}) @@ -232,8 +286,18 @@ def test_accessible_only_without_search(self, get_repositories: MagicMock) -> No def test_accessible_only_with_installable_only(self, get_repositories: MagicMock) -> None: """Both filters compose: accessible scopes the fetch, installable excludes installed repos.""" get_repositories.return_value = [ - {"name": "rad-repo", "identifier": "Example/rad-repo", "default_branch": "main"}, - {"name": "cool-repo", "identifier": "Example/cool-repo", "default_branch": "dev"}, + { + "name": "rad-repo", + "identifier": "Example/rad-repo", + "default_branch": "main", + "external_id": "rad-repo", + }, + { + "name": "cool-repo", + "identifier": "Example/cool-repo", + "default_branch": "dev", + "external_id": "cool-repo", + }, ] self.create_repo( @@ -257,6 +321,7 @@ def test_accessible_only_with_installable_only(self, get_repositories: MagicMock "identifier": "Example/cool-repo", "defaultBranch": "dev", "isInstalled": False, + "externalId": "cool-repo", }, ], "searchable": True, From e02f1b3fe4b07e22399a515acb01430c5f0d48aa Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 8 Apr 2026 15:11:25 -0700 Subject: [PATCH 2/2] fix: Add external_id to mock repos in SCM onboarding acceptance tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new externalId field in OrganizationIntegrationReposEndpoint requires external_id in the repository data. The acceptance test mocks were missing this field, causing KeyError → 500 errors. --- tests/acceptance/test_scm_onboarding.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/acceptance/test_scm_onboarding.py b/tests/acceptance/test_scm_onboarding.py index 0f872712336f2d..3cff96bb92db7e 100644 --- a/tests/acceptance/test_scm_onboarding.py +++ b/tests/acceptance/test_scm_onboarding.py @@ -52,6 +52,7 @@ def test_scm_onboarding_happy_path(self) -> None: "name": "sentry", "identifier": "getsentry/sentry", "default_branch": "master", + "external_id": "12345", }, ] @@ -162,6 +163,7 @@ def test_scm_onboarding_with_integration_install(self) -> None: "name": "sentry", "identifier": "getsentry/sentry", "default_branch": "master", + "external_id": "12345", }, ] @@ -273,6 +275,7 @@ def test_scm_onboarding_detection_error_falls_back_to_manual_picker(self) -> Non "name": "sentry", "identifier": "getsentry/sentry", "default_branch": "master", + "external_id": "12345", }, ]