From 9037f50cb313438df1ecbcf0771b668560cafd93 Mon Sep 17 00:00:00 2001 From: Serge Gatezh <2880401+gatezh@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:13:24 -0600 Subject: [PATCH 1/4] chore(ci): bump action versions in reusable workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update checkout@v4→v6, setup-qemu@v3→v4, setup-buildx@v3→v4, login@v3→v4, metadata@v5→v6, build-push@v5→v7. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/reusable-docker-build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index 4037138..24eeecf 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -42,7 +42,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Generate image tags id: tags @@ -60,13 +60,13 @@ jobs: echo "$TAGS" | tr ',' '\n' - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -74,12 +74,12 @@ jobs: - name: Extract metadata for Docker id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ steps.tags.outputs.namespace }}/${{ inputs.image-name }} - name: Build and push Docker image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: ${{ inputs.context }} file: ${{ inputs.dockerfile }} From 3077dbce5071746bef1149adb53e0198af1d3ec2 Mon Sep 17 00:00:00 2001 From: Serge Gatezh <2880401+gatezh@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:17:18 -0600 Subject: [PATCH 2/4] feat: add claude-code shared devcontainer image Multi-stage Dockerfile with two build targets: - default: full dev environment with agent-browser and passwordless sudo - sandbox: network-restricted with iptables firewall packages Replaces the outdated devcontainer-claude-bun image. Projects consume these pre-built images and control tool versions via .mise.toml. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/build-claude-code.yml | 94 +++++++++++++ claude-code/.devcontainer/Dockerfile | 180 ++++++++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 .github/workflows/build-claude-code.yml create mode 100644 claude-code/.devcontainer/Dockerfile diff --git a/.github/workflows/build-claude-code.yml b/.github/workflows/build-claude-code.yml new file mode 100644 index 0000000..83665f1 --- /dev/null +++ b/.github/workflows/build-claude-code.yml @@ -0,0 +1,94 @@ +name: Build claude-code + +on: + push: + branches: [master] + paths: + - "claude-code/.devcontainer/Dockerfile" + workflow_dispatch: + +permissions: + contents: read + packages: write + attestations: write + id-token: write + +env: + REGISTRY: ghcr.io + +concurrency: + group: build-claude-code-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-push: + name: Build & Push (${{ matrix.target }}) + runs-on: ubuntu-24.04 + strategy: + matrix: + include: + - target: default + image-suffix: claude-code + verify-command: "bun --version || true && claude --version && mise --version && fish --version" + - target: sandbox + image-suffix: claude-code-sandbox + verify-command: "claude --version && mise --version && fish --version && iptables --version" + steps: + - uses: actions/checkout@v6 + + - name: Lowercase image base + id: repo + run: echo "image_base=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ steps.repo.outputs.image_base }}/${{ matrix.image-suffix }} + tags: | + type=raw,value=latest + type=sha,prefix= + + - name: Build and push + id: push + uses: docker/build-push-action@v7 + with: + context: claude-code/.devcontainer + file: claude-code/.devcontainer/Dockerfile + target: ${{ matrix.target }} + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ matrix.target }} + cache-to: type=gha,mode=max,scope=${{ matrix.target }} + + - name: Generate artifact attestation + uses: actions/attest@v4 + with: + subject-name: ${{ steps.repo.outputs.image_base }}/${{ matrix.image-suffix }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + + - name: Verify image (amd64) + run: | + docker pull --platform linux/amd64 ${{ steps.repo.outputs.image_base }}/${{ matrix.image-suffix }}:latest + docker run --rm --platform linux/amd64 ${{ steps.repo.outputs.image_base }}/${{ matrix.image-suffix }}:latest bash -c "${{ matrix.verify-command }}" + + - name: Verify image (arm64) + run: | + docker pull --platform linux/arm64 ${{ steps.repo.outputs.image_base }}/${{ matrix.image-suffix }}:latest + docker run --rm --platform linux/arm64 ${{ steps.repo.outputs.image_base }}/${{ matrix.image-suffix }}:latest bash -c "${{ matrix.verify-command }}" diff --git a/claude-code/.devcontainer/Dockerfile b/claude-code/.devcontainer/Dockerfile new file mode 100644 index 0000000..aa10114 --- /dev/null +++ b/claude-code/.devcontainer/Dockerfile @@ -0,0 +1,180 @@ +# ═══════════════════════════════════════════════════════════════════════════════ +# Shared devcontainer image — two build targets: +# default — full dev environment with agent-browser and passwordless sudo +# sandbox — network-restricted environment with firewall packages +# +# Projects consume these pre-built images and run `mise install` at container +# creation to install their specific tool versions (Bun, Hugo, etc.). +# +# Build: +# docker build --target default -t claude-code:default . +# docker build --target sandbox -t claude-code:sandbox . +# ═══════════════════════════════════════════════════════════════════════════════ + +# ─── BASE ───────────────────────────────────────────────────────────────────── +FROM node:22-trixie-slim AS base + +ARG GIT_DELTA_VERSION=0.18.2 +ARG PLAYWRIGHT_VERSION=1.50.1 +ARG AGENT_BROWSER_VERSION=0.14.0 + +# System packages shared by all targets +# - ca-certificates: SSL/TLS for HTTPS connections +# - curl/wget: downloading tools and installers +# - fish: interactive shell (built-in syntax highlighting, autosuggestions, completions) +# - fzf: fuzzy finder (fish integration via fzf.fish or built-in) +# - gh: GitHub CLI +# - git: version control +# - gnupg2: package signing verification +# - jq: JSON processing (firewall script, onboarding patch) +# - less: pager for git delta output +# - man-db: manual pages +# - nano/vim: editors +# - procps: ps, top (debugging) +# - sudo: privilege escalation for firewall setup +# - unzip: extracting archives +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + fish \ + fzf \ + gh \ + git \ + gnupg2 \ + jq \ + less \ + man-db \ + nano \ + procps \ + sudo \ + unzip \ + vim \ + wget \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# npm global directory with proper permissions for node user +# Pre-create /lib to prevent "ENOENT" errors during npx commands +RUN mkdir -p /usr/local/share/npm-global/lib \ + && chown -R node:node /usr/local/share/npm-global + +ENV DEVCONTAINER=true + +# Create workspace, Claude config, and node_modules volume mount points. +# Pre-creating these dirs with node ownership ensures Docker's volume +# population seeds fresh named volumes with correct permissions. +# Standard monorepo layout shared across projects. +# See: https://docs.docker.com/engine/storage/volumes/#populate-a-volume-using-a-container +RUN mkdir -p /workspace/node_modules \ + /workspace/services/api/node_modules \ + /workspace/services/app/node_modules \ + /workspace/services/www/node_modules \ + /workspace/packages/shared/node_modules \ + /workspace/packages/database/node_modules \ + /home/node/.claude \ + /home/node/.local/share/fish \ + && chown -R node:node /workspace /home/node/.claude /home/node/.local + +WORKDIR /workspace + +# Install git-delta (better git diffs) +RUN ARCH=$(dpkg --print-architecture) \ + && wget -q "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" \ + && dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" \ + && rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" + +# ── Non-root user setup ────────────────────────────────────────────────────── +USER node + +ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global +ENV PATH=$PATH:/usr/local/share/npm-global/bin +ENV SHELL=/usr/bin/fish +ENV EDITOR=nano +ENV VISUAL=nano + +# ── Starship + Mise (install as root, configure as node) ─────────────────────── +USER root +RUN curl -sS https://starship.rs/install.sh | sh -s -- --yes +RUN curl https://mise.run | sh \ + && cp /root/.local/bin/mise /usr/local/bin/mise + +# Configure starship and fish shell. +# Mise is installed as a tool manager — projects run `mise install` at container +# creation to install their specific tool versions from .mise.toml. +# Node is NOT installed via mise — it's provided by the base image; +# mise shims would shadow the base image's npx, breaking Playwright installation. +USER node +RUN mkdir -p /home/node/.config/fish \ + && starship preset no-runtime-versions -o /home/node/.config/starship.toml \ + && printf '%s\n' 'set -g fish_greeting' 'starship init fish | source' > /home/node/.config/fish/config.fish +ENV PATH="/home/node/.local/share/mise/shims:$PATH" +ENV MISE_TRUSTED_CONFIG_PATHS="/workspace" + +# ── Playwright (headless shell for browser testing) ─────────────────────────── +# Browser binary baked at a pinned version (PLAYWRIGHT_VERSION build arg). +# If a project's @playwright/test version differs from PLAYWRIGHT_VERSION, +# Playwright auto-downloads the correct browser on first test run (~10s). +# This is a best-effort optimization, not a hard contract. +# Using --only-shell for smaller image (agent-browser installs full chromium separately). +USER root +RUN npx -y playwright@${PLAYWRIGHT_VERSION} install-deps chromium +USER node +RUN npx -y playwright@${PLAYWRIGHT_VERSION} install --only-shell + +# ── Claude Code CLI (native installer) ─────────────────────────────────────── +# Native installer replaces deprecated npm method (npm install -g @anthropic-ai/claude-code) +# See: https://code.claude.com/docs/en/getting-started +# The installer places the binary in ~/.local/bin/ (previously ~/.claude/bin/). +# Runtime volume mounts over ~/.claude/ (for config persistence) could shadow it, +# so copy to a system path to ensure the binary survives volume mounts. +RUN curl -fsSL https://claude.ai/install.sh | bash +USER root +RUN if [ -d /home/node/.local/bin ] && ls /home/node/.local/bin/claude* >/dev/null 2>&1; then \ + cp /home/node/.local/bin/claude* /usr/local/bin/; \ + elif [ -d /home/node/.claude/bin ]; then \ + cp /home/node/.claude/bin/* /usr/local/bin/; \ + else \ + echo "ERROR: Claude Code binary not found in ~/.local/bin/ or ~/.claude/bin/" && exit 1; \ + fi +USER node + +# ─── DEFAULT — full dev environment ─────────────────────────────────────────── +FROM base AS default + +# Passwordless sudo for node user — standard practice for devcontainer images. +# Required by the canonical "sudo chown" pattern for named volume ownership. +# See: https://code.visualstudio.com/remote/advancedcontainers/improve-performance +USER root +RUN echo "node ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/node-nopasswd \ + && chmod 0440 /etc/sudoers.d/node-nopasswd +USER node + +# agent-browser: headless browser automation for AI agents +# Installs its own bundled playwright-core + full chromium (separate from Playwright above) +# Requires root for system deps (apt-get install triggered by --with-deps) +USER root +RUN npm install -g agent-browser@${AGENT_BROWSER_VERSION} \ + && $(npm root -g)/agent-browser/node_modules/.bin/playwright-core install --with-deps chromium \ + && chown -R node:node /usr/local/share/npm-global +USER node + +# ─── SANDBOX — network-restricted environment ───────────────────────────────── +FROM base AS sandbox + +# Firewall packages (not needed in default target) +USER root +RUN apt-get update && apt-get install -y --no-install-recommends \ + iptables \ + ipset \ + iproute2 \ + dnsutils \ + aggregate \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Firewall sudo rule for node user. +# The init-firewall.sh script is NOT baked into the image — each project +# mounts its own script via bind mount in devcontainer.json: +# "source=${localWorkspaceFolder}/.devcontainer/claude-sandbox/init-firewall.sh,target=/usr/local/bin/init-firewall.sh,type=bind" +# This allows different projects to define their own domain allowlists. +RUN echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall \ + && chmod 0440 /etc/sudoers.d/node-firewall +USER node From 0eb4da78a42f1398430ab7e56824c97978e49932 Mon Sep 17 00:00:00 2001 From: Serge Gatezh <2880401+gatezh@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:25:15 -0600 Subject: [PATCH 3/4] docs: add README for claude-code shared devcontainer image Covers image variants, what's included, project setup guide with devcontainer.json examples for both default and sandbox, supporting files, workspace layout, Playwright version strategy, and build args. Co-Authored-By: Claude Opus 4.6 (1M context) --- claude-code/README.md | 261 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 claude-code/README.md diff --git a/claude-code/README.md b/claude-code/README.md new file mode 100644 index 0000000..a4522bc --- /dev/null +++ b/claude-code/README.md @@ -0,0 +1,261 @@ +# claude-code + +Shared devcontainer image for Claude Code development environments. Two variants from a single multi-stage Dockerfile: **default** (full dev environment) and **sandbox** (network-restricted). + +Projects consume these pre-built images and control their own tool versions via `.mise.toml`. + +## Image Variants + +| Variant | Image | Use Case | +|---------|-------|----------| +| **default** | `ghcr.io/gatezh/devcontainer-images/claude-code:latest` | Full dev environment with agent-browser and passwordless sudo | +| **sandbox** | `ghcr.io/gatezh/devcontainer-images/claude-code-sandbox:latest` | Network-restricted environment with iptables firewall packages | + +## What's Included + +| Layer | What | Why | +|-------|------|-----| +| OS | `node:22-trixie-slim` + system packages | Node is needed during the build (Playwright, npm globals) | +| Shell | Fish, Starship, fzf | Built-in syntax highlighting, autosuggestions, completions | +| Tools | git-delta, gh CLI, jq, nano, vim, wget, unzip, less, man-db, procps | Standard dev utilities | +| Mise | The tool manager itself (not the tools) | Projects run `mise install` at container creation for their tool versions | +| Claude Code | Native CLI installer | Binary copied to `/usr/local/bin/` to survive volume mounts | +| Playwright | System deps + browser binary at a pinned version | See [Playwright version strategy](#playwright-version-strategy) | + +**Default-only:** passwordless sudo, agent-browser + full Chromium + +**Sandbox-only:** iptables, ipset, iproute2, dnsutils, aggregate, firewall sudo rule + +## Multi-platform Support + +Both variants are built for: +- `linux/amd64` (x86_64) +- `linux/arm64` (ARM64/Apple Silicon) + +## Image Tags + +- `:latest` — most recent build +- `:` — pinned to a specific commit +- `:` — date-based tag (e.g., `20260319`) + +## Quick Start + +### Default variant + +Add to your project's `.devcontainer/devcontainer.json`: + +```jsonc +{ + "name": "Local Development", + "image": "ghcr.io/gatezh/devcontainer-images/claude-code:latest", + "init": true, + "remoteUser": "node", + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind", + "workspaceFolder": "/workspace", + // Named volumes persist node_modules, Claude config, and fish history across rebuilds. + // Dirs are pre-created in the image with node:node ownership, so fresh volumes + // inherit correct permissions via Docker volume population. + "mounts": [ + "source=myproject-node-modules-${devcontainerId},target=/workspace/node_modules,type=volume", + "source=myproject-claude-config-${devcontainerId},target=/home/node/.claude,type=volume", + "source=myproject-fish-data-${devcontainerId},target=/home/node/.local/share/fish,type=volume" + ], + "containerEnv": { + "TZ": "${localEnv:TZ:America/Los_Angeles}", + "DEVCONTAINER": "true", + "NODE_OPTIONS": "--max-old-space-size=4096", + "CLAUDE_CONFIG_DIR": "/home/node/.claude" + }, + // mise install reads .mise.toml and installs project-specific tool versions. + // sudo chown fixes volume ownership — safety net in case Docker volume population didn't apply. + "updateContentCommand": "sudo chown node /workspace/node_modules && sudo chown -R node /home/node/.claude && mise install && bun install", + "waitFor": "postCreateCommand" +} +``` + +### Sandbox variant + +```jsonc +{ + "name": "Claude Sandbox", + "image": "ghcr.io/gatezh/devcontainer-images/claude-code-sandbox:latest", + // Capabilities required for iptables firewall setup + "capAdd": ["NET_ADMIN", "NET_RAW"], + "init": true, + "remoteUser": "node", + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind", + "workspaceFolder": "/workspace", + "mounts": [ + "source=sandbox-fish-${devcontainerId},target=/home/node/.local/share/fish,type=volume", + "source=sandbox-config-${devcontainerId},target=/home/node/.claude,type=volume", + // Mount project's firewall script into the expected path. + // The image provides iptables/ipset packages and sudo rule but NOT the script itself. + "source=${localWorkspaceFolder}/.devcontainer/claude-sandbox/init-firewall.sh,target=/usr/local/bin/init-firewall.sh,type=bind" + ], + "containerEnv": { + "TZ": "${localEnv:TZ:America/Los_Angeles}", + "DEVCONTAINER": "true", + "NODE_OPTIONS": "--max-old-space-size=4096", + "CLAUDE_CONFIG_DIR": "/home/node/.claude" + }, + "postCreateCommand": "mise install", + // Firewall init — script is bind-mounted from the project + "postStartCommand": "sudo /usr/local/bin/init-firewall.sh", + "waitFor": "postStartCommand" +} +``` + +## Project Setup Guide + +Projects consuming these images need the following files in their repository. + +### Required: `.mise.toml` (project root) + +Each project defines its own tool versions: + +```toml +[tools] +bun = "1.3.8" +# node is provided by the base image (node:22-trixie-slim). +# This entry is for CI environments where the base image isn't available. +# Inside the devcontainer, mise detects Node is already on $PATH and skips installation. +node = "22" +``` + +### Optional: `.devcontainer/init-plugins.sh` + +Claude Code plugin initialization. Runs once at container creation. Idempotent. + +```bash +#!/bin/bash +set -euo pipefail + +# Mark onboarding complete so claude CLI doesn't hang on interactive prompts +if [ -f "$HOME/.claude/.claude.json" ]; then + jq '.hasCompletedOnboarding = true' "$HOME/.claude/.claude.json" > /tmp/.claude.json \ + && mv /tmp/.claude.json "$HOME/.claude/.claude.json" +else + mkdir -p "$HOME/.claude" + echo '{"hasCompletedOnboarding":true}' > "$HOME/.claude/.claude.json" +fi + +# Install plugins (customize this list) +for plugin in \ + "frontend-design@claude-plugins-official" \ + "code-review@claude-plugins-official"; do + claude plugin install "$plugin" 2>/dev/null || true +done +``` + +Mark as executable: `chmod +x init-plugins.sh` + +### Sandbox-only: `.devcontainer/claude-sandbox/init-firewall.sh` + +Default-deny iptables firewall. The image provides the packages and sudo rule; the project provides this script via bind mount. Customize the domain allowlist for your project. + +See the [devcontainer-claude-bun firewall script](../devcontainer-claude-bun/.devcontainer/init-firewall.sh) for a complete example. + +Mark as executable: `chmod +x init-firewall.sh` + +### Sandbox-only: `.devcontainer/claude-sandbox/.env.example` + +Template for sandbox authentication: + +```bash +# Claude Code authentication +# Generate a token with: claude setup-token +# Copy this file to .env.local and fill in your token: +# cp .env.example .env.local +CLAUDE_CODE_OAUTH_TOKEN=your-token-here +``` + +Add `.env.local` to `.gitignore`. + +### Complete file structure + +``` +.devcontainer/ +├── devcontainer.json ← default devcontainer +├── init-plugins.sh ← Claude Code plugin setup (optional) +└── claude-sandbox/ + ├── devcontainer.json ← sandbox devcontainer + ├── init-firewall.sh ← firewall script (customize domain allowlist) + ├── .env.example ← template for auth token + └── .env.local ← actual auth token (gitignored) +``` + +## Workspace Directory Layout + +The image pre-creates these directories with `node:node` ownership so Docker's volume population seeds fresh named volumes with correct permissions: + +``` +/workspace/ +├── node_modules/ +├── services/ +│ ├── api/node_modules/ +│ ├── app/node_modules/ +│ └── www/node_modules/ +└── packages/ + ├── shared/node_modules/ + └── database/node_modules/ +``` + +If your project has additional services, create volume mounts in `devcontainer.json` — Docker creates directories at container start. The `sudo chown` in `updateContentCommand` fixes ownership. + +## Playwright Version Strategy + +The image bakes in a Playwright browser binary at a specific version (controlled by the `PLAYWRIGHT_VERSION` build arg). + +- If your project's `@playwright/test` version **matches** the image — zero startup cost, browser is ready +- If your project's version **differs** — Playwright auto-downloads the correct browser on first test run (~10s graceful fallback) +- This is a **best-effort optimization**, not a hard contract + +## Build Args + +| Arg | Default | Description | +|-----|---------|-------------| +| `GIT_DELTA_VERSION` | `0.18.2` | git-delta version | +| `PLAYWRIGHT_VERSION` | `1.50.1` | Playwright browser binary version | +| `AGENT_BROWSER_VERSION` | `0.14.0` | agent-browser version (default target only) | + +## Building Locally + +```bash +# Default variant +docker build --target default -t claude-code:default .devcontainer + +# Sandbox variant +docker build --target sandbox -t claude-code:sandbox .devcontainer + +# Multi-platform +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --target default \ + -t ghcr.io/gatezh/devcontainer-images/claude-code:latest \ + --push \ + .devcontainer +``` + +## Startup Timeline + +### Warm start (Playwright version matches image) +``` +Pull image ───────────────────── (cached) +mise install (bun, hugo, etc.) ─ (~15s, downloads pre-built binaries) +bun install ─────────────────── (~15s, cached in named volume) +project setup ───────────────── (db:migrate, init-plugins, etc.) + Total: ~45s warm +``` + +### Playwright version mismatch +``` +Same as above. First test run triggers browser download (~10s, one-time). +Not a blocking startup cost — tests work, just slightly slower first run. +``` + +## Resources + +- [Claude Code Documentation](https://docs.anthropic.com/en/docs/claude-code) +- [VS Code Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers) +- [Mise Documentation](https://mise.jdx.dev/) +- [Docker Volume Population](https://docs.docker.com/engine/storage/volumes/#populate-a-volume-using-a-container) From 039bfffc2da9a4009b676141cb0e77e4b4d43146 Mon Sep 17 00:00:00 2001 From: Serge Gatezh <2880401+gatezh@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:25:17 -0600 Subject: [PATCH 4/4] feat(ci): add date tag to claude-code image builds Adds YYYYMMDD tag alongside latest and SHA for human-readable version pinning. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/build-claude-code.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-claude-code.yml b/.github/workflows/build-claude-code.yml index 83665f1..7fd1ad4 100644 --- a/.github/workflows/build-claude-code.yml +++ b/.github/workflows/build-claude-code.yml @@ -61,6 +61,7 @@ jobs: tags: | type=raw,value=latest type=sha,prefix= + type=raw,value={{date 'YYYYMMDD'}} - name: Build and push id: push