feat(release): add backport-audit, a pre-release backport triage tool - #3527
feat(release): add backport-audit, a pre-release backport triage tool#3527myasnikovdaniil wants to merge 2 commits into
Conversation
Before a release or an rc is cut there was no way to answer whether everything labelled for backport had actually reached the release branch. The documented approach, `gh pr list --search "is:merged label:backport"`, lists every PR ever labelled and says nothing about whether the change arrived, which is the only thing that matters at that point. Answering it by hand means reading the branch history per PR, so backports go missing quietly -- and one class of them was being lost with no draft PR and no failed check to notice. A `backport` label does not name a branch: backport.yaml resolves the target from getLatestRelease at merge time, so the same label means release-1.5 on a PR merged in June and release-1.6 on one merged in August. The audit reproduces that rule rather than trusting the label text, treating a line as current from the publication of its first non-prerelease release. Landing is then established from three independent kinds of evidence: the merge commit already being reachable from the branch (it merged before the cut, so nothing was needed); a linked backport PR, found by the bot's head branch or by a "Backport of #N" body reference, which is what a hand-written backport carries; or the branch's own history -- the bot's merge subject, a commit subject identical to one of the PR's, or an -x cherry-pick reference. What is left over is reported as MISSING, pending on an open backport PR (including the drafts the bot opens with the conflict committed), or dropped with whatever reason someone recorded on the closed backport PR, since that reason is what stops the next release re-opening the same investigation. The exit code is computed from the results rather than as a side effect of printing them, so it holds under --json, which is the mode a gate would consume. Color follows cmd/check-readiness and auto-disables on a non-TTY and under --json, so piping into jq stays clean. Verified against an independently written prototype rather than against its own reimplementation of these rules: over release-1.6, release-1.5 and release-1.4 the JSON is byte-identical across all 68 verdicts, as is the text report and the exit code. That parity also covers the one shortcut taken -- the history walk the audit already makes collects the reachable commit oids, so a set lookup replaces one `git merge-base --is-ancestor` per candidate per branch, 813 subprocesses for three lines. Reachable from the branch tip is exactly what --is-ancestor tests. It is not faster end to end: wall-clock is dominated by the gh API calls. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The "Cherry-pick triage before a patch" step 1 told the release manager to inventory backports with `gh pr list --search "is:merged label:backport"`, annotated "merged but not yet on release-X.Y" -- which that command cannot tell you. It lists every PR ever labelled, whether or not the change reached the branch. Step 1 now runs backport-audit, which resolves each label to the line it meant at merge time and reports what landed, what is waiting on an open backport PR, what was deliberately dropped and with what reason, and what is missing outright. Steps 2 and 3 stay: they cover changes that were never labelled, which the audit structurally cannot see. Also records the failure mode that makes the audit worth running. Before #3155, `conflict_resolution` was passed top-level instead of nested under `experimental`, so the action ignored it and fell back to `fail`, which on a conflicting cherry-pick opens no PR and reports no failure. Those backports were dropped with no draft to find and no red check to notice, so a cluster of MISSING verdicts in one time window is the signature of that, not of maintainers forgetting. Pointers added where someone is standing when they need this: the cherry-pick step of Patch Releases, and the agent-facing release quick reference, which now says not to call a branch ready to cut on the strength of a label search. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
What this PR does
Adds
cmd/backport-audit, which answers one question before a release or an rc is cut: did everything labelled for backport actually land on the release branch? It prints the URLs of the ones that did not and exits non-zero, so it can gate the cut.docs/release.mdis routed through it.The gap it closes is in the docs it replaces.
docs/release.md's "Cherry-pick triage before a patch" told the release manager to inventory backports withgh pr list --search "is:merged label:backport", annotated "merged but not yet on release-X.Y" — which that command cannot tell you. It lists every PR ever labelled, whether or not the change reached the branch, and whether it arrived is the only thing that matters at that point in a cut. Answering it by hand means reading the branch history per PR, so backports go missing quietly.A
backportlabel does not name a branch:backport.yamlresolves the target fromgetLatestReleaseat merge time, so the same label meansrelease-1.5on a PR merged in June andrelease-1.6on one merged in August. The audit reproduces that rule rather than trusting the label text, then establishes landing from three independent kinds of evidence — the merge commit already being reachable from the branch, a linked backport PR (bot head branch or aBackport of #Nbody reference), or the branch's own history (the bot's merge subject, an identical commit subject, an-xcherry-pick reference). Each candidate comes out asin-branch,backported,pending,dropped(with whatever reason someone recorded on the closed backport PR) orMISSING.What running it found
Against
release-1.6,release-1.5andrelease-1.4, over the 271 merged PRs that carry a backport label:Every one of the 9 MISSING items merged before 2026-07-03 07:53Z, which is when #3155 landed. Before it,
conflict_resolutionwas passed as a top-level input instead of nested underexperimental, so the action ignored it and fell back tofail— which on a conflicting cherry-pick opens no PR and reports no failure. Those backports were dropped with no draft to find and no red check to notice. The correlation holds in both directions: every candidate merged after #3155, across all three lines, has a backport PR. So this is a bounded debt pile from that window rather than an ongoing leak, anddocs/release.mdnow records the signature so a future cluster of MISSING verdicts is read as bot failure rather than as maintainers forgetting.All 9 MISSING verdicts were confirmed absent independently of the tool, at diff level: every file each PR touched still differs from
mainon the target branch, and for 7 of 9 at least one touched file does not exist on the branch at all. Two were checked by hand —release-1.4still carries the pre-#2704$host := .Values._namespace.host | default ...form inpackages/extra/info/templates/kubeconfig.yaml, anddata/versionsappears nowhere in itstags.yaml. No false positives in that run.Verification
The implementation was cross-checked against an independently written prototype rather than against its own re-reading of the rules: run over the same three lines, the JSON is byte-identical across all 68 verdicts, as is the 104-line text report and the exit code.
That parity also covers one deliberate shortcut. Presence on a branch could be one
git merge-base --is-ancestorper candidate per branch, 813 subprocesses for three lines; instead the history walk the audit already makes for subjects and cherry-pick references also collects the reachable commit oids, and a set lookup answers the same question. Reachable from the branch tip is exactly what--is-ancestortests, and identical output over 68 verdicts is the evidence. It is not faster end to end — roughly ten seconds either way, because wall-clock is dominated by theghAPI calls, not by local git.gofmtandgo vetare clean. Color followscmd/check-readiness: auto-disabled on a non-TTY and under--json, with--no-colorto force it off, so piping intojqstays clean.Limits
Deliberately stated in the README rather than papered over. A
MISSINGverdict is a prompt to check, not proof of absence: a hand-backport that was squash-merged, reworded and referenced no original PR is invisible to every evidence layer and reads asMISSING. Abackportlabel added long after merge, once the release line has moved on, resolves to the newer line. And the audit reports that something is missing, never why.make generatedoes not apply: this adds a standalonemainpackage with no API types, no CRDs and no packagevalues.yaml, sohack/update-codegen.shhas nothing to regenerate from it.Screenshots
Not applicable — no UI change. Sample CLI output is above.
Downstream repositories
Walked the trigger map in
docs/agents/contributing.mdfile by file against this diff, which is:cmd/backport-audit/main.goandcmd/backport-audit/README.md(both new),docs/release.md,docs/agents/releasing.md, and one.gitignoreline for the root binary thecmd/convention requires.Nothing in it reaches a downstream repository. No package under
packages/apps/orpackages/extra/is added, renamed or removed, and novalues.schema.json,values.yamldefault or version enum changes, so the website generator lists and the hand-written Terraform provider schemas are untouched. NoApplicationDefinitionsemantics,release.prefix, output Secret or Service name, namespace, platform variant or bundle changes. Nothing underhack/moves or is renamed and no make target changes behaviour, soccp's anchor files andexternal-apps-example's vendored copies ofhack/package.mkandhack/update-crd.share unaffected. No node prerequisite inhack/e2e-prepare-cluster.batschanges, sotalm'scharts/cozystack/and the Ansible prepare playbooks do not need a matching PR. No telemetry metric or label, nocozyhrannotation, nocozy-proxylabel or annotation contract. The website's "developer tooling" trigger coverscozyvalues-gen,cozypkgand the packageMakefiles, which contributors use on packages; this is maintainer release tooling whose process doc lives in this repository and is not mirrored to the site.Release note