diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 072ae42..798df06 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,42 +1,109 @@ -FROM mcr.microsoft.com/devcontainers/typescript-node:24-bookworm +# syntax=docker/dockerfile:1 +# +# Dockerfile-only devcontainer: no devcontainer `features:`. +# +# Coder builds this workspace with envbuilder, and envbuilder's prebuilt-image +# cache probe (the `envbuilder_cached_image` Terraform resource) cannot reproduce +# devcontainer *feature* layers, so a feature-based image always misses the cache +# and rebuilds on every workspace start (coder/terraform-provider-envbuilder#68). +# Everything is therefore installed from plain RUN layers here. Tool versions +# live in mise.toml (installed system-wide with mise); only the few things mise +# cannot manage (apt packages, nsc, Playwright) are installed directly. +FROM mcr.microsoft.com/devcontainers/typescript-node:26-trixie -# Wire up the tmux configuration for the default container user (node) -COPY tmux.conf /home/node/.tmux.conf -RUN chown node:node /home/node/.tmux.conf +USER root +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +ARG DEBIAN_FRONTEND=noninteractive + +# --------------------------------------------------------------------------- +# APT packages: general toolbox, sshd, and the headers native Ruby gems and +# ruby-build need (libyaml/openssl/readline/ffi/gdbm/zlib). +# tmux is intentionally NOT installed from apt; a current release comes from mise. +# --------------------------------------------------------------------------- +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential git postgresql-client curl htop vim \ + ca-certificates unzip \ + openssh-server \ + autoconf bison libssl-dev libyaml-dev libreadline-dev zlib1g-dev \ + libncurses-dev libffi-dev libgdbm-dev libdb-dev uuid-dev \ + && rm -rf /var/lib/apt/lists/* + +# --------------------------------------------------------------------------- +# sshd (replaces ghcr.io/devcontainers/features/sshd). Listens on 2222 and is +# started by postStartCommand in devcontainer.json. +# --------------------------------------------------------------------------- +RUN mkdir -p /var/run/sshd \ + && ssh-keygen -A \ + && sed -i 's/^#\?Port .*/Port 2222/' /etc/ssh/sshd_config \ + && sed -i 's/^#\?PasswordAuthentication .*/PasswordAuthentication yes/' /etc/ssh/sshd_config -# Also wire up tmux configuration for root (used when connecting via `coder ssh`) +# --------------------------------------------------------------------------- +# /workspaces is created root-owned by the base image and envbuilder does not +# chown it, so `git clone` into it failed for the node user. Hand it over. +# --------------------------------------------------------------------------- +RUN mkdir -p /workspaces && chown node:node /workspaces + +# --------------------------------------------------------------------------- +# tmux configuration for both `node` (default) and `root` (used by `coder ssh`). +# --------------------------------------------------------------------------- +COPY tmux.conf /home/node/.tmux.conf COPY tmux.conf /root/.tmux.conf -RUN chown root:root /root/.tmux.conf +RUN chown node:node /home/node/.tmux.conf && chown root:root /root/.tmux.conf -# Install the Namespace (nsc) CLI - https://namespace.so/docs/reference/cli/installation -# Install system-wide so it is on PATH for both the `node` and `root` users. +# --------------------------------------------------------------------------- +# Namespace (nsc) CLI - https://namespace.so/docs/reference/cli/installation +# Installed system-wide so it is on PATH for both `node` and `root`. +# --------------------------------------------------------------------------- RUN curl -fsSL https://get.namespace.so/cloud/install.sh | NS_INSTALL_DIR=/usr/local/bin sh -# Install Playwright with the Chromium browser and its required OS dependencies. -# Browsers are stored in a shared, system-wide location (PLAYWRIGHT_BROWSERS_PATH) -# so both the `node` and `root` users can run Playwright without re-downloading. +# --------------------------------------------------------------------------- +# Playwright with Chromium and its OS dependencies. Browsers live in a shared, +# system-wide location so both users can run Playwright without re-downloading. +# --------------------------------------------------------------------------- ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright RUN npm install -g playwright \ && playwright install --with-deps chromium \ - && chmod -R a+rx /ms-playwright - -# Tell dev container features where the target user's home directory is. -# The devcontainer CLI passes `_REMOTE_USER_HOME`/`_CONTAINER_USER_HOME` to every -# feature's install.sh, but envbuilder (used by Coder) only sets `_REMOTE_USER` -# and `_CONTAINER_USER` -- it leaves the *_HOME variables empty. Features that -# rely on them then break: e.g. the claude-code feature runs -# `cp "$_REMOTE_USER_HOME/.local/bin/claude" /usr/local/bin/claude`, which under -# envbuilder expands to `cp /.local/bin/claude ...` and fails the whole build. -# Supplying the values here (as build ARGs, so they don't leak into the running -# container's environment) keeps feature installs working on the envbuilder path. -# ARGs declared here remain in scope for the feature install steps envbuilder -# appends after this Dockerfile, but reset for the devcontainer CLI (which sets -# these variables itself), so this is a no-op there. -ARG _REMOTE_USER_HOME=/home/node -ARG _CONTAINER_USER_HOME=/home/node - -# Default to the non-root `node` user. envbuilder (used by Coder) picks its target + && chmod -R a+rx /ms-playwright \ + && rm -rf /var/lib/apt/lists/* + +# --------------------------------------------------------------------------- +# mise (https://mise.jdx.dev) manages every versioned tool: languages, k8s +# CLIs, gh/az, coding agents, tmux. Pinned binary copied from the official +# image; versions pinned in mise.toml (-> /etc/mise/config.toml). +# +# Layout: +# - MISE_DATA_DIR=/usr/local/share/mise : installs + shims, shared by all +# users and outside $HOME so a mounted/persisted home cannot shadow it. +# - shims dir on PATH via ENV, so tools work in non-interactive shells +# (VS Code server, Coder agent, CI `devcontainer exec`) without `mise +# activate`. Interactive shells may still `eval "$(mise activate bash)"`. +# - GOPATH / CARGO_HOME / RUSTUP_HOME are system-wide and node-owned so +# `go install` / `cargo install` work and persist in the image. +# --------------------------------------------------------------------------- +COPY --from=ghcr.io/jdx/mise:2026.10.23 /usr/local/bin/mise /usr/local/bin/mise +ENV MISE_DATA_DIR=/usr/local/share/mise \ + MISE_TRUSTED_CONFIG_PATHS=/etc/mise \ + GOPATH=/go \ + RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo +ENV PATH=${MISE_DATA_DIR}/shims:${GOPATH}/bin:${CARGO_HOME}/bin:${PATH} +COPY mise.toml /etc/mise/config.toml +RUN mkdir -p "${MISE_DATA_DIR}" "${GOPATH}" "${RUSTUP_HOME}" "${CARGO_HOME}" \ + && chown -R node:node "${MISE_DATA_DIR}" "${GOPATH}" "${RUSTUP_HOME}" "${CARGO_HOME}" + +# Install as `node` so the shared tool tree is owned by the user who will add +# to it (gems, `go install`, `cargo install`, `mise use`). root can still run +# and modify everything. +USER node +RUN MISE_YES=1 mise install --jobs 8 \ + && mise reshim \ + && missing="$(mise ls --missing)" \ + && if [ -n "$missing" ]; then echo "mise tools missing after install:" "$missing" >&2; exit 1; fi \ + && rm -rf /home/node/.cache/mise + +# Stay on the non-root `node` user. envbuilder (used by Coder) picks its target # user from the last `USER` directive when `containerUser`/`remoteUser` are not -# applied from image metadata, so make the intended user explicit here to avoid -# dropping into a root shell. +# applied from image metadata, so the intended user must be explicit here to +# avoid dropping into a root shell. USER node diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json deleted file mode 100644 index fc2ad44..0000000 --- a/.devcontainer/devcontainer-lock.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "features": { - "ghcr.io/devcontainers-extra/features/apt-packages:1": { - "version": "1.0.6", - "resolved": "ghcr.io/devcontainers-extra/features/apt-packages@sha256:55c54412112da81b9381e470cdbbe55278564950d1ff536ce925b1e8e096babd", - "integrity": "sha256:55c54412112da81b9381e470cdbbe55278564950d1ff536ce925b1e8e096babd" - }, - "ghcr.io/devcontainers-extra/features/claude-code:2": { - "version": "2.0.0", - "resolved": "ghcr.io/devcontainers-extra/features/claude-code@sha256:37b0d444a704021ee5f6d24242a4621bf337867d110e4e3c06b863a3a78122ac", - "integrity": "sha256:37b0d444a704021ee5f6d24242a4621bf337867d110e4e3c06b863a3a78122ac" - }, - "ghcr.io/devcontainers-extra/features/npm-package:1": { - "version": "1.0.4", - "resolved": "ghcr.io/devcontainers-extra/features/npm-package@sha256:ee315c064588643e1178eec33275efa55739f43761fc965b95a5d05eed70c76d", - "integrity": "sha256:ee315c064588643e1178eec33275efa55739f43761fc965b95a5d05eed70c76d" - }, - "ghcr.io/devcontainers-extra/features/opencode:1": { - "version": "1.0.0", - "resolved": "ghcr.io/devcontainers-extra/features/opencode@sha256:7c15617cc9e501bc218097adc4910a00f81ed52fc5a5eef463aa1cc01317f9ba", - "integrity": "sha256:7c15617cc9e501bc218097adc4910a00f81ed52fc5a5eef463aa1cc01317f9ba" - }, - "ghcr.io/devcontainers-extra/features/ruby-asdf:0": { - "version": "0.0.2", - "resolved": "ghcr.io/devcontainers-extra/features/ruby-asdf@sha256:2109262319dcbc83232919ec0b15bc5336b60999d9c6231498ff59f48f81f670", - "integrity": "sha256:2109262319dcbc83232919ec0b15bc5336b60999d9c6231498ff59f48f81f670" - }, - "ghcr.io/devcontainers/features/azure-cli:1": { - "version": "1.3.0", - "resolved": "ghcr.io/devcontainers/features/azure-cli@sha256:d98f1066c077be0fa9d115b718f458bd803e415181b4a96f82a6f5d9f77241ac", - "integrity": "sha256:d98f1066c077be0fa9d115b718f458bd803e415181b4a96f82a6f5d9f77241ac" - }, - "ghcr.io/devcontainers/features/copilot-cli:1": { - "version": "1.1.3", - "resolved": "ghcr.io/devcontainers/features/copilot-cli@sha256:e10d091ae7ef9b8d2ed5f601d75f5a090bc04acbac1a26d4cd3c0d5edde4ea10", - "integrity": "sha256:e10d091ae7ef9b8d2ed5f601d75f5a090bc04acbac1a26d4cd3c0d5edde4ea10" - }, - "ghcr.io/devcontainers/features/github-cli:1": { - "version": "1.1.0", - "resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671", - "integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671" - }, - "ghcr.io/devcontainers/features/go:1": { - "version": "1.3.4", - "resolved": "ghcr.io/devcontainers/features/go@sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032", - "integrity": "sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032" - }, - "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": { - "version": "1.3.1", - "resolved": "ghcr.io/devcontainers/features/kubectl-helm-minikube@sha256:bbe8adf6b37fff8c67412ab0a4579f4c2f30bbaba1d9a5cebd9e38bade54025b", - "integrity": "sha256:bbe8adf6b37fff8c67412ab0a4579f4c2f30bbaba1d9a5cebd9e38bade54025b" - }, - "ghcr.io/devcontainers/features/rust:1": { - "version": "1.5.0", - "resolved": "ghcr.io/devcontainers/features/rust@sha256:0c55e65f2e3df736e478f26ee4d5ed41bae6b54dac1318c443e31444c8ed283c", - "integrity": "sha256:0c55e65f2e3df736e478f26ee4d5ed41bae6b54dac1318c443e31444c8ed283c" - }, - "ghcr.io/devcontainers/features/sshd:1": { - "version": "1.1.0", - "resolved": "ghcr.io/devcontainers/features/sshd@sha256:f5251b8e4325f68f7280973c6cd65daff414449c66f240621502d4e8e74eb7ee", - "integrity": "sha256:f5251b8e4325f68f7280973c6cd65daff414449c66f240621502d4e8e74eb7ee" - } - } -} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 92b0684..c968596 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,35 +1,14 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +// For format details, see https://aka.ms/devcontainer.json. +// +// This devcontainer deliberately uses NO `features:`. Everything is installed by +// the Dockerfile (tool versions in mise.toml) so that Coder's envbuilder cache +// probe can reproduce the build and boot workspaces straight from the prebuilt +// image. See the Dockerfile header and README for details. { "name": "bmorton/devcontainer", "build": { "dockerfile": "Dockerfile" }, - "features": { - "ghcr.io/devcontainers-extra/features/apt-packages:1": { - "packages": "build-essential,git,postgresql-client,curl,htop,tmux,vim" - }, - "ghcr.io/devcontainers/features/go:1": { - "version": "1.26.3" - }, - "ghcr.io/devcontainers-extra/features/ruby-asdf:0": { - "version": "4.0.5" - }, - "ghcr.io/devcontainers/features/rust:1": {}, - "ghcr.io/devcontainers/features/sshd:1": {}, - "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {}, - "ghcr.io/devcontainers/features/github-cli:1": {}, - "ghcr.io/devcontainers/features/azure-cli:1": { - "version": "latest" - }, - "ghcr.io/devcontainers/features/copilot-cli:1": {}, - "ghcr.io/devcontainers-extra/features/claude-code:2": {}, - "ghcr.io/devcontainers-extra/features/opencode:1": {}, - "ghcr.io/devcontainers-extra/features/npm-package:1": { - "package": "@openprose/prose-cli" - } - }, - "overrideFeatureInstallOrder": ["ghcr.io/devcontainers/features/rust"], // Connect as the `node` user. Set this explicitly rather than relying on the // base image's `remoteUser` metadata: envbuilder (used by Coder) does not honor @@ -38,16 +17,12 @@ "remoteUser": "node", "containerUser": "node", - // Don't remap the container user's UID/GID to match the host user. The sshd - // feature creates an `ssh` group at GID 1001, which collides with the host - // UID/GID (1001) used by CI runners. The CLI's automatic remap then fails with + // Don't remap the container user's UID/GID to match the host user. CI runners + // use UID/GID 1001 and the automatic remap has previously failed with // `groupmod: GID '1001' already exists`, breaking `devcontainer up`. // (envbuilder ignores this key; it only affects the devcontainer CLI.) "updateRemoteUserUID": false, - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], @@ -56,13 +31,19 @@ // completion (both the devcontainer CLI and envbuilder honor this hook). "postCreateCommand": "kubectl version --client 2>/dev/null || true; touch \"$HOME/.devcontainer-postcreate-done\"", + // Start sshd (port 2222) on every container start; this replaces the entrypoint + // the sshd devcontainer feature used to install. Never fails the start. + "postStartCommand": "sudo /usr/sbin/sshd 2>/dev/null || true", + // Configure tool-specific properties. "customizations": { "vscode": { "extensions": [ // General development "eamodio.gitlens", - "sourcegraph.cody-ai", + "github.vscode-pull-request-github", + "github.vscode-github-actions", + "redhat.vscode-yaml", // Ruby extensions "shopify.ruby-lsp", @@ -75,9 +56,7 @@ "ms-kubernetes-tools.vscode-kubernetes-tools", ], "settings": { - // Ruby settings - "ruby.useBundler": true, - "ruby.format": "rubocop", + // Ruby settings (Ruby LSP; the old rebornix `ruby.*` keys were removed) "rubyLsp.formatter": "rubocop", "rubyLsp.enabledFeatures": { "documentHighlights": true, @@ -89,9 +68,6 @@ "codeActions": true, "diagnostics": true }, - "ruby.lint": { - "rubocop": true - }, // Go settings "go.useLanguageServer": true, @@ -111,8 +87,5 @@ "files.insertFinalNewline": true } } - }, - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" + } } diff --git a/.devcontainer/mise.toml b/.devcontainer/mise.toml new file mode 100644 index 0000000..53228d7 --- /dev/null +++ b/.devcontainer/mise.toml @@ -0,0 +1,61 @@ +# System-wide tool versions for the devcontainer. +# +# This file is baked into the image as /etc/mise/config.toml and installed +# during the Docker build with `mise install`, so every tool below is present +# for both the `node` and `root` users, on PATH via mise shims, with no shell +# activation required. +# +# Versions are pinned exactly so the image (and the envbuilder cache built from +# it) is reproducible. The weekly "Bump mise tools" workflow runs +# `mise upgrade --bump` and opens a PR when newer versions are available. +# +# Projects opened inside the container can add their own mise.toml to override +# any of these (e.g. a different Go or Ruby) -- project config layers on top of +# this system config. + +[settings] +# Run pipx-backed tools (azure-cli) through uv instead of pipx. +pipx.uvx = true + +[tools] +# --- Languages --------------------------------------------------------------- +go = "1.27.3" +ruby = "4.0.9" +rust = "1.99.1" +uv = "0.13.6" + +# --- Go tooling (previously installed by the Go devcontainer feature) -------- +golangci-lint = "2.15.0" +staticcheck = "2026.2.2" +"go:golang.org/x/tools/gopls" = "0.24.0" +"go:github.com/go-delve/delve/cmd/dlv" = "1.27.2" +"go:golang.org/x/vuln/cmd/govulncheck" = "1.9.0" + +# --- Kubernetes -------------------------------------------------------------- +kubectl = "1.37.2" +helm = "4.3.1" +minikube = "1.39.0" + +# --- Cloud / platform CLIs --------------------------------------------------- +github-cli = "2.103.3" +"pipx:azure-cli" = "2.91.0" + +# --- Coding agents ----------------------------------------------------------- +# Claude Code self-updates into ~/.local/bin, which is ahead of the mise shims on +# PATH, so this pin is the floor version in a fresh container, not a ceiling. +# (mise ignores releases younger than 24h by default, so `mise upgrade --bump` +# may trail the very latest release by a day.) +claude = "2.1.312" +copilot-cli = "1.0.105" +opencode = "2.2.1" +pi = "1.4.0" + +# --- Utilities --------------------------------------------------------------- +# witr: "why is this running?" process explainer (was proposed in PR #21). +witr = "0.3.4" + +# --- Terminal ---------------------------------------------------------------- +# Official static builds from github.com/tmux/tmux-builds; far newer than the +# Debian package and required for allow-passthrough/extended-keys behaviour +# that Claude Code and iTerm2 rely on. +tmux = "3.7c" diff --git a/.devcontainer/tmux.conf b/.devcontainer/tmux.conf index 28d565d..bd0fbed 100644 --- a/.devcontainer/tmux.conf +++ b/.devcontainer/tmux.conf @@ -1,4 +1,33 @@ -set -g default-terminal "screen-256color" +#---------------------------- +# Terminal compatibility +#---------------------------- +# Advertise a tmux terminfo (true color, italics, strikethrough) to programs +# running inside tmux instead of the legacy screen-256color. +set -g default-terminal "tmux-256color" +# Tell tmux what the *outer* terminal (iTerm2 et al.) can do: 24-bit color, +# modified-key reporting (Shift+Enter), OSC 52 clipboard, hyperlinks, focus +# reporting, titles. +set -as terminal-features ",xterm*:RGB:extkeys:clipboard:hyperlinks:usstyle:focus:title" + +# Let applications send escape sequences through to the outer terminal. Claude +# Code uses this for iTerm2/Kitty/Ghostty desktop notifications, the tab +# progress bar and inline images; without it tmux swallows them. On tmux 3.4+ +# upgrade to `all` so sequences from windows that are not currently visible get +# through too, which is exactly when a "Claude finished" notification matters. +set -g allow-passthrough on +if -F '#{>=:#{version},3.4}' 'set -g allow-passthrough all' +# Report pane focus in/out to applications (Claude Code, vim, etc.). +set -g focus-events on +# Pass modified keys (Shift+Enter, Ctrl+Enter, ...) through to applications +# so Shift+Enter inserts a newline in Claude Code instead of submitting. +set -s extended-keys on +# Forward OSC 52 clipboard writes to the outer terminal. In iTerm2 also enable +# Settings > General > Selection > "Applications in terminal may access +# clipboard" (Claude Code's /terminal-setup toggles this for you). +set -g set-clipboard on +# Don't wait after Esc for a possible Alt-sequence; the default 500ms makes +# Esc feel broken in Claude Code and vim. +set -s escape-time 10 #---------------------------- # Key bindings @@ -49,16 +78,19 @@ set-window-option -g window-status-style fg=white,bg=default,dim set-window-option -g window-status-current-style fg=white,bg=default,bright # command/message line colors set -g message-style fg=white,bg=black,bright +# Keep messages (e.g. the reload confirmation) on screen a little longer. +set -g display-time 2000 #---------------------------- # Miscellaneous #---------------------------- # Reload configuration -bind r source-file ~/.tmux.conf +bind r source-file ~/.tmux.conf \; display "tmux.conf reloaded" # History -set -g history-limit 5000 +set -g history-limit 50000 # Start window and pane indices at 1. set -g base-index 1 +set -g pane-base-index 1 # Terminal emulator window title set -g set-titles on set -g set-titles-string '#S:#I.#P #W' @@ -66,4 +98,3 @@ set -g set-titles-string '#S:#I.#P #W' setw -g monitor-activity off set -g visual-activity on set -g mouse on -set -g extended-keys on diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d4746dd..1fcea87 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,13 +1,8 @@ version: 2 updates: - # Devcontainer feature references in .devcontainer/devcontainer.json - - package-ecosystem: "devcontainers" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 10 - - # Base image (FROM) in .devcontainer/Dockerfile + # Base image (FROM) in .devcontainer/Dockerfile. Tool versions are NOT here: + # they live in .devcontainer/mise.toml, which Dependabot does not understand; + # the "Bump mise tools" workflow handles those. - package-ecosystem: "docker" directory: "/.devcontainer" schedule: diff --git a/.github/workflows/build-containers.yml b/.github/workflows/build-containers.yml index 7e3afd2..bd83c51 100644 --- a/.github/workflows/build-containers.yml +++ b/.github/workflows/build-containers.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 @@ -33,7 +33,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 # Coder builds workspaces with envbuilder, NOT the devcontainer CLI. The two # diverge on user selection, UID/GID remapping and feature install order, so @@ -54,7 +54,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 diff --git a/.github/workflows/bump-mise-tools.yml b/.github/workflows/bump-mise-tools.yml new file mode 100644 index 0000000..5617853 --- /dev/null +++ b/.github/workflows/bump-mise-tools.yml @@ -0,0 +1,59 @@ +name: Bump mise tools + +# Dependabot cannot read .devcontainer/mise.toml, so this does the equivalent: +# weekly, resolve the newest version of every pinned tool, rewrite the pins and +# open (or update) a single PR. The normal "Build Containers" checks then prove +# the new versions build and start before anything is merged. +on: + schedule: + # Mondays 05:00 UTC, an hour before the weekly cache rebuild. + - cron: '0 5 * * 1' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + bump: + name: Bump pinned tool versions + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Install mise + uses: jdx/mise-action@v5 + with: + install: false + cache: false + + # `mise upgrade --bump` rewrites each pin in mise.toml to the latest + # available version. Run from .devcontainer so that file is the config in + # scope; MISE_YES skips prompts. Tools are not actually installed here + # (that is what the build workflow is for). + - name: Bump pins in .devcontainer/mise.toml + working-directory: .devcontainer + env: + MISE_YES: "1" + MISE_TRUSTED_CONFIG_PATHS: ${{ github.workspace }}/.devcontainer + run: | + mise upgrade --bump --dry-run + mise upgrade --bump + git diff --stat -- mise.toml + + - name: Open pull request + uses: peter-evans/create-pull-request@v8 + with: + branch: automation/bump-mise-tools + commit-message: "Bump mise tool versions" + title: "Bump mise tool versions" + body: | + Weekly automated bump of the tool pins in `.devcontainer/mise.toml` + (`mise upgrade --bump`). Review the diff, let **Build Containers** + validate the image, then merge; the cache workflow republishes the + prebuilt image on merge. + labels: dependencies + delete-branch: true diff --git a/.github/workflows/cache-devcontainer.yml b/.github/workflows/cache-devcontainer.yml index 45b3609..55cddcc 100644 --- a/.github/workflows/cache-devcontainer.yml +++ b/.github/workflows/cache-devcontainer.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Build and push devcontainer cache env: diff --git a/README.md b/README.md index 03bc461..dfd302f 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,95 @@ # devcontainer -My personal devcontainer for VSCode. - -## Base image - -- `mcr.microsoft.com/devcontainers/typescript-node:24-bookworm` - -## Devcontainer features - -- Go -- Ruby (via asdf) -- Rust -- sshd -- kubectl / helm / minikube -- GitHub CLI (`gh`) -- Azure CLI (`az`, latest) -- GitHub Copilot CLI -- Anthropic Claude Code -- opencode -- `@openprose/prose-cli` (npm) -- Namespace CLI (`nsc`) -- Playwright with the Chromium browser (and required OS dependencies) - -## APT packages - -- build-essential -- git -- postgresql-client -- curl -- htop -- tmux -- vim - -## VSCode extensions +My personal devcontainer for VS Code and Coder. + +## How it is put together + +- **Base image:** `mcr.microsoft.com/devcontainers/typescript-node:26-trixie` + (Node 26 LTS on Debian 13). +- **No devcontainer features.** Everything is installed by + [`.devcontainer/Dockerfile`](.devcontainer/Dockerfile). Coder's envbuilder + prebuilt-image cache probe cannot reproduce feature layers + ([coder/terraform-provider-envbuilder#68](https://github.com/coder/terraform-provider-envbuilder/issues/68)), + so a feature-based image always rebuilds on workspace start; a pure Dockerfile + lets workspaces boot straight from the cached image. +- **Tool versions live in [`.devcontainer/mise.toml`](.devcontainer/mise.toml)** + and are installed system-wide by [mise](https://mise.jdx.dev) during the image + build. Every version is pinned; the weekly + [`Bump mise tools`](.github/workflows/bump-mise-tools.yml) workflow opens a PR + with newer pins. Dependabot covers the base image and GitHub Actions. + +### Tools managed by mise + +| Area | Tools | +| --- | --- | +| Languages | Go, Ruby, Rust (rustup), uv | +| Go tooling | golangci-lint, staticcheck, gopls, dlv (delve), govulncheck | +| Kubernetes | kubectl, helm, minikube | +| Cloud / platform | GitHub CLI (`gh`), Azure CLI (`az`) | +| Coding agents | Claude Code (`claude`), GitHub Copilot CLI (`copilot`), opencode, pi | +| Utilities | witr | +| Terminal | tmux (official static build, newer than Debian's package) | + +The exact versions are in `mise.toml`. Tools are reached through mise shims +(`/usr/local/share/mise/shims`, already on `PATH` for every shell, including +non-interactive ones used by VS Code, Coder and CI), so nothing needs +`mise activate`. Project checkouts can carry their own `mise.toml` to override +a version for that project; `mise use` / `mise install` work as usual. + +### Installed directly by the Dockerfile + +- APT: build-essential, git, postgresql-client, curl, htop, vim, openssh-server, + and the headers native Ruby gems need. +- sshd on port 2222, started by `postStartCommand`. +- Namespace CLI (`nsc`). +- Playwright with Chromium and its OS dependencies (`PLAYWRIGHT_BROWSERS_PATH=/ms-playwright`). +- `/workspaces` is owned by `node` so `git clone` works there on Coder. +- tmux configuration for `node` and `root`. + +### Shared tool homes + +`GOPATH=/go`, `CARGO_HOME=/usr/local/cargo` and `RUSTUP_HOME=/usr/local/rustup` +are system-wide and owned by `node`, so `go install` and `cargo install` work +without sudo and survive a persisted home directory. + +## VS Code extensions - eamodio.gitlens -- sourcegraph.cody-ai +- github.vscode-pull-request-github +- github.vscode-github-actions +- redhat.vscode-yaml - shopify.ruby-lsp - aki77.rails-db-schema - golang.go - ms-kubernetes-tools.vscode-kubernetes-tools +## tmux, iTerm2 and Claude Code + +[`.devcontainer/tmux.conf`](.devcontainer/tmux.conf) keeps the `C-a` prefix and +custom key bindings, and is configured so terminal-aware tools behave inside +tmux the way they do outside it: + +| Setting | Why | +| --- | --- | +| `default-terminal tmux-256color`, `terminal-features xterm*:RGB,...` | True color and modern terminfo inside panes. | +| `allow-passthrough all` | Claude Code's desktop notifications, tab progress bar and inline images reach iTerm2/Kitty/Ghostty, even from a window that is not visible. | +| `focus-events on` | Applications learn when their pane gains/loses focus. | +| `extended-keys on` + `extkeys` feature | Shift+Enter inserts a newline in Claude Code instead of submitting. | +| `set-clipboard on` | OSC 52 copies (Claude Code `/copy`) land on the local clipboard. | +| `escape-time 10` | Esc is instant in Claude Code and vim. | + +On the iTerm2 side, once: Settings → Profiles → Terminal → enable +"Notification Center Alerts" and, under "Filter Alerts", "Send escape +sequence-generated alerts"; Settings → General → Selection → "Applications in +terminal may access clipboard" (running `/terminal-setup` in Claude Code does +this one for you, even from inside tmux). Restart Claude Code after changing +tmux settings; it only inspects `$TMUX` at startup. + +iTerm2's **Session Status** indicator for Claude Code (OSC 7501) is not relayed +by a normal `tmux attach`. To get it inside tmux, attach with iTerm2's tmux +integration instead: `tmux -CC attach` (iTerm2 3.7.5+ shows Claude Code sessions +running in tmux-integration panes). + ## GitHub MCP server with the Copilot CLI The GitHub Copilot CLI ships with GitHub's MCP server enabled by default, so no @@ -65,14 +114,14 @@ To wire everything up inside the container: ## Faster Coder startup: prebuilt image cache on GHCR Coder builds this dev container with [envbuilder](https://github.com/coder/envbuilder), -which otherwise rebuilds the whole image (Dockerfile **and** every feature — -Go, Ruby, Rust, kubectl/helm/minikube, Azure CLI, Playwright, …) on every -workspace start. +which otherwise rebuilds the whole image on every workspace start. The [`Cache Devcontainer`](.github/workflows/cache-devcontainer.yml) workflow runs the same envbuilder and pushes a fully cached image to `ghcr.io/bmorton/devcontainer-cache` on every push to `main`, weekly, and on -manual dispatch. Coder then reuses the cache instead of rebuilding. +manual dispatch. Coder then reuses the cache instead of rebuilding. Because the +image is now feature-free, the `envbuilder_cached_image` probe can match it and +boot directly from the prebuilt image. ### One-time setup @@ -123,6 +172,14 @@ The cache repo is public, so no pull credentials are needed. Cache hits require identical build inputs and tooling between CI and Coder: the **same envbuilder version**, the **same architecture** (amd64), and the -**same repo content** (Dockerfile, `devcontainer.json`, feature versions). +**same repo content** (Dockerfile, `devcontainer.json`, `mise.toml`, `tmux.conf`). When you bump the envbuilder version, update it in both `.github/workflows/cache-devcontainer.yml` and the Coder template. + +## Verifying a build + +`scripts/verify-devcontainer.sh` runs inside the built container (CI uses it as +envbuilder's init script) and asserts: user is `node`, tmux starts with the +shipped config, the lifecycle completed, `/workspaces` is writable, and every +mise-pinned tool is installed and runnable from a plain `PATH` lookup. +`scripts/test-envbuilder.sh` reproduces the Coder build locally with Docker. diff --git a/scripts/verify-devcontainer.sh b/scripts/verify-devcontainer.sh index f2148c4..bbb387f 100755 --- a/scripts/verify-devcontainer.sh +++ b/scripts/verify-devcontainer.sh @@ -10,7 +10,9 @@ # It asserts the things that have previously broken on Coder/envbuilder: # 1. the effective user is `node`, not root, # 2. tmux is on PATH and can start a session that reads ~/.tmux.conf, -# 3. the lifecycle ran to completion (postCreateCommand sentinel exists). +# 3. the lifecycle ran to completion (postCreateCommand sentinel exists), +# 4. /workspaces is writable by the user (git clone used to fail there), +# 5. every tool pinned in mise.toml is installed and resolvable via shims. # # Any failed check exits non-zero so CI fails loudly instead of silently # dropping you into a broken shell. @@ -80,6 +82,51 @@ else fail "lifecycle sentinel missing ($sentinel); postCreateCommand did not complete" fi +# 4. /workspaces must be writable by the current user (envbuilder leaves the +# base image's root-owned directory alone; the Dockerfile chowns it). +probe="/workspaces/.verify-write-$$" +if mkdir "$probe" 2>/dev/null; then + rmdir "$probe" + pass "/workspaces is writable by $(id -un)" +else + fail "/workspaces is not writable by $(id -un) (owner: $(stat -c '%U:%G' /workspaces 2>/dev/null || echo unknown))" +fi + +# 5. mise-managed tools: nothing pinned in /etc/mise/config.toml may be missing, +# and a representative set of shims must execute from a plain PATH lookup +# (no `mise activate`), which is how VS Code / Coder / CI shells see them. +if command -v mise >/dev/null 2>&1; then + pass "mise is on PATH ($(mise --version 2>/dev/null | head -1))" + missing="$(mise ls --missing 2>/dev/null || true)" + if [ -z "$missing" ]; then + pass "all mise tools from the system config are installed" + else + fail "mise reports missing tools:"$'\n'"$missing" + fi + # Each entry is ":" since not every tool + # accepts --version. + for entry in "go:version" "ruby:--version" "cargo:--version" "kubectl:version --client" \ + "helm:version" "gh:--version" "az:version" "claude:--version" \ + "opencode:--version" "copilot:--version" "pi:--version" "witr:--version" \ + "tmux:-V" "gopls:version" "golangci-lint:--version"; do + tool="${entry%%:*}" + args="${entry#*:}" + # shellcheck disable=SC2086 # intentional word splitting of $args + if command -v "$tool" >/dev/null 2>&1 && "$tool" $args >/dev/null 2>&1; then + pass "$tool runs via shim ($(command -v "$tool"))" + else + fail "$tool is missing or fails to run ($tool $args)" + fi + done + if [ -x /usr/local/bin/nsc ]; then + pass "nsc installed" + else + fail "nsc missing from /usr/local/bin" + fi +else + fail "mise is not on PATH" +fi + echo "=== Verification complete ===" if [ "$failures" -ne 0 ]; then echo "$failures check(s) failed." >&2