Repository navigation
canary-rollout: auto-cut a new version when a reusable changes on main (close the last manual seam) #1069
Description
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
mainHEAD: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].reusablepath). - Reusable blob at the current
nextcandidate 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.shalready refuses to overwrite an existing immutablevX.Y.Z. - Only cut from the host's default branch HEAD.
- Best-effort (
|| ::warning), never fails the scheduled run. --dry-runprints the intended cut without pushing.- Ordering:
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)
- Detection is on the reusable file blob only (the registry
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.) - Applies to every registered agent, both hosts (
.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).- Idempotent — a second run with no new change is a clean no-op.
- Gated behind
CANARY_AUTO_CUT(default off);--dry-runsupported; best-effort. - Runs before
promote-all; freshly-cut candidate SOAKs (does not promote) the same tick. - Bump default patch; registry
autocut.bumpoverride 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).
Activity
github-actions commentedon Jul 4, 2026
Thanks for this thorough write-up — the design is well thought out and the acceptance criteria make it easy to scope the work. One question to help prioritize: is there a specific deploy window or milestone (e.g., tied to #495 or an upcoming release cadence) driving the timeline here, or is this best-effort whenever capacity allows?
don-petry commentedon Jul 4, 2026
Dev-Lead Implementation Plan
Issue: #1069 — canary-rollout: auto-cut a new version when a reusable changes on main (close the last manual seam)
Scope
Add a poll-based
canary-rollout.sh autocut [--dry-run]subcommand that, gated byCANARY_AUTO_CUT, detects when a registered reusable's blob on its hostmaindiffers from the version on<agent>/nextand auto-cuts a new immutablevX.Y.Z(patch bump default, registry override) + movesnextonto it — seeding the existing soak/promote pipeline with no manualcut-release.sh.Implementation Checklist
scripts/lib/canary-rollout.sh— pure version-bump math:bump_version <ver> <patch|minor|major>,_semver_gt,max_semverscripts/canary-rollout.sh—cmd_autocut+_autocut_agent, host-aware detection helpers (_gh_default_branch,_gh_head_sha,_gh_blob_sha,_host_release_versions,_next_release_version,_autocut_bump);CUT_RELEASEindirection; kill-switch check; registerautocutinmain(); header docs.github/workflows/canary-rollout.yml— schedule-time autocut step (ordering: autocut → promote-all → sync-issues), best-effort, gated byvars.CANARY_AUTO_CUT; addautocutto dispatch optionsstandards/canary-rings.json— one-line note documenting the.agents[<a>].autocut.bumpknob (default patch)Tests to Write (tests/canary_rollout.bats)
bump_versionpatch/minor/majormax_semver/_semver_gt(highest wins, ignores non-semver, empty)CANARY_AUTO_CUTunset) → no-op, no cut invocation--dry-run→ prints intended cut, no push2.1.1from main HEAD (cross-repo aware)autocut.bumpoverride honored (minor)Test Command
bats tests/canary_rollout.bats(full suite:bats tests/); lint:bash .dev-lead/scripts/dev-lead-lint.shdon-petry commentedon Jul 4, 2026
✅ Completion Report — Issue #1069: auto-cut on reusable change (close the last manual seam)
Implemented
canary-rollout.sh autocut [--dry-run]— a front-end stage that seeds the existing soak/promote pipeline the moment a reusable changes on hostmain, eliminating the last manualcut-release.shstep.Plan Execution
autocutcommand + workflow wiring + registry knob + docsdev-lead-lint.sh+bash -n+ shellcheck + JSON/YAML all cleanAcceptance Criteria
vX.Y.Z+ movesnextwhen the reusable'smainblob differs from thenextcandidate; cross-repo aware (auto-rebase → v2.1.1 fromece45480, covered by test fix: pin actions/checkout to SHA in pr-review.yml #74)CANARY_AUTO_CUT(default off),--dry-runsupported, best-effort (non-fatal::warning::on failure)promote-allin the scheduled job; a freshly-cut candidate SOAKs the same tick.agents[<a>].autocut.bumpoverride honored (test ci: add dependabot-automerge.yml workflow #73 → minor)bash -n+ shellcheck (-S warning -x) + YAML/JSON cleanTest Results
dev-lead-lint.sh: all checks passed (shellcheck on 88 files, 4 agent profiles, CODEOWNERS).Files Changed (+347 / -4)
scripts/lib/canary-rollout.shbump_version,_semver_gt,max_semverscripts/canary-rollout.shautocutcommand: gh-api host resolution, blob-diff detection, idempotency guard,cut-release.shinvocation (overridable for stubbing), kill-switch.github/workflows/canary-rollout.ymlautocutdispatch option;CANARY_AUTO_CUTenvstandards/canary-rings.json_autocut_notedocumenting theautocut.bumpknobtests/canary_rollout.batsNotes / v1 Scope
sources — without the reusable file itself changing — is not detected in v1. This is the intended MVP boundary; deeper transitive-dependency detection is a follow-up.channel_commit.nextcandidate yet is handled gracefully (emptynext_blob→ treated as differ, cuts the first candidate).feat: implement issue #1069 — canary-rollout: auto-cut a new version …
fix(cut-release): set a git identity before the this-repo annotated-t…
feat: implement issue #1069 — canary-rollout: auto-cut a new version …
fix(cut-release): set a git identity before the this-repo annotated-t…
feat: implement issue #1069 — canary-rollout: auto-cut a new version …
feat: implement issue #1069 — canary-rollout: auto-cut a new version …
fix(cut-release): set a git identity before the this-repo annotated-t…
feat: implement issue #1069 — canary-rollout: auto-cut a new version …
fix(cut-release): set a git identity before the this-repo annotated-t…
11 remaining items
feat: implement issue #1069 — canary-rollout: auto-cut a new version …
feat: implement issue #1069 — canary-rollout: auto-cut a new version …
fix(cut-release): set a git identity before the this-repo annotated-t…
feat: implement issue #1069 — canary-rollout: auto-cut a new version …
fix(cut-release): set a git identity before the this-repo annotated-t…
feat: implement issue #1069 — canary-rollout: auto-cut a new version …
feat: implement issue #1069 — canary-rollout: auto-cut a new version …
fix(cut-release): set a git identity before the this-repo annotated-t…
feat: implement issue #1069 — canary-rollout: auto-cut a new version …
fix(cut-release): set a git identity before the this-repo annotated-t…
feat: implement issue #1069 — canary-rollout: auto-cut a new version …
fix(cut-release): set a git identity before the this-repo annotated-t…
feat: implement issue #1069 — canary-rollout: auto-cut a new version …
fix(cut-release): set a git identity before the this-repo annotated-t…
feat: implement issue #1069 — canary-rollout: auto-cut a new version …