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
10 changes: 10 additions & 0 deletions .devcontainer/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# The build context is `.devcontainer/` and the Dockerfile has no COPY
# instruction, so the correct context is empty.
#
# Not just tidiness: `.env` is gitignored and holds POSTGRES_PASSWORD and the
# MinIO root credentials. An un-ignored context still reaches the Docker daemon
# and can land in the build cache.
#
# Context-root form, not a `Dockerfile.dockerignore` sibling — with Features
# present the devcontainer CLI builds from a temp dir, so the sibling is missed.
*
24 changes: 9 additions & 15 deletions .devcontainer/.env.example
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
# ============================================================
# Dev Container Environment Template
# ============================================================
# This file is the source of truth for local-dev environment vars.
# On first container build, `initializeCommand` copies it to
# `.devcontainer/.env` (gitignored) on the host. From there:
# Source of truth for local-dev environment vars. On first build
# `initializeCommand` copies this to `.devcontainer/.env` (gitignored), which
# feeds both Docker Compose (via an explicit --env-file) and the dev container
# itself (via runArgs --env-file). See CONFIGURATION.md → "Environment
# Variables"; `task env:check` reports drift between the two files.
#
# * Docker Compose auto-discovers it (sibling to compose.yaml)
# and interpolates ${VAR:-default} references.
# * `runArgs --env-file` loads it into the dev container itself
# so shells, runtimes, and `task` runs see the same values.
#
# Drift checks: `task env:check` compares this file against your
# local `.env` and flags missing keys.
#
# Convention (three states):
# 1. Filled defaults — `VAR=value` safe demo values; override only if needed.
# 2. Required (empty) — `VAR=` must be filled in; container warns at startup.
# 3. Optional overrides — `# VAR=value` uncomment to enable.
# Three states, by convention:
# 1. Filled default — `VAR=value` safe demo value; override if needed.
# 2. Required (empty) — `VAR=` must be filled; startup warns.
# 3. Optional override — `# VAR=value` uncomment to enable.
# ============================================================


Expand Down
65 changes: 65 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# syntax=docker/dockerfile:1
# Musher dev container base image.
#
# bun, uv and task are installed here rather than as Features: their Features
# resolve release assets through an unauthenticated api.github.com call, which
# the shared egress IPs used by Codespaces and CI rate-limit, failing the whole
# build. mise is here because it was otherwise the one unpinned tool.
#
# Full rationale: CONFIGURATION.md → "Runtimes & Tools"
# Enforced by: repo toolchain check (TC-01..TC-03)
#
# Pinned to the LTS tag: floating `:ubuntu` rolls to interim releases that
# docker-in-docker does not support.
FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04

# Without pipefail a truncated `curl | bash` reports bash's exit status, giving
# a successful build and an image with no tool in it.
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# The single source of truth for what this image bakes. verify-toolchain.sh
# reads them back to assert the built container matches.
ARG BUN_VERSION=1.3.14
ARG UV_VERSION=0.11.28
ARG TASK_VERSION=3.52.0
ARG MISE_VERSION=v2026.8.6

RUN set -eux; \
command -v unzip >/dev/null 2>&1 || { echo "unzip missing from the base image; bun's installer requires it" >&2; exit 1; }; \
\
# --- bun -> /usr/local/bin/{bun,bunx}
# HOME is a scratch dir because the installer appends PATH exports to
# ~/.bashrc and ~/.zshrc whenever they are writable.
mkdir -p /tmp/bun-home; \
HOME=/tmp/bun-home BUN_INSTALL=/usr/local bash -c \
"curl --retry 5 --retry-all-errors --retry-delay 3 -fsSL https://bun.sh/install \
| bash -s bun-v${BUN_VERSION}"; \
rm -rf /tmp/bun-home; \
# Never write to /usr/local/bin/bunx: it is a symlink to bun, so doing so
# overwrites the binary and every `bun` call then recurses into itself.
\
# --- uv -> /usr/local/bin/{uv,uvx}
curl --retry 5 --retry-all-errors --retry-delay 3 -LsSf \
"https://astral.sh/uv/${UV_VERSION}/install.sh" \
| env UV_INSTALL_DIR=/usr/local/bin INSTALLER_NO_MODIFY_PATH=1 sh; \
\
# --- task -> /usr/local/bin/task
curl --retry 5 --retry-all-errors --retry-delay 3 -fsSL https://taskfile.dev/install.sh \
| sh -s -- -b /usr/local/bin "v${TASK_VERSION}"; \
\
# --- mise -> /usr/local/bin/mise
# System-wide; base-setup.sh still runs `mise reshim` to build the per-user
# shim dir that remoteEnv's PATH expects.
MISE_VERSION="${MISE_VERSION}" MISE_INSTALL_PATH=/usr/local/bin/mise \
bash -c 'curl --retry 5 --retry-all-errors --retry-delay 3 -fsSL https://mise.run | sh'; \
\
# Presence-only: the runtime version check lives in verify-toolchain.sh.
test -x /usr/local/bin/bun; \
test -x /usr/local/bin/bunx; \
test -x /usr/local/bin/uv; \
test -x /usr/local/bin/uvx; \
test -x /usr/local/bin/task; \
test -x /usr/local/bin/mise

# No USER: Features install after this stage and updateRemoteUserUID expects
# root at build time. devcontainer.json's remoteUser owns runtime identity.
27 changes: 0 additions & 27 deletions .devcontainer/compose.yaml

This file was deleted.

15 changes: 0 additions & 15 deletions .devcontainer/devcontainer-lock.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,10 @@
{
"features": {
"ghcr.io/devcontainers-extra/features/bun:1": {
"version": "1.1.0",
"resolved": "ghcr.io/devcontainers-extra/features/bun@sha256:0624284ecaead9dd4c6654616a7f939cfa4ebcbc60593700a74e35b1767befa5",
"integrity": "sha256:0624284ecaead9dd4c6654616a7f939cfa4ebcbc60593700a74e35b1767befa5"
},
"ghcr.io/devcontainers-extra/features/deno:1": {
"version": "1.0.4",
"resolved": "ghcr.io/devcontainers-extra/features/deno@sha256:7013bf7726828a33579604fe1aa5c36a253b578d5991e55800b418b56fd2cca5",
"integrity": "sha256:7013bf7726828a33579604fe1aa5c36a253b578d5991e55800b418b56fd2cca5"
},
"ghcr.io/devcontainers-extra/features/go-task:1": {
"version": "1.0.6",
"resolved": "ghcr.io/devcontainers-extra/features/go-task@sha256:4d1db153919976cadd3209ca05d655a761a01707767716994dad677b4538dc1b",
"integrity": "sha256:4d1db153919976cadd3209ca05d655a761a01707767716994dad677b4538dc1b"
},
"ghcr.io/devcontainers-extra/features/uv:1": {
"version": "1.0.2",
"resolved": "ghcr.io/devcontainers-extra/features/uv@sha256:1ac5b9f17a9e9e745933d0ac2ecf758e06ed3da4423cd22c94cce4d482fd2dd8",
"integrity": "sha256:1ac5b9f17a9e9e745933d0ac2ecf758e06ed3da4423cd22c94cce4d482fd2dd8"
},
"ghcr.io/devcontainers/features/common-utils:2": {
"version": "2.5.9",
"resolved": "ghcr.io/devcontainers/features/common-utils@sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a",
Expand Down
27 changes: 13 additions & 14 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
// Uncomment optional blocks as needed. Comment out what you don't use.
{
"name": "Musher Dev",
// Pin to the LTS, not the floating :ubuntu tag — it rolls to interim releases
// (e.g. 25.10) that upstream Features like docker-in-docker don't support.
"image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04",
// Dockerfile bakes the tools whose Features cannot be relied on; the
// Features below layer on top of it. Context is emptied by .dockerignore.
"build": {
"dockerfile": "Dockerfile",
"context": "."
},

"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/${localWorkspaceFolderBasename},type=bind,consistency=cached",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",

// Developer tools are pinned here as Features (baked into the image). Tools
// with no Feature — the Codex and Lefthook CLIs — live in
// .devcontainer/mise.toml; Claude Code self-updates via its native installer.
// See CONFIGURATION.md → "Runtimes & Tools".
// Which tier a tool belongs in: CONFIGURATION.md → "Runtimes & Tools".
"features": {
// --- Platform ---
"ghcr.io/devcontainers/features/common-utils:2": {
Expand All @@ -35,14 +35,12 @@
"installGradle": false,
"installMaven": false
},
// deno is safe as a Feature; bun, uv and task are not — do not re-add
// them. They are baked by the Dockerfile (TC-01 fails the build if they
// reappear here). Why: CONFIGURATION.md → "Runtimes & Tools".
"ghcr.io/devcontainers-extra/features/deno:1": { "version": "2.9.2" },
"ghcr.io/devcontainers-extra/features/bun:1": { "version": "1.3.14" },

// --- Package managers ---
"ghcr.io/devcontainers-extra/features/uv:1": { "version": "0.11.28" },

// --- Task runner & linting ---
"ghcr.io/devcontainers-extra/features/go-task:1": { "version": "3.52.0" },
// --- Linting ---
"ghcr.io/lukewiwa/features/shellcheck:0": { "version": "v0.11.0" },

// --- Database tooling ---
Expand Down Expand Up @@ -98,7 +96,8 @@
},

"remoteEnv": {
// mise shims (codex, lefthook) + ~/.local/bin (mise, Claude Code) on PATH.
// mise shims (codex, lefthook) + ~/.local/bin (Claude Code). The baked
// tools live in /usr/local/bin and are already on PATH.
"PATH": "/home/vscode/.local/share/mise/shims:/home/vscode/.local/bin:${containerEnv:PATH}"
},

Expand Down
23 changes: 8 additions & 15 deletions .devcontainer/mise.toml
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
# Developer tools that have no devcontainer Feature.
# Developer CLIs that have no devcontainer Feature and are only needed at
# runtime. Tools with a Feature are pinned in devcontainer.json; bun, uv, task
# and mise are baked by the Dockerfile.
# Tier rules: CONFIGURATION.md → "Runtimes & Tools".
#
# Everything with a Feature is pinned in devcontainer.json (baked into the
# image); the CLIs below are npm-distributed with no Feature, so mise installs
# and version-pins them instead. Claude Code is the one other exception — it
# self-updates via its native installer (scripts/lib/base-setup.sh).
# This file is the single source of these versions: post-create runs
# `mise install` against it, and CI resolves the same pins via jdx/mise-action.
# Edit a version here, then `task tools:install`.
#
# Change a version here, then run `task tools:install`. Devcontainer post-create
# runs `mise install` against this file. Docs: https://mise.jdx.dev
# See CONFIGURATION.md → "Runtimes & Tools".

# This file is the SINGLE source of tool versions. CI resolves the same pins
# from here via jdx/mise-action (MISE_GLOBAL_CONFIG_FILE), so a version is
# never stated twice and local and CI cannot drift.
#
# Backends are fully qualified (npm:, pipx:, aqua:) rather than short names so
# resolution does not depend on mise's registry.
# Backends are fully qualified so resolution does not depend on mise's registry.

[tools]
# --- AI + git hooks ------------------------------------------------------
Expand Down
25 changes: 9 additions & 16 deletions .devcontainer/scripts/initialize.sh
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
#!/usr/bin/env bash
# initialize.sh — Host-side bootstrap for the dev container.
#
# Runs on the host (via devcontainer.json `initializeCommand`) BEFORE
# `docker run` is invoked. Because `runArgs --env-file` is evaluated at
# `docker run` time, the .env file must exist on the host before the
# container starts — that's why this work lives here, not in
# post-create.sh.
# Runs on the host via `initializeCommand`, before `docker run` — it has to,
# because `runArgs --env-file` is evaluated at `docker run` time, so .env must
# already exist. post-create.sh would be too late.
#
# Responsibilities:
# * Create .devcontainer/.env from .env.example on first clone.
# * Touch an empty .env if no example exists, so --env-file never hard-fails.
# * Strip CRLF from .env (Windows/WSL safety — docker --env-file
# rejects files with CRLF line endings).
#
# Why this CRLF guard survives while the postCreateCommand one did not:
# .gitattributes (`* text=auto eol=lf`) normalizes every file Git checks
# out, which covers scripts/ and made the old `fix-crlf` step redundant.
# It cannot cover .env — that file is gitignored, generated locally, and
# hand-edited, so a Windows editor can reintroduce CR at any time.
# The CRLF guard here is not redundant with .gitattributes. That normalizes
# every file Git checks out, which is why the old postCreate fix-crlf step
# could go; it cannot reach .env, which is gitignored, generated locally and
# hand-edited, so a Windows editor can reintroduce CR at any time. Docker
# rejects an --env-file containing CRLF.
#
# Idempotent: safe to run on every container start.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Expand Down
13 changes: 11 additions & 2 deletions .devcontainer/scripts/lib/base-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ base_fix_nvm_permissions() {

# --- mise (pins the CLIs that have no devcontainer Feature) ---

# Fallback only: the image bakes mise at /usr/local/bin/mise, which
# `command -v` finds first. This path covers the base_install_mise fallback,
# which installs per-user.
readonly _MISE_BIN="${_HOME}/.local/bin/mise"
readonly _MISE_SHIMS="${_HOME}/.local/share/mise/shims"

Expand All @@ -83,7 +86,13 @@ base_setup_path() {
export PATH="${_MISE_SHIMS}:${_HOME}/.local/bin:${PATH}"
}

# Installs mise via the official installer if not already present.
# Installs mise if it is not already present.
#
# The dev container image bakes a pinned mise at /usr/local/bin/mise
# (ARG MISE_VERSION in .devcontainer/Dockerfile), so this normally short-circuits.
# The installer below is the fallback for a consuming repo that strips the
# Dockerfile, and is deliberately unpinned because in that case there is no ARG
# to read the pin from.
#
# Outputs:
# Writes progress to stderr via log()
Expand All @@ -94,7 +103,7 @@ base_install_mise() {
log "mise already installed, skipping"
return 0
fi
log "Installing mise (https://mise.run)..."
log "mise not baked into the image; falling back to https://mise.run..."
retry 3 5 bash -c 'curl -fsSL https://mise.run | sh'
}

Expand Down
6 changes: 0 additions & 6 deletions .devcontainer/scripts/lib/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
# and tool verification functions used by all setup scripts.
set -euo pipefail

# --- Logging ---

# Logs a timestamped message to stderr.
#
# Arguments:
Expand Down Expand Up @@ -105,8 +103,6 @@ setup_config_dirs() {
done
}

# --- NVM helpers ---

# Fixes NVM directory ownership to the current user.
#
# Globals:
Expand All @@ -121,8 +117,6 @@ fix_nvm_permissions() {
fi
}

# --- NPM install helper ---

# Installs an npm package globally with retry logic.
#
# Arguments:
Expand Down
Loading