Skip to content
Open
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
5 changes: 3 additions & 2 deletions .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// 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
// Pin + auto-update the five 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.
Expand All @@ -28,7 +28,7 @@
extractVersion: '^v?(?<version>.+)$',
},
{
// Group the four tools into one PR and auto-merge once CI passes.
// Group the five tools into one PR and auto-merge once CI passes.
//
// Relies on Renovate's default platformAutomerge:true — GitHub's native
// auto-merge merges on green with no second Renovate run. The previous
Expand All @@ -44,6 +44,7 @@
'rtk-ai/rtk',
'umputun/ralphex',
'@anthropic-ai/claude-code',
'happy',
'agent-browser',
],
groupName: 'devcontainer agent tools',
Expand Down
31 changes: 31 additions & 0 deletions .github/verify-commands.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$comment": [
"Single source of truth for post-build image verification, keyed by published",
"image name (ghcr.io/gatezh/devcontainers/<key>).",

"Read by .github/workflows/ci.yml (pull-request builds) and by",
".github/workflows/build-claude-code.yml (post-publish verification). Before",
"this file the claude-code commands lived in both, in nine copies of three",
"unique strings, and #131 shipped an update to one file and not the other.",

"A command must be able to FAIL. Assert on something that is absent from an",
"image lacking the tool, and check that it does: a CLI that exits 0 whatever",
"you pass it verifies nothing. happy is the worked example — every one of its",
"subcommands exits 0, including unknown flags, so claude-code-happy asserts",
"the installed package with `npm ls -g` instead of running the binary.",

"The remaining build-*.yml workflows still pass their own verify-command to",
"reusable-docker-build.yml. Migrating them is mechanical but touches publish",
"pipelines this repo cannot exercise from a pull request, so it is deliberately",
"left for a change of its own."
],

"bun": "bun --version",
"claude-bun": "bun --version",
"claude-code": "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",
"claude-code-sandbox": "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",
"claude-code-happy": "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 && npm ls -g --depth=0 happy",
"hugo-bun": "bun --version && hugo version",
"hugo-bun-node": "bun --version && hugo version && node --version",
"ralphex-fe": "bun --version && hugo version && /srv/ralphex --version && rtk --version"
}
54 changes: 47 additions & 7 deletions .github/workflows/build-claude-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,39 +67,79 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

build-happy:
uses: docker/github-builder/.github/workflows/build.yml@v1
permissions:
contents: read
packages: write
id-token: write
with:
output: image
push: true
target: happy
context: claude-code/.devcontainer

platforms: linux/amd64,linux/arm64
meta-images: ghcr.io/gatezh/devcontainers/claude-code-happy
meta-tags: |
type=raw,value=latest
type=sha,prefix=
type=raw,value={{date 'YYYYMMDD'}}
secrets:
registry-auths: |
- registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

verify:
name: Verify ${{ matrix.image-suffix }} (${{ matrix.arch }})
needs: [build-default, build-sandbox]
needs: [build-default, build-sandbox, build-happy]
strategy:
fail-fast: false
matrix:
include:
- image-suffix: claude-code
verify-command: "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"
runner: ubuntu-24.04
arch: amd64
- image-suffix: claude-code
verify-command: "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"
runner: ubuntu-24.04-arm
arch: arm64
- image-suffix: claude-code-sandbox
verify-command: "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"
runner: ubuntu-24.04
arch: amd64
- image-suffix: claude-code-sandbox
verify-command: "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"
runner: ubuntu-24.04-arm
arch: arm64
- image-suffix: claude-code-happy
runner: ubuntu-24.04
arch: amd64
- image-suffix: claude-code-happy
runner: ubuntu-24.04-arm
arch: arm64
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# The command comes from .github/verify-commands.json, the same file
# ci.yml reads, so pull-request and post-publish checks cannot drift.
# Reading it into a shell variable also keeps the command out of the
# `run:` script that Actions expands, so nothing from the workflow
# context is interpolated into shell.
- name: Verify image
env:
IMAGE: ${{ matrix.image-suffix }}
run: |
docker pull ghcr.io/gatezh/devcontainers/${{ matrix.image-suffix }}:latest
docker run --rm ghcr.io/gatezh/devcontainers/${{ matrix.image-suffix }}:latest bash -c "${{ matrix.verify-command }}"
VERIFY=$(jq -er --arg k "$IMAGE" '.[$k]' .github/verify-commands.json) || {
echo "::error::no verify command for '$IMAGE' in .github/verify-commands.json"
exit 1
}
docker pull "ghcr.io/gatezh/devcontainers/${IMAGE}:latest"
docker run --rm "ghcr.io/gatezh/devcontainers/${IMAGE}:latest" bash -c "$VERIFY"
42 changes: 29 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,29 @@ jobs:
uses: dorny/paths-filter@v4
id: filter
with:
# Each image also watches .github/verify-commands.json: editing a
# verify command should re-run it against a real image, not sit
# unexercised until the next Dockerfile change. A manifest edit
# therefore rebuilds every image — rare enough to be worth it.
filters: |
bun:
- 'bun/**'
- '.github/verify-commands.json'
claude-bun:
- 'claude-bun/**'
- '.github/verify-commands.json'
claude-code:
- 'claude-code/**'
- '.github/verify-commands.json'
hugo-bun:
- 'hugo-bun/**'
- '.github/verify-commands.json'
hugo-bun-node:
- 'hugo-bun-node/**'
- '.github/verify-commands.json'
ralphex-fe:
- 'ralphex-fe/**'
- '.github/verify-commands.json'

- name: Build matrix from changes
id: set-matrix
Expand All @@ -88,8 +98,17 @@ jobs:
run: |
INCLUDES="[]"

# Verify commands live in .github/verify-commands.json so that this
# workflow and build-claude-code.yml cannot drift apart. Looking the
# command up here (rather than passing it in) means an image added
# without an entry fails the job instead of silently verifying nothing.
add_image() {
local image="$1" context="$2" dockerfile="$3" verify="$4" target="${5:-}"
local image="$1" context="$2" dockerfile="$3" target="${4:-}"
local verify
verify=$(jq -er --arg k "$image" '.[$k]' .github/verify-commands.json) || {
echo "::error::no verify command for '$image' in .github/verify-commands.json"
exit 1
}
INCLUDES=$(echo "$INCLUDES" | jq -c \
--arg img "$image" \
--arg ctx "$context" \
Expand All @@ -102,49 +121,46 @@ jobs:
if [ "$CHANGED_BUN" = "true" ]; then
add_image "bun" \
"bun/.devcontainer" \
"bun/.devcontainer/Dockerfile" \
"bun --version"
"bun/.devcontainer/Dockerfile"
fi

if [ "$CHANGED_CLAUDE_BUN" = "true" ]; then
add_image "claude-bun" \
"claude-bun/.devcontainer" \
"claude-bun/.devcontainer/Dockerfile" \
"bun --version"
"claude-bun/.devcontainer/Dockerfile"
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"
add_image "claude-code-happy" \
"claude-code/.devcontainer" \
"claude-code/.devcontainer/Dockerfile" \
"happy"
fi

if [ "$CHANGED_HUGO_BUN" = "true" ]; then
add_image "hugo-bun" \
"hugo-bun/.devcontainer" \
"hugo-bun/.devcontainer/Dockerfile" \
"bun --version && hugo version"
"hugo-bun/.devcontainer/Dockerfile"
fi

if [ "$CHANGED_HUGO_BUN_NODE" = "true" ]; then
add_image "hugo-bun-node" \
"hugo-bun-node/.devcontainer" \
"hugo-bun-node/.devcontainer/Dockerfile" \
"bun --version && hugo version && node --version"
"hugo-bun-node/.devcontainer/Dockerfile"
fi

if [ "$CHANGED_RALPHEX" = "true" ]; then
add_image "ralphex-fe" \
"ralphex-fe" \
"ralphex-fe/Dockerfile" \
"bun --version && hugo version && /srv/ralphex --version && rtk --version"
"ralphex-fe/Dockerfile"
fi

if [ "$INCLUDES" = "[]" ]; then
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/cleanup-claude-code-ghcr.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Manually-triggered cleanup of old GHCR versions for the claude-code images.
#
# Scope: ghcr.io/gatezh/devcontainers/claude-code and
# ghcr.io/gatezh/devcontainers/claude-code-sandbox only.
# Scope: ghcr.io/gatezh/devcontainers/claude-code,
# ghcr.io/gatezh/devcontainers/claude-code-sandbox and
# ghcr.io/gatezh/devcontainers/claude-code-happy only.
# Other packages in this repo (bun, hugo-bun, hugo-bun-node, ralphex-fe,
# claude-bun) are unreachable from this workflow.
#
Expand Down Expand Up @@ -36,7 +37,7 @@ jobs:
strategy:
fail-fast: false
matrix:
package: [claude-code, claude-code-sandbox]
package: [claude-code, claude-code-sandbox, claude-code-happy]
steps:
- name: Delete old container versions
uses: dataaxiom/ghcr-cleanup-action@v1
Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This repository contains Dockerfiles for custom Docker images hosted on GitHub C

### Devcontainer Images

- **[claude-code](./claude-code/README.md)** - Shared Claude Code devcontainer image (default + sandbox variants)
- **[claude-code](./claude-code/README.md)** - Shared Claude Code devcontainer image (default + sandbox + happy variants)
- **[bun](./bun/README.md)** - Bun development container
- **[claude-bun](./claude-bun/README.md)** - Claude Code development container with firewall sandbox
- **[hugo-bun](./hugo-bun/README.md)** - Hugo Extended + Bun development container
Expand Down 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 when its pinned tools receive a new release (managed by Renovate), not on a schedule.
Shared devcontainer base image for Claude Code projects. Three variants from a single multi-stage Dockerfile: **default** (full dev environment with agent-browser), **sandbox** (network-restricted with iptables firewall), and **happy** (default plus the happy CLI for phone/web remote control). 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 All @@ -55,6 +55,12 @@ Shared devcontainer base image for Claude Code projects. Two variants from a sin
"image": "ghcr.io/gatezh/devcontainers/claude-code-sandbox:latest",
"capAdd": ["NET_ADMIN", "NET_RAW"]
}

// Happy variant — default plus the happy CLI. ~785 MB larger than default,
// so only worth pulling if you actually pair a phone or the web app.
{
"image": "ghcr.io/gatezh/devcontainers/claude-code-happy:latest"
}
```

See the [claude-code README](./claude-code/README.md) for full setup guide.
Expand Down Expand Up @@ -155,7 +161,7 @@ Images from this repository are built and published to GitHub Container Registry
### Automatically, via Renovate

The agent tooling in the `claude-code` and `ralphex-fe` images — `rtk`, `ralphex`, the Claude Code
CLI, and `agent-browser` — is pinned as `ARG`s carrying `# renovate:` annotations. Renovate watches
CLI, `agent-browser`, and `happy` — is pinned as `ARG`s carrying `# renovate:` annotations. Renovate watches
their releases and opens a single grouped bump PR when one ships; CI verifies it, it auto-merges, and
that merge rebuilds the affected images. No upstream release means no PR and no rebuild. Scope and
grouping live in [`.github/renovate.json5`](./.github/renovate.json5); the Dependency Dashboard
Expand Down
Loading