Problem
The canary automation promotes a candidate once it's on <agent>/next, but it never cuts a new version. Today a human runs cut-release.sh <agent> <X.Y.Z> --ref origin/main --channel next --push after a reusable's change merges. That's the last manual seam in an otherwise hands-off pipeline (#1045 armed the promoter; this closes the front end).
Evidence the gap is live: auto-rebase's reusable blob at .github main (ece45480) already differs from what auto-rebase/next points at (27637e50) — un-cut changes are sitting on main.
Goal
When a registered reusable's content on its host main differs from the version currently on next, automatically cut a new immutable <agent>/vX.Y.Z tag at that commit and move <agent>/next onto it — seeding the candidate into the existing soak/promote pipeline. No manual cut-release.sh.
Recommended design — poll-based, on the existing timer (NOT per-merge events)
The soak dwell floors are already 4–12h, so sub-4h latency to enter next buys nothing — and polling on the existing schedule avoids all cross-repo Actions plumbing (the 6 reusables live in .github, but cut-release.sh + canary-rings.json live here in .github-private). Do the detection in .github-private where everything is local, via the App token that already reads every host.
New subcommand canary-rollout.sh autocut [--dry-run], run at the START of the scheduled job (before promote-all). For each registered agent:
- Resolve host
main HEAD: gh api repos/<host>/commits/<default_branch> --jq .sha.
- Reusable blob at main:
gh api repos/<host>/contents/<reusable>?ref=<mainsha> --jq .sha (the registry's .agents[a].reusable path).
- Reusable blob at the current
next candidate commit (same call, ref=<next commit>).
- If the blobs differ (and main HEAD ≠ next commit): compute the next version and cut:
cut-release.sh <agent> <newver> --ref <mainsha> --channel next --push (already cross-repo aware).
- Else no-op.
Version bump
Add a --bump <patch|minor|major> mode to cut-release.sh (or a next-version <agent> <level> helper): read the highest existing <agent>/vMAJOR.MINOR.PATCH on the host, bump. Default patch. Override per agent via a registry knob .agents[a].autocut.bump (patch|minor), and/or the merge commit trailer Release-Bump: minor. Keep v1 minimal: patch default + registry knob.
Arming + safety (mirror #1045)
- Gate behind org variable
CANARY_AUTO_CUT (default off — the single kill-switch). Only run autocut when == 'true'.
- Idempotent: no-op when main's reusable blob == next's (no duplicate cuts);
cut-release.sh already refuses to overwrite an existing immutable vX.Y.Z.
- Only cut from the host's default branch HEAD.
- Best-effort (
|| ::warning), never fails the scheduled run.
--dry-run prints the intended cut without pushing.
- Ordering:
autocut → promote-all → sync-issues in the scheduled job. A freshly cut candidate begins soaking the same tick (dwell measured from the new tag; it will SOAK, not promote, because dwell=0 < floor).
Scope / limitations (note in v1)
- Detection is on the reusable file blob only (the registry
reusable path). If the reusable sources a shared lib that changed but the reusable file didn't, this won't detect it — acceptable for v1; note it. (Could extend to a path-set later.)
- Applies to every registered agent, both hosts (
.github reusables + .github-private dev-lead).
Acceptance criteria
autocut cuts a new vX.Y.Z (patch bump) + moves next when a reusable's main blob differs from its next candidate; cross-repo aware; verified against auto-rebase (should cut v2.1.1 from ece45480).
- Idempotent — a second run with no new change is a clean no-op.
- Gated behind
CANARY_AUTO_CUT (default off); --dry-run supported; best-effort.
- Runs before
promote-all; freshly-cut candidate SOAKs (does not promote) the same tick.
- Bump default patch; registry
autocut.bump override honored.
- bats coverage (version-bump math, blob-diff detection, cut invocation stubbed, dry-run, kill-switch off);
bash -n + shellcheck + YAML clean.
- Docs: note the closed seam in the canary-rollout header + a one-line registry note.
Alternative (documented, not recommended for v1)
Event-based: a push-to-main path-filtered stub in each host repo calls a .github-private reusable that cuts. Sub-4h latency, but needs cross-repo private-Actions access config + per-host stubs. Only worth it if immediate cut is ever required.
Refs
#1045 (armed promoter — this is its front end) · #495 (epic) · #548 (gate) · #959/#992 (cut-release cross-repo + --promote) · #993 (autonomous promoter design).
Problem
The canary automation promotes a candidate once it's on
<agent>/next, but it never cuts a new version. Today a human runscut-release.sh <agent> <X.Y.Z> --ref origin/main --channel next --pushafter a reusable's change merges. That's the last manual seam in an otherwise hands-off pipeline (#1045 armed the promoter; this closes the front end).Evidence the gap is live:
auto-rebase's reusable blob at.githubmain (ece45480) already differs from whatauto-rebase/nextpoints at (27637e50) — un-cut changes are sitting on main.Goal
When a registered reusable's content on its host
maindiffers from the version currently onnext, automatically cut a new immutable<agent>/vX.Y.Ztag at that commit and move<agent>/nextonto it — seeding the candidate into the existing soak/promote pipeline. No manualcut-release.sh.Recommended design — poll-based, on the existing timer (NOT per-merge events)
The soak dwell floors are already 4–12h, so sub-4h latency to enter
nextbuys nothing — and polling on the existing schedule avoids all cross-repo Actions plumbing (the 6 reusables live in.github, butcut-release.sh+canary-rings.jsonlive here in.github-private). Do the detection in.github-privatewhere everything is local, via the App token that already reads every host.New subcommand
canary-rollout.sh autocut [--dry-run], run at the START of the scheduled job (beforepromote-all). For each registered agent:mainHEAD:gh api repos/<host>/commits/<default_branch> --jq .sha.gh api repos/<host>/contents/<reusable>?ref=<mainsha> --jq .sha(the registry's.agents[a].reusablepath).nextcandidate commit (same call,ref=<next commit>).cut-release.sh <agent> <newver> --ref <mainsha> --channel next --push(already cross-repo aware).Version bump
Add a
--bump <patch|minor|major>mode tocut-release.sh(or anext-version <agent> <level>helper): read the highest existing<agent>/vMAJOR.MINOR.PATCHon the host, bump. Default patch. Override per agent via a registry knob.agents[a].autocut.bump(patch|minor), and/or the merge commit trailerRelease-Bump: minor. Keep v1 minimal: patch default + registry knob.Arming + safety (mirror #1045)
CANARY_AUTO_CUT(default off — the single kill-switch). Only run autocut when== 'true'.cut-release.shalready refuses to overwrite an existing immutablevX.Y.Z.|| ::warning), never fails the scheduled run.--dry-runprints the intended cut without pushing.autocut→promote-all→sync-issuesin the scheduled job. A freshly cut candidate begins soaking the same tick (dwell measured from the new tag; it will SOAK, not promote, because dwell=0 < floor).Scope / limitations (note in v1)
reusablepath). If the reusable sources a shared lib that changed but the reusable file didn't, this won't detect it — acceptable for v1; note it. (Could extend to a path-set later.).githubreusables +.github-privatedev-lead).Acceptance criteria
autocutcuts a newvX.Y.Z(patch bump) + movesnextwhen a reusable's main blob differs from itsnextcandidate; cross-repo aware; verified againstauto-rebase(should cutv2.1.1fromece45480).CANARY_AUTO_CUT(default off);--dry-runsupported; best-effort.promote-all; freshly-cut candidate SOAKs (does not promote) the same tick.autocut.bumpoverride honored.bash -n+ shellcheck + YAML clean.Alternative (documented, not recommended for v1)
Event-based: a
push-to-main path-filtered stub in each host repo calls a.github-privatereusable that cuts. Sub-4h latency, but needs cross-repo private-Actions access config + per-host stubs. Only worth it if immediate cut is ever required.Refs
#1045 (armed promoter — this is its front end) · #495 (epic) · #548 (gate) · #959/#992 (cut-release cross-repo +
--promote) · #993 (autonomous promoter design).