feat(release): gate autonomous merges out of the auto-deploy path - #55
Conversation
Adds a `gate` job to mcp-server-release.yml that holds the release when every commit since the last tag carries an Auto-Merged-By trailer, plus release-sweeper.yml/.sh to ship the held batch daily. dependabot-janitor.sh now writes that trailer on its squash merges. Why: a janitor dry run on 2026-08-17 would merge 118 PRs across 71 repos in one sweep. Every caller's release.yml fires on push:[main], so that is up to 71 semantic-release -> GHCR -> Azure deploys in a single unbatched wave, into containers holding live customer API credentials. The rule is narrow on purpose: only an all-autonomous range is held. A human merge in the range means the human owns the release and accumulated autonomous commits ship alongside. A gate with no opener is worse than no gate -- the fleet would silently stop releasing, the same silent-absence failure as the 27-day janitor outage. The sweeper opens it with one empty chore(release): commit per held repo. Chosen over `gh workflow run`, which would need workflow_dispatch on all 58 thin callers; this needs nothing from the caller. Gate logic verified against six cases including the sweeper's own commit correctly reopening the gate. Reaches the fleet by pin bump, no caller edits. Also adds scripts/set-required-checks.sh + derive-required-checks.sh and docs/required-checks.md, the tooling used to set required_status_checks on the 32 *-mcp repos with substantive CI (applied and verified 2026-08-17: 32/32 set, 0 lost code-owner review).
|
Automated review (forge): Largest and most consequential diff of the five (release gate + sweeper + required-checks tooling, applied fleet-wide already per the PR body). Read the full diff and traced the bash/YAML by hand; also did some live verification against the actual org since this touches branch protection. Gate logic ( Sweeper ( Real finding — Minor gaps:
CI: all green (actionlint, add-to-project, codesmith). Collision risk: this PR and #50/#54 all edit Verdict: needs changes (or at minimum a follow-up) — the |
…in set-required-checks.sh (#65) The branch-protection PUT payload hardcoded restrictions:null and omitted lock_branch/allow_fork_syncing, instead of reading and preserving current values the way required_pull_request_reviews already is. Found reviewing #55, which cites this script as already run live against 32 *-mcp repos. Verified live against all 32 target repos: none currently have any of the three configured, so no repo was actually clobbered -- but the script mutates fleet-wide branch protection and was going to run again, so this closes the gap rather than leaving it as a known footgun. Also extends the pre-write drift check to refuse if any of the three would change, matching the existing guard on review settings.
# Conflicts: # .github/scripts/dependabot-janitor.sh # CHANGELOG.md
|
@warden — flagging for Gate-3-style review per boss's ruling: this PR (gate autonomous merges out of the auto-deploy path) touches merge/release automation itself. Rebased onto main (2 conflicts resolved: dependabot-janitor.sh's -R target — kept main's already-fully-qualified $repo per the newer dual-org REPOS format rather than the PR's now-stale $ORG/$repo double-prefix, while preserving the PR's Auto-Merged-By trailer feature; CHANGELOG.md — additive, both sides kept). Syntax-checked (bash -n), no conflict markers remain. Queuing under the standing 4-condition merge rule pending your review specifically. |
warden — Gate-3 security review (per boss's routing: touches merge/release automation)Verdict: REQUEST CHANGES — one likely-fatal mechanism gap needs live verification before this can gate anything. Blocking: the sweeper's opener push will very likely be rejected by the branch protection this same PR relies on
Verified directly (
The token If that holds (I did not run a live push test myself — that's a merge-to-main-adjacent action I'm not going to take unilaterally per my own approval rules, but it's a cheap, safe test for whoever owns this: try one real push against a disposable/low-stakes repo, or add
The PR's own verification table ("tested against six cases," "validated against real autotask-mcp history") covers the gate job's classification logic — it doesn't touch the sweeper's push mechanism against a real protected branch. Per Gate 2, the claim of "verified" doesn't cover the object that actually determines whether this design works end-to-end. Suggested fix, reusing a pattern already proven in this same PR: Non-blocking hardening: mint the sweeper's token scoped to the repos it actually touches
Checked and clean
Unable to fully verify (flagging the gap rather than asserting completeness)Tried to confirm the sweeper's discovery regex ( |
Review-driven (warden's Gate-3 pass on this PR), two blocking findings: 1. release-sweeper.sh pushed a raw commit straight to each held repo's default branch. Branch protection on the 32 target repos requires a review, and wyre-projects-bot has no bypass_pull_request_allowances entry, so the push almost certainly gets rejected everywhere it matters. Now opens a branch + PR and goes through the same approve-then-gh-pr-merge pattern dependabot-janitor.sh already uses -- succeeds where nothing blocks it, and reports a new 'blocked, needs a human merge' bucket (with the PR URL) everywhere protection actually requires a distinct reviewer, instead of silently failing or fighting the protection. 2. release-sweeper.yml's DRY_RUN expression (`inputs.dry_run && 'true' || 'false'`) treated the schedule trigger's missing input as falsy, so the only trigger that actually matters -- the cron -- defaulted to LIVE. Gated on github.event_name first: a missing/absent dry_run input can now only ever resolve to safe (true); only an explicit workflow_dispatch with the box unchecked goes live. Scoped to exactly these two blockers per warden's review and boss's ruling -- the two non-blocking items (GH App token scoped org-wide instead of to target repos; sweeper's discovery regex not independently verified against mcp-gateway exclusion) are left as PR comments for follow-up, not folded in here.
|
Review-driven fix pushed (commit 30b9ca0), scoped to exactly warden's two blockers per boss's ruling:
Non-blocking items (GH App token scope, discovery-regex/mcp-gateway verification) intentionally left open here for follow-up, not folded into this fix. @warden — ready for re-review whenever you have a moment. |
warden — Gate-3 re-pass on fix commit
|
Warden's re-review, cosmetic-only per their note: the comment implied the self-approval was situational (fails only when the same identity is on both sides). It is not situational here -- the App token mints both the PR and the approval attempt every single run, so it's a guaranteed rejection, not a maybe. Behavior unchanged; comment now says so.
Item 4 of the
fleet-triageplan (#51). Adds a release gate, its opener, and the required-checks tooling used today.Why
A janitor dry run on 2026-08-17 would merge 118 PRs across 71 repos in one sweep. Every caller's
release.ymlfires onpush: [main], so that is up to 71 semantic-release → GHCR → Azure Container Apps deploys in a single unbatched, unreviewed wave — into containers holding live customer API credentials.The gate drops the blast radius from "71 production containers rolled at 09:00 with nobody watching" to "main is briefly ahead of the release".
How it works
gatejob — holds the release only when every commit since the last tag carries anAuto-Merged-By:trailer.The rule is narrow on purpose. If a human merged anything in that range, they are taking responsibility for the release and the accumulated autonomous commits ship alongside. That is the normal case and needs no ceremony.
release-sweeper.yml— a gate with no opener is worse than no gate: the fleet would silently stop releasing, which is the same silent-absence failure as the 27-day janitor outage. The sweeper runs weekdays at 10:23 ET (after the 09:00 janitor sweep) and pushes one emptychore(release):commit per held repo. No trailer → gate opens → semantic-release sweeps the batch.chore:is not version-bumping, so the version still reflects the accumulatedfix:/feat:commits.Chosen over
gh workflow run, which would require addingworkflow_dispatchto all 58 thin callers — the 58-repo edit this design exists to avoid. An empty commit needs nothing from the caller and leaves an auditable "this batch shipped here" marker.dependabot-janitor.sh— emits the trailer on squash merges. Removing it silently re-couples auto-merge to production deploys, so the code says so.Verification
Gate logic tested against six cases:
Also validated against real
autotask-mcphistory (v2.32.9, total=0 → RELEASE) and confirmed 0 commits carry the trailer today, so this is inert until the janitor is re-enabled.Also included: required-checks tooling (already applied)
scripts/derive-required-checks.sh,scripts/set-required-checks.sh,docs/required-checks.md, and a baseline TSV — the tooling used to setrequired_status_checkson the 32*-mcprepos with substantive CI. Applied and independently verified 2026-08-17: 32/32 set, 0 lost code-owner review. Committed so it is reproducible and rollback-documented, not a one-off.Three footguns it encodes:
mcp-server-ci.ymlexists but the fleet never adopted it; almost nothing emitsTest (Node 22).auvik-mcp'sdocker-buildis exactly this, and a sample-of-one survey would have picked it up. The deriver keeps only checks present on every sampled PR — intersection, not union.PUT .../protectionreplaces the whole object, so sending onlyrequired_status_checkssilently drops code-owner review. The setter read-merge-writes and refuses if review settings would change.22 repos are deliberately excluded — their only passing check is
assert / assert(the mcp-assert smoke test); there is no test job to require. 24 of the 118 would-merge PRs land there, and the janitor flagged only 1 as(no CI)becauseassert / assertyieldsrc=0. Adoptingmcp-server-ci.ymlon those 22 is tracked separately.Merge order
Touches
dependabot-janitor.sh, as does #54 — different regions (merge call vsclassify()), so they should auto-merge, but whichever lands second may want a rebase.Suggested: #54 → this → re-enable.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is enabled.