Repository navigation
fix(ci): make Renovate auto-merge actually fire - #126
Merged
Merged
Conversation
`automerge: true` has never merged a PR since it was added in #116. #121 sat open, green and CLEAN for 3.5 weeks; #124 was on the same path. Root cause is a race created by `platformAutomerge: false`. That setting means only a Renovate run can merge, and a run merges when it observes an already-green branch. But @anthropic-ai/claude-code ships ~2 releases/day while Renovate runs every 2-9 days, so every run found a newer version, force-pushed the branch (resetting CI to pending) and ended seconds later. The last run is typical: pushed at 19:56:39, run ended 19:56:45, first check went green at 19:56:53, last at 19:59:40 -- nobody was watching. The run that could merge is always the run that just invalidated CI. Note this is not fixable with `minimumReleaseAge`: at any threshold there are still newly-eligible versions by the next run, so the force-push repeats. Switch to `platformAutomerge: true` so GitHub's native auto-merge merges on green with no Renovate run involved. This is also the freshest option -- no version-age delay at all. Native auto-merge needs something to wait for, i.e. branch protection with a required check, and a required check that never runs blocks a PR forever. CI is currently path-filtered at the `on:` level, so a docs-only PR (#123 touches only README.md and docs/*.md) triggers no CI at all and would deadlock. So drop the paths filter and add one `CI complete` job aggregating the others, passing on success-or-skipped so path-filtered builds still don't block. Per-image builds are still gated by detect-changes; the always-on jobs are lint-only. Verified with actionlint (exit 0, no findings).
…rd the gate
Review follow-ups on this branch.
platformAutomerge:true is Renovate's own default (renovate-schema.json:
platformAutomerge.default = true), so the explicit setting was noise. Deleted
it and kept only the part of the comment that is still load-bearing: what the
config depends on being configured on the GitHub side.
These bumps merge unreviewed and publish to ghcr.io, so a compromised upstream
release would reach the published images with no human in the loop. Added
minimumReleaseAge: '3 days' as a soak period, with a second packageRule
clearing it for @anthropic-ai/claude-code, which is tracked at latest on
purpose. internalChecksFilter defaults to 'strict', so a too-young version is
never offered and the group PR simply carries whichever tools are eligible.
ci-complete is about to become the only required check on master, gating
unattended merges, so two hardening changes:
- A new job added to this workflow but omitted from `needs` would fail while
the gate stayed green. The first step now derives the job list from the
workflow file with yq and fails if `needs` has drifted.
- The failure message named a bare result ('failure') with no job attached.
Iterating toJSON(needs) instead of join(needs.*.result) keeps the job ids,
so the error now says which job failed and how.
Also recorded why this job uses always() rather than !cancelled(): GitHub
counts a skipped required check as passing, so !cancelled() would turn a
cancelled run into a green gate.
Verified: actionlint exit 0; renovate-config-validator "Config validated
successfully"; both jq filters and the yq job-list extraction exercised
locally against success/skipped/failure/cancelled fixtures.
gatezh
added a commit
that referenced
this pull request
Sep 9, 2026
Resolves the renovate.json5 conflict: master (#126) rewrote the "devcontainer agent tools" rationale to explain why platformAutomerge must stay at its default and added a 3-day minimumReleaseAge soak with a claude-code exemption. Keep master's version verbatim and carry over only this branch's "four tools" -> "five tools" count. Effect on happy: it inherits the 3-day soak, so a happy release is not adopted until it has been public for three days.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Warning
This PR changes nothing on its own. It only becomes effective once
CI completeis a required check on master (step 2 below). Until thenmasterhas no protection at all, every Renovate PR isCLEANfrom the moment it opens, GitHub never offers native auto-merge, and Renovate silently falls back to the same broken path this PR is fixing. Merging this and stopping is a no-op.automerge: truehas never merged a PR since it was added in #116. #121 sat open, green andCLEANfor 3.5 weeks before being merged by hand; #124 is on the same path right now.Root cause
Not a permissions or config-matching problem — everything on the GitHub side checked out clean:
groupName, soautomerge: truewas in effectmergeable=MERGEABLE,mergeStateStatus=CLEANIt's a race created by
platformAutomerge: false. That setting means only a Renovate run can merge, and a run merges when it observes an already-green branch. But@anthropic-ai/claude-codeships ~2 releases/day (2.1.216 → 2.1.263= 47 releases in 23 days) while Renovate runs every 2–9 days — 6 force-pushes over the life of #121. So every run found a newer version, force-pushed the branch, reset CI to pending, and ended.The last run is typical:
The run that could merge is always the run that just invalidated CI.
minimumReleaseAgedoes not fix this at any threshold — by the next run there are always newly-eligible versions, so the force-push repeats. (It is added below anyway, for an unrelated reason.)The fix
Let GitHub's native auto-merge do it, so the merge happens on green with no Renovate run involved. It's immune to the race, and it's also the freshest option — zero version-age delay.
That means simply deleting
platformAutomerge: false:trueis Renovate's own default (renovate-schema.json→platformAutomerge.default = true), so the explicit setting was the whole bug.Native auto-merge needs something to wait for (a required check), and a required check that never runs blocks a PR forever. CI was path-filtered at the
on:level, so a docs-only PR triggered no CI at all — #123 touches onlyREADME.mdanddocs/*.mdand would deadlock permanently. Hence:paths:filter fromon: pull_request.CI completejob aggregating the others, passing on success-or-skipped so path-filtered image builds still don't block.Per-image builds remain gated by
detect-changes; the always-on jobs are lint-only and take seconds.Hardening (second commit)
Supply chain. These bumps merge unreviewed and the merge publishes straight to
ghcr.iowithpackages: write. CI only runs--versionsmoke checks, which would not notice a malicious release. AddedminimumReleaseAge: '3 days'as a soak period, with a secondpackageRuleclearing it back tonullfor@anthropic-ai/claude-code, which is tracked at latest on purpose.internalChecksFilterdefaults to'strict', so a too-young version is simply never offered — the group PR carries whichever tools are currently eligible, and claude-code is never delayed.The gate itself.
ci-completeis about to be the only thing standing between a red job and an unattended merge, so:needswould fail while the gate stayed green. The first step now derives the job list from the workflow file withyqand fails on drift, instead of relying on anyone remembering.join(needs.*.result, ' ')discarded the job names, so a failure only ever said'failure'. IteratingtoJSON(needs)keeps the ids — the error now names the job and its result.always()and not!cancelled(): GitHub counts a skipped required check as passing, so!cancelled()would let a cancelled run report a green gate.Verification
actionlint— exit 0, zero findings.renovate-config-validator(official image, validated as repo config) — "Config validated successfully".jqfilters and theyqjob-list extraction exercised locally againstsuccess/skipped/failure/cancelledfixtures, including a simulated unwatched job.CI completeshould appear as a check and pass here before it's ever made required.Merge order matters
CI completecannot be a required check until it exists on master.ci-completeexisted, so their head SHAs produce noCI completecheck at all — once the ruleset is active they can never satisfy it and would deadlock exactly the way docs: add Docker disk usage runbook and maintenance cheatsheet #123 would have. Merge (or rebase) them before step 3, not after.CI complete, strict policy off, and no required reviews (those would block Renovate, which cannot approve its own PR).Allow auto-mergeis already enabled on the repo (it wasfalse, which would have silently defeated this change).Everything opened after that auto-merges on green.
Why a ruleset, and why it needs a bypass actor
update-and-build-ralphex-fe.yml:119pushes a version-bump commit directly to master asgithub-actions[bot].ci.ymlonly triggers onpull_request, so no pushed commit can ever carry aCI completestatus — a required-status-check rule would make that push unsatisfiable. Classic branch protection cannot exempt it (enforce_admins=falseexempts repo admins, not the bot), but a ruleset can name a GitHub App as a bypass actor. Rulesets are free on public repos.Add to the ruleset's bypass list:
15368) — keeps the ralphex-fe bump workflow working.enforce_admins=falsedid, so without this your own direct pushes to master would be blocked too.The UI route (Settings → Rules → Rulesets → New branch ruleset) is preferable, because the bypass dropdown avoids hand-writing role IDs. API equivalent for the rule itself:
{ "name": "master", "target": "branch", "enforcement": "active", "bypass_actors": [ { "actor_id": 15368, "actor_type": "Integration", "bypass_mode": "always" } ], "conditions": { "ref_name": { "include": ["refs/heads/master"], "exclude": [] } }, "rules": [ { "type": "required_status_checks", "parameters": { "strict_required_status_checks_policy": false, "required_status_checks": [{ "context": "CI complete" }] } } ] }If the bypass turns out not to be workable, the alternative is to convert
update-and-build-ralphex-fe.ymlto open a PR rather than push to master — arguably the better end state anyway, since the bump would then flow through CI and auto-merge like every Renovate bump.Caveat
88a2161records that Mend portal toggles can override a correctrenovate.json5. PRs are being created, so Silent mode is off — but if auto-merge still doesn't fire after this, the portal is the next place to look, not the config.Note: the commits on this branch are unsigned (authored in an environment without access to the signing key). Squash-merging through GitHub re-creates and signs them, so master's fully-verified history is preserved.