Skip to content

Commit 5e7fff3

Browse files
authored
Rebuild claude-code & ralphex-fe only on real tool updates (Renovate) (#116)
* docs(renovate): design spec for Renovate-driven image rebuilds 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). * docs(renovate): move config to .github/renovate.json5 * docs(renovate): implementation plan for Renovate-driven rebuilds * feat(claude-code): pin rtk/ralphex/claude-code/agent-browser for Renovate * feat(ralphex-fe): pin rtk/ralphex/claude-code for Renovate * ci(renovate): add config to pin and auto-update devcontainer agent tools * ci: build and verify claude-code (default + sandbox) on PRs * ci: drop daily rebuild crons (Renovate triggers rebuilds now) * docs(plan): add .hadolint.yaml comment fix to Task 6 * docs: describe Renovate-driven rebuilds instead of daily cron * fix(renovate): scope to custom.regex manager, tolerant v-prefix, tidy header * ci(ralphex-fe): drop daily rebuild cron carried in by master merge 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.
1 parent a61dce3 commit 5e7fff3

11 files changed

Lines changed: 1026 additions & 33 deletions

File tree

‎.github/renovate.json5‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
$schema: 'https://docs.renovatebot.com/renovate-schema.json',
3+
extends: ['config:recommended'],
4+
5+
// Only the custom regex manager below — do NOT let the built-in dockerfile /
6+
// github-actions managers open PRs for base images or action pins (out of scope).
7+
enabledManagers: ['custom.regex'],
8+
9+
// Pin + auto-update the four dev tools these images used to pull from
10+
// "latest" at build time. Replaces the old daily rebuild cron: a Renovate
11+
// bump PR (auto-merged on green CI) triggers the existing push-based image
12+
// build. No upstream release -> no PR -> no rebuild.
13+
customManagers: [
14+
{
15+
customType: 'regex',
16+
managerFilePatterns: ['/(^|/)Dockerfile$/'],
17+
matchStrings: [
18+
'# renovate: datasource=(?<datasource>[a-z-]+) depName=(?<depName>\\S+)\\s+ARG [A-Z_]+_VERSION=(?<currentValue>\\S+)',
19+
],
20+
},
21+
],
22+
23+
packageRules: [
24+
{
25+
// rtk / ralphex release tags look like "v0.43.0"; strip the leading "v"
26+
// so the datasource version matches the bare ARG value ("0.43.0").
27+
matchDatasources: ['github-releases'],
28+
extractVersion: '^v?(?<version>.+)$',
29+
},
30+
{
31+
// Group the four tools into one PR and auto-merge once CI passes.
32+
// platformAutomerge:false => Renovate performs the merge itself only
33+
// after it observes the branch tests are green, so CI gating needs no
34+
// branch-protection rule. (Optional hardening: enable branch protection
35+
// requiring the CI checks and set platformAutomerge:true for native
36+
// GitHub auto-merge.)
37+
matchPackageNames: [
38+
'rtk-ai/rtk',
39+
'umputun/ralphex',
40+
'@anthropic-ai/claude-code',
41+
'agent-browser',
42+
],
43+
groupName: 'devcontainer agent tools',
44+
automerge: true,
45+
platformAutomerge: false,
46+
},
47+
],
48+
}

‎.github/workflows/build-claude-code.yml‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ on:
77
- "claude-code/.devcontainer/Dockerfile"
88
- "claude-code/.devcontainer/*.sh"
99
- "claude-code/.devcontainer/managed-settings.json"
10-
schedule:
11-
- cron: '13 11 * * *'
1210
workflow_dispatch:
1311

1412
permissions:

‎.github/workflows/build-ralphex-fe.yml‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@ on:
77
paths:
88
- 'ralphex-fe/Dockerfile'
99
- 'ralphex-fe/*.sh'
10-
schedule:
11-
# Daily rebuild to bake in the latest ralphex and rtk releases, which the
12-
# Dockerfile pulls from GitHub "latest" at build time. Offset from
13-
# build-claude-code.yml's 11:13 to avoid hitting the GitHub API at the same minute.
14-
- cron: '41 11 * * *'
1510
workflow_dispatch:
1611

1712
permissions:

‎.github/workflows/ci.yml‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jobs:
2424
dockerfile:
2525
- bun/.devcontainer/Dockerfile
2626
- claude-bun/.devcontainer/Dockerfile
27+
- claude-code/.devcontainer/Dockerfile
2728
- hugo-bun/.devcontainer/Dockerfile
2829
- hugo-bun-node/.devcontainer/Dockerfile
2930
- ralphex-fe/Dockerfile
@@ -67,6 +68,8 @@ jobs:
6768
- 'bun/**'
6869
claude-bun:
6970
- 'claude-bun/**'
71+
claude-code:
72+
- 'claude-code/**'
7073
hugo-bun:
7174
- 'hugo-bun/**'
7275
hugo-bun-node:
@@ -79,20 +82,22 @@ jobs:
7982
env:
8083
CHANGED_BUN: ${{ steps.filter.outputs.bun }}
8184
CHANGED_CLAUDE_BUN: ${{ steps.filter.outputs.claude-bun }}
85+
CHANGED_CLAUDE_CODE: ${{ steps.filter.outputs.claude-code }}
8286
CHANGED_HUGO_BUN: ${{ steps.filter.outputs.hugo-bun }}
8387
CHANGED_HUGO_BUN_NODE: ${{ steps.filter.outputs.hugo-bun-node }}
8488
CHANGED_RALPHEX: ${{ steps.filter.outputs.ralphex-fe }}
8589
run: |
8690
INCLUDES="[]"
8791
8892
add_image() {
89-
local image="$1" context="$2" dockerfile="$3" verify="$4"
93+
local image="$1" context="$2" dockerfile="$3" verify="$4" target="${5:-}"
9094
INCLUDES=$(echo "$INCLUDES" | jq -c \
9195
--arg img "$image" \
9296
--arg ctx "$context" \
9397
--arg df "$dockerfile" \
9498
--arg v "$verify" \
95-
'. + [{"image":$img,"context":$ctx,"dockerfile":$df,"verify":$v}]')
99+
--arg tgt "$target" \
100+
'. + [{"image":$img,"context":$ctx,"dockerfile":$df,"verify":$v,"target":$tgt}]')
96101
}
97102
98103
if [ "$CHANGED_BUN" = "true" ]; then
@@ -109,6 +114,19 @@ jobs:
109114
"bun --version"
110115
fi
111116
117+
if [ "$CHANGED_CLAUDE_CODE" = "true" ]; then
118+
add_image "claude-code" \
119+
"claude-code/.devcontainer" \
120+
"claude-code/.devcontainer/Dockerfile" \
121+
"bun --version || true && claude --version && mise --version && fish --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium" \
122+
"default"
123+
add_image "claude-code-sandbox" \
124+
"claude-code/.devcontainer" \
125+
"claude-code/.devcontainer/Dockerfile" \
126+
"claude --version && mise --version && fish --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp" \
127+
"sandbox"
128+
fi
129+
112130
if [ "$CHANGED_HUGO_BUN" = "true" ]; then
113131
add_image "hugo-bun" \
114132
"hugo-bun/.devcontainer" \
@@ -164,6 +182,7 @@ jobs:
164182
with:
165183
context: ${{ matrix.context }}
166184
file: ${{ matrix.dockerfile }}
185+
target: ${{ matrix.target }}
167186
platforms: linux/amd64
168187
load: true
169188
tags: ${{ matrix.image }}:test

‎.hadolint.yaml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ ignored:
2323
# then drops to app user via gosu. No final USER directive is correct.
2424
- DL3002 # last USER should not be root
2525

26-
# Dev tools (Claude Code) intentionally unpinned — images rebuild daily
27-
# to always get latest. Pinning would defeat the purpose.
26+
# Dev tools (e.g. Claude Code) are installed via npm; some images pin the
27+
# version via a Renovate-managed ARG, others intentionally don't.
2828
- DL3016 # pin versions in npm install
2929

3030
# Pipes inside command substitutions (e.g. grep | cut) are intentional;

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ image-name/
4040

4141
### claude-code
4242

43-
Shared devcontainer base image for Claude Code projects. Two variants from a single multi-stage Dockerfile: **default** (full dev environment with agent-browser) and **sandbox** (network-restricted with iptables firewall). Projects consume pre-built images and control tool versions via `.mise.toml`. Rebuilds daily to pick up latest Claude Code.
43+
Shared devcontainer base image for Claude Code projects. Two variants from a single multi-stage Dockerfile: **default** (full dev environment with agent-browser) and **sandbox** (network-restricted with iptables firewall). Projects consume pre-built images and control tool versions via `.mise.toml`. Rebuilds when its pinned tools receive a new release (managed by Renovate), not on a schedule.
4444

4545
**Usage in other projects:**
4646

‎claude-code/.devcontainer/Dockerfile‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,29 @@
1616
# ═════════════════════════════════════════════════════════════════════════════
1717

1818
# ── rtk (token-optimized CLI proxy) ──────────────────────────────────────
19+
# Version pinned and kept up to date by Renovate (see .github/renovate.json5).
1920
FROM alpine:3.21 AS rtk-download
20-
RUN apk add --no-cache curl jq
21+
RUN apk add --no-cache curl
22+
# renovate: datasource=github-releases depName=rtk-ai/rtk
23+
ARG RTK_VERSION=0.43.0
2124
RUN set -eux; \
2225
ARCH="$(uname -m)"; \
2326
case "$ARCH" in \
2427
x86_64) RTK_TARGET="x86_64-unknown-linux-musl" ;; \
2528
aarch64) RTK_TARGET="aarch64-unknown-linux-gnu" ;; \
2629
esac; \
27-
RTK_VERSION=$(curl -fsSL https://api.github.com/repos/rtk-ai/rtk/releases/latest \
28-
| jq -r '.tag_name' | sed 's/^v//'); \
2930
curl -fsSL -o /tmp/rtk.tar.gz \
3031
"https://github.com/rtk-ai/rtk/releases/download/v${RTK_VERSION}/rtk-${RTK_TARGET}.tar.gz"; \
3132
tar -xzf /tmp/rtk.tar.gz -C /usr/local/bin rtk
3233

3334
# ── ralphex (autonomous plan execution) ──────────────────────────────────
35+
# Version pinned and kept up to date by Renovate (see .github/renovate.json5).
3436
FROM alpine:3.21 AS ralphex-download
35-
RUN apk add --no-cache curl jq
37+
RUN apk add --no-cache curl
38+
# renovate: datasource=github-releases depName=umputun/ralphex
39+
ARG RALPHEX_VERSION=1.6.0
3640
RUN set -eux; \
3741
ARCH="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"; \
38-
RALPHEX_VERSION=$(curl -fsSL https://api.github.com/repos/umputun/ralphex/releases/latest \
39-
| jq -r '.tag_name' | sed 's/^v//'); \
4042
curl -fsSL -o /tmp/ralphex.tar.gz \
4143
"https://github.com/umputun/ralphex/releases/download/v${RALPHEX_VERSION}/ralphex_${RALPHEX_VERSION}_linux_${ARCH}.tar.gz"; \
4244
tar -xzf /tmp/ralphex.tar.gz -C /usr/local/bin ralphex
@@ -141,7 +143,7 @@ ENV MISE_TRUSTED_CONFIG_PATHS="/workspace"
141143
# ── Dev tools (copied from parallel download stages) ─────────────────────────
142144
# rtk (token-optimized CLI proxy) and ralphex (autonomous plan execution).
143145
# Like Claude Code itself, these are dev infrastructure — not project dependencies.
144-
# Downloaded from GitHub Releases; refreshed on each daily image rebuild.
146+
# Downloaded from GitHub Releases at Renovate-pinned versions (see .github/renovate.json5).
145147
COPY --from=rtk-download /usr/local/bin/rtk /usr/local/bin/rtk
146148
COPY --from=ralphex-download /usr/local/bin/ralphex /usr/local/bin/ralphex
147149

@@ -175,12 +177,15 @@ USER node
175177
# npm is deprecated for interactive users but still supported "for compatibility
176178
# reasons" — Docker/CI parallel builds are exactly that reason.
177179
# See: https://code.claude.com/docs/en/getting-started#install-with-npm
178-
# Auto-updates don't matter here — the image rebuilds daily.
180+
# Version pinned and kept up to date by Renovate (see .github/renovate.json5);
181+
# a bump PR triggers a rebuild.
179182
#
180183
# Install as the node user so all files land with node ownership from the start.
181184
# A later `chown -R` in another layer would duplicate every file (overlayfs
182185
# treats an ownership change as a rewrite), adding hundreds of MB.
183-
RUN npm install -g @anthropic-ai/claude-code
186+
# renovate: datasource=npm depName=@anthropic-ai/claude-code
187+
ARG CLAUDE_CODE_VERSION=2.1.216
188+
RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}
184189

185190
# ─── DEFAULT — full dev environment ───────────────────────────────────────────
186191
FROM base AS default
@@ -222,7 +227,9 @@ ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \
222227
# agent-browser: headless browser automation for AI agents.
223228
# Uses the apt-installed chromium above (via AGENT_BROWSER_EXECUTABLE_PATH).
224229
# Installed as the node user (see claude-code install above for rationale).
225-
ARG AGENT_BROWSER_VERSION=latest
230+
# Version pinned and kept up to date by Renovate (see .github/renovate.json5).
231+
# renovate: datasource=npm depName=agent-browser
232+
ARG AGENT_BROWSER_VERSION=0.32.3
226233
RUN npm install -g agent-browser@${AGENT_BROWSER_VERSION}
227234

228235
LABEL org.opencontainers.image.source="https://github.com/gatezh/devcontainers" \

‎claude-code/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Both variants are built for:
4242

4343
## Automatic Rebuilds
4444

45-
The image rebuilds daily at 5am MT (11:00 UTC) using native runners for both amd64 and arm64 (no QEMU emulation). Each rebuild picks up the latest Claude Code and agent-browser. Manual rebuilds can be triggered via the "Run workflow" button in the Actions UI.
45+
The image rebuilds automatically whenever one of its pinned tools — Claude Code, agent-browser, rtk, or ralphex — publishes a new release: Renovate opens a version-bump PR, CI verifies it, it auto-merges, and the merge builds the image on native runners for both amd64 and arm64 (no QEMU emulation). Manual rebuilds can be triggered via the "Run workflow" button in the Actions UI.
4646

4747
## Quick Start
4848

@@ -115,7 +115,7 @@ Mark as executable: `chmod +x init-plugins.sh`
115115

116116
To remove a plugin in your project, delete its entry from the local `init-plugins.sh` — the script is a template, not image-baked, so each consumer controls its own list.
117117

118-
> **Why `init-plugins.sh` stays per-project but `patch-playwright-mcp` doesn't:** `init-plugins.sh` carries project-specific configuration (marketplace list, plugin list) — it's *meant* to be edited per project. The patch script has zero project-specific config and is identical across every consumer, so it's baked into the image and flows through the same daily-rebuild + `initializeCommand` image-pull channel as the rest of the image. That boundary is the rule: project-specific config stays per-project; universal logic moves into the image.
118+
> **Why `init-plugins.sh` stays per-project but `patch-playwright-mcp` doesn't:** `init-plugins.sh` carries project-specific configuration (marketplace list, plugin list) — it's *meant* to be edited per project. The patch script has zero project-specific config and is identical across every consumer, so it's baked into the image and flows through the same Renovate-triggered rebuild + `initializeCommand` image-pull channel as the rest of the image. That boundary is the rule: project-specific config stays per-project; universal logic moves into the image.
119119
120120
### Sandbox-only: `.devcontainer/claude-sandbox/init-firewall.sh`
121121

0 commit comments

Comments
 (0)