Repository navigation
fix(gitlab): Build web links from the repository path - #157
Merged
Merged
Conversation
GitLabProvider built file, commit and merge request links from Sentry's
name for the repository with its spaces removed, but that name is
GitLab's display name ("GitLab.org / GitLab FOSS"), which differs from
the path (gitlab-org/gitlab-foss) in more than spaces. Repository gains
an optional path, carried through the RPC payload, which
getsentry/sentry#128198 fills in. The links use it and refuse with
missing_repository_path without it. API calls address projects by id
and are unaffected. An older server that leaves path out of the payload
deserializes as None.
Refs CW-2221
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 20, 2026
vaind
marked this pull request as ready for review
October 20, 2026 20:02
billyvg
approved these changes
Oct 20, 2026
cmanallen
requested changes
Oct 20, 2026
"Could not be found" or "unknown on its provider" read as a failed lookup. The path is absent from the repository the caller passed in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cmanallen
approved these changes
Oct 22, 2026
vaind
added a commit
that referenced
this pull request
Oct 22, 2026
Sentry's git endpoint for coding jobs ([CW-2221](https://linear.app/getsentry/issue/CW-2221/git-grants-and-relay-in-scm-platform)) relays git to a repository's provider with a token. git's protocol is the same everywhere; what differs per provider is the clone URL and the basic-auth username that goes with a token. The providers already build a repository's other URLs, so this makes the clone URL a provider capability too. Second of three stacked PRs, after #157 and before #156. ## Approach 1. **Providers fall back to the repository's `web_base_url`** (first commit). Sentry builds `GitHubProvider` for GitHub Enterprise without one, so its links pointed at github.com ([SCM-204](https://linear.app/getsentry/issue/SCM-204)), and a clone URL would have sent the token there. `GitLabProvider` now prefers `web_base_url` over the external ID's host, so an instance served under a path prefix ([getsentry/sentry#128164](getsentry/sentry#128164)) gets correct links. 2. **`GetGitRemoteProtocol.get_git_remote() -> GitRemote`:** | Provider | Clone URL | Username | |---|---|---| | GitHub, GitHub Enterprise | `<web_base_url>/<owner>/<repo>.git` | `x-access-token` | | GitLab | `<web_base_url>/<path>.git` | `oauth2` | | Cursor Origin | `https://origin.cursor.com/<owner>/<repo>.git` | `x-access-token` | Origin's values come from [its docs](https://cursor.com/docs/api/origin); its git endpoint refuses a bearer header. The caller supplies the token. Per `AGENTS.md`, the action, `FakeProvider` and a `get-git-remote` command in both CLIs come with it. ## Tests `get_git_remote` for each provider (including a GitLab instance with a port or a path prefix, and a repository without a path), the fallbacks through web links, the action, and the RPC client. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
vaind
added a commit
that referenced
this pull request
Oct 22, 2026
Coding-job pods need to `git clone` a branch, and Seer needs to fetch and push, without either holding a provider credential ([Git transport for coding jobs](https://linear.app/getsentry/project/git-transport-for-coding-jobs-c694524ec733), CW-2221). Both sides of Sentry's new git endpoint depend on `sentry-scm`, so what they share goes in a new `scm.git` module: Seer signs grants with it, and Sentry's endpoint (CW-2222) verifies them and relays with it. Third of three stacked PRs, after #157 and #158; this diff is only `scm.git`. ## Flow ```mermaid flowchart LR pod[git client] -->|git request| proxy[Credential proxy] proxy -->|same request + grant| server["Sentry git endpoint (GitServer)"] server -->|same request + token| provider[GitHub, GitLab or Origin] ``` The pod holds neither the grant nor a token: the credential proxy adds the grant (getsentry/sandbox-router#153, getsentry/sandbox-router#154), and the endpoint swaps it for a token. The response streams back the same way. For each request, `GitServer`: 1. verifies the grant, and checks the route and repository against it; 2. looks up the repository and asks its provider for the clone URL; 3. gets a token from Sentry and relays the request with it. A request the grant doesn't allow is refused at step 1, so it never reaches the database, a credential or the provider. Refusals are plain text and never a 401, because git asks for credentials on a 401. ## Approach - **Grants** (`grant.py`): HS256 JWTs, the format Sentry and Seer already sign viewer contexts with, naming an organization, a referrer, one operation (`fetch` or `push`), `RepositoryId`s, an expiry and a version. The referrer says who the grant is for, e.g. a coding job or Seer itself, and tags the endpoint's metrics, since git requests carry nothing else that tells callers apart. The `aud` keeps other tokens signed with the same secret from passing as grants, and the `kid` header lets secrets rotate. Unknown claims or versions are refused, so an older verifier never reads a newer grant as allowing more. `sign_grant` refuses a grant over 4096 characters (`GRANT_MAX_LENGTH`), the limit getsentry/sandbox-router#153 enforces because the grant travels in a request header, so an oversized grant fails in Seer's tests rather than at the router. Adds PyJWT. - **Relay** (`relay.py`): maps the four smart-HTTP routes to `fetch` or `push` and refuses anything else (`authorize_request`), then streams both bodies without buffering or parsing them, passes only git's own headers, accepts only https remotes, never follows redirects, and drops requests' exceptions so the `Authorization` header can't reach an error report. Each request gets its own connection, so nothing one organization leaves on a connection, such as a cookie or an unread response, reaches another's. The caller passes an adapter factory, so Sentry can use `BlacklistAdapter` to refuse internal addresses. Proxy, CA and `.netrc` settings aren't read from the environment, since through a proxy that check would see the proxy's address; `ca_bundle` replaces `REQUESTS_CA_BUNDLE`. - **`GitServer`** (`server.py`): runs the steps above, as `RpcServer` does for the RPC endpoint, and counts each request and its bytes when its response stream ends. Refusals and provider failures are also passed to `emit_error`, with what the request had resolved (organization, repository, operation, provider, remote host) as tags, since the message the client sees leaves out where the request was going. The token comes from a `GetToken` callable, so Sentry's token getters now and a central token mint later plug in unchanged. ## Not in this PR - Sentry's endpoint ([CW-2222](https://linear.app/getsentry/issue/CW-2222/git-endpoint-view-in-sentry)). - Honouring `verify_ssl=False` ([CW-2224](https://linear.app/getsentry/issue/CW-2224/honour-verify-ssl-in-the-git-relay)): certificates are always verified. - Git LFS, and caps on size, duration and concurrency ([CW-2225](https://linear.app/getsentry/issue/CW-2225/cap-git-clone-size-duration-and-concurrency)). ## Tests - Grants that are expired, tampered with, signed with another or a rotated secret, or malformed. - Every route and grant combination. - The relay against a local HTTPS server: headers and streamed bodies in each direction, a connection per request, cookies, redirects, timeouts, untrusted certificates, plain-http remotes, and no token in any error. The streaming and connection tests were checked to fail when the relay buffers, shares a session or leaves a connection open. - The server's check order and metrics. The starting point was `grant.py` and parts of `scm_git_endpoint.py` in getsentry/sandbox-router#145. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
3 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
GitLab web links are built from Sentry's repository name with its spaces removed. That name is GitLab's display name ("GitLab.org / GitLab FOSS"), not its path (
gitlab-org/gitlab-foss), so most of the links are wrong. The git endpoint for coding jobs (CW-2221) needs the path for clone URLs too.First of three stacked PRs, before #158 and #156.
Approach
Repositorygains an optionalpath, which Sentry fills for GitLab fromrepo.config["path"](getsentry/sentry#128198). The RPC payload carries it, and a payload from an older server without it reads asNone.GitLabProvider.web_repository_pathreturnspath, and raisesmissing_repository_pathwithout it. API calls use project ids and don't change.Changes existing behaviour
Without a
path, GitLab's link methods raisemissing_repository_pathinstead of returning a wrong link, so getsentry/sentry#128198 must be deployed before Sentry or Seer takes a release with this.Tests
Links use the path and refuse without one;
pathround-trips through the RPC payload and reads asNonewhen absent.🤖 Generated with Claude Code