From ff09b68b72f4b15389b553305316ba50b06d3a1f Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 20 Oct 2026 17:03:58 +0200 Subject: [PATCH] feat(scm): Pass GitLab repository paths to scm-platform Sentry stores a GitLab project's display name ("Group / Project") as the repository name, so scm-platform can't use it to address the project on its host. Its git relay builds GitLab clone URLs from the repository's path and refuses GitLab repositories without one, so take the real path from the repository config. Other providers already store the path as the name and get no `path` key. --- src/sentry/scm/private/helpers.py | 32 +++++++------ .../integration/test_helpers_integration.py | 48 +++++++++++++++++++ 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/src/sentry/scm/private/helpers.py b/src/sentry/scm/private/helpers.py index 6d7cf78bf97c..38a7f8ac89ce 100644 --- a/src/sentry/scm/private/helpers.py +++ b/src/sentry/scm/private/helpers.py @@ -99,20 +99,24 @@ def fetch_repository(organization_id: int, repository_id: RepositoryId) -> Repos if integration: installation_id = integration.external_id - return cast( - Repository, - { - "external_id": repo.external_id, - "id": repo.id, - "integration_id": repo.integration_id, - "is_active": repo.status == ObjectStatus.ACTIVE, - "name": repo.name, - "organization_id": repo.organization_id, - "provider_name": provider_name, - "web_base_url": web_base_url, - "installation_id": installation_id, - }, - ) + repository: dict[str, object] = { + "external_id": repo.external_id, + "id": repo.id, + "integration_id": repo.integration_id, + "is_active": repo.status == ObjectStatus.ACTIVE, + "name": repo.name, + "organization_id": repo.organization_id, + "provider_name": provider_name, + "web_base_url": web_base_url, + "installation_id": installation_id, + } + # A GitLab repository's name is its display name ("Group / Project"), which can't be used to + # address the project on its host, e.g. in a clone URL. The project's real path is only kept + # in its config. + if provider_name == "gitlab": + repository["path"] = repo.config.get("path") + + return cast(Repository, repository) def report_error_to_sentry(e: Exception) -> None: diff --git a/tests/sentry/scm/integration/test_helpers_integration.py b/tests/sentry/scm/integration/test_helpers_integration.py index c4c5c1e1068e..8de4c5c7299b 100644 --- a/tests/sentry/scm/integration/test_helpers_integration.py +++ b/tests/sentry/scm/integration/test_helpers_integration.py @@ -164,6 +164,54 @@ def test_fetch_cursor_origin_repo_populates_installation_id(self) -> None: assert result is not None assert result["installation_id"] == "inst_01example" + def test_fetch_gitlab_repo_populates_path_from_config(self) -> None: + repo = RepositoryModel.objects.create( + organization_id=self.organization.id, + name="GitLab.org / GitLab FOSS", + provider="integrations:gitlab", + external_id="gitlab.example.com:13083", + status=ObjectStatus.ACTIVE, + integration_id=1, + config={"path": "gitlab-org/gitlab-foss", "project_id": 13083}, + ) + + result = fetch_repository(self.organization.id, repo.id) + + assert result is not None + # Sentry's pinned sentry-scm may predate the `path` key, so read it untyped. + assert dict(result)["path"] == "gitlab-org/gitlab-foss" + + def test_fetch_gitlab_repo_without_config_path_has_none_path(self) -> None: + repo = RepositoryModel.objects.create( + organization_id=self.organization.id, + name="GitLab.org / GitLab FOSS", + provider="integrations:gitlab", + external_id="gitlab.example.com:13083", + status=ObjectStatus.ACTIVE, + integration_id=1, + ) + + result = fetch_repository(self.organization.id, repo.id) + + assert result is not None + assert dict(result)["path"] is None + + def test_fetch_non_gitlab_repo_has_no_path(self) -> None: + repo = RepositoryModel.objects.create( + organization_id=self.organization.id, + name="test-org/test-repo", + provider="integrations:github", + external_id="22222", + status=ObjectStatus.ACTIVE, + integration_id=1, + config={"path": "test-org/test-repo"}, + ) + + result = fetch_repository(self.organization.id, repo.id) + + assert result is not None + assert "path" not in result + class TestFetchServiceProvider(TestCase): def test_returns_provider_from_map_to_provider(self) -> None: