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
5 changes: 2 additions & 3 deletions .config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Every linter, formatter, and hook config lives here. One directory, one purpose:
if a tool needs a config file and it is not provisioning the container, it goes
in here.

Policy and rationale: [`CONFIGURATION.md`](../CONFIGURATION.md). Enforcement:
Policy and rationale: [`docs/configuration.md`](../docs/configuration.md). Enforcement:
engineering-conventions' CONF family (`conventions check`).

## Index
Expand All @@ -17,7 +17,6 @@ engineering-conventions' CONF family (`conventions check`).
| `yaml/yamllint.yaml` | yamllint | `-c .config/yaml/yamllint.yaml` |
| `actions/actionlint.yaml` | actionlint | `-config-file .config/actions/actionlint.yaml` |
| `spelling/codespell.cfg` | codespell | `--config .config/spelling/codespell.cfg` |
| `docker/hadolint.yaml` | hadolint | `--config .config/docker/hadolint.yaml` |
| `commits/committed.toml` | committed | `--config .config/commits/committed.toml` (commit-msg hook, PR title workflow) |
| `mise/config.toml` | mise | **Auto-discovered.** mise reads `.config/mise/config.toml` from the project root (TOOL-01) |
| `mise/mise.lock` | mise | Auto-discovered beside the config; `mise lock` writes it (TOOL-05) |
Expand Down Expand Up @@ -58,7 +57,7 @@ from that same file via `jdx/mise-action`.
| `compose.yaml`, stack configs | `.devcontainer/` | Same — environment, not code quality |
| `.gitattributes`, `.gitignore` | Repo root | Git reads these from the root only |
| VS Code settings | `devcontainer.json` | `customizations.vscode.settings` is the single editor source |
| Product-native configs (`rustfmt.toml`, `.cargo/`, `tsconfig.json`, `ruff.toml`) | `<product>/` | Their tools find them by walking up from the product; see [LAYOUT.md](../LAYOUT.md) |
| Product-native configs (`rustfmt.toml`, `.cargo/`, `tsconfig.json`, `ruff.toml`) | `<product>/` | Their tools find them by walking up from the product; see [docs/layout.md](../docs/layout.md) |

## A trap worth knowing

Expand Down
3 changes: 1 addition & 2 deletions .config/commits/committed.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ no_fixup = false
no_wip = true
allowed_types = ["feat", "fix", "perf", "docs", "chore", "refactor", "test", "ci", "build"]
allowed_scopes = [
"devcontainer", # .devcontainer/ -- the image, its scripts, the template config
"devcontainer", # .devcontainer/ -- the config, its Features and scripts
"stacks", # .devcontainer/stacks/ -- the compose stacks
"config", # .config/ -- tool configuration
"repo", # .repo/, docs, Taskfiles -- repository machinery
"ci", # .github/ -- workflows and rulesets
"deps", # dependency updates (Dependabot)
"release", # release pull requests (release-please)
]
4 changes: 0 additions & 4 deletions .config/docker/hadolint.yaml

This file was deleted.

9 changes: 2 additions & 7 deletions .config/lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
# Location: this file lives in .config/, not the repo root. Lefthook
# discovers it natively — `MainConfigNames` in internal/config/loader.go is
# ["lefthook", ".lefthook", ".config/lefthook"] — so no flag, env var or
# wrapper is involved. See CONFIGURATION.md → "Where configuration lives".
# wrapper is involved. See docs/configuration.md → "Where Configuration Lives".
#
# Search order is first-match-wins: a stray root lefthook.yml would silently
# shadow this file. `repo config check` fails the build if one appears.
# shadow this file. `conventions check` (CONF-06) fails if one appears.
#
# Local overrides: .config/lefthook-local.yml (gitignored) is auto-merged.
# Debug: lefthook run pre-commit | lefthook dump | lefthook validate
Expand Down Expand Up @@ -74,11 +74,6 @@ pre-commit:
run: actionlint -config-file .config/actions/actionlint.yaml {staged_files}
fail_text: "Workflow lint failed. Run 'task lint:actions' for the full output."

- name: dockerfile
glob: "**/{Dockerfile,*.Dockerfile}"
run: hadolint --config .config/docker/hadolint.yaml {staged_files}
fail_text: "hadolint found a problem in a staged Dockerfile. Run 'task lint:docker'."

- name: governance
# Structural policy (`repo check`). No glob: the layout policy reads
# every root entry and a product dir whose name the template cannot
Expand Down
24 changes: 17 additions & 7 deletions .config/mise/config.toml
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
# The repository's one mise configuration (TOOL-01): every CLI that is not a
# devcontainer Feature. The Dockerfile bakes only mise itself; post-create runs
# `mise install --locked` against this file, and CI resolves the same pins via
# jdx/mise-action. Tier rules: CONFIGURATION.md → "Runtimes & Tools".
# devcontainer Feature. The mise Feature installs only mise itself;
# post-create runs `mise install --locked` against this file, and CI resolves
# the same pins via jdx/mise-action. Tier rules: docs/configuration.md → "Runtimes & Tools".
#
# Edit a version here, run `mise lock`, commit both files, then
# `task tools:install`. Backends are fully qualified so resolution does not
# depend on mise's registry.

# Keep in lockstep with ARG MISE_VERSION in .devcontainer/Dockerfile and the
# jdx/mise-action `version:` in .github/workflows/ (TOOL-04).
# Keep in lockstep with the mise Feature's `version` in
# .devcontainer/devcontainer.json and the jdx/mise-action `version:` in
# .github/workflows/ (TOOL-04).
min_version = "2026.8.6"

[tools]
# --- Conventions ---------------------------------------------------------
"github:musher-dev/engineering-conventions" = "0.8.0"

# --- Toolchain (was baked in the Dockerfile) -------------------------------
# --- Toolchain -------------------------------------------------------------
# mise.lock records each download URL and checksum, so `install --locked`
# makes none of the GitHub API calls that rate-limited the Features.
"aqua:oven-sh/bun" = "1.3.14"
Expand All @@ -33,5 +34,14 @@ min_version = "2026.8.6"
"pipx:yamllint" = "1.38.0"
"pipx:codespell" = "2.4.3"
"aqua:rhysd/actionlint" = "1.7.12"
"aqua:hadolint/hadolint" = "2.15.1"

# --- Optional (uncomment what the product needs, then `mise lock`) -------
# Infrastructure:
# "aqua:opentofu/opentofu" = "1.13.1"
# "aqua:terraform-linters/tflint" = "0.64.0"
# "aqua:terraform-docs/terraform-docs" = "0.24.0"
# "aqua:digitalocean/doctl" = "1.183.0"
# "aqua:cloudflare/cloudflared" = "2026.10.0"
# Protobuf:
# "aqua:bufbuild/buf" = "1.73.0"
"aqua:mikefarah/yq" = "4.53.6"
22 changes: 0 additions & 22 deletions .config/mise/mise.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 29 additions & 22 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
// Musher Dev Container — Batteries-included template.
// Uncomment optional blocks as needed. Comment out what you don't use.
//
// This builds the image from source in image/, so the template tests its own
// changes. Once a release is published, it becomes
// "image": "ghcr.io/musher-dev/development-container:<version>"
// and drops everything the image's metadata already carries (the platform
// Features, mounts, env and lifecycle commands) -- README.md → "Use the image".
// No image of our own: the Microsoft base image, the Features below, and the
// lifecycle scripts in scripts/. Decision: docs/decisions/0004.
{
"name": "Musher Dev",
"build": {
"dockerfile": "image/Dockerfile",
"context": "image"
},
// The LTS tag, not floating `:ubuntu`, which rolls to interim releases that
// docker-in-docker does not support. Dependabot raises Features, not this
// tag: bump it by hand.
"image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04",

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

// Which tier a tool belongs in: CONFIGURATION.md → "Runtimes & Tools".
// Which tier a tool belongs in: docs/configuration.md → "Runtimes & Tools".
"features": {
// --- Platform ---
"ghcr.io/devcontainers/features/common-utils:2": {
Expand All @@ -29,6 +26,11 @@
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/github-cli:1": { "version": "2.96.0" },
"ghcr.io/devcontainers/features/docker-in-docker:4": {},
// Every CLI that is not a Feature is a mise pin in .config/mise/config.toml.
// Local, not devcontainers-extra's mise Feature: that one calls
// api.github.com on every build. Keep `version` equal to the config's
// min_version (TOOL-04); verify-toolchain.sh fails on drift.
"./features/mise": { "version": "2026.8.6" },

// --- Runtimes ---
"ghcr.io/devcontainers/features/node:2": { "version": "24.18.0" },
Expand All @@ -45,20 +47,20 @@
"installGradle": false,
"installMaven": false
},
// bun, uv and task are mise pins in .config/mise/config.toml, not
// Features. Why: CONFIGURATION.md → "Runtimes & Tools".
"ghcr.io/devcontainers-extra/features/deno:1": { "version": "2.9.2" },
// Rust (uncomment for a Cargo product, as host-agent). Pair it with the
// target/ volume under "mounts", and pin the toolchain in the product's
// rust-toolchain.toml.
// "ghcr.io/devcontainers/features/rust:1": { "version": "1.98.1", "profile": "minimal" },

// --- Linting ---
"ghcr.io/lukewiwa/features/shellcheck:0": { "version": "v0.11.0" },

// --- Database tooling ---
"ghcr.io/robbert229/devcontainer-features/postgresql-client:1": { "version": "16" }

// --- Optional infrastructure tooling (uncomment as needed) ---
// "ghcr.io/devcontainers-extra/features/opentofu:1": {},
// "ghcr.io/devcontainers-extra/features/digitalocean-cli:1": {},
// "ghcr.io/devcontainers-extra/features/cloudflared:1": {}
// Optional infrastructure (OpenTofu, tflint, doctl, cloudflared) and
// protobuf (buf) tools are commented mise pins in .config/mise/config.toml.
},

"containerUser": "vscode",
Expand All @@ -70,14 +72,15 @@
// Allow debuggers (gdb, strace, delve) to attach to processes.
"capAdd": ["SYS_PTRACE"],

// One volume per tool per container (DEVC-08): logins survive a rebuild.
"mounts": [
"source=musher-${devcontainerId}-gh-config,target=/home/vscode/.config/gh,type=volume",
"source=musher-${devcontainerId}-claude-config,target=/home/vscode/.claude,type=volume",
"source=musher-${devcontainerId}-codex-config,target=/home/vscode/.codex,type=volume"
// Uncomment to persist Docker-in-Docker data across rebuilds:
// "source=musher-${devcontainerId}-dind-data,target=/var/lib/docker/volumes,type=volume"
// Product build output (e.g. Rust's target/), kept off the bind mount.
// Must sit under the declared product -- LAYOUT.md → "Ecosystem Adapters":
// Must sit under the declared product -- docs/layout.md → "Ecosystem Adapters":
// "source=musher-${devcontainerId}-target,target=/workspaces/${localWorkspaceFolderBasename}/<product>/target,type=volume"
],

Expand All @@ -98,6 +101,10 @@
// lands in ~/ -- outside the claude volume -- and is lost on every rebuild.
"CLAUDE_CONFIG_DIR": "/home/vscode/.claude",

// corepack resolves pnpm/yarn from package.json `packageManager` without
// asking first; post-create runs `corepack enable`.
"COREPACK_ENABLE_DOWNLOAD_PROMPT": "0",

"PYTHONUNBUFFERED": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"UV_LINK_MODE": "copy",
Expand All @@ -114,17 +121,17 @@
},

"remoteEnv": {
// mise shims (task, uv, codex, lefthook, ...) + ~/.local/bin (Claude
// Code). mise itself is baked in /usr/local/bin.
"PATH": "/home/vscode/.local/share/mise/shims:/home/vscode/.local/bin:${containerEnv:PATH}"
// ~/.local/bin (Claude Code). The mise Feature puts mise itself and its
// shims (task, uv, codex, lefthook, ...) on PATH.
"PATH": "/home/vscode/.local/bin:${containerEnv:PATH}"
},

// No initializeCommand: nothing runs on the host. post-create writes
// .devcontainer/.env from env.schema.yaml inside the container.
// Line endings are enforced repo-wide by .gitattributes (`* text=auto eol=lf`).
"waitFor": "postCreateCommand",
"postCreateCommand": ["bash", ".devcontainer/image/scripts/post-create.sh"],
"postStartCommand": ["bash", ".devcontainer/image/scripts/startup.sh"],
"postCreateCommand": ["bash", ".devcontainer/scripts/post-create.sh"],
"postStartCommand": ["bash", ".devcontainer/scripts/startup.sh"],

"forwardPorts": [15432, 15433, 15434, 15435, 15436, 15440, 15441, 15442, 15443, 15444, 15445, 15446, 15447, 15448, 15460],
"portsAttributes": {
Expand Down
2 changes: 1 addition & 1 deletion .devcontainer/env.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# The source of truth for every variable the *dev environment* reads:
# `.devcontainer/.env.example` is rendered from this file (`task env:render`),
# and `repo env check` fails when the two disagree. A product's own runtime
# contract is a separate schema at <product>/env.schema.yaml — LAYOUT.md
# contract is a separate schema at <product>/env.schema.yaml — docs/layout.md
# explains the split.
#
# `consumers:` names the stacks that read a binding, so `repo env doctor` only
Expand Down
20 changes: 20 additions & 0 deletions .devcontainer/features/mise/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"id": "mise",
"version": "1.0.0",
"name": "mise",
"description": "Installs a pinned mise at /usr/local/bin/mise from its GitHub release, with no api.github.com call, and puts the remote user's mise shims on PATH.",
"documentationURL": "https://github.com/musher-dev/development-container/blob/main/docs/configuration.md#the-mise-feature",
"options": {
"version": {
"type": "string",
"default": "2026.8.6",
"description": "The mise version, with or without a leading 'v'. Keep it equal to min_version in .config/mise/config.toml."
}
},
"containerEnv": {
"PATH": "/home/vscode/.local/share/mise/shims:${PATH}"
},
"installsAfter": [
"ghcr.io/devcontainers/features/common-utils"
]
}
18 changes: 18 additions & 0 deletions .devcontainer/features/mise/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# install.sh — the local mise Feature (.devcontainer/features/mise/).
#
# Runs as root at build time. mise.run downloads the release asset straight
# from github.com/jdx/mise/releases/download/, so a pinned version makes no
# api.github.com call: the rate limit that ruled out devcontainers-extra's mise
# Feature (it resolves every version through the API) never applies.
#
# Presence-only check here; verify-toolchain.sh asserts the version, because
# running a binary in the layer that installed it is a known BuildKit hazard.
set -euo pipefail

VERSION="${VERSION:?set the version option}"
readonly INSTALL_PATH=/usr/local/bin/mise

curl --retry 5 --retry-all-errors --retry-delay 3 -fsSL https://mise.run \
| MISE_VERSION="v${VERSION#v}" MISE_INSTALL_PATH="${INSTALL_PATH}" sh
test -x "${INSTALL_PATH}"
38 changes: 0 additions & 38 deletions .devcontainer/image/Dockerfile

This file was deleted.

34 changes: 0 additions & 34 deletions .devcontainer/image/devcontainer-lock.json

This file was deleted.

Loading