Git providers
The interface is deliberately small: it carries what the workflow needs and
nothing more. The methods group into four jobs: read a pull request, read what
has been said on it, say something, and push a fix. gitprovider/provider.go
is the authoritative list, and the listing below goes stale the moment a method
is added.
ADR 0004 committed to four methods. Reading a pull request, discovering open ones, editing a comment and publishing a commit status all turned out to be workflow needs too, which is the growth its “lowest common denominator” cost paragraph predicted.
On identity. A token grants the right access and the wrong name: it belongs
to whoever minted it, so the agent’s comments arrive under that person’s avatar
and read like a colleague’s until the footer. On GitHub, set git.app.appId
and give it a private key; an App comments as yourapp[bot], with a face of
its own, and its installation tokens expire hourly instead of never. On Gitea,
create a dedicated bot user and mint the token as that user, which is what
local/ does.
// read a pull requestGetPullRequest(ctx, number) // title, branch, head SHA, labelsListOpenPullRequests(ctx) // how the gate discovers work: no webhook, no CI event
// read what has been said on itListComments(ctx, number) // every comment, see belowCheckStatus(ctx, sha, checkName) // pending | success | failure | missing
// say somethingComment(ctx, number, body)UpdateComment(ctx, id, body) // edit in place rather than appending per pushSetCommitStatus(ctx, sha, name, state, description)AddLabel(ctx, number, label) // the attempt cap lives here
// push a fixPushFix(ctx, pr, root, message) // to the PR's branch, never the default
Name() string| Provider | Status |
|---|---|
| GitHub | implemented, exercised |
| Gitea | implemented, exercised against a live instance |
| GitLab | extension point |
| Bitbucket | extension point |
GIT_API_BASE means different things per provider, because the providers do.
On GitHub it is the API root (.../api/v3 for Enterprise); on Gitea it is the
instance root, because the client appends /api/v1 itself.
GIT_API_BASE is required for Gitea and the process refuses to start without
it, because there is no public Gitea to default to. GitHub defaults to the
public API.
Reads and writes must name the same host. The GitHub client builds its push
remote from GIT_REPO_URL, the same value the checkouts clone, so an
Enterprise deployment pushes where it read. Deriving the remote from
owner/repo against github.com sends the fix, and a live installation token
with it, to whatever repository happens to hold that name on the public host.
No credential reaches git’s argv, the push token’s or the operator’s.
Both providers used to spell it into the remote URL,
https://x-access-token:<token>@host/..., and hand that to git push as an
argument. Nothing persisted it and the error text was
redacted, but /proc/<pid>/cmdline is world-readable, so for the length of the
push a live token was there for ps and for every other process in the
namespace. git -c http.<url>.extraHeader=… is the obvious repair and is the
same mistake, because -c is argv too; the credential goes through
GIT_CONFIG_COUNT/KEY/VALUE instead, where /proc/<pid>/environ is
readable only by the process owner. The key is scoped to the remote’s own URL
and never the bare http.extraHeader, which would attach a bearer credential
for one host to whatever host git ended up talking to.
The same applies to a credential an operator writes into GIT_REPO_URL, which
bosun used to hand git as part of the URL on every clone. gitprovider.Remote
splits it the same way, and the remote stored as origin carries no
credential. Bosun attaches the credential per command, to the commands that
contact a host, which is also why it passes the remote to EnsureHead and
MergeBase instead of letting them find it in the checkout. Only
gitprovider may run a git command that contacts a remote, and a test derives
that from git’s own remote-facing subcommands instead of from a list of call
sites.
Things a new implementation has to get right
Section titled “Things a new implementation has to get right”The gate’s report is a comment. A comment is the only artifact surface
every git host has, so the gate publishes there rather than into a
provider-specific artifact store. ListComments finds it by an HTML marker.
ListComments must return every comment, not the first page. The gate’s
report is found by scanning that list, so a client that silently stops at one
hundred makes the report vanish on a busy pull request, and the agent cannot
tell that from a gate that published nothing, which is a different situation
with a different answer.
Check state has two surfaces. On GitHub a gate may report as a check run (Actions) or a legacy commit status, and a repository can use either. Reading only one makes a gate you did not look at indistinguishable from no gate. Expect the same split elsewhere.
Pushes must not re-trigger nothing. Most hosts suppress workflow triggers for pushes made with the CI system’s own token. If the agent pushes with that token, the gate never re-runs, the status stays red at its previous conclusion, and the promotion waits on a result that will never change. Use a separate credential.
Never implement a merge or a close. The interface deliberately has neither. The agent proposes; the gate and the merge policy dispose.
Token permissions
Section titled “Token permissions”For GitHub, a fine-grained token scoped to the repository:
| Permission | Level | Why |
|---|---|---|
| Contents | read & write | push the fix to the bot branch |
| Pull requests | read & write | comment |
| Issues | read & write | labels, which carry the attempt cap |
| Commit statuses | read and write | read the gate, and publish the agent’s own verdict beside it |
| Metadata | read | required baseline |
| Workflows | none | without it GitHub rejects any push touching .github/workflows/**, making “the agent cannot edit the gate” a server-side guarantee as well as a local one |
That last row is worth keeping even though edits.DefaultDeny already refuses
those paths. Two independent mechanisms, one of which is enforced by someone
else’s server.