Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/renovate.json5
Original file line number Diff line number Diff line change
@@ -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=(?<datasource>[a-z-]+) depName=(?<depName>\\S+)\\s+ARG [A-Z_]+_VERSION=(?<currentValue>\\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?(?<version>.+)$',
},
{
// 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,
},
],
}
2 changes: 0 additions & 2 deletions .github/workflows/build-claude-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/build-ralphex-fe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
23 changes: 21 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -67,6 +68,8 @@ jobs:
- 'bun/**'
claude-bun:
- 'claude-bun/**'
claude-code:
- 'claude-code/**'
hugo-bun:
- 'hugo-bun/**'
hugo-bun-node:
Expand All @@ -79,20 +82,22 @@ 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 }}
run: |
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
Expand All @@ -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" \
Expand Down Expand Up @@ -164,6 +182,7 @@ jobs:
with:
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
target: ${{ matrix.target }}
platforms: linux/amd64
load: true
tags: ${{ matrix.image }}:test
Expand Down
4 changes: 2 additions & 2 deletions .hadolint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**

Expand Down
27 changes: 17 additions & 10 deletions claude-code/.devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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" \
Expand Down
4 changes: 2 additions & 2 deletions claude-code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`

Expand Down
Loading