From 65484a6296b41c8da1adf491745bcbf6fd58a7eb Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 20 Oct 2026 21:01:21 +0200 Subject: [PATCH 1/3] fix(scm): Fall back to the repository's web_base_url in providers Sentry's provider factory builds GitHubProvider for GitHub Enterprise repositories without a web_base_url, so their file, commit and pull request links pointed at github.com. The repository already carries the instance's URL, so the provider now falls back to it before github.com. GitLabProvider built its host from the external ID, which holds only the hostname, so links for an instance served under a path prefix or on another scheme were wrong. It now prefers the repository's web_base_url, which getsentry/sentry#128164 fills with the instance's URL, and keeps the external ID's host as the fallback. Co-Authored-By: Claude Opus 5.5 --- src/scm/providers/github/provider.py | 6 ++++-- src/scm/providers/gitlab/provider.py | 4 +++- tests/unit/provider/test_github.py | 12 ++++++++++++ tests/unit/provider/test_gitlab.py | 7 +++++++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/scm/providers/github/provider.py b/src/scm/providers/github/provider.py index 64b79527..ae7a9e0e 100644 --- a/src/scm/providers/github/provider.py +++ b/src/scm/providers/github/provider.py @@ -437,12 +437,14 @@ def __init__( client: ApiClient, organization_id: int, repository: Repository, - web_base_url: str = GITHUB_WEB_BASE_URL, + web_base_url: str | None = None, ) -> None: self.client = client self.organization_id = organization_id self.repository = repository - self._web_base_url = web_base_url + # A GitHub Enterprise repository carries its instance's URL. Falling back to it keeps a caller that builds + # the provider without one from pointing web links at github.com. + self._web_base_url = web_base_url or repository["web_base_url"] or GITHUB_WEB_BASE_URL def request( self, diff --git a/src/scm/providers/gitlab/provider.py b/src/scm/providers/gitlab/provider.py index 7f7e62c2..3f0ae520 100644 --- a/src/scm/providers/gitlab/provider.py +++ b/src/scm/providers/gitlab/provider.py @@ -240,7 +240,9 @@ def __init__(self, client: ApiClient, organization_id: int, repository: Reposito raise MalformedExternalId() netloc, self.project_id = repository["external_id"].rsplit(":", maxsplit=1) - self.web_base_url = f"https://{netloc}" + # Sentry sends the instance's URL with the scheme and any path prefix it is served under. The external ID + # holds only the host, which is enough for an instance served at the root over https. + self.web_base_url = repository["web_base_url"] or f"https://{netloc}" @property def web_repository_path(self) -> str: diff --git a/tests/unit/provider/test_github.py b/tests/unit/provider/test_github.py index 299797de..bb309614 100644 --- a/tests/unit/provider/test_github.py +++ b/tests/unit/provider/test_github.py @@ -2081,6 +2081,18 @@ def test_get_pull_request_url_builds_pr_url() -> None: assert provider.get_pull_request_url("42") == "https://github.com/test-org/test-repo/pull/42" +def test_ghe_web_base_url_defaults_to_the_repositorys() -> None: + # Sentry's provider factory builds GitHub Enterprise providers without a web_base_url. + repository = { + **make_repository(), + "provider_name": "github_enterprise", + "web_base_url": "https://github.example.com", + } + provider = GitHubProvider(MagicMock(spec=ApiClient), organization_id=1, repository=repository) # type: ignore[arg-type] + + assert provider.get_pull_request_url("42") == "https://github.example.com/test-org/test-repo/pull/42" + + def test_ghe_web_base_url_used_in_url_methods() -> None: provider = GitHubProvider( MagicMock(spec=ApiClient), diff --git a/tests/unit/provider/test_gitlab.py b/tests/unit/provider/test_gitlab.py index 35c560cd..f7a3842b 100644 --- a/tests/unit/provider/test_gitlab.py +++ b/tests/unit/provider/test_gitlab.py @@ -13969,6 +13969,13 @@ def test_web_repository_path_used_in_commit_and_mr_urls(client: ApiClient) -> No ) +def test_web_links_keep_the_instances_path_prefix(client: ApiClient) -> None: + repository = {**_make_gitlab_provider(client).repository, "web_base_url": "https://example.com/gitlab"} + provider = GitLabProvider(client=client, organization_id=1, repository=repository) # type: ignore[arg-type] + + assert provider.get_commit_url("abc123") == "https://example.com/gitlab/test-repo/-/commit/abc123" + + def test_get_file_url_builds_blob_url(provider: GitLabProvider): assert provider.get_file_url("src/main.py", "abc123") == ("https://gitlab.com/test-repo/-/blob/abc123/src/main.py") assert provider.get_file_url("src/main.py", "abc123", start_line=10) == ( From ce28433d0fafb4f6b838a9812f4079ccdb6c38d8 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 20 Oct 2026 21:01:30 +0200 Subject: [PATCH 2/3] feat(scm): Ask providers for a repository's git remote Sentry's git endpoint relays git smart HTTP to a repository's provider with a token, so it needs the repository's clone URL and the basic-auth username its provider pairs with a token. The protocol is the same for every provider; these two values are what differ, and the providers already build every other URL for a repository. GetGitRemoteProtocol puts them next to the web-link protocols: - GitHub and GitHub Enterprise: //.git with x-access-token. - GitLab: /.git with oauth2. - Cursor Origin: https://origin.cursor.com//.git with x-access-token, per Origin's docs (cursor.com/docs/api/origin), whose git endpoint refuses a bearer header. Refs CW-2221 Co-Authored-By: Claude Opus 5.5 --- bin/github-client | 8 +++++ bin/gitlab-client | 8 +++++ src/scm/actions.py | 8 +++++ src/scm/providers/cursor_origin/provider.py | 7 ++++ src/scm/providers/github/provider.py | 6 +++- src/scm/providers/gitlab/provider.py | 4 +++ src/scm/test_fixtures.py | 4 +++ src/scm/types.py | 15 +++++++++ tests/unit/provider/test_cursor_origin.py | 7 ++++ tests/unit/provider/test_github.py | 26 ++++++++++++++- tests/unit/provider/test_gitlab.py | 36 +++++++++++++++++++++ tests/unit/test_actions.py | 7 ++++ tests/unit/test_rpc_integration.py | 5 +++ 13 files changed, 139 insertions(+), 2 deletions(-) diff --git a/bin/github-client b/bin/github-client index 8b96aa83..38ce0da0 100755 --- a/bin/github-client +++ b/bin/github-client @@ -21,6 +21,7 @@ Commands: get-pull-request-review get-review-comments [--cursor ] [--per-page ] get-pull-request-url + get-git-remote mark-pull-request-ready-for-review mark-pull-request-as-draft get-issue @@ -129,6 +130,7 @@ from scm.types import ( GetFileContentProtocol, GetFileUrlProtocol, GetGitRefProtocol, + GetGitRemoteProtocol, GetIssueCommentsProtocol, GetIssueProtocol, GetPullRequestCommentsProtocol, @@ -283,6 +285,8 @@ def main() -> None: p = sub.add_parser("get-pull-request-url") p.add_argument("id") + sub.add_parser("get-git-remote") + p = sub.add_parser("mark-pull-request-ready-for-review") p.add_argument("id") @@ -567,6 +571,10 @@ def main() -> None: assert isinstance(scm, GetPullRequestUrlProtocol) dump(scm.get_pull_request_url(args.id)) + elif args.command == "get-git-remote": + assert isinstance(scm, GetGitRemoteProtocol) + dump(scm.get_git_remote()) + elif args.command == "mark-pull-request-ready-for-review": assert isinstance(scm, MarkPullRequestDraftStateProtocol) scm.mark_pull_request_ready_for_review(args.id) diff --git a/bin/gitlab-client b/bin/gitlab-client index d9fcb90e..4a5a52f5 100755 --- a/bin/gitlab-client +++ b/bin/gitlab-client @@ -23,6 +23,7 @@ Commands: get-pull-request-diff get-pull-request-review-threads [--include-reactions] get-pull-request-url + get-git-remote get-issue create-issue <body> [--assignee <user_id> ...] [--label <name> ...] update-issue <issue_id> [--state open|closed] [--assignee <user_id> ...] [--label <name> ...] @@ -117,6 +118,7 @@ from scm.types import ( GetDirectoryContentsProtocol, GetFileContentProtocol, GetFileUrlProtocol, + GetGitRemoteProtocol, GetIssueCommentsProtocol, GetIssueProtocol, GetPullRequestCommentsProtocol, @@ -248,6 +250,8 @@ def main(): p = sub.add_parser("get-pull-request-url") p.add_argument("id") + sub.add_parser("get-git-remote") + p = sub.add_parser("get-issue") p.add_argument("id") @@ -453,6 +457,10 @@ def main(): assert isinstance(scm, GetPullRequestUrlProtocol) dump(scm.get_pull_request_url(args.id)) + elif args.command == "get-git-remote": + assert isinstance(scm, GetGitRemoteProtocol) + dump(scm.get_git_remote()) + elif args.command == "get-issue": assert isinstance(scm, GetIssueProtocol) dump(scm.get_issue(args.id)) diff --git a/src/scm/actions.py b/src/scm/actions.py index dfa2071a..1ed97067 100644 --- a/src/scm/actions.py +++ b/src/scm/actions.py @@ -78,6 +78,7 @@ GetFullTreeProtocol, GetGitCommitProtocol, GetGitRefProtocol, + GetGitRemoteProtocol, GetIssueCommentReactionsProtocol, GetIssueCommentsProtocol, GetIssueProtocol, @@ -106,6 +107,7 @@ GitBlob, GitCommitObject, GitRef, + GitRemote, GitRepository, GitTree, InputTreeEntry, @@ -528,6 +530,11 @@ def get_pull_request_url(scm: GetPullRequestUrlProtocol, pull_request_id: str) - return scm.get_pull_request_url(pull_request_id) +def get_git_remote(scm: GetGitRemoteProtocol) -> GitRemote: + """Return the repository's git smart-HTTP endpoint and the basic-auth username its provider pairs with a token.""" + return scm.get_git_remote() + + def create_git_blob(scm: CreateGitBlobProtocol, content: str, encoding: str) -> ActionResult[GitBlob]: """Create a git blob object.""" return scm.create_git_blob(content, encoding) @@ -1202,6 +1209,7 @@ def download_archive( "get_file_url", "get_git_commit", "get_git_ref", + "get_git_remote", "get_issue_comment_reactions", "get_issue_comments", "get_issue_reactions", diff --git a/src/scm/providers/cursor_origin/provider.py b/src/scm/providers/cursor_origin/provider.py index 6606f2c1..f0299dc5 100644 --- a/src/scm/providers/cursor_origin/provider.py +++ b/src/scm/providers/cursor_origin/provider.py @@ -48,6 +48,7 @@ GitCommitObject, GitCommitTree, GitRef, + GitRemote, GitRepository, GitTree, MoveCommitAction, @@ -79,6 +80,8 @@ PULL_REQUEST_TEMPLATE_FILENAME = "pull_request_template.md" PULL_REQUEST_TEMPLATE_DIRNAME = "pull_request_template" CURSOR_ORIGIN_WEB_BASE_URL = "https://cursor.com/codebase" +# Git over HTTPS is served from its own host, not the web UI's. +CURSOR_ORIGIN_GIT_BASE_URL = "https://origin.cursor.com" # Origin has no no-reply address for an app. CURSOR_ORIGIN_APP_COMMIT_EMAIL = "noreply@sentry.io" PAGE_TOKEN_PARAM = "pageToken" @@ -354,6 +357,10 @@ def get_commits_url( def get_pull_request_url(self, pull_request_id: str) -> str: return f"{self._web_base_url}/{self.repository_path}/pull/{pull_request_id}" + def get_git_remote(self) -> GitRemote: + # Origin's git endpoint takes an installation token as the basic-auth password and refuses a bearer header. + return GitRemote(url=f"{CURSOR_ORIGIN_GIT_BASE_URL}/{self.repository_path}.git", username="x-access-token") + def get_file_content( self, path: str, diff --git a/src/scm/providers/github/provider.py b/src/scm/providers/github/provider.py index ae7a9e0e..b133dec3 100644 --- a/src/scm/providers/github/provider.py +++ b/src/scm/providers/github/provider.py @@ -54,6 +54,7 @@ GitCommitObject, GitCommitTree, GitRef, + GitRemote, GitRepository, GitTree, InputTreeEntry, @@ -443,7 +444,7 @@ def __init__( self.organization_id = organization_id self.repository = repository # A GitHub Enterprise repository carries its instance's URL. Falling back to it keeps a caller that builds - # the provider without one from pointing web links at github.com. + # the provider without one from pointing web links, and the git remote a token is sent to, at github.com. self._web_base_url = web_base_url or repository["web_base_url"] or GITHUB_WEB_BASE_URL def request( @@ -1033,6 +1034,9 @@ def get_commits_url( def get_pull_request_url(self, pull_request_id: str) -> str: return f"{self._web_base_url}/{self.repository['name']}/pull/{pull_request_id}" + def get_git_remote(self) -> GitRemote: + return GitRemote(url=f"{self._web_base_url}/{self.repository['name']}.git", username="x-access-token") + def create_git_blob(self, content: str, encoding: str) -> ActionResult[GitBlob]: response = self.post( f"/repos/{self.repository['name']}/git/blobs", diff --git a/src/scm/providers/gitlab/provider.py b/src/scm/providers/gitlab/provider.py index 3f0ae520..91bdf81a 100644 --- a/src/scm/providers/gitlab/provider.py +++ b/src/scm/providers/gitlab/provider.py @@ -50,6 +50,7 @@ GitCommitObject, GitCommitTree, GitRef, + GitRemote, GitRepository, GitTree, Issue, @@ -746,6 +747,9 @@ def get_commits_url( def get_pull_request_url(self, pull_request_id: str) -> str: return f"{self.web_base_url}/{self.web_repository_path}/-/merge_requests/{pull_request_id}" + def get_git_remote(self) -> GitRemote: + return GitRemote(url=f"{self.web_base_url}/{self.web_repository_path}.git", username="oauth2") + def get_tree( self, tree_sha: SHA, diff --git a/src/scm/test_fixtures.py b/src/scm/test_fixtures.py index ad6c0bca..d0c92b5e 100644 --- a/src/scm/test_fixtures.py +++ b/src/scm/test_fixtures.py @@ -28,6 +28,7 @@ GitCommitObject, GitCommitTree, GitRef, + GitRemote, GitTree, InputTreeEntry, Issue, @@ -1190,6 +1191,9 @@ def get_commits_url( def get_pull_request_url(self, pull_request_id: str) -> str: return f"https://github.com/test-org/test-repo/pull/{pull_request_id}" + def get_git_remote(self) -> GitRemote: + return GitRemote(url="https://github.com/test-org/test-repo.git", username="x-access-token") + # Git blob operations def create_git_blob(self, content: str, encoding: str) -> ActionResult[GitBlob]: diff --git a/src/scm/types.py b/src/scm/types.py index 5a17f0b7..35c84f9a 100644 --- a/src/scm/types.py +++ b/src/scm/types.py @@ -65,6 +65,15 @@ class ArchiveLink(TypedDict): headers: dict[str, str] +class GitRemote(TypedDict): + """Where a repository's git smart-HTTP endpoint lives and how a token authenticates against it.""" + + url: str + """The repository's clone URL, e.g. ``https://github.com/getsentry/sentry.git``.""" + username: str + """The basic-auth username the provider pairs with a token, e.g. ``x-access-token`` on GitHub.""" + + type BuildStatus = Literal["pending", "running", "completed"] """The lifecycle stage of a CI build. @@ -1100,6 +1109,11 @@ class GetPullRequestUrlProtocol(Protocol): def get_pull_request_url(self, pull_request_id: str) -> str: ... +@runtime_checkable +class GetGitRemoteProtocol(Protocol): + def get_git_remote(self) -> GitRemote: ... + + # Commit Protocols @@ -1675,6 +1689,7 @@ def update_and_collapse_pull_request_comment( GetFileUrlProtocol, GetGitCommitProtocol, GetGitRefProtocol, + GetGitRemoteProtocol, GetIssueCommentReactionsProtocol, GetIssueCommentsProtocol, GetIssueProtocol, diff --git a/tests/unit/provider/test_cursor_origin.py b/tests/unit/provider/test_cursor_origin.py index a317a74a..d4be281e 100644 --- a/tests/unit/provider/test_cursor_origin.py +++ b/tests/unit/provider/test_cursor_origin.py @@ -34,6 +34,7 @@ ChmodCommitAction, CommitAuthorParam, DeleteCommitAction, + GitRemote, MoveCommitAction, Repository, WriteCommitAction, @@ -1083,6 +1084,12 @@ def test_a_pull_request_url(self, provider: CursorOriginProvider) -> None: def test_a_commit_url(self, provider: CursorOriginProvider) -> None: assert provider.get_commit_url("abc123") == f"https://cursor.com/codebase/{REPO}/commit/abc123" + def test_a_git_remote(self, provider: CursorOriginProvider) -> None: + # Git is served from origin.cursor.com rather than the web UI's cursor.com/codebase. + assert provider.get_git_remote() == GitRemote( + url=f"https://origin.cursor.com/{REPO}.git", username="x-access-token" + ) + def _commit_raw(sha: str, date: str = "2026-08-01T09:30:00Z") -> dict[str, Any]: identity = {"name": "Jane Doe", "email": "jane@example.com", "date": date} diff --git a/tests/unit/provider/test_github.py b/tests/unit/provider/test_github.py index bb309614..1fb61c79 100644 --- a/tests/unit/provider/test_github.py +++ b/tests/unit/provider/test_github.py @@ -74,6 +74,7 @@ CredentialsSet, DeleteCommitAction, DiffLine, + GitRemote, MoveCommitAction, Repository, WriteCommitAction, @@ -2081,8 +2082,29 @@ def test_get_pull_request_url_builds_pr_url() -> None: assert provider.get_pull_request_url("42") == "https://github.com/test-org/test-repo/pull/42" +def test_get_git_remote_builds_clone_url() -> None: + provider = GitHubProvider(MagicMock(spec=ApiClient), organization_id=1, repository=make_repository()) + + assert provider.get_git_remote() == GitRemote( + url="https://github.com/test-org/test-repo.git", username="x-access-token" + ) + + +def test_ghe_web_base_url_used_in_git_remote() -> None: + provider = GitHubProvider( + MagicMock(spec=ApiClient), + organization_id=1, + repository=make_repository(), + web_base_url="https://github.example.com", + ) + + assert provider.get_git_remote() == GitRemote( + url="https://github.example.com/test-org/test-repo.git", username="x-access-token" + ) + + def test_ghe_web_base_url_defaults_to_the_repositorys() -> None: - # Sentry's provider factory builds GitHub Enterprise providers without a web_base_url. + # Built without a web_base_url, a GitHub Enterprise provider would otherwise send its token to github.com. repository = { **make_repository(), "provider_name": "github_enterprise", @@ -2090,6 +2112,7 @@ def test_ghe_web_base_url_defaults_to_the_repositorys() -> None: } provider = GitHubProvider(MagicMock(spec=ApiClient), organization_id=1, repository=repository) # type: ignore[arg-type] + assert provider.get_git_remote()["url"] == "https://github.example.com/test-org/test-repo.git" assert provider.get_pull_request_url("42") == "https://github.example.com/test-org/test-repo/pull/42" @@ -3670,6 +3693,7 @@ def test_public_methods_are_accounted_for() -> None: "get_commit_url", "get_commits_url", "get_pull_request_url", + "get_git_remote", "create_commit", "update_issue", "get_thread_id_from_review_comment_unique_id", diff --git a/tests/unit/provider/test_gitlab.py b/tests/unit/provider/test_gitlab.py index f7a3842b..a9a6bb90 100644 --- a/tests/unit/provider/test_gitlab.py +++ b/tests/unit/provider/test_gitlab.py @@ -37,6 +37,7 @@ ChmodCommitAction, DeleteCommitAction, DiffLine, + GitRemote, MoveCommitAction, Repository, WriteCommitAction, @@ -13976,6 +13977,41 @@ def test_web_links_keep_the_instances_path_prefix(client: ApiClient) -> None: assert provider.get_commit_url("abc123") == "https://example.com/gitlab/test-repo/-/commit/abc123" +def test_get_git_remote_is_the_repository_path_on_its_instance(client: ApiClient) -> None: + provider = _make_gitlab_provider(client, name="GitLab.org / GitLab FOSS", path="gitlab-org/gitlab-foss") + + assert provider.get_git_remote() == GitRemote( + url="https://gitlab.com/gitlab-org/gitlab-foss.git", username="oauth2" + ) + + +def test_get_git_remote_on_a_self_managed_instance(client: ApiClient) -> None: + provider = GitLabProvider( + client=client, + organization_id=1, + repository={**_make_gitlab_provider(client).repository, "external_id": "gitlab.example.com:8443:7"}, + ) + + assert provider.get_git_remote() == GitRemote( + url="https://gitlab.example.com:8443/test-repo.git", username="oauth2" + ) + + +def test_get_git_remote_keeps_the_instances_path_prefix(client: ApiClient) -> None: + repository = {**_make_gitlab_provider(client).repository, "web_base_url": "https://example.com/gitlab"} + provider = GitLabProvider(client=client, organization_id=1, repository=repository) # type: ignore[arg-type] + + assert provider.get_git_remote()["url"] == "https://example.com/gitlab/test-repo.git" + + +@pytest.mark.parametrize("path", [None, ""]) +def test_get_git_remote_needs_the_repository_path(client: ApiClient, path: str | None) -> None: + provider = _make_gitlab_provider(client, name="GitLab.org / GitLab FOSS", path=path) + + with pytest.raises(MissingRepositoryPath): + provider.get_git_remote() + + def test_get_file_url_builds_blob_url(provider: GitLabProvider): assert provider.get_file_url("src/main.py", "abc123") == ("https://gitlab.com/test-repo/-/blob/abc123/src/main.py") assert provider.get_file_url("src/main.py", "abc123", start_line=10) == ( diff --git a/tests/unit/test_actions.py b/tests/unit/test_actions.py index 19706e52..e3687288 100644 --- a/tests/unit/test_actions.py +++ b/tests/unit/test_actions.py @@ -53,6 +53,7 @@ get_full_tree, get_git_commit, get_git_ref, + get_git_remote, get_issue, get_issue_comment_reactions, get_issue_comments, @@ -194,6 +195,7 @@ def fetch_repository(oid, rid) -> Repository: (get_commit_url, {"commit_sha": "abc123"}), (get_commits_url, {"commit_sha": "abc123"}), (get_pull_request_url, {"pull_request_id": "1"}), + (get_git_remote, {}), # Git blob operations (create_git_blob, {"content": "hello", "encoding": "utf-8"}), # File content operations @@ -475,6 +477,10 @@ def _check_get_pull_request_url(result: Any) -> None: assert result == "https://github.com/test-org/test-repo/pull/1" +def _check_get_git_remote(result: Any) -> None: + assert result == {"url": "https://github.com/test-org/test-repo.git", "username": "x-access-token"} + + def _check_create_git_blob(result: Any) -> None: assert result["data"]["sha"] == "blob123abc" assert result["type"] == "github" @@ -831,6 +837,7 @@ def _check_download_archive(result: Any) -> None: (get_commit_url, {"commit_sha": "abc123"}, _check_get_commit_url), (get_commits_url, {"commit_sha": "abc123"}, _check_get_commits_url), (get_pull_request_url, {"pull_request_id": "1"}, _check_get_pull_request_url), + (get_git_remote, {}, _check_get_git_remote), ( create_git_blob, {"content": "hello", "encoding": "utf-8"}, diff --git a/tests/unit/test_rpc_integration.py b/tests/unit/test_rpc_integration.py index ec266701..f2caa522 100644 --- a/tests/unit/test_rpc_integration.py +++ b/tests/unit/test_rpc_integration.py @@ -965,6 +965,11 @@ def make_client_scm(organization_id, repository_id, server): lambda scm: actions.get_pull_request_url(scm, "42"), "https://github.com/org/repo/pull/42", ), + ( + "get_git_remote", + lambda scm: actions.get_git_remote(scm), + {"url": "https://github.com/org/repo.git", "username": "x-access-token"}, + ), ] From 2f705f2e44888a41fe1ed9ebfc226b2698d477c6 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <dlugos.ivan@gmail.com> Date: Wed, 21 Oct 2026 08:27:25 +0200 Subject: [PATCH 3/3] ref(scm): Rename GitRemote.username to basic_auth_username Out of the Basic auth context, "username" read like a person's account. It is the fixed name a provider expects alongside a token, such as x-access-token or oauth2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- src/scm/providers/cursor_origin/provider.py | 4 +++- src/scm/providers/github/provider.py | 4 +++- src/scm/providers/gitlab/provider.py | 2 +- src/scm/test_fixtures.py | 2 +- src/scm/types.py | 4 ++-- tests/unit/provider/test_cursor_origin.py | 2 +- tests/unit/provider/test_github.py | 4 ++-- tests/unit/provider/test_gitlab.py | 4 ++-- tests/unit/test_actions.py | 2 +- tests/unit/test_rpc_integration.py | 2 +- 10 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/scm/providers/cursor_origin/provider.py b/src/scm/providers/cursor_origin/provider.py index f0299dc5..0e3c5699 100644 --- a/src/scm/providers/cursor_origin/provider.py +++ b/src/scm/providers/cursor_origin/provider.py @@ -359,7 +359,9 @@ def get_pull_request_url(self, pull_request_id: str) -> str: def get_git_remote(self) -> GitRemote: # Origin's git endpoint takes an installation token as the basic-auth password and refuses a bearer header. - return GitRemote(url=f"{CURSOR_ORIGIN_GIT_BASE_URL}/{self.repository_path}.git", username="x-access-token") + return GitRemote( + url=f"{CURSOR_ORIGIN_GIT_BASE_URL}/{self.repository_path}.git", basic_auth_username="x-access-token" + ) def get_file_content( self, diff --git a/src/scm/providers/github/provider.py b/src/scm/providers/github/provider.py index b133dec3..b3591743 100644 --- a/src/scm/providers/github/provider.py +++ b/src/scm/providers/github/provider.py @@ -1035,7 +1035,9 @@ def get_pull_request_url(self, pull_request_id: str) -> str: return f"{self._web_base_url}/{self.repository['name']}/pull/{pull_request_id}" def get_git_remote(self) -> GitRemote: - return GitRemote(url=f"{self._web_base_url}/{self.repository['name']}.git", username="x-access-token") + return GitRemote( + url=f"{self._web_base_url}/{self.repository['name']}.git", basic_auth_username="x-access-token" + ) def create_git_blob(self, content: str, encoding: str) -> ActionResult[GitBlob]: response = self.post( diff --git a/src/scm/providers/gitlab/provider.py b/src/scm/providers/gitlab/provider.py index 91bdf81a..d1e592cd 100644 --- a/src/scm/providers/gitlab/provider.py +++ b/src/scm/providers/gitlab/provider.py @@ -748,7 +748,7 @@ def get_pull_request_url(self, pull_request_id: str) -> str: return f"{self.web_base_url}/{self.web_repository_path}/-/merge_requests/{pull_request_id}" def get_git_remote(self) -> GitRemote: - return GitRemote(url=f"{self.web_base_url}/{self.web_repository_path}.git", username="oauth2") + return GitRemote(url=f"{self.web_base_url}/{self.web_repository_path}.git", basic_auth_username="oauth2") def get_tree( self, diff --git a/src/scm/test_fixtures.py b/src/scm/test_fixtures.py index d0c92b5e..50500046 100644 --- a/src/scm/test_fixtures.py +++ b/src/scm/test_fixtures.py @@ -1192,7 +1192,7 @@ def get_pull_request_url(self, pull_request_id: str) -> str: return f"https://github.com/test-org/test-repo/pull/{pull_request_id}" def get_git_remote(self) -> GitRemote: - return GitRemote(url="https://github.com/test-org/test-repo.git", username="x-access-token") + return GitRemote(url="https://github.com/test-org/test-repo.git", basic_auth_username="x-access-token") # Git blob operations diff --git a/src/scm/types.py b/src/scm/types.py index 35c84f9a..a4e789ef 100644 --- a/src/scm/types.py +++ b/src/scm/types.py @@ -70,8 +70,8 @@ class GitRemote(TypedDict): url: str """The repository's clone URL, e.g. ``https://github.com/getsentry/sentry.git``.""" - username: str - """The basic-auth username the provider pairs with a token, e.g. ``x-access-token`` on GitHub.""" + basic_auth_username: str + """Sent with the token as the Basic auth username, e.g. ``x-access-token`` on GitHub.""" type BuildStatus = Literal["pending", "running", "completed"] diff --git a/tests/unit/provider/test_cursor_origin.py b/tests/unit/provider/test_cursor_origin.py index d4be281e..9da8184e 100644 --- a/tests/unit/provider/test_cursor_origin.py +++ b/tests/unit/provider/test_cursor_origin.py @@ -1087,7 +1087,7 @@ def test_a_commit_url(self, provider: CursorOriginProvider) -> None: def test_a_git_remote(self, provider: CursorOriginProvider) -> None: # Git is served from origin.cursor.com rather than the web UI's cursor.com/codebase. assert provider.get_git_remote() == GitRemote( - url=f"https://origin.cursor.com/{REPO}.git", username="x-access-token" + url=f"https://origin.cursor.com/{REPO}.git", basic_auth_username="x-access-token" ) diff --git a/tests/unit/provider/test_github.py b/tests/unit/provider/test_github.py index 1fb61c79..738722f1 100644 --- a/tests/unit/provider/test_github.py +++ b/tests/unit/provider/test_github.py @@ -2086,7 +2086,7 @@ def test_get_git_remote_builds_clone_url() -> None: provider = GitHubProvider(MagicMock(spec=ApiClient), organization_id=1, repository=make_repository()) assert provider.get_git_remote() == GitRemote( - url="https://github.com/test-org/test-repo.git", username="x-access-token" + url="https://github.com/test-org/test-repo.git", basic_auth_username="x-access-token" ) @@ -2099,7 +2099,7 @@ def test_ghe_web_base_url_used_in_git_remote() -> None: ) assert provider.get_git_remote() == GitRemote( - url="https://github.example.com/test-org/test-repo.git", username="x-access-token" + url="https://github.example.com/test-org/test-repo.git", basic_auth_username="x-access-token" ) diff --git a/tests/unit/provider/test_gitlab.py b/tests/unit/provider/test_gitlab.py index a9a6bb90..711c002f 100644 --- a/tests/unit/provider/test_gitlab.py +++ b/tests/unit/provider/test_gitlab.py @@ -13981,7 +13981,7 @@ def test_get_git_remote_is_the_repository_path_on_its_instance(client: ApiClient provider = _make_gitlab_provider(client, name="GitLab.org / GitLab FOSS", path="gitlab-org/gitlab-foss") assert provider.get_git_remote() == GitRemote( - url="https://gitlab.com/gitlab-org/gitlab-foss.git", username="oauth2" + url="https://gitlab.com/gitlab-org/gitlab-foss.git", basic_auth_username="oauth2" ) @@ -13993,7 +13993,7 @@ def test_get_git_remote_on_a_self_managed_instance(client: ApiClient) -> None: ) assert provider.get_git_remote() == GitRemote( - url="https://gitlab.example.com:8443/test-repo.git", username="oauth2" + url="https://gitlab.example.com:8443/test-repo.git", basic_auth_username="oauth2" ) diff --git a/tests/unit/test_actions.py b/tests/unit/test_actions.py index e3687288..51b47293 100644 --- a/tests/unit/test_actions.py +++ b/tests/unit/test_actions.py @@ -478,7 +478,7 @@ def _check_get_pull_request_url(result: Any) -> None: def _check_get_git_remote(result: Any) -> None: - assert result == {"url": "https://github.com/test-org/test-repo.git", "username": "x-access-token"} + assert result == {"url": "https://github.com/test-org/test-repo.git", "basic_auth_username": "x-access-token"} def _check_create_git_blob(result: Any) -> None: diff --git a/tests/unit/test_rpc_integration.py b/tests/unit/test_rpc_integration.py index f2caa522..d17c05b5 100644 --- a/tests/unit/test_rpc_integration.py +++ b/tests/unit/test_rpc_integration.py @@ -968,7 +968,7 @@ def make_client_scm(organization_id, repository_id, server): ( "get_git_remote", lambda scm: actions.get_git_remote(scm), - {"url": "https://github.com/org/repo.git", "username": "x-access-token"}, + {"url": "https://github.com/org/repo.git", "basic_auth_username": "x-access-token"}, ), ]