ci: standardize bot auto-merge - #98
Conversation
Pairs with the eligibility change that follows. bot-automerge.yml held every semver-major, and because the actions here are pinned to floating major tags Dependabot raises a pull request only when a major moves -- so that hold was a permanent hold on every actions pull request rather than a policy. Arming those needs a soak period to replace the hold, which is what this is. default-days covers every update and not only majors, because this is the one ecosystem whose packages execute with the repository's own token inside jobs holding contents and pull-requests write. When the risk is a compromised publisher rather than a changed interface, a patch is not safer than a major. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings this file onto the shape the sibling repositories share. The parts are interdependent enough that splitting them would leave a tree that does not work, so they land together and are listed here instead. Removes the check-polling step. `gh pr checks --required --watch` races check registration: it reports "no required checks reported" before CI has posted and fails the job on a pull request that was never unhealthy. Native auto-merge already gates on the ruleset server-side, and main-protect requires both Run CI legs, so the wait was redundant as well as wrong. Narrows permissions to what the calls actually use. packages: read was never read by anything here -- this repository is shell scripts and BATS. Declaring the block sets every unlisted scope to none, so a scope kept for symmetry grants real authority for no reader. Replaces the github.actor guard with pull_request.user.login. github.actor is whoever triggered this event, not whoever opened the pull request, so any later push by another identity made the synchronize event's actor that identity and the job skipped -- leaving only the opened event as a chance to act, with no retry. Adds a commit-provenance check, which is the guard the author test cannot be: .author.login resolves from the commit author email, so anyone who can push here can set it to the bot's noreply address. Requiring GitHub's own signature, whose payload names committer GitHub <noreply@github.com>, cannot be forged without GitHub's key. Merges are exempt from the author check but not the signature one, which is what admits a required Update branch merge without admitting a local evil merge. Arms github-actions majors, per the cooldown added in the previous commit, and holds every other ecosystem. Nothing here publishes from a workflow no pull request runs, so there is no per-workflow hold list of the kind the publishing siblings carry. Re-reads the kill-switch label live before arming, because a label applied while the run was in flight is absent from the event payload. Also replaces the ::info:: lines, which are not a workflow command GitHub recognises and so printed literally rather than annotating anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The kill switch was a condition rather than a control. bot-automerge.yml tested the label before arming and never again, so a label applied afterwards stopped the workflow re-running while GitHub went on merging the moment requirements passed. Once auto-merge is enabled it lives server-side; something has to actively turn it off, and nothing did. Fails closed on purpose. The tempting shape -- read the label back, disable only when the answer is true -- inverts the guard, because a swallowed read then reports the label absent and leaves the pull request armed. So only a positively confirmed "off" exits early; anything else attempts the disable, retries, and verifies. A final read after the loop stops a disarm that worked on the last attempt from reporting failure. No branch filter and no fork guard: a suppressor has to be at least as broad as the thing it suppresses, and disarming a pull request that was never armed is a no-op, so running when it need not is the cheap failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR Summary by QodoCI: standardize bot auto-merge with disarm + provenance guardrails
AI Description
Diagram
High-Level Assessment
Files changed (3)
|
There was a problem hiding this comment.
Pull request overview
This PR standardizes the bot auto-merge setup to match sibling repositories, fixes the prior “required checks” polling race, makes the do not auto-merge kill switch effective by adding an explicit disarm workflow, and adjusts Dependabot policy to use cooldowns instead of a blanket major hold.
Changes:
- Rebuilds
bot-automerge.ymlto rely on native GitHub auto-merge gating (no check polling), add commit provenance verification, and re-check the kill switch right before arming. - Adds
bot-automerge-disarm.ymlto actively disable auto-merge when the kill-switch label is applied (fail-closed loop). - Updates
.github/dependabot.ymlto apply cooldowns for GitHub Actions updates (7 days default; 30 days for semver majors).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| .github/dependabot.yml | Adds cooldown settings to replace a blanket major hold and provide a soak period for Actions updates. |
| .github/workflows/bot-automerge.yml | Reworks arming logic (no polling), adds provenance checks, and strengthens kill-switch handling before approval/arming. |
| .github/workflows/bot-automerge-disarm.yml | New workflow to disable auto-merge when the kill-switch label is applied, using a retry/verify loop. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Code Review by Qodo
1.
|
A review flagged the provenance gate as spoofable and was half right. It classified a commit as GitHub-created from `verified == true` plus a payload line naming `committer GitHub <noreply@github.com>` -- and committer name and email are git metadata the pusher controls. The claimed bypass, a locally signed commit carrying that committer line, most likely fails anyway, because GitHub declines to mark a commit verified when the committer email is not a verified email of the signing key's owner. But that is a premise the reader cannot check from the file, and a security guard should not rest on one. So the gate now requires `.committer.login == "web-flow"`, which GitHub resolves from the signing key rather than from metadata, plus `reason == "valid"`, and keeps the payload test as an independent second condition. Measured across four repositories before changing anything: Dependabot and pre-commit-ci commits both report web-flow with verified true and reason valid, while a human's own commits report that human's login -- so this rejects exactly what it should and admits both bots. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Rebuilds
bot-automerge.ymlon the shape now shared with the sibling repositories, and adds the disarm workflow that makes arming revocable. Two defects motivate it. The check-polling step could not succeed on a healthy pull request —gh pr checks --required --watchraces check registration and reports "no required checks reported" before CI has posted — and thedo not auto-mergelabel the workflow tested did not exist in this repository at all, so the kill switch was a condition satisfied by a label nobody could apply. Both are fixed here; the label has been created separately.The kill switch needed more than a label. Even applied, it only stopped the job from arming again: once auto-merge is enabled it lives server-side, so GitHub goes on merging the moment requirements pass. The new
bot-automerge-disarm.ymlis the half that actively turns it off, and it fails closed — the tempting shape of "read the label back, disable only when the answer is true" inverts the guard, because a swallowed read reports the label absent and leaves the pull request armed.dependabot.ymlgains a cooldown and the majors policy changes with it, as a matched pair rather than two settings. The actions here are pinned to floating major tags, so Dependabot raises an actions pull request only when a major moves — which made the blanket major hold a permanent hold on every actions pull request there would ever be. Arming those needs a soak period instead, so the cooldown is 7 days on every actions update and 30 on majors. The wider net is earned because this is the one ecosystem whose packages execute with this repository's own token inside jobs holding write scopes; when the risk is a compromised publisher rather than a changed interface, a patch is not safer than a major.The provenance check is the one addition about safety rather than throughput.
.author.loginresolves from the commit's author email, so anyone who can push here can set it to Dependabot's noreply address and be read as the bot. Requiring that every commit carry GitHub's own signature — whose payload namescommitter GitHub <noreply@github.com>— cannot be forged without GitHub's private key, and the payload sits inside the signature so it cannot be edited either. Merges are exempt from the author check but not the signature check, which admits a required Update branch merge without admitting a locally crafted evil merge.Test plan
pre-commit run --filespasses on all three changed files —Validate Dependabot Config (v2),Validate GitHub Workflows,Lint GitHub Actions workflow files,yamllint,prettier,typos,codespellactionlintclean across the whole workflows directory, which covers the embeddedshellcheckpass on everyrun:blockyamllintcaught a 110-character line on the first pass; shortened and re-run cleando not auto-mergelabel now exists here — confirmed viarepos/michen00/bin/labels/do%20not%20auto-mergetitle-max-length=50Reviewer guide
Decide eligibilitystep'ssemver-majorbranch, theVerify every commit was created by GitHubstep, and the fail-closed loop inbot-automerge-disarm.yml.github-actionsmajors on a 7-day cooldown is the trade you want here. The alternative that keeps a hold meaningful is a per-workflow list — hold when the diff reaches a workflow no pull request runs — which the publishing siblings do carry. This repository publishes nothing: every workflow exceptchangelog-autoupdate.ymlandpre-commit-autoupdate.ymlruns on pull requests, and a break in either of those is a scheduled or dispatched job that fails visibly. So a list here would be maintenance with nothing behind it, and a stale list fails by arming. Worth checking rather than accepting.APP_IDandAPP_PRIVATE_KEYare not set in this repository. Nothing breaks — arming falls back to the default token, which is what happens today — but bot merges keep firing no downstreampushorpull_request_target: closedevents. If those settings are ever added, the private key has to go into both the Actions and Dependabot secret stores, or the App path stays inert on exactly the Dependabot pull requests it was added for. The comment in the file says so; this is the sibling repository's arrangement and the reason both use the same two names.Notes
main-protecthere also carries a Copilot review rule, andrequire-maintainer-reviewasks for an approving review on top. Nothing in this workflow waits on either — that is the point of letting native auto-merge do the waiting server-side. The approve step is therefore not optional in this repository, unlike in the siblings that require no review: without it auto-merge would arm and then wait forever on a requirement no bot pull request can satisfy for itself.The three commits are ordered so each leaves a working tree: the cooldown lands before the policy that depends on it. The workflow rewrite is one commit rather than four because its parts are interdependent — removing the polling step requires native auto-merge to be the gate, and the majors policy lives inside an eligibility step the provenance check gates — so a split would commit a tree that does not work. The commit message enumerates the parts instead.
Files touched
Status legend:
+added,~modified,-removed,→renamed,~/→renamed and modified.~~+