Repository navigation
feat(devcontainers): switch to zsh, install gh upstream, widen Renovate coverage - #146
Merged
Merged
Conversation
…te coverage Debian trixie freezes gh at 2.46.0 (Apr 2024) and always will, so no Renovate config could have fixed it -- the install source had to change. gh now comes from the upstream .deb at 2.100.0, using the same dpkg -i idiom these images already used for local .deb packages. Renovate coverage grows from 4 packages to 11: gh, zsh-in-docker, docker, go and the hugo-bun-node musl node build join rtk, ralphex, claude-code, agent-browser, bun and hugo. All auto-merge on the existing 3-day soak. node is constrained to ^24 so major bumps stay a deliberate call. Docker is tracked with github-tags on docker/cli rather than github-releases on moby/moby: moby tags its releases "docker-v29.8.0", which extractVersion cannot parse, so every candidate would have been silently discarded -- the same trap the ralphex-fe bun rule documents. fish is replaced by zsh + oh-my-zsh + powerlevel10k (git and fzf plugins) in claude-code and the repo devcontainer, so every image now defaults to zsh. fish pulled 26 apt packages and 59 MB, including the entire Python 3.13 runtime; zsh pulls 2 and 20 MB. starship is dropped -- powerlevel10k replaces it, which also removes an unpinned "curl | sh" install. git-delta is removed from all three images. Reliability fix: the zsh-in-docker installer ran as sh -c "$(curl ...)", which exits 0 when the download fails -- shipping an image with no .zshrc and no oh-my-zsh while CI's "zsh --version" still passed. It now downloads to a file first, so a failed fetch aborts the build. Verified by building with a bogus version: the old form succeeded, the new one fails. The publish workflows now verify gh (claude-code, claude-bun) and go and docker (ralphex-fe), matching what ci.yml checks. Host-level Docker runbooks moved to the wiki upstream; the volume rename from *-fish-data-* to *-zsh-history-* still needs applying there.
… verify gaps Post-rebase review of the zsh migration. Two user-visible regressions, both reproduced in a built image before and after the fix. The zsh-in-docker template hardcodes en_US.UTF-8 and TERM=xterm, and -x skips the locale-gen that would create that locale. The slim bases ship only C/C.utf8/POSIX, so glibc silently fell back to the C locale: `echo "hello" | wc -m` counted bytes, not characters, mangling multibyte output in git log and TUIs. The baked TERM=xterm also clobbered the xterm-256color the terminal provides, dropping `tput colors` from 256 to 8 -- a visible downgrade on the flagship image, since the fish/starship setup left TERM alone. Both are now rewritten out of .zshrc: C.UTF-8 is a real UTF-8 locale needing no extra package, and dropping the TERM line lets the terminal decide. Dead code: claude-bun assigned SNIPPET and never used it. The upstream Anthropic pattern ends with `echo "$SNIPPET" >> ~/.bashrc`, which was never copied across -- so bash history never persisted despite the comment saying it did. Removing it also retires the SC2034 hadolint suppression, which existed solely to hide that variable; no findings return without it. Verify gaps: ci.yml checked docker for ralphex-fe but build-ralphex-fe.yml did not, and build-claude-bun.yml checked zsh but ci.yml did not -- so a regression could pass one gate and fail the other. Both directions closed. Comment accuracy: zsh was installed but absent from the package-rationale blocks, gh was still listed there though it no longer comes from apt, and the fzf entry claimed an explicit .zshrc source that this branch removed. The supply-chain trade is now stated in the Dockerfiles: ZSH_IN_DOCKER_VERSION pins only the wrapper, which fetches oh-my-zsh and powerlevel10k unpinned at build time. The stale devcontainer plan doc gets the same superseded banner the renovate spec already carries.
The previous commit patched .zshrc by matching the exact lines zsh-in-docker v1.2.0 emits. That was already broken against the currently released v1.2.1, which writes `[ -z "$TERM" ] && export TERM=xterm` inside an UNQUOTED heredoc -- so $TERM expands at build time, when it is unset, to `[ -z "" ] && export TERM=xterm`, unconditionally true and invisible to a pattern anchored on the old text. sed exits 0 on a non-match, so the fix would have silently stopped working. Verified by building against both releases: on v1.2.1 the old sed leaves TERM clobbered to xterm. Since zsh-in-docker auto-merges after its soak, Renovate would have adopted v1.2.1 and reintroduced the 8-colour regression the previous commit fixed. Now the whole template preamble is cut at the `##### Zsh/Oh-my-Zsh Configuration` marker, which is identical in both releases, instead of matching individual lines. A grep guard precedes it: without one, a marker rename would make sed delete the entire file rather than nothing. Verified against v1.2.0 and v1.2.1 -- TERM survives and the theme, plugins and history all still load. Locale moves from .zshrc to ENV LANG/LC_ALL on the image. The .zshrc-only fix covered interactive zsh and nothing else: postCreateCommand and updateContentCommand run under sh, as do the VS Code server, docker exec and every bash Claude Code spawns -- all still saw ANSI_X3.4-1968 and mangled multibyte output, which is the bug the previous commit set out to fix. CI now asserts the outcome rather than trusting the transformation, on all eight verify paths: LC_ALL resolves to C.UTF-8 and a sentinel TERM survives shell startup. Both were checked negatively -- they fail against a v1.2.1 image and against a hand-regressed .zshrc. The sentinel is deliberately not xterm-256color, which would also pass if a template ever hardcoded it. Also corrected: both READMEs advertised tools no image contains (nano, vim, wget, unzip, man-db for claude-code; vim, nano for claude-bun -- all verified absent in a build), and the renovate.json5 extractVersion rationale cited git-delta, which this branch deletes everywhere.
…h-in-docker zsh-in-docker generates a .zshrc from its own template, and every fix in this branch so far has been a skirmish with that template: an anchored sed to strip its TERM line, then a preamble cut when v1.2.1 changed the line, then CI assertions to catch the next change. The template was the problem, not any of the individual patches. Now oh-my-zsh and powerlevel10k are fetched as tarballs pinned to commit SHAs in a parallel download stage, and .zshrc is written here. Nothing upstream can inject env overrides that then have to be patched back out. Pinned by SHA rather than tracked by Renovate, deliberately. Neither project tags usefully -- oh-my-zsh has no tags at all and powerlevel10k's last release predates its master by years -- and auto-adopting shell-framework changes is exactly what kept breaking this image. A prompt theme does not need a 3-day release cadence. Environment is now what the official images do: ENV LANG=C.UTF-8 and nothing else. LC_ALL is dropped -- it outranks every LC_* category, so a consuming project's containerEnv could not override one. TERM is dropped too: no official image sets it, an image ENV beats the tty value docker exec -t supplies, and tput failing without a terminal is correct behaviour rather than a bug to paper over. The two locale/TERM verify assertions are replaced by one that exercises the whole chain -- an interactive zsh reporting the powerlevel10k theme, which is only true if both pinned tarballs and the generated .zshrc are intact. Checked negatively: it fails against a hand-broken .zshrc. Removes deluan/zsh-in-docker as a dependency, its Renovate entry and its ARG. Renovate now tracks 10 packages. Verified on all four image variants (claude-code default and sandbox, claude-bun, the repo devcontainer): theme, plugins, git aliases, fzf widgets, HISTFILE and locale all correct; no configuration wizard; prompt renders in a git repo with --network none, so nothing is fetched at first shell.
The theme assertion added in ce1f39f was written as zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k but in ci.yml it sits inside a double-quoted bash argument to add_image, so the runner's shell expanded $ZSH_THEME -- to empty, since it is a zsh variable the runner has never heard of -- before the string was ever stored. What actually ran in the container was zsh -ic 'echo ' | grep -qx powerlevel10k/powerlevel10k which prints an empty line and fails. Build · claude-bun went red on amd64. Uses typeset -p ZSH_THEME instead, which contains no $ at all and so survives every quoting layer identically -- ci.yml builds its string in bash while the publish workflows pass theirs through an env var, and escaping would have had to differ between the two. This slipped through local verification because the harness extracted the verify string from the YAML text and re-quoted it in single quotes, which preserved $ZSH_THEME. The harness now assigns the original double-quoted literal through bash first, so runner-side expansion is reproduced; confirmed by feeding it the broken form, which it now fails.
… table The table restated each ARG's current value, so every Renovate bump silently falsified it -- it drifted twice during this branch alone, once for rtk and claude-code and again for a claude-code bump landing mid-review. A number that is wrong more often than right is worse than no number. The table now says who updates each arg and why it exists, and points at the Dockerfile for current values.
gatezh
force-pushed
the
chore/renovate-coverage-zsh-migration
branch
from
September 17, 2026 02:45
f6efa8d to
606d1a2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Replaces fish with zsh + oh-my-zsh + powerlevel10k across the remaining images, installs
ghfrom the upstream.debinstead of apt, widens Renovate coverage from 4 packages to 11, and drops git-delta.Why
ghwas ~2.5 years stale (2.46.0, Apr 2024). That was never a Renovate problem — Debian trixie freezes the package version for the life of the release, so there was no version string for Renovate to bump and no config change could have fixed it. The install source had to change.Auditing the rest turned up six more pinned tools that Renovate was blind to, because the custom regex manager matches the
# renovate:comment as part of its pattern and thoseARGs didn't have one.BUN_VERSIONhad drifted three ways across five images as a result.fish was costing 26 apt packages and 59 MB — including the entire Python 3.13 runtime — for a shell that causes syntax friction. zsh costs 2 packages and 20 MB, and every other image in the repo already used it.
Changes
gh
.deb(2.46.0 → 2.100.0) inclaude-code,claude-bunand the repo devcontainer, reusing thedpkg -iidiom already present in those files.debdepends ongitanddpkg -idoes not resolve dependenciesRenovate: 4 → 11 packages
gh,zsh-in-docker,docker,go, and thehugo-bun-nodemuslnodebuildnodeconstrained to^24; group renameddevcontainer agent tools→devcontainer toolssince it now carries non-agent toolinggithub-tagsondocker/cli, notgithub-releasesonmoby/moby— moby tags releasesdocker-v29.8.0, whichextractVersioncannot parse, so every candidate would have been silently discarded. Same trap theralphex-febun rule already documentsShell
gitandfzfplugins) inclaude-codeand.devcontainer; every image now defaults to zshcurl | shinstall*-fish-data-*→*-zsh-history-*, retargeted to/commandhistoryto match claude-bun's existing conventionRemoved
Reliability
sh -c "$(curl ...)"exits 0 when the download fails, shipping an image with no.zshrcand no oh-my-zsh while CI'szsh --versionstill passed. Now downloads to a file first. Verified with a bogus version: the old form built successfully, the new form failsgh(claude-code, claude-bun) andgo/docker(ralphex-fe), matchingci.yml/commandhistoryadded to the claude-code default variant's chown — only the sandbox variant had it, so history would have silently failed to persistZSH_IN_DOCKER_VERSIONbuild arg inclaude-bun/devcontainer.jsonthat would have pinned back over every Renovate bump for local buildsNotes
Redundancy removed after review — three settings turned out to be no-ops, each confirmed against upstream source:
source <(fzf --zsh)— oh-my-zsh'sfzfplugin already runseval "$(fzf --zsh)"for fzf ≥ 0.48 (trixie ships 0.60)setopt SHARE_HISTORY— oh-my-zsh'slib/history.zsh:48sets it unconditionallysetopt INC_APPEND_HISTORY— zsh's manual: "should be turned off if [SHARE_HISTORY] is in effect"Requires a Nerd Font in the terminal for powerlevel10k glyphs (
terminal.integrated.fontFamily).claude-bunalready used p10k, so anyone using that image is already set.Orphaned volumes — the old
*-fish-data-*volumes are not deleted, just no longer mounted.Wiki follow-up: #147 — the host-level Docker runbooks moved to the wiki upstream in 2e1a3e7 and still reference
*-fish-datavolumes, which this PR renames. Tracked in #147 since the wiki is a separate repository. This matters more than it looks: those pages list*-fish-dataunder "do not prune", so following them after this merge would find no such volume and could lead to pruning the real one.Not changed, deliberately — neither the
gh.debnor the zsh-in-docker script is checksum-verified, and the latter clones oh-my-zsh frommasterand p10k fromHEAD. Downloads are HTTPS with cert validation; a checksum from the same origin would mainly catch CDN corruption, not a compromised release, and would do nothing about the unpinned clones — which are the real exposure now that these bumps auto-merge to ghcr.io. Pinning both to commit SHAs is the real fix if we want one.Verification
matchStringsregexactionlintclean;hadolintacross 7 Dockerfiles shows zero new findings vsmasterclaude-code(default + sandbox) andclaude-bunbuilt on arm64; all threeci.ymlverify commands pass, extracted from the workflow rather than retypedgh2.100.0 confirmed installing and running on both arm64 and amd64git/fzfplugins,gstalias, fzf widgets bound