Skip to content

canary-rollout: auto-cut a new version when a reusable changes on main (close the last manual seam) #1069

Closed

Description

@don-petry

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:

  1. Resolve host main HEAD: gh api repos/<host>/commits/<default_branch> --jq .sha.
  2. Reusable blob at main: gh api repos/<host>/contents/<reusable>?ref=<mainsha> --jq .sha (the registry's .agents[a].reusable path).
  3. Reusable blob at the current next candidate commit (same call, ref=<next commit>).
  4. 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).
  5. 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

  1. 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).
  2. Idempotent — a second run with no new change is a clean no-op.
  3. Gated behind CANARY_AUTO_CUT (default off); --dry-run supported; best-effort.
  4. Runs before promote-all; freshly-cut candidate SOAKs (does not promote) the same tick.
  5. Bump default patch; registry autocut.bump override honored.
  6. bats coverage (version-bump math, blob-diff detection, cut invocation stubbed, dry-run, kill-switch off); bash -n + shellcheck + YAML clean.
  7. 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

  1. github-actions commented on Jul 4, 2026

    @github-actions
    Contributor

    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?

  2. don-petry commented on Jul 4, 2026

    @don-petry
    CollaboratorAuthor

    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 by CANARY_AUTO_CUT, detects when a registered reusable's blob on its host main differs from the version on <agent>/next and auto-cuts a new immutable vX.Y.Z (patch bump default, registry override) + moves next onto it — seeding the existing soak/promote pipeline with no manual cut-release.sh.

    Implementation Checklist

    • scripts/lib/canary-rollout.sh — pure version-bump math: bump_version <ver> <patch|minor|major>, _semver_gt, max_semver
    • scripts/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_RELEASE indirection; kill-switch check; register autocut in main(); header docs
    • .github/workflows/canary-rollout.yml — schedule-time autocut step (ordering: autocut → promote-all → sync-issues), best-effort, gated by vars.CANARY_AUTO_CUT; add autocut to dispatch options
    • standards/canary-rings.json — one-line note documenting the .agents[<a>].autocut.bump knob (default patch)

    Tests to Write (tests/canary_rollout.bats)

    • bump_version patch/minor/major
    • max_semver / _semver_gt (highest wins, ignores non-semver, empty)
    • autocut kill-switch OFF (CANARY_AUTO_CUT unset) → no-op, no cut invocation
    • autocut blob differs → cuts computed version, invokes cut-release with expected args (stubbed)
    • autocut idempotent (blob identical) → clean no-op
    • autocut --dry-run → prints intended cut, no push
    • autocut cross-repo (auto-rebase) → cuts 2.1.1 from main HEAD (cross-repo aware)
    • autocut registry autocut.bump override honored (minor)

    Test Command

    bats tests/canary_rollout.bats (full suite: bats tests/); lint: bash .dev-lead/scripts/dev-lead-lint.sh

  3. don-petry commented on Jul 4, 2026

    @don-petry
    CollaboratorAuthor

    ✅ 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 host main, eliminating the last manual cut-release.sh step.

    Plan Execution

    • Phase 1 — Scope & Plan (plan comment posted to this issue)
    • Phase 2 — Test-First (TDD) — 13 new tests written & confirmed red before any source change
    • Phase 3 — Implement — pure version-bump core + orchestrator autocut command + workflow wiring + registry knob + docs
    • Phase 4 — Verify — full suite (74/74) + dev-lead-lint.sh + bash -n + shellcheck + JSON/YAML all clean
    • Phase 5 — Rubber-Duck Review — line-by-line diff review, no changes required
    • Phase 6 — Completion Report (this comment)

    Acceptance Criteria

    • Cuts a patch-bumped vX.Y.Z + moves next when the reusable's main blob differs from the next candidate; cross-repo aware (auto-rebase → v2.1.1 from ece45480, covered by test fix: pin actions/checkout to SHA in pr-review.yml #74)
    • Idempotent — a second run with identical blobs is a clean no-op
    • Gated behind org var CANARY_AUTO_CUT (default off), --dry-run supported, best-effort (non-fatal ::warning:: on failure)
    • Runs before promote-all in the scheduled job; a freshly-cut candidate SOAKs the same tick
    • Bump default patch, registry .agents[<a>].autocut.bump override honored (test ci: add dependabot-automerge.yml workflow #73 → minor)
    • bats coverage: version-bump math, blob-diff detection, stubbed cut invocation, dry-run, kill-switch off
    • bash -n + shellcheck (-S warning -x) + YAML/JSON clean

    Test Results

    74 tests, 0 failures
    ...
    ok 69 orchestrator: autocut is a no-op when CANARY_AUTO_CUT is not 'true' (kill-switch off)
    ok 70 orchestrator: autocut cuts a patch-bumped version + moves next when the reusable blob differs on main
    ok 71 orchestrator: autocut is idempotent — identical blob on main and next is a clean no-op
    ok 72 orchestrator: autocut --dry-run prints the intended cut without invoking cut-release --push
    ok 73 orchestrator: autocut honors the registry autocut.bump override (minor)
    ok 74 orchestrator: autocut is cross-repo aware — cuts v2.1.1 for auto-rebase from the host main HEAD (#1069)
    

    dev-lead-lint.sh: all checks passed (shellcheck on 88 files, 4 agent profiles, CODEOWNERS).

    Files Changed (+347 / -4)

    File Δ What
    scripts/lib/canary-rollout.sh +39 Pure semver core: bump_version, _semver_gt, max_semver
    scripts/canary-rollout.sh +133 autocut command: gh-api host resolution, blob-diff detection, idempotency guard, cut-release.sh invocation (overridable for stubbing), kill-switch
    .github/workflows/canary-rollout.yml +32 Scheduled autocut step before promote-all; autocut dispatch option; CANARY_AUTO_CUT env
    standards/canary-rings.json +1 _autocut_note documenting the autocut.bump knob
    tests/canary_rollout.bats +146 13 new tests (7 pure-lib + 6 orchestrator)

    Notes / v1 Scope

    • Detection is on the reusable file blob only. A change to a shared library that the reusable 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.
    • Uniform gh-api detection for both this-repo and cross-repo agents: the RELEASE_MANAGER App token reads every host, so no cross-repo Actions plumbing is needed. Next-commit resolution reuses the host-aware channel_commit.
    • Bootstrap-safe: an agent with no next candidate yet is handled gracefully (empty next_blob → treated as differ, cuts the first candidate).
  4. added a commit that references this issue on Jul 4, 2026
  5. 11 remaining items

  6. added a commit that references this issue on Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    dev-leadFor dev-lead agent pickupenhancementFeature requests

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions