Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
a579c40
feat(repo): rebuild exact-version schemas from tags, not from main
justinmerrell Aug 17, 2026
b5f8d78
feat(component): rename discriminator, and compile schemas under Ajv …
justinmerrell Aug 17, 2026
cd50993
feat(component): specify the Musher YAML profile
justinmerrell Aug 17, 2026
9c7cc5d
docs(repo): state one precedence rule, and adopt BCP 14
justinmerrell Aug 17, 2026
deb5a46
feat(tools): enforce what the schemas currently only assert
justinmerrell Aug 17, 2026
6219403
ci(repo): enforce DCO, pin actionlint, and close the tag-ruleset gaps
justinmerrell Aug 17, 2026
c5f0164
docs(repo): add security considerations and the format policy
justinmerrell Aug 17, 2026
4653dcb
feat(conformance): define named profiles for a conformance claim
justinmerrell Aug 17, 2026
7108f50
feat(tools): replay released documents against the candidate schema
justinmerrell Aug 17, 2026
1f8e9a3
ci(repo): check internal links and spelling
justinmerrell Aug 17, 2026
4203abb
ci(repo): cross-check the schemas with a second implementation
justinmerrell Aug 17, 2026
b643b45
feat(conformance): give requirements stable identifiers
justinmerrell Aug 17, 2026
ef42606
fix(conformance): stop git normalising the two byte-level parser fixt…
justinmerrell Aug 17, 2026
60dcfa1
style(tools): use the en-GB spelling the dictionary expects
justinmerrell Aug 17, 2026
2b54182
docs(component): close the last debt v1 declared blocking, and stop a…
justinmerrell Aug 17, 2026
68e7e36
ci(repo): pin the Task version instead of resolving a range
justinmerrell Aug 17, 2026
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
51 changes: 51 additions & 0 deletions .cspell/musher.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Musher domain vocabulary and the tooling this repository uses.
#
# A word belongs here when it is a term of art this repository uses
# deliberately, not when a spell checker merely has not heard of it. Prefer
# rewording over adding an entry: the list is a record of vocabulary the
# specification has committed to.

# --- Musher domain -----------------------------------------------------------
storefront
storefronts
endpointless
undeployable

# --- Specification craft -----------------------------------------------------
acyclicity
basename
basenames
extensionless
fixtured
modeline
negotiables
tiebreak
tightenings
vendoring

# --- British spellings the base dictionary lacks ------------------------------
deserialisation

# --- Names and acronyms ------------------------------------------------------
bluemonday
Pydantic
SLSA

# --- Hardware and format vocabulary the schemas name --------------------------
buildpacks
cuda
nvlink
nvme
webp
xlarge

# --- Tooling and JSON Schema terms -------------------------------------------
commonmark
gpgsign
metaschema
nojekyll
subschema
subschemas
unindexed
unioned
vendored
12 changes: 9 additions & 3 deletions .devcontainer/mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
# 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.
# actionlint and task must stay in step with .github/workflows/*.yml, which
# install the same versions themselves. Two environments, one version — CI is
# not a mise host and does not read this file.
#
# Both are pinned exactly rather than to a range. `setup-task` with a range
# resolves the newest match through the GitHub API on every run, so a degraded
# API takes every workflow down before it reaches a single check — which is how
# this pin came to be written.
#
# Change a version here, then rebuild the container or run `mise install`.
# Docs: https://mise.jdx.dev

[tools]
"actionlint" = "1.7.11"
"task" = "3.52.0"
"npm:@openai/codex" = "0.143.0"
"npm:lefthook" = "2.1.10"
7 changes: 7 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,10 @@ specifications/*/v*/schemas/dist/*.json linguist-generated=true
*.png binary
*.jpg binary
*.ico binary

# Two parser fixtures are *about* their bytes, so normalization would quietly
# destroy what they test. The CRLF case asserts CRLF endings are accepted; the
# `* text=auto eol=lf` rule above rewrote it to LF, and it passed while testing
# nothing. `-text` opts both out of every normalization.
conformance/component/v1/parser/013-crlf-line-endings/case.yaml -text
conformance/component/v1/parser/012-byte-order-mark/case.yaml -text
8 changes: 6 additions & 2 deletions .github/conventional-commits.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Single source of truth for the Conventional Commits vocabulary.
#
# Consumed by .github/workflows/lint-pr.yml. The lefthook commit-msg hook
# inlines the same type list — keep them in step.
# Nothing reads this file at runtime and nothing can: the lint-pr.yml action
# takes static YAML inputs, and the lefthook commit-msg hook is a POSIX regex.
# Both therefore inline these lists. What makes this file the source of truth
# rather than a third copy is `task check:commits`, which fails when any of the
# three disagree — the enforced form of the "keep them in step" this comment
# used to ask for politely.
types:
- feat
- fix
Expand Down
37 changes: 28 additions & 9 deletions .github/rulesets/RULESETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ Protects the default branch:
- **Stale reviews dismissed on push**, so an approval cannot survive a rewrite.
- **Linear history**, squash-merge only. The specification's history should read
as a sequence of deliberate changes.
- **Required status checks**: `Lint`, `Schema`, `Site Build`. `Schema` is the
one that matters most — it carries the bundle drift gate.
- **Required status checks**: `Lint`, `Schema`, `Site Build`, `Signed off`.
`Schema` is the one that matters most — it carries the bundle drift gate.
`Site Build` carries the publication-ledger gates. `Signed off` is the DCO
check CONTRIBUTING.md requires.
- **Deletion and force-push blocked.**

## `release-tags.json`
Expand All @@ -49,14 +51,31 @@ Makes releases immutable:

- Applies to `refs/tags/component/**`, `refs/tags/blueprint/**`, and
`refs/tags/listing/**`.
- **Blocks tag deletion and any non-fast-forward update.** A published schema
version can never be silently altered — a flaw is corrected by superseding it
with a new patch, never by moving a tag.
- Tag creation is restricted to repository administrators and the
release-please workflow.
- **Blocks tag deletion, tag update, and any non-fast-forward move.** A
published schema version can never be silently altered — a flaw is corrected
by superseding it with a new patch, never by moving a tag.

Tag **creation** is deliberately unrestricted. An earlier version of this file
claimed it was limited to administrators and the release-please workflow; no
such rule existed, and the claim was worse than the gap because it described a
control a reader would then not think to add. Creation is left open because
`published.json` is the control that matters: a tag with no ledger entry fails
`task check:published` and stops the deploy, so an unauthorised tag cannot
become a published version. See
[ADR 0006](../../docs/adr/0006-publication-from-tags.md).

## Bypass

Both rulesets allow `OrganizationAdmin` bypass, matching the org-level
`main-branch` allows `OrganizationAdmin` bypass, matching the org-level
convention. That is an escape hatch for incident response, not a workflow.
Using it on `release-tags` means mutating a published artifact — do not.

**`release-tags` allows no bypass at all.** Using a bypass there means mutating
a published artifact, which is the one thing this repository promises never
happens — and an escape hatch nobody may legitimately use is an escape hatch an
attacker inherits. An administrator who genuinely must intervene can disable
the ruleset, which is a logged, deliberate, visible act rather than a silent
one.

`published.json` remains the backstop either way: it makes a rewritten tag a
red build and a reviewable diff, which is a control that survives someone
holding the permissions to move the tag in the first place.
3 changes: 2 additions & 1 deletion .github/rulesets/main-branch.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"required_status_checks": [
{ "context": "Lint" },
{ "context": "Schema" },
{ "context": "Site Build" }
{ "context": "Site Build" },
{ "context": "Signed off" }
]
}
}
Expand Down
8 changes: 1 addition & 7 deletions .github/rulesets/release-tags.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,7 @@
"exclude": []
}
},
"bypass_actors": [
{
"actor_id": 1,
"actor_type": "OrganizationAdmin",
"bypass_mode": "always"
}
],
"bypass_actors": [],
"rules": [
{ "type": "deletion" },
{ "type": "non_fast_forward" },
Expand Down
54 changes: 49 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,33 @@ jobs:

- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.x
version: 3.52.0
repo-token: ${{ secrets.GITHUB_TOKEN }}

- name: Install tooling dependencies
working-directory: tools
run: bun install --frozen-lockfile

# The release tarball, verified against a pinned SHA-256, rather than
# piping an install script from a URL into bash. The script is fetched
# from a mutable ref and executed unverified, which made it the one place
# in CI where an upstream compromise would run arbitrary code with the
# workflow's token.
#
# Keep ACTIONLINT_VERSION in step with .devcontainer/mise.toml. CI is not
# a mise host and does not read that file.
- name: Install actionlint
env:
ACTIONLINT_VERSION: 1.7.11
ACTIONLINT_SHA256: 900919a84f2229bac68ca9cd4103ea297abc35e9689ebb842c6e34a3d1b01b0a
run: |
bash <(curl -sSfL https://raw.githubusercontent.com/rhysd/actionlint/v1.7.11/scripts/download-actionlint.bash) 1.7.11
sudo mv actionlint /usr/local/bin/
set -euo pipefail
archive="actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
curl -sSfL --retry 3 -o "$archive" \
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/${archive}"
echo "${ACTIONLINT_SHA256} ${archive}" | sha256sum -c -
tar -xzf "$archive" actionlint
sudo install -m 0755 actionlint /usr/local/bin/actionlint

- name: Run lint suite
run: task ci:lint
Expand All @@ -56,15 +72,19 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
# Full history and tags: check:compat replays each released version's
# accepted documents out of that version's own tag.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14

- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.x
version: 3.52.0
repo-token: ${{ secrets.GITHUB_TOKEN }}

- name: Install tooling dependencies
Expand All @@ -77,6 +97,15 @@ jobs:
- name: Schema, drift, examples, conformance
run: task ci:test

- name: Traceability matrix is current
run: |
task traceability
if ! git diff --quiet -- docs/traceability.md; then
echo "::error file=docs/traceability.md::stale. Run 'task traceability' and commit." >&2
git --no-pager diff -- docs/traceability.md
exit 1
fi

- name: Catalog is current
run: |
task catalog
Expand All @@ -94,21 +123,36 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
# Full history and every tag: pinned paths are rebuilt from the tags, and
# the ledger's append-only check compares against the base commit.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14

- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.x
version: 3.52.0
repo-token: ${{ secrets.GITHUB_TOKEN }}

- name: Install tooling dependencies
working-directory: tools
run: bun install --frozen-lockfile

# Every released version must still hash to what published.json recorded,
# and nothing already recorded may be edited or removed.
- name: Verify published versions
run: task check:published

- name: Ledger is append-only
if: github.event_name == 'pull_request'
env:
BASE_REF: ${{ github.event.pull_request.base.sha }}
run: task check:ledger

- name: Assemble publication tree
run: |
task bundle
Expand Down
72 changes: 72 additions & 0 deletions .github/workflows/dco.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: DCO

# CONTRIBUTING.md, GOVERNANCE.md, and the pull request template all require a
# Developer Certificate of Origin sign-off on every commit. Until this workflow
# existed, nothing checked it — the requirement lived entirely in prose, which
# is the same as not having it.

on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
check:
name: Signed off
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- name: Every commit carries a Signed-off-by trailer
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
failed=0

# --no-merges: a merge commit is not authored content, and requiring a
# trailer on one would fail every branch that has been brought up to
# date rather than rebased.
commits="$(git rev-list --no-merges "${BASE_SHA}..${HEAD_SHA}")"
if [ -z "$commits" ]; then
echo "No non-merge commits to check."
exit 0
fi

for sha in $commits; do
author="$(git show -s --format='%an <%ae>' "$sha")"
subject="$(git show -s --format='%s' "$sha")"

# Trailers only — a "Signed-off-by" written into the body is not a
# sign-off, and %(trailers) is what git itself considers one.
if git show -s --format='%(trailers:key=Signed-off-by,valueonly)' "$sha" \
| grep -qxF "$author"; then
echo " ✓ ${sha:0:8} ${subject}"
else
echo "::error::${sha:0:8} (${subject}) has no Signed-off-by matching its author ${author}"
failed=1
fi
done

if [ "$failed" -ne 0 ]; then
cat >&2 <<'MSG'

Every commit must carry a Developer Certificate of Origin sign-off
whose name and email match the commit author:

git commit -s

To fix commits already pushed:

git rebase --signoff origin/main && git push --force-with-lease

See CONTRIBUTING.md → Sign your work.
MSG
exit 1
fi
2 changes: 1 addition & 1 deletion .github/workflows/devcontainer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.x
version: 3.52.0
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Verify .env and .env.example stay in sync
run: |
Expand Down
Loading
Loading