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
12 changes: 9 additions & 3 deletions .devcontainer/mise.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
# Developer tools with no devcontainer Feature.
#
# Everything that has a Feature is pinned in devcontainer.json and baked into
# the image. The CLIs below are npm-distributed with no Feature, so mise
# installs and version-pins them. Claude Code is the exception — it self-updates
# through its own installer (scripts/lib/base-setup.sh).
# the image. Everything else is pinned here, whatever it is distributed as —
# mise resolves each tool for the container's own CPU architecture, which is
# what a hand-placed release binary does not do. Claude Code is the exception:
# it self-updates through its own installer (scripts/lib/base-setup.sh).
#
# actionlint must stay in step with .github/workflows/ci.yml, which installs the
# same version through the upstream download script. Two environments, one
# version — CI is not a mise host and does not read this file.
#
# Change a version here, then rebuild the container or run `mise install`.
# Docs: https://mise.jdx.dev

[tools]
"actionlint" = "1.7.11"
"npm:@openai/codex" = "0.143.0"
"npm:lefthook" = "2.1.10"
6 changes: 5 additions & 1 deletion .devcontainer/scripts/lib/base-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,16 @@ base_install_claude() {
# Verifies the CLIs this script installs (plus a couple of key Feature tools)
# are on PATH. Runtimes are validated by the container build itself.
#
# actionlint and shellcheck are here because `task check` runs both, and a tool
# that is absent or built for the wrong CPU architecture should fail loudly at
# container build rather than at the first push.
#
# Outputs:
# Writes tool status to stderr via log()
# Returns:
# 0 if all tools found, 1 if any are missing
base_verify_tools() {
verify_tools gh task codex lefthook claude
verify_tools gh task codex lefthook claude actionlint shellcheck
}

# --- Orchestrator ---
Expand Down
24 changes: 19 additions & 5 deletions .devcontainer/scripts/lib/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -139,23 +139,37 @@ install_npm_cli() {

# --- Verification ---

# Verifies that a list of commands are available on the PATH.
# Verifies that a list of commands are available on the PATH and can run.
#
# Being on the PATH is not enough. A release binary fetched for the wrong CPU
# architecture resolves fine and then dies with `exec format error` (exit 126)
# the first time anything invokes it, which is a failure worth catching at
# container build rather than mid-task. Every tool checked here exits 0 on
# `--version`, so a non-zero status means the binary is not runnable.
#
# Arguments:
# $@ — command names to check
# Outputs:
# Writes status of each tool to stderr via log()
# Returns:
# 0 if all tools found, 1 if any are missing
# 0 if all tools found and runnable, 1 otherwise
verify_tools() {
log "Verifying installed tools..."
local all_ok=true
local version
for cmd in "$@"; do
if has_cmd "$cmd"; then
log " ✓ ${cmd}: $("$cmd" --version 2>/dev/null || echo 'installed')"
else
if ! has_cmd "$cmd"; then
log " ✗ ${cmd}: MISSING"
all_ok=false
elif ! version="$("$cmd" --version 2>/dev/null)"; then
log " ✗ ${cmd}: on PATH at $(command -v "$cmd") but does not run"
all_ok=false
else
# First line only — shellcheck and actionlint both print a banner, and the
# status line is meant to be one line per tool. Trimmed after the status
# check rather than by piping through `head`, which would report the
# pipeline's exit code instead of the tool's.
log " ✓ ${cmd}: ${version%%$'\n'*}"
fi
done
[[ "${all_ok}" == true ]]
Expand Down
16 changes: 14 additions & 2 deletions .devcontainer/scripts/lib/env-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,24 @@ set -euo pipefail
# Recognizes lines that start with an uppercase identifier followed by '='.
# Skips blanks, comments (`#`), and commented-out overrides.
#
# A file declaring no keys is a legitimate state, not an error: this
# repository's template is entirely commented defaults by design. grep exits 1
# when it matches nothing, and `set -o pipefail` above would turn that into a
# failure of the whole check — so the match is captured before the pipeline
# rather than inside it.
#
# Arguments:
# $1 — path to env file
# Outputs:
# One KEY per line on stdout (deduplicated, in file order)
env_check_keys() {
local file="${1:?usage: env_check_keys <file>}"
[[ -f "${file}" ]] || return 0
grep -E '^[A-Z_][A-Z0-9_]*=' "${file}" | sed 's/=.*//' | awk '!seen[$0]++'

local declarations
declarations="$(grep -E '^[A-Z_][A-Z0-9_]*=' "${file}" || true)"
[[ -n "${declarations}" ]] || return 0
printf '%s\n' "${declarations}" | sed 's/=.*//' | awk '!seen[$0]++'
}

# Reports keys present in <example> but missing from <env>.
Expand All @@ -42,7 +52,9 @@ env_check_drift() {
local expected actual missing
expected="$(env_check_keys "${example_file}" | sort -u)"
actual="$(env_check_keys "${env_file}" | sort -u)"
missing="$(comm -23 <(echo "${expected}") <(echo "${actual}"))"
# An empty key set echoes as one blank line, which comm would read as a key
# named "". Dropping blanks keeps an empty set genuinely empty.
missing="$(comm -23 <(echo "${expected}") <(echo "${actual}") | grep -v '^$' || true)"

if [[ -n "${missing}" ]]; then
echo "${missing}" >&2
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
with:
bun-version: 1.3.14

- uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
Expand Down Expand Up @@ -62,7 +62,7 @@ jobs:
with:
bun-version: 1.3.14

- uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
Expand Down Expand Up @@ -100,7 +100,7 @@ jobs:
with:
bun-version: 1.3.14

- uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/devcontainer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
with:
bun-version: 1.3.14

- uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,5 @@ now rejects both, but the seeding still shows in the prose: some `description`
text uses platform vocabulary (`snapshot compute`) that a reader outside
`musher-dev/platform` cannot resolve. Compute Profile slugs used to be on that
list; blueprint §4.3 now carries the grammar and names where the vocabulary is
published, per docs/adr/0003. The `TODO` sections remaining in `blueprint` and
`listing` `spec.md` are the larger gap — they are what keeps v1 pre-stable.
published, per docs/adr/0003. Only `listing/v1/spec.md` still carries `TODO`
sections — they are what keeps v1 pre-stable.
2 changes: 0 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ version: '3'
vars:
TOOLS_DIR: tools
SITE_DIR: site
LEFTHOOK_VERSION: v2.1.10
ACTIONLINT_VERSION: v1.7.11

env:
# Keep bun's cache inside the workspace-visible cache dir the dev container
Expand Down
35 changes: 35 additions & 0 deletions conformance/blueprint/v1/cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@
"phase": "structural",
"path": "structural/021-unknown-cpu-dedication"
},
{
"id": "structural-022-generated-parameter-not-sensitive",
"phase": "structural",
"path": "structural/022-generated-parameter-not-sensitive"
},
{
"id": "structural-023-generated-parameter-marked-insensitive",
"phase": "structural",
"path": "structural/023-generated-parameter-marked-insensitive"
},
{
"id": "semantic-001-connection-names-unknown-role",
"phase": "semantic",
Expand Down Expand Up @@ -201,6 +211,31 @@
"id": "semantic-015-required-connection-input-unwired",
"phase": "semantic",
"path": "semantic/015-required-connection-input-unwired"
},
{
"id": "semantic-016-parameter-covers-no-input",
"phase": "semantic",
"path": "semantic/016-parameter-covers-no-input"
},
{
"id": "semantic-017-required-input-uncovered",
"phase": "semantic",
"path": "semantic/017-required-input-uncovered"
},
{
"id": "semantic-018-optional-parameter-does-not-cover",
"phase": "semantic",
"path": "semantic/018-optional-parameter-does-not-cover"
},
{
"id": "semantic-019-parameter-type-disagrees",
"phase": "semantic",
"path": "semantic/019-parameter-type-disagrees"
},
{
"id": "semantic-020-authored-override-well-formed",
"phase": "semantic",
"path": "semantic/020-authored-override-well-formed"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"code": "ERR_UNBOUND_PARAMETER",
"path": "/spec/parameters/legacyMode"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id": "semantic-016-parameter-covers-no-input",
"phase": "semantic",
"expected": "fail",
"clause": "specifications/blueprint/v1/spec.md#authored-parameters",
"summary": "An authored parameter whose key names no USER input is rejected.",
"document": "acme-wiki/blueprint.yaml"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# `legacyMode` is asked of the deploying user and read by nothing: neither
# component declares an input of that name. Binding is by key and there is no
# other correspondence to fall back on, so the value is collected and dropped.
specVersion: v1
kind: BLUEPRINT
metadata:
slug: acme-wiki
version: 1
spec:
components:
api:
component: ./components/api.yaml
size: general.standard.small
connections: {}
db:
component: ./components/postgres.yaml
size: general.standard.small
connections: {}
parameters:
legacyMode:
schema:
type: BOOLEAN
isRequired: true
ui:
label: Legacy mode
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
specVersion: v1
kind: COMPONENT
metadata:
version: 1
spec:
workload:
kind: SERVICE
source:
type: IMAGE
ref: postgres:17.10-alpine
endpoints:
primary:
containerPort: 5432
protocol: TCP
visibility: PRIVATE
contract:
inputs:
siteTitle:
schema:
type: STRING
default: Acme Wiki
isRequired: false
suppliedBy: USER
ui:
label: Site title
outputs: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
specVersion: v1
kind: COMPONENT
metadata:
version: 1
spec:
workload:
kind: SERVICE
source:
type: IMAGE
ref: postgres:17.10-alpine
endpoints:
primary:
containerPort: 5432
protocol: TCP
visibility: PRIVATE
contract:
inputs:
siteTitle:
schema:
type: STRING
default: Acme Wiki
isRequired: false
suppliedBy: USER
ui:
label: Site title
outputs: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
specVersion: v1
kind: LISTING
metadata:
slug: acme-wiki
version: 1
spec:
listingKind: BLUEPRINT
displayName: Acme Wiki
summary: A wiki backed by PostgreSQL
category: PRODUCTIVITY
lifecycleStage: STABLE
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"code": "ERR_UNCOVERED_REQUIRED_INPUT",
"path": "/spec/parameters"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id": "semantic-017-required-input-uncovered",
"phase": "semantic",
"expected": "fail",
"clause": "specifications/blueprint/v1/spec.md#authored-parameters",
"summary": "An input the deploying user must supply that no parameter covers is rejected.",
"document": "acme-wiki/blueprint.yaml"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# `postgres` requires an `adminPassword` the deploying user has to type: no
# generator mints it, no platform default derives it, and its schema carries no
# default. The override covers `siteTitle` and stops there, so the form never
# asks and the workload starts without it.
specVersion: v1
kind: BLUEPRINT
metadata:
slug: acme-wiki
version: 1
spec:
components:
api:
component: ./components/api.yaml
size: general.standard.small
connections: {}
db:
component: ./components/postgres.yaml
size: general.standard.small
connections: {}
parameters:
siteTitle:
schema:
type: STRING
default: Acme Wiki
ui:
label: Site title
Loading