CI adapters
This is the fallback path. Since ADR
0008 the agent runs the gate
in-cluster by default (gate.mode: cluster), and a repository onboarding
today needs nothing in this directory — see
docs/onboarding.md. An adapter is for
gate.mode: ci: fork pull requests on a public repository, a gate that must
keep answering while the cluster is down, or the gate with no agent at all.
The gate is a container. An adapter’s whole job is:
- Check out the pull request and its merge base — the gate compares two revisions, so a shallow single-revision checkout is not enough.
- Run
gitops-gateagainst both. - Turn the exit code into a commit status the branch protection rule can require.
- Post
report.mdto the pull request as a comment, verbatim, before the step that fails the build.
That is roughly fifteen lines. Everything opinionated lives in the image, on purpose — see ADR 0002, deterministic checks in CI, judgement in the cluster.
Step 4 is not optional
Section titled “Step 4 is not optional”The comment is the verdict channel. A triage agent reads the gate’s verdict by listing the pull request’s comments and taking the most recent one that begins with the gate’s marker:
<!-- gitops-gate -->Publishing render-diff.json to an artifact store is not a substitute: nothing
fetches that file. An adapter that skips the comment leaves the verdict
reachable only by a human with the job summary open — the gate runs, the agent
finds nothing to read, and no step fails.
The marker is emitted by the binary, as the first line of -report output.
An adapter posts the file verbatim and is correct by construction; it does not
need to know the string. Do not prepend your own — you will get two.
Two more lines follow it, and they are emitted by the binary for the same
reason: a verdict headline (## 🔴 … / ## ✅ …) so a reader knows the answer
before reading the findings, and a machine-readable breakdown of why:
<!-- gitops-gate:blockers targeting=0 source=0 apiVersion=1 consumers=0 unscanned=0 valuesDropped=48 -->An adapter posting the report verbatim carries both. Do not strip or rewrite them — the agent reads the breakdown to decide whether anything in the repository could fix the red at all, and a report without it falls back to guessing from prose.
Three details worth copying from github/:
- Post before you fail. The step that turns exit
1into a failed build must come after the comment, or the report is published only when the gate is green, which is precisely when nobody needs it. - Update in place. Find the existing comment by marker and edit it. A gate that appends one comment per push is a gate people collapse and stop reading.
- Post on green too. A green report says “no change to what gets deployed”, which is worth reading — and it means the publishing path runs on every pull request rather than for the first time during an incident.
| Adapter | Status |
|---|---|
github/ | Reference implementation. Exercised in anger. |
gitlab/ | Written against documentation. Unproven — see the README. |
bitbucket/ | Written against documentation. Unproven — see the README. |
Exit codes
Section titled “Exit codes”| Code | Status to report | Meaning |
|---|---|---|
0 | success | No blocking change. |
1 | failure | Targeting moved, or validation failed. |
2 | error | The gate could not run. Distinct from 1 on purpose: “this change is bad” and “the gate is broken” want different reactions, and conflating them trains people to ignore the check. |
One aggregate status
Section titled “One aggregate status”Report one status, aggregating every job. Branch protection then names a single check, and adding or splitting jobs later never requires editing the protection rule — a rule change that is easy to forget and silently drops your gate.
Never filter the required check by path
Section titled “Never filter the required check by path”A required status check that has never reported blocks a pull request —
that is the mechanism by which the gate protects anything. But a workflow
skipped by a paths: filter never reports at all. It is not “passed” and it
is not “failed”; it sits at “expected” forever, and the pull request becomes
permanently unmergeable with no way to clear it short of editing the
protection rule.
So a docs-only change, or a change to an unrelated directory, is bricked by a gate that was never meant to apply to it.
The fix is structural: the workflow runs on every pull request, a cheap first job decides whether the expensive work is needed, and the aggregate job reports either way.
scope always runs; outputs relevant=true|falserender if relevantdiff if relevantvalidate if relevantaddons-gate always runs; treats `skipped` as a passTwo details make it work. The aggregate needs if: always(), or it inherits
the same never-reports problem from its own dependencies. And it must treat a
skipped dependency as a pass while still failing on cancelled — skipped
means “this change cannot affect what gets deployed”, cancelled means the
answer is unknown.
Enabling the gate on pull requests that already exist
Section titled “Enabling the gate on pull requests that already exist”Merging the workflow does not retroactively run it on open pull requests; that needs a new event. Two things follow:
- Turning on branch protection blocks every open pull request immediately, because none of them has reported the required check yet. That is correct, and it is the protection.
- To give them a verdict, push to each branch — a rebase is the tidy way, and
it makes the diff current at the same time. Workflows for
pull_requestare read from the merge commit, so a rebased branch picks up the gate from the base even though the branch predates it.
Two things adapters get wrong
Section titled “Two things adapters get wrong”The gate must be fast. Kargo polls a pull request it is waiting to merge on a fixed interval, so gate latency is added to every automated merge. Skip the expensive chart-render diff when no chart version changed — that is the common case and it roughly halves the wall clock.
A push made by CI’s own token usually does not trigger CI. Most hosts suppress it to prevent loops. If the agent pushes a fix with that token, the gate never re-runs, the status stays red at its previous conclusion, and the promotion waits against a result that will never change. Use a separate credential for agent pushes.