Skip to content

The loop, end to end

One pull request, from a new chart version appearing to the change verified running. Every component appears once, doing the one thing it does; component reference and configuration live in their own documents.

The worked example is a real promotion, generalised: external-secrets moving 0.10.3 → 2.9.0. The visible diff is one line and every check is initially green, and it would have broken every manifest in the repository still declaring the API versions the new chart stops serving.

PieceJobNever does
Kargonotices new versions, opens the pull request, merges it when policy allowsjudge what the change does
the gate (gate/)renders what the repository actually deploys, at base and head, and diffs it; publishes the report and the addons-gate checkfix anything, call a model
Bosun (the agent, in-cluster)reads the gate’s verdict; repairs what is provable, explains what is not, escalates what needs a decisionclose a PR, merge a PR, fail a check, touch the cluster
branch protectionrequires addons-gate, so a blocking finding is an unmergeable pull request
ArgoCDreconciles main into the cluster after the mergeanything before the merge
the verification (AnalysisRun)asks the metrics whether what deployed is actually healthyblock a merge — it gates the next promotion in a chain

A Kargo Warehouse watching the chart repository discovers 2.9.0. The Stage rewrites the one pinned line in the target list, pushes a branch, and opens a pull request. The visible diff is a version number.

Whether anyone is asked to look depends on the target’s merge policy: patch bumps of trusted charts merge themselves once the gate is green; anything the policy holds — a major boundary, an artifact whose failure mode is silence — waits for a human, and those held pull requests are exactly the ones Kargo’s promotion also POSTs to Bosun.

Where the gate runs is a configuration value. By default the agent runs it in-cluster against the live ArgoCD inventory (ADR 0008); gate.mode: ci runs the same engine as a container in CI. Nothing below this point differs between the two — same renders, same report, same check name.

The gate runs twice — once at the base revision, once at the head — and each run expands every bootstrap ApplicationSet for every cluster in the inventory into the full set of Applications the repository generates. The diff of those two renders is what the pull request actually does, which a one-line text diff cannot show. Because the version moved, the gate also pulls the chart at both versions and diffs the rendered resources, down to the fields that changed.

For our example the render says: eleven CRDs added, twenty-five resources changed — and four CustomResourceDefinitions stop serving v1alpha1 and v1beta1. The gate scans the repository for manifests still declaring those versions, finds them, and blocks:

A CustomResourceDefinition stopped serving a version — anything still declaring it breaks on apply.

  • CustomResourceDefinition/externalsecrets.external-secrets.io: no longer serves v1alpha1, v1beta1ExternalSecret manifests must move to v1
    • N manifest(s) in this repository still declare a dropped version — blocking until they move: …

Everything the gate publishes lands in two artifacts on the pull request: a report comment led by an invisible marker, and the addons-gate check. The report is the wire format for everything downstream — the agent has no channel to the gate except reading it.

Kargo’s promotion POSTed the pull-request context to Bosun the moment it opened, so the agent is already waiting for addons-gate to settle. Its own commit status says so — pending, “reading addons-gate” — because a reader must be able to tell working from done with nothing to say.

The gate is red. Five things can happen, in strict order of preference.

The deterministic repair. If the only blocking finding is dropped served versions, no judgement is needed: the report names the consumer kind, the dropped versions, the destination, and the rule for finding the declaring manifests. Bosun rewrites every one of them — apiVersion values only, preserving quoting and comments, deny-list and allowlist consulted for every file — and pushes the migration to the pull request’s branch. No model is involved. The gate re-runs on the new commit, counts the consumers again, finds none, and goes green — so the check that verifies the repair is the same scanner that demanded it.

The reshape. Where swapping the version is not the whole job — the target schema prunes a field the old one carried, so the document parses, applies and quietly loses a value while the render, the gate and the repository all look fine — a model is asked for the migrated document itself, one document at a time and capped per pull request. This is the only path on which a model authors file content, and it still does not apply it. The proposal is refused whole unless it keeps the object’s identity, fits the target schema, and contains no value that is not either at that same path in the original, displaced by the schema change, or dictated by the schema itself. What lands is re-serialised from the structure the harness validated rather than written back as the model’s text. A refusal escalates, and values dropped along the way are listed in the comment even when dropping them was right. See ADR 0007.

The deterministic escalation. Some reds have nothing in this repository to change: the chart renders an object whose apiVersion moved, and no manifest here declares it. The gate blocks — somebody should look — but there is no edit to propose. The gate’s own blocker counts are enough to say so, so this escalates without calling a model at all, and the comment says in one line that there are no values to change. Asking a model to explain an absence produced a paragraph restating the report with the one sentence that mattered buried in it.

The mechanical fix. For a red the render proves — a chart default this repository needs pinned back, a coupled pin the new version requires — the model is asked to classify and to propose scalar edits. It selects from an inventory of editable keys rather than writing a patch, and the applier enforces what the prompt only describes — deny-list, promotion scope, from-value equality, and the rule that a version-shaped value must appear verbatim in the gate’s report. What survives those checks is committed to the branch; the gate re-runs and judges the result. An attempt cap, tracked by label, bounds the loop.

The handoff. Everything else — a targeting change, a moved namespace, a migration the evidence does not fully specify — is a human’s decision, and the comment is written as a handoff rather than an announcement: which file and key to open, what the choice is, and the one fact that stopped a mechanical fix. The needs-human label goes on, and Bosun stops. It never closes the pull request, and its status is never a failure: branch protection requires the gate, and a second red check would make the agent a second gate.

A green gate is a verdict on the render, not on the bump — the most dangerous promotions render perfectly. So on held pull requests Bosun also explains green gates: what the version bump actually changed, grounded in the render diff and, when the publisher’s image labels lead to them, the maintainers’ own release notes. A green render that still warrants eyes — a major boundary crossed, RBAC that vanished, notes describing a manual step — gets Worth a look before merging and the label, and blocks nothing.

Escalations on this path are where gate rules come from. The example on this page was first caught here, by the model flagging a version distance no render reveals; that judgement then became code — the gate’s deterministic served-version rule, and then the deterministic repair. A finding the model makes once should become a check the gate makes every time.

The merge is Kargo’s when policy allows and a human’s otherwise; either way addons-gate is required, so nothing blocking merges. ArgoCD reconciles main into the cluster. Then the promotion’s verification asks the metrics whether the Applications the target names are actually Synced and Healthy — and in a promotion chain, that answer is what unlocks the next stage, so “it merged” and “it works” stay different facts.

The local proving ground builds a disposable cluster and runs this entire page against it — make demo for the happy path, make demo-triage for a pull request the gate refuses, make scenarios to replay the recorded red-gate incidents in evals/ against the live agent. (The explain-path cases in that suite are not replayed there — they need a green gate, and the scenario script seeds a red one.)