Skip to content

feat(scm): Ask providers for a repository's git remote - #158

Merged
vaind merged 4 commits into
ivan/gitlab-repository-pathfrom
ivan/git-remote-from-provider
Oct 22, 2026
Merged

vaind merged 4 commits into
ivan/gitlab-repository-pathfrom
ivan/git-remote-from-provider

Conversation

@vaind

@vaind vaind commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Sentry's git endpoint for coding jobs (CW-2221) 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), 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) 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; 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

vaind and others added 2 commits October 20, 2026 21:01
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 <noreply@anthropic.com>
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: <web_base_url>/<owner>/<repo>.git with
  x-access-token.
- GitLab: <web_base_url>/<path>.git with oauth2.
- Cursor Origin: https://origin.cursor.com/<owner>/<repo>.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 <noreply@anthropic.com>
@vaind
vaind added this pull request to stack #159 October 20, 2026 19:03
@vaind
vaind marked this pull request as ready for review October 20, 2026 20:04
@vaind
vaind requested a review from a team as a code owner October 20, 2026 20:04
Comment thread src/scm/types.py Outdated
vaind and others added 2 commits October 21, 2026 08:21
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>
@vaind
vaind merged commit bb498e0 into main Oct 22, 2026
14 checks passed
vaind added a commit that referenced this pull request Oct 22, 2026
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](https://linear.app/getsentry/issue/CW-2221/git-grants-and-relay-in-scm-platform))
needs the path for clone URLs too.

First of three stacked PRs, before #158 and
#156.

## Approach

- `Repository` gains an optional `path`, which Sentry fills for GitLab
from `repo.config["path"]`
([getsentry/sentry#128198](getsentry/sentry#128198)).
The RPC payload carries it, and a payload from an older server without
it reads as `None`.
- `GitLabProvider.web_repository_path` returns `path`, and raises
`missing_repository_path` without it. API calls use project ids and
don't change.

## Changes existing behaviour

Without a `path`, GitLab's link methods raise `missing_repository_path`
instead 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; `path` round-trips through
the RPC payload and reads as `None` when absent.

🤖 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>
@vaind
vaind deleted the ivan/git-remote-from-provider branch October 22, 2026 16:20
@linear-code

linear-code Bot commented Oct 27, 2026

Copy link
Copy Markdown

SCM-204

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants