Problem
We maintain master plus two release branches (currently V6.2 and V5.4). The branch set shifts at every minor release. Today the workflow around them is manual and has these recurring pain points, ordered by impact:
- Nothing guarantees a master fix actually lands on the release branches. A backport can be forgotten entirely, or opened and then left unmerged.
- Nothing forces the backport question. Whether a master fix applies to V6.2 or V5.4 is decided ad hoc, and V5.4 has diverged enough that the answer needs judgment.
- Reviewing backports is a chore. Most are identical to an already-approved master PR, so reviewers rubber-stamp them. That habit is exactly what lets the rare backport that did need branch-specific changes slip through unexamined.
- Backports are created by hand, including the clean cherry-picks that need no judgment at all.
- Dependabot only covers master. Release branches get dependency and CVE fixes only when someone notices (as with GO-2026-6061). When such a fix lands on a release branch, every open PR against that branch fails CI until it merges the base branch back in.
- The vulnerability scan runs on every PR on every branch. On release-branch PRs it regularly blocks a merge for an advisory unrelated to the PR, forcing the author to wait for a base-branch fix and a branch update. Scan failures already reach a private Slack channel, so per-PR blocking adds friction without adding signal.
- The master PR gives no view of backport progress. You have to search for the sibling PRs to know if a fix has landed everywhere.
- The PR list does not show the base branch. A fix plus its two backports appear as three PRs with the same title, and reviewers cannot tell them apart.
Goals
- No approved master fix silently misses a release branch: the decision is recorded, and unexecuted decisions surface automatically.
- Every master fix carries an explicit, recorded backport decision.
- Reviewer attention goes where it matters: none for machine-verified identical backports, focused for backports that needed branch-specific changes.
- Clean backports happen without human work; conflicted ones are flagged, not silently dropped.
- Release branches get CVE fixes without waiting for someone to notice.
- Vulnerability scanning alerts on branch builds and Slack, never by blocking unrelated PRs.
- The master PR shows the state of its backports.
- Every PR shows its target branch at a glance.
- All automation reads the supported branch list from one file, so a minor release rotation is a one-line change.
Design overview
Three building blocks, introduced gradually:
A branch registry. One file, .github/supported-branches.json, listing the maintained release branches. Labeler config, the backport workflow, and the scheduled vulnerability scan all read it. Rotating branches at a minor release means editing this file and creating the new labels.
A label taxonomy. Two label families with distinct prefixes and colors:
target/v6.2, target/v5.4: applied automatically from the PR's base branch. Pure visibility; never set by hand.
backport/v6.2, backport/v5.4, backport/none: the decision labels, set by the author or reviewer on master PRs. backport/none exists so that "no backport needed" is a recorded decision, not an omission.
GitHub Actions. A labeler workflow for target/*, a decision-check workflow that blocks master PRs until a backport/* label is present, and a backport workflow that acts on the labels at merge time.
Phases
Each phase is independently useful and can ship without the later ones. Phases are ordered by dependency and risk, not by problem impact: the labels in phases 1 and 3 are low-value on their own but are the plumbing that the high-impact phases (4, 5, and 7) build on. Phase 2 is independent quick relief.
Phase 1: visibility (labeler)
Add the branch registry and a workflow using actions/labeler to apply target/* labels from the base branch. Optionally add a saved search per branch to the repo README (is:pr is:open base:V6.2).
Effort: small. Risk: none, no behavior changes.
Phase 2: remove the vulnerability scan from PR builds
Remove govulncheck from PR workflows. Keep it on the branch builds of master and the release branches, which run after every merge and already report failures to the private Slack channel. A PR is not the place to fix an advisory that came in through an outdated dependency or language version, and today such an advisory blocks every open PR on the branch until a base-branch fix lands and is merged back in.
Accepted trade-off: a PR that itself introduces a vulnerable dependency is now caught by the branch build minutes after merge instead of before it. The fix then follows the normal branch fix flow. If post-merge catches turn out to be frequent, a later refinement is a diff-against-base scan on PRs (fail only on advisories the PR introduces); we skip that complexity until the need is shown.
Effort: small (delete a job from the PR workflow). Risk: low, and reversible with a one-line revert.
Phase 3: explicit backport decision
Add a required status check on master PRs that fails until exactly one decision is recorded: one or more backport/* branch labels, or backport/none. The check description links to a short guideline on when a fix applies to V5.4 (which has diverged) versus V6.2 (which tracks master closely).
Effort: small (one workflow, one guideline paragraph in CONTRIBUTING). Risk: low; the check can start as non-required to trial it.
Note: this phase records the decision. It does not guarantee execution; that gap is closed by the sweep in phase 5.
Phase 4: automated backport PRs
On merge of a master PR with backport/<branch> labels, korthout/backport-action cherry-picks the squashed commit onto each target branch and opens a PR. The action comments the outcome on the original PR: a link to each backport PR, or a conflict notice with the manual steps.
Conflicted backports (the common case for V5.4) stay manual for now. The conflict comment is the handoff point; a later phase can attach an agent to it.
The workflow also labels each backport PR with how it was produced: backport-clean (automated cherry-pick, no conflicts) or backport-manual (a human resolved conflicts or wrote an equivalent fix). This tells reviewers where to spend attention: a backport-clean PR is a sanity check, a backport-manual PR is a real review of the branch-specific changes. Phase 7 builds on this distinction.
Known cost: PRs created with the default GITHUB_TOKEN do not trigger CI. The workflow needs a GitHub App token or PAT so backport PRs run checks like any other PR.
Effort: medium (workflow plus token setup). Risk: low; a bad backport PR is just a PR you close.
Phase 5: backport status on the master PR
Extend phase 4 with a bot-maintained comment on the master PR: a table with one row per target branch showing the backport PR and its state (open, merged, conflict, pending). A small workflow updates the comment when a backport PR changes state, matching backport PRs to their origin by branch naming convention (backport/<origin-pr>/<branch>).
This phase also adds the backport-debt sweep: a scheduled job that lists every merged master PR whose backport/<branch> label has no merged backport PR on that branch. The result goes to the existing private Slack channel (and optionally a pinned tracking issue). This closes the gap left by phase 3: a recorded decision that was never executed, an auto-created PR left unmerged, or an ignored conflict comment all show up in the sweep.
Effort: medium (two workflows, roughly 100 lines of script each). Risk: none; they only write comments and messages.
Phase 6: dependency updates for release branches
Two options, team decision needed:
- Renovate instead of Dependabot. Renovate supports multiple base branches with vulnerability alerts, which covers master and release branches in one tool. Cost: migration from Dependabot, new config to learn, and more PR noise unless tuned.
- Scheduled agent job. Keep Dependabot for master. A scheduled workflow runs govulncheck per release branch and opens fix PRs, automating what we now do by hand with the fix-go-cves skill. Cost: custom workflow to maintain; only covers advisories, not routine updates (which is arguably the right scope for frozen release branches).
Recommendation: the scheduled job. Release branches should only receive security fixes, so a security-only tool fits better than adapting a general update tool. Revisit if the job grows Renovate-shaped features.
Phase 7: review-free clean backports
Goal: a backport that is provably identical to an approved master fix, with green CI, merges without a second human review. This removes the rubber-stamp chore (problem 3), and by removing it, makes the remaining reviews (the backport-manual ones) meaningful again.
This is safe only if "provably identical" is machine-verified, never claimed. An attacker (or a mistake) must not be able to get unreviewed code onto a release branch by presenting a PR as a backport of something approved. Conditions, all enforced by a GitHub App:
- Provenance: the PR was created by the backport App itself (phase 4), on a branch matching its naming convention. Human-created PRs never qualify, whatever they claim to be.
- Patch identity:
git patch-id of the backport commit equals the patch-id of the merged, approved master commit. Equal patch-ids mean an identical diff modulo line offsets; different code cannot pass.
- Approval lineage: the referenced master PR was approved through the normal required-review flow and is merged.
- Freshness: the App approves a specific head SHA. Branch protection has "dismiss stale approvals on new commits" enabled, and the App re-runs checks 1 and 2 on every push. A commit pushed onto the bot's branch after approval voids the approval.
- CI: all required checks pass on the release branch, including its branch-specific tests.
Residual risk that these checks cannot catch: an identical diff can behave differently on a diverged branch (a renamed function, a changed contract in a caller). Only the branch's test suite guards against that. Therefore auto-merge is enabled per branch in the registry: on for branches that track master closely (V6.2 today), off for diverged branches (V5.4 today), where every backport keeps a human review.
Effort: medium to large (GitHub App with review permission, verification workflow, branch protection changes). Risk: the highest-trust component in this proposal; it should get its own security review before deployment and can be trialed in log-only mode (the App comments "would have auto-approved" without approving).
Out of scope, recorded as open issues
- Reverse flow: a fix that originates on a release branch (like GO-2026-6061, where master was already fixed) has no forward-port tracking. Possible later: a
forward-port label with the same machinery.
- Embargoed fixes: public labels and bot comments leak information. Embargoed security fixes stay on the current manual path.
- Auto-updating open PRs after a base branch fix: deliberately not proposed. Phase 2 removes the reason to want it; auto-update actions have poor behavior with fork PRs and cause CI storms.
- Semantically different fixes: when V5.4 needs a different change (for example a different dependency major), the backport label still applies but the conflict path is the expected route. The label means "needs an equivalent fix", not "cherry-pick will succeed".
Rollout
Phases 1 to 3 in one PR each, mergeable this week; phase 2 (dropping the PR scan) is the quickest win and independent of the rest. Phase 4 next, since it depends on the labels being trusted. Phases 5 and 6 as separate proposals once 1 to 4 have run for a few weeks. Phase 7 last, after the backport App has a track record and after its own security review.
Problem
We maintain master plus two release branches (currently V6.2 and V5.4). The branch set shifts at every minor release. Today the workflow around them is manual and has these recurring pain points, ordered by impact:
Goals
Design overview
Three building blocks, introduced gradually:
A branch registry. One file,
.github/supported-branches.json, listing the maintained release branches. Labeler config, the backport workflow, and the scheduled vulnerability scan all read it. Rotating branches at a minor release means editing this file and creating the new labels.A label taxonomy. Two label families with distinct prefixes and colors:
target/v6.2,target/v5.4: applied automatically from the PR's base branch. Pure visibility; never set by hand.backport/v6.2,backport/v5.4,backport/none: the decision labels, set by the author or reviewer on master PRs.backport/noneexists so that "no backport needed" is a recorded decision, not an omission.GitHub Actions. A labeler workflow for
target/*, a decision-check workflow that blocks master PRs until abackport/*label is present, and a backport workflow that acts on the labels at merge time.Phases
Each phase is independently useful and can ship without the later ones. Phases are ordered by dependency and risk, not by problem impact: the labels in phases 1 and 3 are low-value on their own but are the plumbing that the high-impact phases (4, 5, and 7) build on. Phase 2 is independent quick relief.
Phase 1: visibility (labeler)
Add the branch registry and a workflow using
actions/labelerto applytarget/*labels from the base branch. Optionally add a saved search per branch to the repo README (is:pr is:open base:V6.2).Effort: small. Risk: none, no behavior changes.
Phase 2: remove the vulnerability scan from PR builds
Remove govulncheck from PR workflows. Keep it on the branch builds of master and the release branches, which run after every merge and already report failures to the private Slack channel. A PR is not the place to fix an advisory that came in through an outdated dependency or language version, and today such an advisory blocks every open PR on the branch until a base-branch fix lands and is merged back in.
Accepted trade-off: a PR that itself introduces a vulnerable dependency is now caught by the branch build minutes after merge instead of before it. The fix then follows the normal branch fix flow. If post-merge catches turn out to be frequent, a later refinement is a diff-against-base scan on PRs (fail only on advisories the PR introduces); we skip that complexity until the need is shown.
Effort: small (delete a job from the PR workflow). Risk: low, and reversible with a one-line revert.
Phase 3: explicit backport decision
Add a required status check on master PRs that fails until exactly one decision is recorded: one or more
backport/*branch labels, orbackport/none. The check description links to a short guideline on when a fix applies to V5.4 (which has diverged) versus V6.2 (which tracks master closely).Effort: small (one workflow, one guideline paragraph in CONTRIBUTING). Risk: low; the check can start as non-required to trial it.
Note: this phase records the decision. It does not guarantee execution; that gap is closed by the sweep in phase 5.
Phase 4: automated backport PRs
On merge of a master PR with
backport/<branch>labels,korthout/backport-actioncherry-picks the squashed commit onto each target branch and opens a PR. The action comments the outcome on the original PR: a link to each backport PR, or a conflict notice with the manual steps.Conflicted backports (the common case for V5.4) stay manual for now. The conflict comment is the handoff point; a later phase can attach an agent to it.
The workflow also labels each backport PR with how it was produced:
backport-clean(automated cherry-pick, no conflicts) orbackport-manual(a human resolved conflicts or wrote an equivalent fix). This tells reviewers where to spend attention: abackport-cleanPR is a sanity check, abackport-manualPR is a real review of the branch-specific changes. Phase 7 builds on this distinction.Known cost: PRs created with the default
GITHUB_TOKENdo not trigger CI. The workflow needs a GitHub App token or PAT so backport PRs run checks like any other PR.Effort: medium (workflow plus token setup). Risk: low; a bad backport PR is just a PR you close.
Phase 5: backport status on the master PR
Extend phase 4 with a bot-maintained comment on the master PR: a table with one row per target branch showing the backport PR and its state (open, merged, conflict, pending). A small workflow updates the comment when a backport PR changes state, matching backport PRs to their origin by branch naming convention (
backport/<origin-pr>/<branch>).This phase also adds the backport-debt sweep: a scheduled job that lists every merged master PR whose
backport/<branch>label has no merged backport PR on that branch. The result goes to the existing private Slack channel (and optionally a pinned tracking issue). This closes the gap left by phase 3: a recorded decision that was never executed, an auto-created PR left unmerged, or an ignored conflict comment all show up in the sweep.Effort: medium (two workflows, roughly 100 lines of script each). Risk: none; they only write comments and messages.
Phase 6: dependency updates for release branches
Two options, team decision needed:
Recommendation: the scheduled job. Release branches should only receive security fixes, so a security-only tool fits better than adapting a general update tool. Revisit if the job grows Renovate-shaped features.
Phase 7: review-free clean backports
Goal: a backport that is provably identical to an approved master fix, with green CI, merges without a second human review. This removes the rubber-stamp chore (problem 3), and by removing it, makes the remaining reviews (the
backport-manualones) meaningful again.This is safe only if "provably identical" is machine-verified, never claimed. An attacker (or a mistake) must not be able to get unreviewed code onto a release branch by presenting a PR as a backport of something approved. Conditions, all enforced by a GitHub App:
git patch-idof the backport commit equals the patch-id of the merged, approved master commit. Equal patch-ids mean an identical diff modulo line offsets; different code cannot pass.Residual risk that these checks cannot catch: an identical diff can behave differently on a diverged branch (a renamed function, a changed contract in a caller). Only the branch's test suite guards against that. Therefore auto-merge is enabled per branch in the registry: on for branches that track master closely (V6.2 today), off for diverged branches (V5.4 today), where every backport keeps a human review.
Effort: medium to large (GitHub App with review permission, verification workflow, branch protection changes). Risk: the highest-trust component in this proposal; it should get its own security review before deployment and can be trialed in log-only mode (the App comments "would have auto-approved" without approving).
Out of scope, recorded as open issues
forward-portlabel with the same machinery.Rollout
Phases 1 to 3 in one PR each, mergeable this week; phase 2 (dropping the PR scan) is the quickest win and independent of the rest. Phase 4 next, since it depends on the labels being trusted. Phases 5 and 6 as separate proposals once 1 to 4 have run for a few weeks. Phase 7 last, after the backport App has a track record and after its own security review.