From 96d8bea08403d140b9df21a0ce34a925479b2231 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 8 Apr 2026 15:05:01 -0700 Subject: [PATCH 1/4] chore(integrations): Fix repo sync to pass installation id consistently Required to get https://github.com/getsentry/sentry/pull/112519 working for all scms. integration_id and installation are the same, but different scms use them. I don't want to refactor how the scms work, so just providing both. --- src/sentry/integrations/github/tasks/link_all_repos.py | 1 + src/sentry/integrations/source_code_management/sync_repos.py | 1 + src/sentry/plugins/providers/integration_repository.py | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sentry/integrations/github/tasks/link_all_repos.py b/src/sentry/integrations/github/tasks/link_all_repos.py index 2eae7324bac0f4..9709c35f97c22b 100644 --- a/src/sentry/integrations/github/tasks/link_all_repos.py +++ b/src/sentry/integrations/github/tasks/link_all_repos.py @@ -28,6 +28,7 @@ def get_repo_config(repo: Mapping[str, Any], integration_id: int) -> RepositoryI return { "external_id": str(repo["id"]), "integration_id": integration_id, + "installation": integration_id, "identifier": repo["full_name"], } diff --git a/src/sentry/integrations/source_code_management/sync_repos.py b/src/sentry/integrations/source_code_management/sync_repos.py index 0096751819ebb2..4816fa4f60fa14 100644 --- a/src/sentry/integrations/source_code_management/sync_repos.py +++ b/src/sentry/integrations/source_code_management/sync_repos.py @@ -207,6 +207,7 @@ def sync_repos_for_org(organization_integration_id: int) -> None: { "external_id": repo["external_id"], "integration_id": integration.id, + "installation": integration.id, "identifier": str(repo["identifier"]), } for repo in provider_repos diff --git a/src/sentry/plugins/providers/integration_repository.py b/src/sentry/plugins/providers/integration_repository.py index 6ec07a7635be7e..2463d25db1b3e9 100644 --- a/src/sentry/plugins/providers/integration_repository.py +++ b/src/sentry/plugins/providers/integration_repository.py @@ -3,7 +3,7 @@ import logging from collections.abc import Mapping from datetime import timezone -from typing import Any, ClassVar, Generic, NotRequired, TypedDict, TypeVar, cast +from typing import Any, ClassVar, Generic, TypedDict, TypeVar, cast from dateutil.parser import parse as parse_date from rest_framework import status @@ -37,7 +37,7 @@ class RepositoryInputConfig(TypedDict): external_id: str integration_id: int identifier: str - installation: NotRequired[str] + installation: int class RepositoryConfig(TypedDict): From 8d4e42c81362fb0e549364a3e08235b3eb38acf4 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Thu, 9 Apr 2026 17:07:00 -0700 Subject: [PATCH 2/4] ref(integrations): Generalize repo sync task to all SCM providers Expand the periodic repo sync from GitHub-only to provider-agnostic. The task now uses each provider's get_repositories() and external_id instead of GitHub-specific API calls, with per-provider feature flags to control rollout. Currently enabled for GitHub, GitHub Enterprise, and VSTS. GitLab and Bitbucket are blocked on a pre-existing issue where create_repositories eagerly creates webhooks for all repos before checking if they already exist. Perforce lacks external_id support. --- fixtures/vsts.py | 5 ++ src/sentry/features/temporary.py | 8 +++ .../github/tasks/link_all_repos.py | 18 ++--- .../tasks/sync_repos_on_install_change.py | 9 +-- src/sentry/integrations/gitlab/integration.py | 5 ++ .../source_code_management/repository.py | 6 +- .../source_code_management/sync_repos.py | 22 +++--- src/sentry/integrations/vsts/integration.py | 3 + .../providers/integration_repository.py | 12 +--- .../source_code_management/test_sync_repos.py | 70 +++++++++++++++++++ .../integrations/vsts/test_integration.py | 3 + 11 files changed, 123 insertions(+), 38 deletions(-) diff --git a/fixtures/vsts.py b/fixtures/vsts.py index 6b660f59e14937..052d2c8e520a58 100644 --- a/fixtures/vsts.py +++ b/fixtures/vsts.py @@ -137,6 +137,11 @@ def _stub_vsts(self): "id": self.repo_id, "name": self.repo_name, "project": {"name": self.project_a["name"]}, + "_links": { + "web": { + "href": f"https://{self.vsts_account_name.lower()}.visualstudio.com/_git/{self.repo_name}" + } + }, } ] }, diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index e44308d8bacf46..1730eb3b249f44 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -144,6 +144,14 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:github-repo-auto-sync-apply", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) manager.add("organizations:github_enterprise-repo-auto-sync", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) manager.add("organizations:github_enterprise-repo-auto-sync-apply", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) + manager.add("organizations:gitlab-repo-auto-sync", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) + manager.add("organizations:gitlab-repo-auto-sync-apply", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) + manager.add("organizations:bitbucket-repo-auto-sync", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) + manager.add("organizations:bitbucket-repo-auto-sync-apply", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) + manager.add("organizations:bitbucket_server-repo-auto-sync", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) + manager.add("organizations:bitbucket_server-repo-auto-sync-apply", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) + manager.add("organizations:vsts-repo-auto-sync", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) + manager.add("organizations:vsts-repo-auto-sync-apply", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) manager.add("organizations:integrations-perforce", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) manager.add("organizations:integrations-slack-staging", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) manager.add("organizations:scm-source-context", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) diff --git a/src/sentry/integrations/github/tasks/link_all_repos.py b/src/sentry/integrations/github/tasks/link_all_repos.py index 9709c35f97c22b..2d2d94aacb43b8 100644 --- a/src/sentry/integrations/github/tasks/link_all_repos.py +++ b/src/sentry/integrations/github/tasks/link_all_repos.py @@ -1,6 +1,6 @@ import logging from collections.abc import Mapping -from typing import Any +from typing import Any, TypedDict from taskbroker_client.retry import Retry @@ -12,10 +12,7 @@ SCMIntegrationInteractionType, ) from sentry.organizations.services.organization import organization_service -from sentry.plugins.providers.integration_repository import ( - RepositoryInputConfig, - get_integration_repository_provider, -) +from sentry.plugins.providers.integration_repository import get_integration_repository_provider from sentry.shared_integrations.exceptions import ApiError from sentry.silo.base import SiloMode from sentry.tasks.base import instrumented_task, retry @@ -24,11 +21,16 @@ logger = logging.getLogger(__name__) -def get_repo_config(repo: Mapping[str, Any], integration_id: int) -> RepositoryInputConfig: +class GitHubRepoInputConfig(TypedDict): + external_id: str + integration_id: int + identifier: str + + +def get_repo_config(repo: Mapping[str, Any], integration_id: int) -> GitHubRepoInputConfig: return { "external_id": str(repo["id"]), "integration_id": integration_id, - "installation": integration_id, "identifier": repo["full_name"], } @@ -79,7 +81,7 @@ def link_all_repos( integration_repo_provider = get_integration_repository_provider(integration) - repo_configs: list[RepositoryInputConfig] = [] + repo_configs: list[GitHubRepoInputConfig] = [] missing_repos = [] for repo in repositories: try: diff --git a/src/sentry/integrations/github/tasks/sync_repos_on_install_change.py b/src/sentry/integrations/github/tasks/sync_repos_on_install_change.py index 3b8b65e97d75e0..5c4ab00fbfa8a2 100644 --- a/src/sentry/integrations/github/tasks/sync_repos_on_install_change.py +++ b/src/sentry/integrations/github/tasks/sync_repos_on_install_change.py @@ -16,15 +16,12 @@ from sentry.integrations.source_code_management.repo_audit import log_repo_change from sentry.organizations.services.organization import organization_service from sentry.organizations.services.organization.model import RpcOrganization -from sentry.plugins.providers.integration_repository import ( - RepositoryInputConfig, - get_integration_repository_provider, -) +from sentry.plugins.providers.integration_repository import get_integration_repository_provider from sentry.silo.base import SiloMode from sentry.tasks.base import instrumented_task, retry from sentry.taskworker.namespaces import integrations_control_tasks -from .link_all_repos import get_repo_config +from .link_all_repos import GitHubRepoInputConfig, get_repo_config logger = logging.getLogger(__name__) @@ -110,7 +107,7 @@ def _sync_repos_for_org( ) -> None: if repos_added: integration_repo_provider = get_integration_repository_provider(integration) - repo_configs: list[RepositoryInputConfig] = [] + repo_configs: list[GitHubRepoInputConfig] = [] for repo in repos_added: try: repo_configs.append(get_repo_config(repo, integration.id)) diff --git a/src/sentry/integrations/gitlab/integration.py b/src/sentry/integrations/gitlab/integration.py index 38e77b70a83ca9..e7770b4645c448 100644 --- a/src/sentry/integrations/gitlab/integration.py +++ b/src/sentry/integrations/gitlab/integration.py @@ -184,11 +184,16 @@ def get_repositories( # Note: gitlab projects are the same things as repos everywhere else group = self.get_group_id() resp = self.get_client().search_projects(group, query) + instance = self.model.metadata["instance"] return [ { "identifier": str(repo["id"]), "name": repo["name_with_namespace"], "external_id": self.get_repo_external_id(repo), + "url": repo["web_url"], + "instance": instance, + "path": repo["path_with_namespace"], + "project_id": repo["id"], } for repo in resp ] diff --git a/src/sentry/integrations/source_code_management/repository.py b/src/sentry/integrations/source_code_management/repository.py index 0ba84a1bfd18f9..4d637ada268567 100644 --- a/src/sentry/integrations/source_code_management/repository.py +++ b/src/sentry/integrations/source_code_management/repository.py @@ -36,7 +36,11 @@ class RepositoryInfo(TypedDict): identifier: str external_id: str default_branch: NotRequired[str | None] # GitHub, GitHub Enterprise - project: NotRequired[str] # Bitbucket Server + url: NotRequired[str] # GitLab, VSTS + instance: NotRequired[str] # GitLab, VSTS + project: NotRequired[str] # Bitbucket Server, VSTS + path: NotRequired[str] # GitLab (path_with_namespace) + project_id: NotRequired[int] # GitLab repo: NotRequired[str] # Bitbucket Server diff --git a/src/sentry/integrations/source_code_management/sync_repos.py b/src/sentry/integrations/source_code_management/sync_repos.py index 4816fa4f60fa14..e6dedd3d42a6b8 100644 --- a/src/sentry/integrations/source_code_management/sync_repos.py +++ b/src/sentry/integrations/source_code_management/sync_repos.py @@ -28,10 +28,7 @@ from sentry.integrations.source_code_management.repo_audit import log_repo_change from sentry.integrations.source_code_management.repository import RepositoryIntegration from sentry.organizations.services.organization import organization_service -from sentry.plugins.providers.integration_repository import ( - RepositoryInputConfig, - get_integration_repository_provider, -) +from sentry.plugins.providers.integration_repository import get_integration_repository_provider from sentry.shared_integrations.exceptions import ApiError from sentry.silo.base import SiloMode from sentry.tasks.base import instrumented_task, retry @@ -45,14 +42,15 @@ # get_repositories() returning RepositoryInfo with external_id. # Perforce is excluded because it cannot derive external_id from its API. # Providers to include in the periodic sync. -# Other providers (GitLab, Bitbucket, Bitbucket Server, VSTS) have -# build_repository_config methods that expect additional data beyond what -# RepositoryInfo provides (e.g. url, instance, project_id). They need -# follow-up work to either enrich the sync config or simplify their -# build_repository_config before they can be added here. +# GitLab, Bitbucket, and Bitbucket Server are excluded because their +# build_repository_config creates webhooks as a side effect, and +# create_repositories calls it for every repo before checking if it already +# exists. This needs to be fixed before adding those providers. +# Perforce is excluded because it cannot derive external_id from its API. SCM_SYNC_PROVIDERS = [ "github", "github_enterprise", + "vsts", ] @@ -203,12 +201,12 @@ def sync_repos_for_org(organization_integration_id: int) -> None: if new_ids: integration_repo_provider = get_integration_repository_provider(integration) - repo_configs: list[RepositoryInputConfig] = [ + repo_configs = [ { - "external_id": repo["external_id"], + **repo, + "identifier": str(repo["identifier"]), "integration_id": integration.id, "installation": integration.id, - "identifier": str(repo["identifier"]), } for repo in provider_repos if repo["external_id"] in new_ids diff --git a/src/sentry/integrations/vsts/integration.py b/src/sentry/integrations/vsts/integration.py index 77393319440e0b..00c5fd4e6ef31b 100644 --- a/src/sentry/integrations/vsts/integration.py +++ b/src/sentry/integrations/vsts/integration.py @@ -327,6 +327,9 @@ def get_repositories( "name": "{}/{}".format(repo["project"]["name"], repo["name"]), "identifier": str(repo["id"]), "external_id": self.get_repo_external_id(repo), + "url": repo["_links"]["web"]["href"], + "instance": self.instance, + "project": repo["project"]["name"], } ) return data diff --git a/src/sentry/plugins/providers/integration_repository.py b/src/sentry/plugins/providers/integration_repository.py index 2463d25db1b3e9..df64441a5f5b32 100644 --- a/src/sentry/plugins/providers/integration_repository.py +++ b/src/sentry/plugins/providers/integration_repository.py @@ -30,16 +30,6 @@ InstT = TypeVar("InstT", bound="RepositoryIntegration[Any]", default=RepositoryIntegration) -class RepositoryInputConfig(TypedDict): - """Input config passed to create_repositories / build_repository_config. - Providers may include additional keys beyond these.""" - - external_id: str - integration_id: int - identifier: str - installation: int - - class RepositoryConfig(TypedDict): name: str external_id: str @@ -240,7 +230,7 @@ def _update_repositories( def create_repositories( self, - configs: list[RepositoryInputConfig], + configs: list[dict[str, Any]], organization: RpcOrganization, ) -> tuple[list[RpcRepository], list[RpcRepository], list[RepositoryConfig]]: """ diff --git a/tests/sentry/integrations/source_code_management/test_sync_repos.py b/tests/sentry/integrations/source_code_management/test_sync_repos.py index c35a450d6ed054..2c6161409cb640 100644 --- a/tests/sentry/integrations/source_code_management/test_sync_repos.py +++ b/tests/sentry/integrations/source_code_management/test_sync_repos.py @@ -266,3 +266,73 @@ def test_creates_new_repos_for_ghe(self, mock_get_repos: MagicMock) -> None: assert len(repos) == 2 assert repos[0].provider == "integrations:github_enterprise" + + +# TODO: Add GitLab and Bitbucket tests once create_repositories is fixed to +# not call build_repository_config (which creates webhooks) for every repo +# upfront. See SyncReposForOrgGitLabTestCase and SyncReposForOrgBitbucketTestCase +# in git history for the test implementations. + + +@control_silo_test +class SyncReposForOrgVstsTestCase(TestCase): + @patch("sentry.integrations.vsts.integration.VstsIntegration.get_client") + def test_creates_new_repos_for_vsts(self, mock_get_client: MagicMock) -> None: + from sentry.users.models.identity import Identity + + integration = self.create_provider_integration( + provider="vsts", + external_id="vsts-account-id", + name="MyVSTSAccount", + metadata={"domain_name": "https://myvstsaccount.visualstudio.com/"}, + ) + identity = Identity.objects.create( + idp=self.create_identity_provider(type="vsts"), + user=self.user, + external_id="vsts123", + data={ + "access_token": "123456789", + "expires": 9999999999, + "refresh_token": "rxxx", + "token_type": "jwt-bearer", + }, + ) + integration.add_organization(self.organization, self.user, identity.id) + + oi = OrganizationIntegration.objects.get( + organization_id=self.organization.id, integration=integration + ) + + mock_client = MagicMock() + mock_client.get_repos.return_value = { + "value": [ + { + "id": "repo-uuid-1", + "name": "cool-service", + "project": {"name": "ProjectA"}, + "_links": { + "web": {"href": "https://myvstsaccount.visualstudio.com/_git/cool-service"} + }, + }, + { + "id": "repo-uuid-2", + "name": "other-service", + "project": {"name": "ProjectA"}, + "_links": { + "web": {"href": "https://myvstsaccount.visualstudio.com/_git/other-service"} + }, + }, + ] + } + mock_get_client.return_value = mock_client + + with self.feature( + ["organizations:vsts-repo-auto-sync", "organizations:vsts-repo-auto-sync-apply"] + ): + sync_repos_for_org(oi.id) + + with assume_test_silo_mode(SiloMode.CELL): + repos = Repository.objects.filter(organization_id=self.organization.id).order_by("name") + + assert len(repos) == 2 + assert repos[0].provider == "integrations:vsts" diff --git a/tests/sentry/integrations/vsts/test_integration.py b/tests/sentry/integrations/vsts/test_integration.py index d3868469ae89a7..2332aeb835c2cd 100644 --- a/tests/sentry/integrations/vsts/test_integration.py +++ b/tests/sentry/integrations/vsts/test_integration.py @@ -732,6 +732,9 @@ def test_get_repositories(self) -> None: "name": "ProjectA/cool-service", "identifier": str(self.repo_id), "external_id": str(self.repo_id), + "url": f"https://{self.vsts_account_name.lower()}.visualstudio.com/_git/{self.repo_name}", + "instance": self.vsts_base_url, + "project": "ProjectA", } == result[0] def test_get_repositories_identity_error(self) -> None: From 1b9e607fd9bf05830bdfafc1967bcb0a136d2daf Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Fri, 10 Apr 2026 10:05:33 -0700 Subject: [PATCH 3/4] fix ci and review comments --- .../source_code_management/repository.py | 1 + src/sentry/integrations/vsts/integration.py | 1 + src/sentry/integrations/vsts/repository.py | 5 +- .../sentry/integrations/gitlab/test_issues.py | 70 ++++++++++++++++--- .../integrations/vsts/test_integration.py | 1 + 5 files changed, 66 insertions(+), 12 deletions(-) diff --git a/src/sentry/integrations/source_code_management/repository.py b/src/sentry/integrations/source_code_management/repository.py index 4d637ada268567..f949c344215596 100644 --- a/src/sentry/integrations/source_code_management/repository.py +++ b/src/sentry/integrations/source_code_management/repository.py @@ -42,6 +42,7 @@ class RepositoryInfo(TypedDict): path: NotRequired[str] # GitLab (path_with_namespace) project_id: NotRequired[int] # GitLab repo: NotRequired[str] # Bitbucket Server + repo_name: NotRequired[str] # VSTS (bare repo name for API calls) class BaseRepositoryIntegration(ABC): diff --git a/src/sentry/integrations/vsts/integration.py b/src/sentry/integrations/vsts/integration.py index 00c5fd4e6ef31b..07e2237ebf6827 100644 --- a/src/sentry/integrations/vsts/integration.py +++ b/src/sentry/integrations/vsts/integration.py @@ -325,6 +325,7 @@ def get_repositories( data.append( { "name": "{}/{}".format(repo["project"]["name"], repo["name"]), + "repo_name": repo["name"], "identifier": str(repo["id"]), "external_id": self.get_repo_external_id(repo), "url": repo["_links"]["web"]["href"], diff --git a/src/sentry/integrations/vsts/repository.py b/src/sentry/integrations/vsts/repository.py index 9a7b9fd66250d7..5ceba1c43adcfe 100644 --- a/src/sentry/integrations/vsts/repository.py +++ b/src/sentry/integrations/vsts/repository.py @@ -49,14 +49,15 @@ def get_repository_data( def build_repository_config( self, organization: RpcOrganization, data: Mapping[str, Any] ) -> RepositoryConfig: + repo_name = data.get("repo_name", data["name"]) return { - "name": data["name"], + "name": repo_name, "external_id": data["external_id"], "url": data["url"], "config": { "instance": data["instance"], "project": data["project"], - "name": data["name"], + "name": repo_name, }, "integration_id": data["installation"], } diff --git a/tests/sentry/integrations/gitlab/test_issues.py b/tests/sentry/integrations/gitlab/test_issues.py index b0098d9cfe9d34..1eff659384dca3 100644 --- a/tests/sentry/integrations/gitlab/test_issues.py +++ b/tests/sentry/integrations/gitlab/test_issues.py @@ -60,8 +60,18 @@ def test_get_create_issue_config(self) -> None: "https://example.gitlab.com/api/v4/groups/%s/projects" % self.installation.model.metadata["group_id"], json=[ - {"name_with_namespace": "getsentry / sentry", "id": 1}, - {"name_with_namespace": "getsentry / hello", "id": 22}, + { + "name_with_namespace": "getsentry / sentry", + "id": 1, + "path_with_namespace": "getsentry/sentry", + "web_url": "https://example.gitlab.com/getsentry/sentry", + }, + { + "name_with_namespace": "getsentry / hello", + "id": 22, + "path_with_namespace": "getsentry/hello", + "web_url": "https://example.gitlab.com/getsentry/hello", + }, ], ) assert self.installation.get_create_issue_config(self.group, self.user) == [ @@ -98,8 +108,18 @@ def test_get_link_issue_config(self) -> None: "https://example.gitlab.com/api/v4/groups/%s/projects" % self.installation.model.metadata["group_id"], json=[ - {"name_with_namespace": "getsentry / sentry", "id": 1}, - {"name_with_namespace": "getsentry / hello", "id": 22}, + { + "name_with_namespace": "getsentry / sentry", + "id": 1, + "path_with_namespace": "getsentry/sentry", + "web_url": "https://example.gitlab.com/getsentry/sentry", + }, + { + "name_with_namespace": "getsentry / hello", + "id": 22, + "path_with_namespace": "getsentry/hello", + "web_url": "https://example.gitlab.com/getsentry/hello", + }, ], ) autocomplete_url = "/extensions/gitlab/search/baz/%d/" % self.installation.model.id @@ -234,9 +254,24 @@ def test_create_issue_default_project_in_group_api_call(self) -> None: "https://example.gitlab.com/api/v4/groups/%s/projects" % self.installation.model.metadata["group_id"], json=[ - {"name_with_namespace": "getsentry / sentry", "id": 1}, - {"name_with_namespace": project_name, "id": project_id}, - {"name_with_namespace": "getsentry / hello", "id": 22}, + { + "name_with_namespace": "getsentry / sentry", + "id": 1, + "path_with_namespace": "getsentry/sentry", + "web_url": "https://example.gitlab.com/getsentry/sentry", + }, + { + "name_with_namespace": project_name, + "id": project_id, + "path_with_namespace": "this_is/a_project", + "web_url": "https://example.gitlab.com/this_is/a_project", + }, + { + "name_with_namespace": "getsentry / hello", + "id": 22, + "path_with_namespace": "getsentry/hello", + "web_url": "https://example.gitlab.com/getsentry/hello", + }, ], ) responses.add( @@ -303,14 +338,29 @@ def test_create_issue_default_project_not_in_api_call(self) -> None: "https://example.gitlab.com/api/v4/groups/%s/projects" % self.installation.model.metadata["group_id"], json=[ - {"name_with_namespace": "getsentry / sentry", "id": 1}, - {"name_with_namespace": "getsentry / hello", "id": 22}, + { + "name_with_namespace": "getsentry / sentry", + "id": 1, + "path_with_namespace": "getsentry/sentry", + "web_url": "https://example.gitlab.com/getsentry/sentry", + }, + { + "name_with_namespace": "getsentry / hello", + "id": 22, + "path_with_namespace": "getsentry/hello", + "web_url": "https://example.gitlab.com/getsentry/hello", + }, ], ) responses.add( responses.GET, "https://example.gitlab.com/api/v4/projects/%s" % project_id, - json={"name_with_namespace": project_name, "id": project_id}, + json={ + "name_with_namespace": project_name, + "id": project_id, + "path_with_namespace": "this_is/a_project", + "web_url": "https://example.gitlab.com/this_is/a_project", + }, ) assert self.installation.get_create_issue_config(self.group, self.user) == [ { diff --git a/tests/sentry/integrations/vsts/test_integration.py b/tests/sentry/integrations/vsts/test_integration.py index 2332aeb835c2cd..4d58197cc85975 100644 --- a/tests/sentry/integrations/vsts/test_integration.py +++ b/tests/sentry/integrations/vsts/test_integration.py @@ -730,6 +730,7 @@ def test_get_repositories(self) -> None: assert len(result) == 1 assert { "name": "ProjectA/cool-service", + "repo_name": "cool-service", "identifier": str(self.repo_id), "external_id": str(self.repo_id), "url": f"https://{self.vsts_account_name.lower()}.visualstudio.com/_git/{self.repo_name}", From 27a9c92d758f9f501c9fa47e0a1130c8d67f6a49 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Fri, 10 Apr 2026 10:18:49 -0700 Subject: [PATCH 4/4] fix dumb comment --- src/sentry/integrations/source_code_management/sync_repos.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sentry/integrations/source_code_management/sync_repos.py b/src/sentry/integrations/source_code_management/sync_repos.py index e6dedd3d42a6b8..caceda91de977c 100644 --- a/src/sentry/integrations/source_code_management/sync_repos.py +++ b/src/sentry/integrations/source_code_management/sync_repos.py @@ -40,8 +40,6 @@ # Providers to include in the periodic sync. Each must implement # get_repositories() returning RepositoryInfo with external_id. -# Perforce is excluded because it cannot derive external_id from its API. -# Providers to include in the periodic sync. # GitLab, Bitbucket, and Bitbucket Server are excluded because their # build_repository_config creates webhooks as a side effect, and # create_repositories calls it for every repo before checking if it already