diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 0000000..d0d24c4 --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,48 @@ +{ + $schema: 'https://docs.renovatebot.com/renovate-schema.json', + extends: ['config:recommended'], + + // Only the custom regex manager below — do NOT let the built-in dockerfile / + // github-actions managers open PRs for base images or action pins (out of scope). + enabledManagers: ['custom.regex'], + + // Pin + auto-update the four dev tools these images used to pull from + // "latest" at build time. Replaces the old daily rebuild cron: a Renovate + // bump PR (auto-merged on green CI) triggers the existing push-based image + // build. No upstream release -> no PR -> no rebuild. + customManagers: [ + { + customType: 'regex', + managerFilePatterns: ['/(^|/)Dockerfile$/'], + matchStrings: [ + '# renovate: datasource=(?[a-z-]+) depName=(?\\S+)\\s+ARG [A-Z_]+_VERSION=(?\\S+)', + ], + }, + ], + + packageRules: [ + { + // rtk / ralphex release tags look like "v0.43.0"; strip the leading "v" + // so the datasource version matches the bare ARG value ("0.43.0"). + matchDatasources: ['github-releases'], + extractVersion: '^v?(?.+)$', + }, + { + // Group the four tools into one PR and auto-merge once CI passes. + // platformAutomerge:false => Renovate performs the merge itself only + // after it observes the branch tests are green, so CI gating needs no + // branch-protection rule. (Optional hardening: enable branch protection + // requiring the CI checks and set platformAutomerge:true for native + // GitHub auto-merge.) + matchPackageNames: [ + 'rtk-ai/rtk', + 'umputun/ralphex', + '@anthropic-ai/claude-code', + 'agent-browser', + ], + groupName: 'devcontainer agent tools', + automerge: true, + platformAutomerge: false, + }, + ], +} diff --git a/.github/workflows/build-claude-code.yml b/.github/workflows/build-claude-code.yml index 3f8b8e0..4977ba6 100644 --- a/.github/workflows/build-claude-code.yml +++ b/.github/workflows/build-claude-code.yml @@ -7,8 +7,6 @@ on: - "claude-code/.devcontainer/Dockerfile" - "claude-code/.devcontainer/*.sh" - "claude-code/.devcontainer/managed-settings.json" - schedule: - - cron: '13 11 * * *' workflow_dispatch: permissions: diff --git a/.github/workflows/build-ralphex-fe.yml b/.github/workflows/build-ralphex-fe.yml index 1cbd746..1a7785c 100644 --- a/.github/workflows/build-ralphex-fe.yml +++ b/.github/workflows/build-ralphex-fe.yml @@ -7,11 +7,6 @@ on: paths: - 'ralphex-fe/Dockerfile' - 'ralphex-fe/*.sh' - schedule: - # Daily rebuild to bake in the latest ralphex and rtk releases, which the - # Dockerfile pulls from GitHub "latest" at build time. Offset from - # build-claude-code.yml's 11:13 to avoid hitting the GitHub API at the same minute. - - cron: '41 11 * * *' workflow_dispatch: permissions: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e614b3..c559235 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,7 @@ jobs: dockerfile: - bun/.devcontainer/Dockerfile - claude-bun/.devcontainer/Dockerfile + - claude-code/.devcontainer/Dockerfile - hugo-bun/.devcontainer/Dockerfile - hugo-bun-node/.devcontainer/Dockerfile - ralphex-fe/Dockerfile @@ -67,6 +68,8 @@ jobs: - 'bun/**' claude-bun: - 'claude-bun/**' + claude-code: + - 'claude-code/**' hugo-bun: - 'hugo-bun/**' hugo-bun-node: @@ -79,6 +82,7 @@ jobs: env: CHANGED_BUN: ${{ steps.filter.outputs.bun }} CHANGED_CLAUDE_BUN: ${{ steps.filter.outputs.claude-bun }} + CHANGED_CLAUDE_CODE: ${{ steps.filter.outputs.claude-code }} CHANGED_HUGO_BUN: ${{ steps.filter.outputs.hugo-bun }} CHANGED_HUGO_BUN_NODE: ${{ steps.filter.outputs.hugo-bun-node }} CHANGED_RALPHEX: ${{ steps.filter.outputs.ralphex-fe }} @@ -86,13 +90,14 @@ jobs: INCLUDES="[]" add_image() { - local image="$1" context="$2" dockerfile="$3" verify="$4" + local image="$1" context="$2" dockerfile="$3" verify="$4" target="${5:-}" INCLUDES=$(echo "$INCLUDES" | jq -c \ --arg img "$image" \ --arg ctx "$context" \ --arg df "$dockerfile" \ --arg v "$verify" \ - '. + [{"image":$img,"context":$ctx,"dockerfile":$df,"verify":$v}]') + --arg tgt "$target" \ + '. + [{"image":$img,"context":$ctx,"dockerfile":$df,"verify":$v,"target":$tgt}]') } if [ "$CHANGED_BUN" = "true" ]; then @@ -109,6 +114,19 @@ jobs: "bun --version" fi + if [ "$CHANGED_CLAUDE_CODE" = "true" ]; then + add_image "claude-code" \ + "claude-code/.devcontainer" \ + "claude-code/.devcontainer/Dockerfile" \ + "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" \ + "default" + add_image "claude-code-sandbox" \ + "claude-code/.devcontainer" \ + "claude-code/.devcontainer/Dockerfile" \ + "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" \ + "sandbox" + fi + if [ "$CHANGED_HUGO_BUN" = "true" ]; then add_image "hugo-bun" \ "hugo-bun/.devcontainer" \ @@ -164,6 +182,7 @@ jobs: with: context: ${{ matrix.context }} file: ${{ matrix.dockerfile }} + target: ${{ matrix.target }} platforms: linux/amd64 load: true tags: ${{ matrix.image }}:test diff --git a/.hadolint.yaml b/.hadolint.yaml index 7561df7..eec7f41 100644 --- a/.hadolint.yaml +++ b/.hadolint.yaml @@ -23,8 +23,8 @@ ignored: # then drops to app user via gosu. No final USER directive is correct. - DL3002 # last USER should not be root - # Dev tools (Claude Code) intentionally unpinned — images rebuild daily - # to always get latest. Pinning would defeat the purpose. + # Dev tools (e.g. Claude Code) are installed via npm; some images pin the + # version via a Renovate-managed ARG, others intentionally don't. - DL3016 # pin versions in npm install # Pipes inside command substitutions (e.g. grep | cut) are intentional; diff --git a/README.md b/README.md index 2614a34..eec5415 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ image-name/ ### claude-code -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. +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. **Usage in other projects:** diff --git a/claude-code/.devcontainer/Dockerfile b/claude-code/.devcontainer/Dockerfile index a98c356..390fcb1 100644 --- a/claude-code/.devcontainer/Dockerfile +++ b/claude-code/.devcontainer/Dockerfile @@ -16,27 +16,29 @@ # ═════════════════════════════════════════════════════════════════════════════ # ── rtk (token-optimized CLI proxy) ────────────────────────────────────── +# Version pinned and kept up to date by Renovate (see .github/renovate.json5). FROM alpine:3.21 AS rtk-download -RUN apk add --no-cache curl jq +RUN apk add --no-cache curl +# renovate: datasource=github-releases depName=rtk-ai/rtk +ARG RTK_VERSION=0.43.0 RUN set -eux; \ ARCH="$(uname -m)"; \ case "$ARCH" in \ x86_64) RTK_TARGET="x86_64-unknown-linux-musl" ;; \ aarch64) RTK_TARGET="aarch64-unknown-linux-gnu" ;; \ esac; \ - RTK_VERSION=$(curl -fsSL https://api.github.com/repos/rtk-ai/rtk/releases/latest \ - | jq -r '.tag_name' | sed 's/^v//'); \ curl -fsSL -o /tmp/rtk.tar.gz \ "https://github.com/rtk-ai/rtk/releases/download/v${RTK_VERSION}/rtk-${RTK_TARGET}.tar.gz"; \ tar -xzf /tmp/rtk.tar.gz -C /usr/local/bin rtk # ── ralphex (autonomous plan execution) ────────────────────────────────── +# Version pinned and kept up to date by Renovate (see .github/renovate.json5). FROM alpine:3.21 AS ralphex-download -RUN apk add --no-cache curl jq +RUN apk add --no-cache curl +# renovate: datasource=github-releases depName=umputun/ralphex +ARG RALPHEX_VERSION=1.6.0 RUN set -eux; \ ARCH="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"; \ - RALPHEX_VERSION=$(curl -fsSL https://api.github.com/repos/umputun/ralphex/releases/latest \ - | jq -r '.tag_name' | sed 's/^v//'); \ curl -fsSL -o /tmp/ralphex.tar.gz \ "https://github.com/umputun/ralphex/releases/download/v${RALPHEX_VERSION}/ralphex_${RALPHEX_VERSION}_linux_${ARCH}.tar.gz"; \ tar -xzf /tmp/ralphex.tar.gz -C /usr/local/bin ralphex @@ -141,7 +143,7 @@ ENV MISE_TRUSTED_CONFIG_PATHS="/workspace" # ── Dev tools (copied from parallel download stages) ───────────────────────── # rtk (token-optimized CLI proxy) and ralphex (autonomous plan execution). # Like Claude Code itself, these are dev infrastructure — not project dependencies. -# Downloaded from GitHub Releases; refreshed on each daily image rebuild. +# Downloaded from GitHub Releases at Renovate-pinned versions (see .github/renovate.json5). COPY --from=rtk-download /usr/local/bin/rtk /usr/local/bin/rtk COPY --from=ralphex-download /usr/local/bin/ralphex /usr/local/bin/ralphex @@ -175,12 +177,15 @@ USER node # npm is deprecated for interactive users but still supported "for compatibility # reasons" — Docker/CI parallel builds are exactly that reason. # See: https://code.claude.com/docs/en/getting-started#install-with-npm -# Auto-updates don't matter here — the image rebuilds daily. +# Version pinned and kept up to date by Renovate (see .github/renovate.json5); +# a bump PR triggers a rebuild. # # Install as the node user so all files land with node ownership from the start. # A later `chown -R` in another layer would duplicate every file (overlayfs # treats an ownership change as a rewrite), adding hundreds of MB. -RUN npm install -g @anthropic-ai/claude-code +# renovate: datasource=npm depName=@anthropic-ai/claude-code +ARG CLAUDE_CODE_VERSION=2.1.216 +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} # ─── DEFAULT — full dev environment ─────────────────────────────────────────── FROM base AS default @@ -222,7 +227,9 @@ ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \ # agent-browser: headless browser automation for AI agents. # Uses the apt-installed chromium above (via AGENT_BROWSER_EXECUTABLE_PATH). # Installed as the node user (see claude-code install above for rationale). -ARG AGENT_BROWSER_VERSION=latest +# Version pinned and kept up to date by Renovate (see .github/renovate.json5). +# renovate: datasource=npm depName=agent-browser +ARG AGENT_BROWSER_VERSION=0.32.3 RUN npm install -g agent-browser@${AGENT_BROWSER_VERSION} LABEL org.opencontainers.image.source="https://github.com/gatezh/devcontainers" \ diff --git a/claude-code/README.md b/claude-code/README.md index db02b7b..5ce2eef 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -42,7 +42,7 @@ Both variants are built for: ## Automatic Rebuilds -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. +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. ## Quick Start @@ -115,7 +115,7 @@ Mark as executable: `chmod +x init-plugins.sh` 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. -> **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. +> **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. ### Sandbox-only: `.devcontainer/claude-sandbox/init-firewall.sh` diff --git a/docs/superpowers/plans/2026-07-20-renovate-tool-updates.md b/docs/superpowers/plans/2026-07-20-renovate-tool-updates.md new file mode 100644 index 0000000..ec22970 --- /dev/null +++ b/docs/superpowers/plans/2026-07-20-renovate-tool-updates.md @@ -0,0 +1,681 @@ +# Renovate-driven Image Rebuilds — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the daily rebuild crons for `claude-code` and `ralphex-fe` with pinned tool versions that Renovate bumps only when an upstream release actually happens. + +**Architecture:** Pin the four moving tools (`rtk`, `ralphex`, `@anthropic-ai/claude-code`, `agent-browser`) as annotated `ARG`s in the Dockerfiles. A `.github/renovate.json5` custom manager reads those annotations, opens a grouped bump PR on any new release, CI verifies it (amd64 build + tool checks — now including `claude-code` both targets), Renovate auto-merges on green, and the existing `push` path-filter trigger performs the real multi-arch build. The `schedule:` crons are deleted. + +**Tech Stack:** Docker multi-stage builds (BuildKit), GitHub Actions, Renovate (Mend-hosted app), `hadolint`, `actionlint`, `renovate-config-validator`. + +## Global Constraints + +- Platform for all workflow shell is Ubuntu/GNU coreutils — never BSD/macOS syntax. +- Images build multi-platform `linux/amd64,linux/arm64`; CI verifies amd64 only. +- ARG version values are **bare semver** (e.g. `0.43.0`), never `v`-prefixed — the `v` lives in the download URL / is stripped from the datasource via `extractVersion`. +- Renovate config lives at `.github/renovate.json5` (JSON5, comments allowed). +- Auto-merge is gated on CI via `platformAutomerge: false` (Renovate waits for green tests itself; no branch-protection rule required). +- Scope is exactly the four tools above — do NOT bring `Bun`/`Hugo`/`Go`/`Docker`/`git-delta`/base images under Renovate. +- Commit messages follow the repo's Conventional Commits style (`feat(...)`, `fix(...)`, `chore(...)`, `docs(...)`, `ci(...)`). +- Current pinned versions to seed the ARGs: `rtk` = `0.43.0`, `ralphex` = `1.6.0`, `@anthropic-ai/claude-code` = `2.1.216`, `agent-browser` = `0.32.3`. (Resolved 2026-07-20. If materially stale at execution, re-resolve: `curl -fsSL https://api.github.com/repos/OWNER/REPO/releases/latest | jq -r .tag_name` and `curl -fsSL https://registry.npmjs.org/PKG/latest | jq -r .version`.) + +### Tooling note (validation commands) + +This repo's dev container ships `hadolint` + `actionlint`. If a command below is not on `PATH` (e.g. running on the macOS host), use the Docker fallback: +- hadolint: `docker run --rm -i hadolint/hadolint < ` +- actionlint: `docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:latest -color ` +- renovate-config-validator: `npx --yes --package renovate -- renovate-config-validator ` + +--- + +### Task 1: Pin the four tools in `claude-code/.devcontainer/Dockerfile` + +**Files:** +- Modify: `claude-code/.devcontainer/Dockerfile` + +**Interfaces:** +- Produces: annotated ARGs `RTK_VERSION`, `RALPHEX_VERSION`, `CLAUDE_CODE_VERSION`, `AGENT_BROWSER_VERSION` that the Renovate custom manager in Task 3 matches via the `# renovate:` comment + `ARG …_VERSION=` pattern. + +- [ ] **Step 1: Pin the `rtk-download` stage** + +Replace the `rtk-download` stage (the block starting `# ── rtk (token-optimized CLI proxy) ──` through the `tar -xzf /tmp/rtk.tar.gz …` line) with: + +```dockerfile +# ── rtk (token-optimized CLI proxy) ────────────────────────────────────── +# Version pinned and kept up to date by Renovate (see .github/renovate.json5). +FROM alpine:3.21 AS rtk-download +RUN apk add --no-cache curl +# renovate: datasource=github-releases depName=rtk-ai/rtk +ARG RTK_VERSION=0.43.0 +RUN set -eux; \ + ARCH="$(uname -m)"; \ + case "$ARCH" in \ + x86_64) RTK_TARGET="x86_64-unknown-linux-musl" ;; \ + aarch64) RTK_TARGET="aarch64-unknown-linux-gnu" ;; \ + esac; \ + curl -fsSL -o /tmp/rtk.tar.gz \ + "https://github.com/rtk-ai/rtk/releases/download/v${RTK_VERSION}/rtk-${RTK_TARGET}.tar.gz"; \ + tar -xzf /tmp/rtk.tar.gz -C /usr/local/bin rtk +``` + +(Removes the GitHub-API version resolution and the now-unused `jq`.) + +- [ ] **Step 2: Pin the `ralphex-download` stage** + +Replace the `ralphex-download` stage with: + +```dockerfile +# ── ralphex (autonomous plan execution) ────────────────────────────────── +# Version pinned and kept up to date by Renovate (see .github/renovate.json5). +FROM alpine:3.21 AS ralphex-download +RUN apk add --no-cache curl +# renovate: datasource=github-releases depName=umputun/ralphex +ARG RALPHEX_VERSION=1.6.0 +RUN set -eux; \ + ARCH="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"; \ + curl -fsSL -o /tmp/ralphex.tar.gz \ + "https://github.com/umputun/ralphex/releases/download/v${RALPHEX_VERSION}/ralphex_${RALPHEX_VERSION}_linux_${ARCH}.tar.gz"; \ + tar -xzf /tmp/ralphex.tar.gz -C /usr/local/bin ralphex +``` + +- [ ] **Step 3: Update the dev-tools COPY comment** + +Find: + +```dockerfile +# Downloaded from GitHub Releases; refreshed on each daily image rebuild. +``` + +Replace with: + +```dockerfile +# Downloaded from GitHub Releases at Renovate-pinned versions (see .github/renovate.json5). +``` + +- [ ] **Step 4: Pin the Claude Code CLI install** + +Find: + +```dockerfile +# See: https://code.claude.com/docs/en/getting-started#install-with-npm +# Auto-updates don't matter here — the image rebuilds daily. +# +# Install as the node user so all files land with node ownership from the start. +# A later `chown -R` in another layer would duplicate every file (overlayfs +# treats an ownership change as a rewrite), adding hundreds of MB. +RUN npm install -g @anthropic-ai/claude-code +``` + +Replace with: + +```dockerfile +# See: https://code.claude.com/docs/en/getting-started#install-with-npm +# Version pinned and kept up to date by Renovate (see .github/renovate.json5); +# a bump PR triggers a rebuild. +# +# Install as the node user so all files land with node ownership from the start. +# A later `chown -R` in another layer would duplicate every file (overlayfs +# treats an ownership change as a rewrite), adding hundreds of MB. +# renovate: datasource=npm depName=@anthropic-ai/claude-code +ARG CLAUDE_CODE_VERSION=2.1.216 +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} +``` + +- [ ] **Step 5: Pin the agent-browser install (default target)** + +Find: + +```dockerfile +# Installed as the node user (see claude-code install above for rationale). +ARG AGENT_BROWSER_VERSION=latest +RUN npm install -g agent-browser@${AGENT_BROWSER_VERSION} +``` + +Replace with: + +```dockerfile +# Installed as the node user (see claude-code install above for rationale). +# Version pinned and kept up to date by Renovate (see .github/renovate.json5). +# renovate: datasource=npm depName=agent-browser +ARG AGENT_BROWSER_VERSION=0.32.3 +RUN npm install -g agent-browser@${AGENT_BROWSER_VERSION} +``` + +- [ ] **Step 6: Lint the Dockerfile** + +Run: `hadolint claude-code/.devcontainer/Dockerfile` +Expected: no output, exit 0. (Docker fallback in the tooling note above.) + +- [ ] **Step 7: Commit** + +```bash +git add claude-code/.devcontainer/Dockerfile +git commit -m "feat(claude-code): pin rtk/ralphex/claude-code/agent-browser for Renovate" +``` + +--- + +### Task 2: Pin the three tools in `ralphex-fe/Dockerfile` + +**Files:** +- Modify: `ralphex-fe/Dockerfile` + +**Interfaces:** +- Produces: annotated ARGs `RTK_VERSION`, `RALPHEX_VERSION`, `CLAUDE_CODE_VERSION` matched by the same Task 3 custom manager. (No `agent-browser` here — this image doesn't install it.) + +- [ ] **Step 1: Pin the `rtk-download` stage** + +Replace the `rtk-download` stage (block starting `# ── RTK (token-optimized CLI proxy for Claude Code) ──`) with: + +```dockerfile +# ── RTK (token-optimized CLI proxy for Claude Code) ───────────────────────── +# Pattern from: .devcontainer/Dockerfile +# Version pinned and kept up to date by Renovate (see .github/renovate.json5). +FROM alpine:3.21 AS rtk-download +RUN apk add --no-cache curl +# renovate: datasource=github-releases depName=rtk-ai/rtk +ARG RTK_VERSION=0.43.0 +RUN set -eux; \ + ARCH="$(uname -m)"; \ + case "$ARCH" in \ + x86_64) RTK_TARGET="x86_64-unknown-linux-musl" ;; \ + aarch64) RTK_TARGET="aarch64-unknown-linux-gnu" ;; \ + esac; \ + curl -fsSL -o /tmp/rtk.tar.gz \ + "https://github.com/rtk-ai/rtk/releases/download/v${RTK_VERSION}/rtk-${RTK_TARGET}.tar.gz"; \ + tar -xzf /tmp/rtk.tar.gz -C /usr/local/bin rtk +``` + +- [ ] **Step 2: Pin the `ralphex-download` stage** + +Replace the `ralphex-download` stage (block starting `# ── Ralphex binary (latest from GitHub Releases) ──`) with: + +```dockerfile +# ── Ralphex binary (pinned; Renovate-managed) ─────────────────────────────── +# Pattern from: claude-code/.devcontainer/Dockerfile +# Version pinned and kept up to date by Renovate (see .github/renovate.json5). +FROM alpine:3.21 AS ralphex-download +RUN apk add --no-cache curl +# renovate: datasource=github-releases depName=umputun/ralphex +ARG RALPHEX_VERSION=1.6.0 +RUN set -eux; \ + ARCH="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"; \ + curl -fsSL -o /tmp/ralphex.tar.gz \ + "https://github.com/umputun/ralphex/releases/download/v${RALPHEX_VERSION}/ralphex_${RALPHEX_VERSION}_linux_${ARCH}.tar.gz"; \ + tar -xzf /tmp/ralphex.tar.gz -C /usr/local/bin ralphex +``` + +- [ ] **Step 3: Pin the Claude Code CLI install** + +Find: + +```dockerfile +# ── Claude Code CLI ────────────────────────────────────────────────────────── +# npm install (not native installer) to avoid rate-limiting in parallel Docker builds. +# See: claude-code/.devcontainer/Dockerfile for rationale. +RUN --mount=type=cache,target=/root/.npm \ + npm install -g @anthropic-ai/claude-code +``` + +Replace with: + +```dockerfile +# ── Claude Code CLI ────────────────────────────────────────────────────────── +# npm install (not native installer) to avoid rate-limiting in parallel Docker builds. +# See: claude-code/.devcontainer/Dockerfile for rationale. +# Version pinned and kept up to date by Renovate (see .github/renovate.json5). +# renovate: datasource=npm depName=@anthropic-ai/claude-code +ARG CLAUDE_CODE_VERSION=2.1.216 +RUN --mount=type=cache,target=/root/.npm \ + npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} +``` + +- [ ] **Step 4: Lint the Dockerfile** + +Run: `hadolint ralphex-fe/Dockerfile` +Expected: no output, exit 0. + +- [ ] **Step 5: Commit** + +```bash +git add ralphex-fe/Dockerfile +git commit -m "feat(ralphex-fe): pin rtk/ralphex/claude-code for Renovate" +``` + +--- + +### Task 3: Add the Renovate config + +**Files:** +- Create: `.github/renovate.json5` + +**Interfaces:** +- Consumes: the `# renovate: datasource=… depName=…` + `ARG …_VERSION=…` annotations from Tasks 1–2. +- Produces: Renovate managed-dependency detection for the four tools; grouped auto-merging bump PRs. + +- [ ] **Step 1: Create `.github/renovate.json5`** + +```json5 +{ + $schema: 'https://docs.renovatebot.com/renovate-schema.json', + extends: ['config:recommended'], + + // Pin + auto-update the four dev tools these images used to pull from + // "latest" at build time. Replaces the old daily rebuild cron: a Renovate + // bump PR (auto-merged on green CI) triggers the existing push-based image + // build. No upstream release -> no PR -> no rebuild. + customManagers: [ + { + customType: 'regex', + managerFilePatterns: ['/(^|/)Dockerfile$/'], + matchStrings: [ + '# renovate: datasource=(?[a-z-]+) depName=(?\\S+)\\s+ARG [A-Z_]+_VERSION=(?\\S+)', + ], + }, + ], + + packageRules: [ + { + // rtk / ralphex release tags look like "v0.43.0"; strip the leading "v" + // so the datasource version matches the bare ARG value ("0.43.0"). + matchDatasources: ['github-releases'], + extractVersion: '^v(?.*)$', + }, + { + // Group the four tools into one PR and auto-merge once CI passes. + // platformAutomerge:false => Renovate performs the merge itself only + // after it observes the branch tests are green, so CI gating needs no + // branch-protection rule. (Optional hardening: enable branch protection + // requiring the CI checks and set platformAutomerge:true for native + // GitHub auto-merge.) + matchPackageNames: [ + 'rtk-ai/rtk', + 'umputun/ralphex', + '@anthropic-ai/claude-code', + 'agent-browser', + ], + groupName: 'devcontainer agent tools', + automerge: true, + platformAutomerge: false, + }, + ], +} +``` + +- [ ] **Step 2: Validate the config** + +Run: `npx --yes --package renovate -- renovate-config-validator .github/renovate.json5` +Expected: output includes `Config validated successfully`. If it reports the file is invalid, fix and re-run before committing. + +- [ ] **Step 3: Sanity-check that the custom manager matches the annotations** + +Confirm the regex matches every annotated ARG across both Dockerfiles (expect **7** matches: rtk+ralphex+claude-code in each file = 6, plus agent-browser in claude-code = 7): + +Run: +```bash +grep -rEc '# renovate: datasource=[a-z-]+ depName=\S+' \ + claude-code/.devcontainer/Dockerfile ralphex-fe/Dockerfile +``` +Expected: `claude-code/.devcontainer/Dockerfile:4` and `ralphex-fe/Dockerfile:3`. + +- [ ] **Step 4: Commit** + +```bash +git add .github/renovate.json5 +git commit -m "ci(renovate): add config to pin and auto-update devcontainer agent tools" +``` + +--- + +### Task 4: Add `claude-code` (both targets) to CI + +**Files:** +- Modify: `.github/workflows/ci.yml` + +**Interfaces:** +- Consumes: nothing from prior tasks (independent). +- Produces: PR-time hadolint + amd64 build&verify for `claude-code` `default` and `sandbox` — the gate that makes Task 3's auto-merge safe. Adds a `target` field to the build matrix. + +- [ ] **Step 1: Add claude-code to the hadolint matrix** + +Find: + +```yaml + dockerfile: + - bun/.devcontainer/Dockerfile + - claude-bun/.devcontainer/Dockerfile + - hugo-bun/.devcontainer/Dockerfile +``` + +Replace with: + +```yaml + dockerfile: + - bun/.devcontainer/Dockerfile + - claude-bun/.devcontainer/Dockerfile + - claude-code/.devcontainer/Dockerfile + - hugo-bun/.devcontainer/Dockerfile +``` + +- [ ] **Step 2: Add the claude-code paths filter** + +Find: + +```yaml + claude-bun: + - 'claude-bun/**' + hugo-bun: + - 'hugo-bun/**' +``` + +Replace with: + +```yaml + claude-bun: + - 'claude-bun/**' + claude-code: + - 'claude-code/**' + hugo-bun: + - 'hugo-bun/**' +``` + +- [ ] **Step 3: Add the CHANGED_CLAUDE_CODE env var** + +Find: + +```yaml + CHANGED_CLAUDE_BUN: ${{ steps.filter.outputs.claude-bun }} + CHANGED_HUGO_BUN: ${{ steps.filter.outputs.hugo-bun }} +``` + +Replace with: + +```yaml + CHANGED_CLAUDE_BUN: ${{ steps.filter.outputs.claude-bun }} + CHANGED_CLAUDE_CODE: ${{ steps.filter.outputs.claude-code }} + CHANGED_HUGO_BUN: ${{ steps.filter.outputs.hugo-bun }} +``` + +- [ ] **Step 4: Extend `add_image` with an optional `target` field** + +Find: + +```bash + add_image() { + local image="$1" context="$2" dockerfile="$3" verify="$4" + INCLUDES=$(echo "$INCLUDES" | jq -c \ + --arg img "$image" \ + --arg ctx "$context" \ + --arg df "$dockerfile" \ + --arg v "$verify" \ + '. + [{"image":$img,"context":$ctx,"dockerfile":$df,"verify":$v}]') + } +``` + +Replace with: + +```bash + add_image() { + local image="$1" context="$2" dockerfile="$3" verify="$4" target="${5:-}" + INCLUDES=$(echo "$INCLUDES" | jq -c \ + --arg img "$image" \ + --arg ctx "$context" \ + --arg df "$dockerfile" \ + --arg v "$verify" \ + --arg tgt "$target" \ + '. + [{"image":$img,"context":$ctx,"dockerfile":$df,"verify":$v,"target":$tgt}]') + } +``` + +(Existing 4-argument calls keep working — `target` defaults to `""`, which `docker/build-push-action` treats as "build the final stage".) + +- [ ] **Step 5: Add the claude-code build entries (both targets)** + +Find: + +```bash + if [ "$CHANGED_CLAUDE_BUN" = "true" ]; then + add_image "claude-bun" \ + "claude-bun/.devcontainer" \ + "claude-bun/.devcontainer/Dockerfile" \ + "bun --version" + fi +``` + +Insert immediately **after** that `fi` (before the `CHANGED_HUGO_BUN` block): + +```bash + if [ "$CHANGED_CLAUDE_CODE" = "true" ]; then + add_image "claude-code" \ + "claude-code/.devcontainer" \ + "claude-code/.devcontainer/Dockerfile" \ + "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" \ + "default" + add_image "claude-code-sandbox" \ + "claude-code/.devcontainer" \ + "claude-code/.devcontainer/Dockerfile" \ + "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" \ + "sandbox" + fi +``` + +(Verify strings copied verbatim from `build-claude-code.yml`.) + +- [ ] **Step 6: Pass the target to the build step** + +Find: + +```yaml + with: + context: ${{ matrix.context }} + file: ${{ matrix.dockerfile }} + platforms: linux/amd64 +``` + +Replace with: + +```yaml + with: + context: ${{ matrix.context }} + file: ${{ matrix.dockerfile }} + target: ${{ matrix.target }} + platforms: linux/amd64 +``` + +- [ ] **Step 7: Lint the workflow** + +Run: `actionlint .github/workflows/ci.yml` +Expected: no output, exit 0. + +- [ ] **Step 8: Dry-run the matrix builder locally** + +Run (reproduces the `add_image` logic with only claude-code changed): + +```bash +INCLUDES="[]" +add_image() { local image="$1" context="$2" dockerfile="$3" verify="$4" target="${5:-}" + INCLUDES=$(echo "$INCLUDES" | jq -c --arg img "$image" --arg ctx "$context" --arg df "$dockerfile" --arg v "$verify" --arg tgt "$target" \ + '. + [{"image":$img,"context":$ctx,"dockerfile":$df,"verify":$v,"target":$tgt}]'); } +add_image "claude-code" "claude-code/.devcontainer" "claude-code/.devcontainer/Dockerfile" "claude --version" "default" +add_image "claude-code-sandbox" "claude-code/.devcontainer" "claude-code/.devcontainer/Dockerfile" "claude --version" "sandbox" +add_image "ralphex-fe" "ralphex-fe" "ralphex-fe/Dockerfile" "bun --version" +echo "$INCLUDES" | jq '{include:.}' +``` +Expected: valid JSON; the two `claude-code*` entries have `"target":"default"` / `"target":"sandbox"`, and `ralphex-fe` has `"target":""`. + +- [ ] **Step 9: Commit** + +```bash +git add .github/workflows/ci.yml +git commit -m "ci: build and verify claude-code (default + sandbox) on PRs" +``` + +--- + +### Task 5: Remove the daily rebuild crons + +**Files:** +- Modify: `.github/workflows/build-claude-code.yml` +- Modify: `.github/workflows/build-ralphex-fe.yml` + +**Interfaces:** +- Consumes: nothing. +- Produces: build workflows triggered only by `push` (path-filtered) and `workflow_dispatch`. + +- [ ] **Step 1: Drop the cron from `build-claude-code.yml`** + +Find: + +```yaml + - "claude-code/.devcontainer/managed-settings.json" + schedule: + - cron: '13 11 * * *' + workflow_dispatch: +``` + +Replace with: + +```yaml + - "claude-code/.devcontainer/managed-settings.json" + workflow_dispatch: +``` + +- [ ] **Step 2: Drop the cron from `build-ralphex-fe.yml`** + +Find: + +```yaml + - 'ralphex-fe/Dockerfile' + - 'ralphex-fe/*.sh' + schedule: + # Daily rebuild to bake in the latest ralphex and rtk releases, which the + # Dockerfile pulls from GitHub "latest" at build time. Offset from + # build-claude-code.yml's 11:13 to avoid hitting the GitHub API at the same minute. + - cron: '41 11 * * *' + workflow_dispatch: +``` + +Replace with: + +```yaml + - 'ralphex-fe/Dockerfile' + - 'ralphex-fe/*.sh' + workflow_dispatch: +``` + +- [ ] **Step 3: Lint both workflows** + +Run: `actionlint .github/workflows/build-claude-code.yml .github/workflows/build-ralphex-fe.yml` +Expected: no output, exit 0. + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/build-claude-code.yml .github/workflows/build-ralphex-fe.yml +git commit -m "ci: drop daily rebuild crons (Renovate triggers rebuilds now)" +``` + +--- + +### Task 6: Correct the "rebuilds daily" documentation + +**Files:** +- Modify: `README.md` +- Modify: `claude-code/README.md` + +**Interfaces:** +- Consumes: nothing. +- Produces: docs consistent with the Renovate-driven rebuild model. + +- [ ] **Step 1: Root README** + +Find: + +``` +Projects consume pre-built images and control tool versions via `.mise.toml`. Rebuilds daily to pick up latest Claude Code. +``` + +Replace with: + +``` +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. +``` + +- [ ] **Step 2: claude-code README — rebuild description** + +Find: + +``` +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. +``` + +Replace with: + +``` +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. +``` + +- [ ] **Step 3: claude-code README — patch-script channel note** + +Find: + +``` +so it's baked into the image and flows through the same daily-rebuild + `initializeCommand` image-pull channel as the rest of the image. +``` + +Replace with: + +``` +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. +``` + +- [ ] **Step 4: Correct the stale comment in `.hadolint.yaml`** + +The `DL3016` ignore block still asserts the old daily-rebuild rationale. The ignore rule stays (other images may still install npm tools unpinned), but the comment is now inaccurate. + +Find: + +```yaml + # Dev tools (Claude Code) intentionally unpinned — images rebuild daily + # to always get latest. Pinning would defeat the purpose. + - DL3016 # pin versions in npm install +``` + +Replace with: + +```yaml + # Dev tools (e.g. Claude Code) are installed via npm; some images pin the + # version via a Renovate-managed ARG, others intentionally don't. + - DL3016 # pin versions in npm install +``` + +- [ ] **Step 5: Confirm no stale wording remains** + +Run: +```bash +grep -rniE 'rebuilds? daily|daily (image )?rebuild' \ + README.md claude-code/README.md ralphex-fe/README.md \ + claude-code/.devcontainer/Dockerfile ralphex-fe/Dockerfile .hadolint.yaml +``` +Expected: no matches (exit 1). + +- [ ] **Step 6: Commit** + +```bash +git add README.md claude-code/README.md .hadolint.yaml +git commit -m "docs: describe Renovate-driven rebuilds instead of daily cron" +``` + +--- + +## Post-implementation manual steps (user, GitHub UI — not code) + +1. **Install the Mend Renovate GitHub App** on the `gatezh/devcontainers` repo (https://github.com/apps/renovate → Configure). Merge the onboarding PR Renovate opens. +2. After onboarding, confirm Renovate's **Dependency Dashboard** issue lists all four tools (`rtk-ai/rtk`, `umputun/ralphex`, `@anthropic-ai/claude-code`, `agent-browser`). If any is missing, the custom-manager regex didn't match — recheck the annotations (Tasks 1–2). +3. *(Optional hardening)* Enable a branch-protection rule on `master` requiring the CI checks, then flip `platformAutomerge` to `true` in the config for faster native GitHub auto-merge. + +## Verification of the end-to-end result (after first real bump) + +- A bump PR appears, groups the changed tool(s), and CI runs hadolint + amd64 build&verify (including `claude-code` default + sandbox). +- On green CI the PR auto-merges; the merge to `master` triggers `build-claude-code.yml` / `build-ralphex-fe.yml`, which build multi-arch and push `:latest`. +- On a day with no upstream release, no PR is opened and no build runs. diff --git a/docs/superpowers/specs/2026-07-20-renovate-tool-updates-design.md b/docs/superpowers/specs/2026-07-20-renovate-tool-updates-design.md new file mode 100644 index 0000000..57ce75a --- /dev/null +++ b/docs/superpowers/specs/2026-07-20-renovate-tool-updates-design.md @@ -0,0 +1,240 @@ +# Renovate-driven rebuilds for `claude-code` & `ralphex-fe` + +- **Date:** 2026-07-20 +- **Status:** Approved (design) — pending implementation plan +- **Branch:** `renovate-agent-tool-updates` + +## Problem + +`build-claude-code.yml` and `build-ralphex-fe.yml` rebuild their images on a daily +`schedule:` cron regardless of whether anything changed. The only reason those +crons exist is that four tools are pulled from *moving* sources at build time: + +| Tool | Source | Images | +|------|--------|--------| +| `rtk` | GitHub `releases/latest` (`rtk-ai/rtk`) | `claude-code`, `ralphex-fe` | +| `ralphex` | GitHub `releases/latest` (`umputun/ralphex`) | `claude-code`, `ralphex-fe` | +| `@anthropic-ai/claude-code` | npm (unpinned `npm install -g`) | `claude-code`, `ralphex-fe` | +| `agent-browser` | npm (`ARG AGENT_BROWSER_VERSION=latest`) | `claude-code` (`default` target only) | + +Everything else (`Bun`, `Hugo`, `Go`, `Docker`, `git-delta`, base images) is +already `ARG`-pinned and only changes on a Dockerfile edit, which has its own +path-filtered `push` trigger. So the scheduled build is pure waste on any day +none of the four tools published a release. + +## Goal + +Rebuild **only when one of the four tracked tools actually publishes a new +version** — no more time-based rebuilds. + +## Non-goals + +- Managing `Bun` / `Hugo` / `Go` / `Docker` / `git-delta` / base images with + Renovate (explicitly out of scope — they stay as they are today, including the + manual `update-and-build-ralphex-fe.yml` version-bump path). +- Adding local GitHub-Actions execution tooling (e.g. `act`). Rejected: the + build workflows' substance is multi-arch `docker buildx` + registry push via + the external reusable workflow `docker/github-builder/...@v1` on native ARM + runners with OIDC — none of which `act` can faithfully emulate. The dev + container already bakes in `hadolint` + `actionlint` for local static checks, + and a real `docker buildx build --platform linux/amd64` reproduces CI's build + job exactly. + +## Decisions (locked with the user) + +1. **Mechanism:** Renovate (the canonical "only rebuild on real dependency + change" pattern) — pin the four tools, let Renovate open bump PRs, and let + the *existing* `push` trigger do the rebuild on merge. The daily crons are + deleted. +2. **Hosting:** Mend-hosted Renovate GitHub App (config in-repo; its PRs trigger + CI, so auto-merge can be gated on green). No self-hosted workflow, no PAT. +3. **Scope:** Only the four moving tools above. +4. **Merge policy:** Auto-merge a bump when CI passes. This requires closing a + pre-existing gap — `claude-code` is currently absent from `ci.yml`, so it has + no PR-time verification. We add it (both targets) so auto-merge is genuinely + gated. + +## Design + +### A. Pin the four tools in the Dockerfiles + +Both `claude-code/.devcontainer/Dockerfile` and `ralphex-fe/Dockerfile` change +their moving downloads to pinned `ARG`s, each preceded by a `# renovate:` +annotation comment that a Renovate custom manager reads. + +**`rtk-download` / `ralphex-download` stages (both files):** replace the +`curl … /releases/latest | jq -r .tag_name | sed 's/^v//'` version resolution +with a pinned ARG used directly in the download URL. `jq` is no longer needed in +these stages (`apk add --no-cache curl` only). Example (rtk): + +```dockerfile +FROM alpine:3.21 AS rtk-download +RUN apk add --no-cache curl +# renovate: datasource=github-releases depName=rtk-ai/rtk +ARG RTK_VERSION= +RUN set -eux; \ + ARCH="$(uname -m)"; \ + case "$ARCH" in \ + x86_64) RTK_TARGET="x86_64-unknown-linux-musl" ;; \ + aarch64) RTK_TARGET="aarch64-unknown-linux-gnu" ;; \ + esac; \ + curl -fsSL -o /tmp/rtk.tar.gz \ + "https://github.com/rtk-ai/rtk/releases/download/v${RTK_VERSION}/rtk-${RTK_TARGET}.tar.gz"; \ + tar -xzf /tmp/rtk.tar.gz -C /usr/local/bin rtk +``` + +`ralphex` is analogous (`depName=umputun/ralphex`, URL keeps `v${VERSION}` in the +path and `${VERSION}` in the filename). + +**`@anthropic-ai/claude-code` (both files):** add an ARG + annotation before the +install and pin the version: + +```dockerfile +# renovate: datasource=npm depName=@anthropic-ai/claude-code +ARG CLAUDE_CODE_VERSION= +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} +``` + +(The ARG is declared inside the stage that runs the install — `base` in +`claude-code`, the final image stage in `ralphex-fe`.) + +**`agent-browser` (`claude-code` `default` target only):** the existing +`ARG AGENT_BROWSER_VERSION=latest` becomes a pinned version with an annotation; +the `npm install -g agent-browser@${AGENT_BROWSER_VERSION}` line is unchanged: + +```dockerfile +# renovate: datasource=npm depName=agent-browser +ARG AGENT_BROWSER_VERSION= +``` + +ARG defaults are set to the **current** latest versions at implementation time +(resolved with the same API/registry calls the Dockerfiles use today), so the +first post-merge build produces an image equivalent to today's. + +### B. `.github/renovate.json5` (new) + +Placed under `.github/` to keep the repo root clean (a standard Renovate config +location). Uses JSON5 (comments allowed, matching the repo's JSONC style). Contents: + +- `extends: ["config:recommended"]`. +- One **custom manager** (regex) matching the `# renovate:` annotation + the + `ARG …_VERSION=` line across both Dockerfiles, capturing `datasource`, + `depName`, and `currentValue`. +- A **packageRules** entry that: + - strips the leading `v` from the two `github-releases` deps via + `extractVersion: "^v?(?.+)$"` so the datasource tag (`v0.11.0`) + compares against the bare ARG value (`0.11.0`); + - **groups** all four tools into a single PR (`groupName: "devcontainer agent + tools"`) so tools that co-release rebuild together (one PR → one rebuild), + matching today's single daily refresh; + - enables auto-merge (`automerge: true`, `automergeType: "pr"`, + `platformAutomerge: true`). + +> **Verify during implementation** against official Renovate docs: exact +> `customManagers[].matchStrings` capture-group syntax, whether `extractVersion` +> is best placed in the custom manager vs. a `packageRules` entry, and the +> config-file discovery location. Use the `sandbox-fetch-docs` skill. + +### C. `ci.yml` — add `claude-code` (both targets) + +`claude-code` is currently in neither the hadolint matrix nor the +`build-and-verify` matrix, so PRs touching it get no build verification. To make +auto-merge safe: + +1. **hadolint matrix:** add `claude-code/.devcontainer/Dockerfile`. +2. **`detect-changes` filters:** add `claude-code: - 'claude-code/**'` and a + corresponding `CHANGED_CLAUDE_CODE` env var in the matrix-builder step. +3. **Matrix builder:** extend the `add_image` helper with a fifth `target` + field (empty for existing single-target images). Emit two entries for + `claude-code`: + - `claude-code` → `target: default`, reusing the **exact** default verify + string from `build-claude-code.yml`. + - `claude-code-sandbox` → `target: sandbox`, reusing the **exact** sandbox + verify string from `build-claude-code.yml`. +4. **`build-and-verify` job:** pass `target: ${{ matrix.target }}` to + `docker/build-push-action`. Empty target builds the Dockerfile's final stage + (unchanged behavior for the other images). Cache scopes stay keyed on + `matrix.image`, so `claude-code` and `claude-code-sandbox` get distinct + scopes. + +CI remains amd64-only (native, no QEMU) — sufficient to verify a version bump; +arm64 is exercised post-merge by `build-claude-code.yml` on native runners. + +### D. Remove the daily crons + +- `build-claude-code.yml`: delete the `schedule:` block (cron `13 11 * * *`). +- `build-ralphex-fe.yml`: delete the `schedule:` block (cron `41 11 * * *`) and + its explanatory comment. +- Both keep `push` (path-filtered, unchanged — this is what rebuilds on a merged + bump) and `workflow_dispatch` (manual force build). + +### E. Comment & docs cleanup (bundle-adjacent consistency) + +- `claude-code/.devcontainer/Dockerfile`: update the "refreshed on each daily + image rebuild" (near the rtk/ralphex `COPY`) and "Auto-updates don't matter + here — the image rebuilds daily" (near the claude-code install) comments to + describe pinned + Renovate-managed versions. +- `ralphex-fe/Dockerfile`: note the four tools are Renovate-managed where the + existing comments describe them as "latest". +- Audit `README.md` (root), `claude-code/README.md`, `ralphex-fe/README.md` for + "rebuilt daily" / "daily rebuild" wording and correct it. + +## Runtime flow (end state) + +``` +Renovate scan → new version of a tracked tool detected + → grouped PR bumps the ARG(s) in the Dockerfile(s) + → ci.yml: hadolint + actionlint + amd64 build&verify of affected image(s) + (now including claude-code default + sandbox) + → CI green? → Renovate auto-merges → push to master + → build-{image}.yml path filter fires → multi-arch build + verify + push :latest + (+ date / sha tags) +``` + +No release → no PR → no rebuild. + +## Error handling / safety + +This is **safer** than the current cron: + +- **Broken / yanked upstream release:** the CI amd64 build fails → PR does not + auto-merge → `:latest` stays intact. Today's cron would build and push a + broken `:latest`. +- **Renovate can't resolve a datasource:** no PR is opened; the image is + unchanged. +- **`v`-prefix mismatch:** handled by `extractVersion`, so ARGs stay bare semver + and the existing `v${VERSION}` URL construction is unchanged. +- **Shared tools (`rtk`/`ralphex`/`claude-code`) live in both Dockerfiles:** the + custom manager matches every occurrence, so a bump updates both to the same + version and both images rebuild — keeping them in sync. + +## Verification plan + +- `npx --yes renovate-config-validator` on the new config file. +- `hadolint` on both changed Dockerfiles; `actionlint` on `ci.yml` and both + build workflows (local, pre-PR — per project convention; the dev container + already ships both linters). +- Confirm the `ci.yml` matrix builds `claude-code` `default` + `sandbox` amd64 + and that both verify strings pass against a locally built image. +- Post-merge manual check: watch the first Renovate PR detect the four deps and + auto-merge on green, then confirm the `push`-triggered multi-arch build runs. + +## One-time manual step (user, GitHub UI — not code) + +Install the **Mend Renovate app** on the repository and merge its onboarding PR. +Everything else in this design is code on this branch. + +## Files touched + +- `claude-code/.devcontainer/Dockerfile` (pin 4 → 3 tools + agent-browser; comments) +- `ralphex-fe/Dockerfile` (pin rtk/ralphex/claude-code; comments) +- `.github/renovate.json5` (new) +- `.github/workflows/ci.yml` (add claude-code, both targets; `target` matrix field) +- `.github/workflows/build-claude-code.yml` (remove `schedule:`) +- `.github/workflows/build-ralphex-fe.yml` (remove `schedule:`) +- `README.md`, `claude-code/README.md`, `ralphex-fe/README.md` (wording audit) + +## Unaffected + +- `update-and-build-ralphex-fe.yml` (manual Bun/Hugo bump path — untouched). +- All other images and workflows. diff --git a/ralphex-fe/Dockerfile b/ralphex-fe/Dockerfile index e232fa6..3741705 100644 --- a/ralphex-fe/Dockerfile +++ b/ralphex-fe/Dockerfile @@ -44,30 +44,32 @@ RUN set -eux; \ # ── RTK (token-optimized CLI proxy for Claude Code) ───────────────────────── # Pattern from: .devcontainer/Dockerfile +# Version pinned and kept up to date by Renovate (see .github/renovate.json5). FROM alpine:3.21 AS rtk-download -RUN apk add --no-cache curl jq +RUN apk add --no-cache curl +# renovate: datasource=github-releases depName=rtk-ai/rtk +ARG RTK_VERSION=0.43.0 RUN set -eux; \ ARCH="$(uname -m)"; \ case "$ARCH" in \ x86_64) RTK_TARGET="x86_64-unknown-linux-musl" ;; \ aarch64) RTK_TARGET="aarch64-unknown-linux-gnu" ;; \ esac; \ - RTK_VERSION=$(curl -fsSL https://api.github.com/repos/rtk-ai/rtk/releases/latest \ - | jq -r '.tag_name' | sed 's/^v//'); \ curl -fsSL -o /tmp/rtk.tar.gz \ "https://github.com/rtk-ai/rtk/releases/download/v${RTK_VERSION}/rtk-${RTK_TARGET}.tar.gz"; \ tar -xzf /tmp/rtk.tar.gz -C /usr/local/bin rtk -# ── Ralphex binary (latest from GitHub Releases) ──────────────────────────── +# ── Ralphex binary (pinned; Renovate-managed) ─────────────────────────────── # Pattern from: claude-code/.devcontainer/Dockerfile +# Version pinned and kept up to date by Renovate (see .github/renovate.json5). FROM alpine:3.21 AS ralphex-download -RUN apk add --no-cache curl jq +RUN apk add --no-cache curl +# renovate: datasource=github-releases depName=umputun/ralphex +ARG RALPHEX_VERSION=1.6.0 RUN set -eux; \ ARCH="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"; \ - VERSION=$(curl -fsSL https://api.github.com/repos/umputun/ralphex/releases/latest \ - | jq -r '.tag_name' | sed 's/^v//'); \ curl -fsSL -o /tmp/ralphex.tar.gz \ - "https://github.com/umputun/ralphex/releases/download/v${VERSION}/ralphex_${VERSION}_linux_${ARCH}.tar.gz"; \ + "https://github.com/umputun/ralphex/releases/download/v${RALPHEX_VERSION}/ralphex_${RALPHEX_VERSION}_linux_${ARCH}.tar.gz"; \ tar -xzf /tmp/ralphex.tar.gz -C /usr/local/bin ralphex # ═════════════════════════════════════════════════════════════════════════════ @@ -156,8 +158,11 @@ WORKDIR /workspace # ── Claude Code CLI ────────────────────────────────────────────────────────── # npm install (not native installer) to avoid rate-limiting in parallel Docker builds. # See: claude-code/.devcontainer/Dockerfile for rationale. +# Version pinned and kept up to date by Renovate (see .github/renovate.json5). +# renovate: datasource=npm depName=@anthropic-ai/claude-code +ARG CLAUDE_CODE_VERSION=2.1.216 RUN --mount=type=cache,target=/root/.npm \ - npm install -g @anthropic-ai/claude-code + npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} # ── Bun ────────────────────────────────────────────────────────────────────── ENV BUN_INSTALL=/usr/local/bun