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
41 changes: 16 additions & 25 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,8 @@

# ── rtk (token-optimized CLI proxy) ──────────────────────────────────────
# ── oh-my-zsh + powerlevel10k ─────────────────────────────────
# Pinned to commit SHAs and fetched as tarballs. Neither project tags usefully --
# oh-my-zsh has no tags at all, and powerlevel10k's last release predates its
# current master by years -- so a SHA is the only real pin. Bump deliberately:
# auto-adopting upstream shell-framework changes is what repeatedly broke this
# image, and a prompt theme does not need a 3-day release cadence.
# Pinned by commit SHA: neither project tags usefully, and auto-adopting
# shell-framework changes has broken this image before. Bump deliberately.
FROM alpine:3.21 AS ohmyzsh-download
RUN apk add --no-cache curl tar
ARG OH_MY_ZSH_REF=0ee67f042872d1dfab74270c31867771ca35aef4
Expand Down Expand Up @@ -116,16 +113,18 @@ RUN mkdir -p /usr/local/share/npm-global/lib \

ENV DEVCONTAINER=true

# Create workspace and config directories with proper ownership
RUN mkdir -p /workspace /home/node/.claude /home/node/.local /commandhistory \
&& chown -R node:node /workspace /home/node/.claude /home/node/.local /commandhistory
# Workspace and XDG dirs, node-owned: a volume mounted at a nested path under one
# of these makes Docker invent the missing parent as root, locking node out.
RUN mkdir -p /workspace /home/node/.claude \
/home/node/.local/share /home/node/.local/state /home/node/.config /home/node/.cache \
/commandhistory \
&& chown -R node:node /workspace /home/node/.claude /home/node/.local \
/home/node/.config /home/node/.cache /commandhistory

WORKDIR /workspace

# gh (GitHub CLI) — installed from the upstream .deb, not apt: Debian trixie
# freezes gh at 2.46.0 (Apr 2024). Version kept current by Renovate.
# Must stay AFTER the apt block: the .deb depends on git, and dpkg -i does not
# resolve dependencies — it fails loudly ("dependency problems") if git is absent.
# gh from the upstream .deb: trixie freezes it at 2.46.0 (Apr 2024). Must follow
# the apt block — the .deb depends on git and dpkg -i won't resolve it.
RUN ARCH=$(dpkg --print-architecture) \
&& curl -fsSL -o "gh_${GH_VERSION}_linux_${ARCH}.deb" \
"https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${ARCH}.deb" \
Expand All @@ -138,28 +137,20 @@ USER node
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
ENV PATH=$PATH:/usr/local/share/npm-global/bin
ENV SHELL=/usr/bin/zsh
# Only LANG, and only C.UTF-8 — what the official images do (ruby sets exactly
# this; node/debian/python set nothing). LC_ALL would override every LC_* category
# and silently defeat a consuming project's containerEnv. TERM is deliberately
# unset: no official image sets it, an image ENV beats the tty value `docker exec -t`
# supplies, and tput/clear failing without a terminal is correct behaviour.
# LANG only, as the official images do. LC_ALL would outrank a consumer's
# containerEnv; TERM stays unset so the terminal's own value wins.
ENV LANG=C.UTF-8
ENV EDITOR="code --wait"
ENV VISUAL="code --wait"

# ── zsh + oh-my-zsh + powerlevel10k ──────────────────────────────────────────
# Written as node: $HOME during RUN follows USER, so this must not run as root.
USER node
# oh-my-zsh tree is COPYed from the parallel download stage above, so the final
# image needs no git or curl for it and nothing is fetched at build time here.
COPY --from=ohmyzsh-download --chown=node:node /omz /home/node/.oh-my-zsh

# .zshrc is written here rather than by an installer, so nothing upstream can
# inject env overrides (locale/TERM) that then have to be patched back out.
# The wizard flag matters: without a POWERLEVEL9K_* config powerlevel10k starts
# its interactive configurator on first shell, which blocks a container.
# $HOME and $ZSH are single-quoted on purpose: they must reach .zshrc
# unexpanded so zsh resolves them at runtime, not at build time.
# .zshrc written here, not by an installer, so nothing upstream injects env
# overrides. The wizard flag stops powerlevel10k prompting on first shell.
# Single-quoted so $HOME/$ZSH reach .zshrc unexpanded.
# hadolint ignore=SC2016
RUN printf '%s\n' \
'export ZSH="$HOME/.oh-my-zsh"' \
Expand Down
48 changes: 16 additions & 32 deletions .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
// 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.
// Pins tools these images used to pull at build time. Replaces the daily cron:
// a bump PR triggers the push-based build. No release -> no PR -> no rebuild.
customManagers: [
{
customType: 'regex',
Expand All @@ -22,25 +20,17 @@

packageRules: [
{
// Most GitHub tags carry a leading "v" ("v0.48.0", "v29.8.1"); strip it so the
// datasource version matches the bare ARG value ("0.48.0"). A tag with no "v"
// passes through unchanged, so this is safe to apply datasource-wide.
// Strip the leading "v" so tags match the bare ARG value. Tags without one
// pass through unchanged, so this is safe datasource-wide.
matchDatasources: ['github-releases', 'github-tags'],
extractVersion: '^v?(?<version>.+)$',
},
{
// Group the tracked 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
// explicit platformAutomerge:false is what broke this: only a Renovate
// run could merge, and every run found a newer claude-code, force-pushed
// the branch and ended before CI finished (#121 sat green for 3.5 weeks).
//
// Requires: repo setting "Allow auto-merge", and a ruleset on master
// requiring the "CI complete" check (.github/workflows/ci.yml). Without
// that required check nothing blocks the PR, GitHub never offers native
// auto-merge, and Renovate silently falls back to the broken path.
// One grouped PR, auto-merged on green via Renovate's default
// platformAutomerge. Setting it false broke this before: only a Renovate run
// could merge, and each run force-pushed a newer claude-code before CI
// finished (#121 sat green 3.5 weeks). Requires "Allow auto-merge" plus a
// master ruleset requiring the "CI complete" check, or it silently regresses.
matchPackageNames: [
'rtk-ai/rtk',
'umputun/ralphex',
Expand All @@ -57,10 +47,8 @@
groupName: 'devcontainer tools',
automerge: true,

// These bumps merge unreviewed and publish straight to ghcr.io, so let a
// release soak before adopting it. internalChecksFilter defaults to
// 'strict', so a too-young version is simply not offered yet — the group
// PR carries whichever tools are currently eligible.
// These merge unreviewed and publish straight to ghcr.io, so let releases
// soak. Too-young versions are simply not offered; the PR carries the rest.
minimumReleaseAge: '3 days',
},
{
Expand All @@ -70,21 +58,17 @@
minimumReleaseAge: null,
},
{
// ralphex-fe bakes its toolchain into the image — it has no mise, and
// its published tag is literally bun<BUN>-hugo<HUGO>. So these are
// image-level versions, not per-project ones, and there is no other
// place to set them. Bun uses the npm datasource because oven-sh/bun
// tags releases as "bun-v1.4.2", which the github-releases extractVersion
// above does not strip.
// ralphex-fe has no mise and its tag is literally bun<BUN>-hugo<HUGO>, so
// these are image-level versions. Bun uses npm: its tags are "bun-v1.4.2",
// which the extractVersion above does not strip.
matchPackageNames: ['bun', 'gohugoio/hugo'],
groupName: 'ralphex-fe toolchain',
automerge: true,
minimumReleaseAge: '3 days',
},
{
// hugo-bun-node installs Node from the unofficial musl builds — its base
// image (oven/bun:*-alpine) ships no node at all. The image is Node 24 LTS
// by design, so major bumps stay a deliberate call, not a Renovate PR.
// hugo-bun-node's alpine base ships no node, so it installs the musl build.
// Node 24 LTS by design — majors stay a deliberate call.
matchDatasources: ['node-version'],
allowedVersions: '^24',
},
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/build-claude-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,19 @@ jobs:
matrix:
include:
- image-suffix: claude-code
verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --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 && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k"
verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --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 && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node"
runner: ubuntu-24.04
arch: amd64
- image-suffix: claude-code
verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --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 && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k"
verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --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 && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node"
runner: ubuntu-24.04-arm
arch: arm64
- image-suffix: claude-code-sandbox
verify-command: "claude --version && mise --version && zsh --version && gh --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 && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k"
verify-command: "claude --version && mise --version && zsh --version && gh --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 && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node"
runner: ubuntu-24.04
arch: amd64
- image-suffix: claude-code-sandbox
verify-command: "claude --version && mise --version && zsh --version && gh --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 && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k"
verify-command: "claude --version && mise --version && zsh --version && gh --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 && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node"
runner: ubuntu-24.04-arm
arch: arm64
runs-on: ${{ matrix.runner }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@ jobs:
add_image "claude-code" \
"claude-code/.devcontainer" \
"claude-code/.devcontainer/Dockerfile" \
"bun --version || true && claude --version && mise --version && zsh --version && gh --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 && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k" \
"bun --version || true && claude --version && mise --version && zsh --version && gh --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 && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node" \
"default"
add_image "claude-code-sandbox" \
"claude-code/.devcontainer" \
"claude-code/.devcontainer/Dockerfile" \
"claude --version && mise --version && zsh --version && gh --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 && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k" \
"claude --version && mise --version && zsh --version && gh --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 && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node" \
"sandbox"
fi

Expand Down
31 changes: 9 additions & 22 deletions claude-bun/.devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
ARG BUN_VERSION=1.3.5

# ── oh-my-zsh + powerlevel10k ─────────────────────────────────
# Pinned to commit SHAs and fetched as tarballs. Neither project tags usefully --
# oh-my-zsh has no tags at all, and powerlevel10k's last release predates its
# current master by years -- so a SHA is the only real pin. Bump deliberately:
# auto-adopting upstream shell-framework changes is what repeatedly broke this
# image, and a prompt theme does not need a 3-day release cadence.
# Pinned by commit SHA: neither project tags usefully, and auto-adopting
# shell-framework changes has broken this image before. Bump deliberately.
FROM alpine:3.21 AS ohmyzsh-download
RUN apk add --no-cache curl tar
ARG OH_MY_ZSH_REF=0ee67f042872d1dfab74270c31867771ca35aef4
Expand Down Expand Up @@ -66,10 +63,8 @@ RUN mkdir -p /workspace /home/bun/.claude /home/bun/.bun && \

WORKDIR /workspace

# gh (GitHub CLI) — installed from the upstream .deb, not apt: Debian trixie
# freezes gh at 2.46.0 (Apr 2024). Version kept current by Renovate.
# Must stay AFTER the apt block: the .deb depends on git, and dpkg -i does not
# resolve dependencies — it fails loudly ("dependency problems") if git is absent.
# gh from the upstream .deb: trixie freezes it at 2.46.0 (Apr 2024). Must follow
# the apt block — the .deb depends on git and dpkg -i won't resolve it.
RUN ARCH=$(dpkg --print-architecture) && \
curl -fsSL -o "gh_${GH_VERSION}_linux_${ARCH}.deb" \
"https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${ARCH}.deb" && \
Expand All @@ -87,27 +82,19 @@ ENV PATH="$BUN_INSTALL/bin:$PATH"
# Set the default shell to zsh rather than sh (/usr/bin/zsh, matching the other
# images; /bin/zsh is the same binary via usrmerge)
ENV SHELL=/usr/bin/zsh
# Only LANG, and only C.UTF-8 — what the official images do (ruby sets exactly
# this; node/debian/python set nothing). LC_ALL would override every LC_* category
# and silently defeat a consuming project's containerEnv. TERM is deliberately
# unset: no official image sets it, an image ENV beats the tty value `docker exec -t`
# supplies, and tput/clear failing without a terminal is correct behaviour.
# LANG only, as the official images do. LC_ALL would outrank a consumer's
# containerEnv; TERM stays unset so the terminal's own value wins.
ENV LANG=C.UTF-8

# zsh + oh-my-zsh + powerlevel10k.
#
# Written as bun: $HOME during RUN follows USER, so this must not run as root.
USER bun
# oh-my-zsh tree is COPYed from the parallel download stage above, so the final
# image needs no git or curl for it and nothing is fetched at build time here.
COPY --from=ohmyzsh-download --chown=bun:bun /omz /home/bun/.oh-my-zsh

# .zshrc is written here rather than by an installer, so nothing upstream can
# inject env overrides (locale/TERM) that then have to be patched back out.
# The wizard flag matters: without a POWERLEVEL9K_* config powerlevel10k starts
# its interactive configurator on first shell, which blocks a container.
# $HOME and $ZSH are single-quoted on purpose: they must reach .zshrc
# unexpanded so zsh resolves them at runtime, not at build time.
# .zshrc written here, not by an installer, so nothing upstream injects env
# overrides. The wizard flag stops powerlevel10k prompting on first shell.
# Single-quoted so $HOME/$ZSH reach .zshrc unexpanded.
# hadolint ignore=SC2016
RUN printf '%s\n' \
'export ZSH="$HOME/.oh-my-zsh"' \
Expand Down
Loading
Loading