Skip to content

Rebuild claude-code & ralphex-fe only on real tool updates (Renovate) - #116

Merged
gatezh merged 13 commits into
masterfrom
renovate-agent-tool-updates
Aug 13, 2026
Merged

gatezh merged 13 commits into
masterfrom
renovate-agent-tool-updates

Conversation

@gatezh

@gatezh gatezh commented Jul 21, 2026 •

Copy link
Copy Markdown
Owner

What

Rebuild the claude-code and ralphex-fe images only when a tracked tool actually publishes a new release, instead of on a daily cron. Versions are pinned and kept current by Renovate.

Why

Both images pinned four dev tools to "latest" at build time — rtk, ralphex, @anthropic-ai/claude-code, and agent-browser — so a daily schedule: cron existed purely to re-pull them. That rebuilt every morning even when nothing upstream had changed. Pinning the versions and letting Renovate open a bump PR on each real release (auto-merged on green CI, which then fires the existing push build) eliminates the no-op rebuilds while keeping the images fresh — and gives a git-history audit trail of every bump.

Changes

  • Pin the four tools as ARGs with # renovate: annotations in claude-code/.devcontainer/Dockerfile (rtk, ralphex, claude-code CLI, agent-browser) and ralphex-fe/Dockerfile (rtk, ralphex, claude-code CLI); drop the build-time GitHub-API curl | jq "latest" resolution (and the now-unused jq from those alpine stages).
  • Add .github/renovate.json5: a custom.regex manager — set as the only enabled manager, so base images and action pins stay out of scope — reads those annotations; extractVersion strips the leading v from GitHub-release tags; the four tools are grouped into a single auto-merging PR (automerge: true, platformAutomerge: false so the merge waits for green CI without requiring a branch-protection rule).
  • CI (ci.yml): add claude-code (both default and sandbox targets) to the hadolint and amd64 build-and-verify matrices — closing a pre-existing gap where claude-code had no PR-time verification. A new optional target matrix field leaves the other single-target images unchanged.
  • Remove the daily schedule: cron from both build-claude-code.yml and build-ralphex-fe.yml. Both keep push (path-filtered) + workflow_dispatch.
  • Correct "rebuilds daily" wording in README.md, claude-code/README.md, and a stale .hadolint.yaml comment.
  • Add the design spec and implementation plan under docs/superpowers/.

Notes

  • One-time setup required, and the order matters: merge this PR first, then install the Mend Renovate app on the repo. Nothing activates until the app is installed. With .github/renovate.json5 already on master, Renovate treats the repo as manually onboarded and goes straight to scanning — expect no "Configure Renovate" onboarding PR. Installing before the merge instead produces one that adds a root renovate.json, which sits ahead of .github/renovate.json5 in Renovate's config lookup order (it stops at the first match), so merging it would silently shadow this scoped config with the all-managers default. Close it if it appears. Afterward, confirm the Dependency Dashboard lists exactly the four tools / seven annotations.
  • Merged master in (312d62a) to remove the ralphex-fe cron. That cron reached master in fix(ralphex-fe): rebuild daily for latest ralphex/rtk + repair update workflow #113 after this branch was cut, and this branch's copy of build-ralphex-fe.yml simply predates it — so git merged cleanly in both directions and the cron would have survived this PR with no conflict and green CI (actionlint lints a cron fine). With the versions now pinned, it would have rebuilt byte-identical images daily. Removed in 708ef80. (Supersedes the earlier note about coordinating with the ralphex-image-rebuild-schedule branch: fix(ralphex-fe): rebuild daily for latest ralphex/rtk + repair update workflow #113 already merged it, squashed, which is why master carried the cron.)
  • Accepted trade-off: the daily cron also silently refreshed the Alpine/Debian base layers and OS packages on every rebuild. Renovate is scoped to custom.regex only, so base images are out of scope and nothing rebuilds unless one of the four tools ships a release. If OS patch freshness starts to matter, add a weekly/monthly cron (no longer a daily no-op) or widen Renovate to the docker manager for base images.
  • BUN_VERSION / HUGO_VERSION in ralphex-fe/Dockerfile are deliberately unannotated and stay manual via update-and-build-ralphex-fe.yml (dispatch-only, so it can't race Renovate).
  • Verified locally: the custom-manager regex matches all 7 annotations with correct captures; renovate-config-validator, hadolint (both Dockerfiles), and actionlint (all workflows) pass — actionlint re-run after the master merge and cron removal; the config and Dockerfiles are untouched by that merge.

gatezh added 13 commits July 20, 2026 14:54
Replace the daily rebuild crons for claude-code and ralphex-fe with
pinned tool versions managed by Renovate; rebuild only on a real
version bump. Mend-hosted app, scope limited to rtk/ralphex/
claude-code/agent-browser, auto-merge gated on CI (which gains
claude-code, both targets).
The cron landed on master in #113, after this branch was cut, so merging
master back in reintroduced it. Git saw no conflict — this branch simply
predates the cron — which is exactly how a no-op rebuild would have
survived the PR unnoticed.

Its own comment justified it by the Dockerfile resolving ralphex/rtk from
GitHub "latest" at build time; those versions are now pinned ARGs that
Renovate bumps, so the cron would rebuild byte-identical images daily.
@gatezh
gatezh merged commit 5e7fff3 into master Aug 13, 2026
11 checks passed
gatezh added a commit that referenced this pull request Aug 14, 2026
Installing the app with "All repositories" defaults the repo to Silent mode
(dryRun=lookup): Renovate scans and lists updates in the developer portal but
creates no PRs and no issues — including the Dependency Dashboard and any
config-warning issue. A correct config then looks indistinguishable from a
broken one, which is exactly how this repo presented after #116 landed.

Record the portal fix and the reason a config change can't substitute for it:
mode is overridden by dryRun, which is admin-level.

Refs #119
gatezh added a commit that referenced this pull request Aug 14, 2026
#116 pinned rtk, ralphex, the Claude Code CLI and agent-browser as Renovate-managed
ARGs but corrected only the rebuild-cadence wording, leaving the version-pinning
claims stale.

- ralphex-fe: replace the three "latest" rows with pinned versions and add an
  Automatic Rebuilds section — the image had no rebuild-trigger docs at all, which
  is why its stale rows never surfaced in a "daily" grep. Records that an agent-tool
  bump overwrites the bun-hugo tag rather than minting a new one, since the version
  tag derives from Bun and Hugo only.
- claude-code: drop "Always-latest from GitHub Releases" and "pre-installed at
  latest"; fix AGENT_BROWSER_VERSION's default and document the three build args
  #116 added but never listed.
- root: document the Renovate path above the manual dispatch path, state what stays
  manual (base images, Bun/Hugo), and record the Mend portal toggles — Silent mode
  off, Automated PRs on — that a correct config still depends on. Silent mode
  (dryRun=lookup) suppresses PRs and every issue including the Dependency Dashboard,
  making a working config indistinguishable from a broken one; `mode` in
  renovate.json5 cannot override it because dryRun takes precedence.

Closes #119
gatezh added a commit that referenced this pull request Sep 9, 2026
`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).
gatezh added a commit that referenced this pull request Sep 9, 2026
* fix(ci): make Renovate auto-merge actually fire

`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).

* fix(ci): drop redundant platformAutomerge, soak non-claude bumps, guard 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant