One commit ships two public surfaces:
- the
shpCLI and the setup action, under the tagvX.Y.Z; - the bundled Shape plugin, under the tag
shapelang--vX.Y.Z.
The GitHub release title is the CLI tag, vX.Y.Z. The publish command sets it
explicitly so GitHub does not display the tagged commit's message as the title.
Four versions must agree on X.Y.Z: packages/shp-cli/package.json,
plugins/shapelang/.codex-plugin/plugin.json,
plugins/shapelang/.claude-plugin/plugin.json, and the # Shape vX.Y.Z heading
in docs/releases/vX.Y.Z.md.
A release moves through six steps: prepare the change, validate it locally, merge
it, run the release candidate on the merged master commit, tag that commit, and
confirm the publication. Both tags go on the commit that has a successful,
manually approved candidate run, and that commit must be current master. The
workflows reject anything else.
One-time repository setup:
- The repository environment
skills-release-approvalhas the maintainer as a required reviewer. - The repository secret
ANTHROPIC_API_KEYorANTHROPIC_AUTH_TOKENis set. The candidate's skills job fails without one, even when it copies an earlier report.ANTHROPIC_BASE_URLis optional, for a gateway.
On the maintainer's machine:
- the Bun version pinned by
packageManagerin the rootpackage.json; confirm it withbun --version. CI and both release workflows resolve the same pin throughoven-sh/setup-bun; - the contributor prerequisites in CONTRIBUTING.md,
including
zstdandsha256sumforbun run build:release; - an authenticated
ghCLI; - a Linux x64 host, because the local smoke commands in step 2 test
shp-linux-x64.tar.gz.
Choose the semver version X.Y.Z, then, on a release-prep branch:
-
Set
X.Y.Zinpackages/shp-cli/package.json,plugins/shapelang/.codex-plugin/plugin.json, andplugins/shapelang/.claude-plugin/plugin.json. -
Write
docs/releases/vX.Y.Z.md. It must contain the heading line# Shape vX.Y.Z, from which the metadata check reads the release-notes version. The file becomes the GitHub release notes verbatim, and the Release workflow fails if the published notes differ from it. -
Run
bun run release:metadata. It fails when the four versions disagree, and it lists every file that is missing its snippet for the new version. The pins come fromreleaseVersionPinsinscripts/check-release-metadata.ts, and they include files outside the docs site:README.md,CONTRIBUTING.md,.github/prompts/shape-skills-release.md,plugins/shapelang/skills/shape-lang/SKILL.md, andshape/delivery.shape. In each pinned file, replace every occurrence of the old version, not just the one the check names: the check confirms only that one occurrence of each snippet exists, so a second, stale copy still passes. Rerun until it printsRelease metadata is synchronized at vX.Y.Z.Plainbun testalso fails on a missing pin. -
Review every shipped skill, including its references and
agents/openai.yaml, against the current CLI and language behaviour. Update any affected skill entrypoint, reference, or agent metadata. The review covers commands and patterns, not syntax alone:- draft unknowns followed by a strict final check;
- explicit
graph all|show|statscommands; - final forbids, forbidden paths, guards, coverage, and bindings remaining hard;
- domain-pack discovery and import and name-resolution behaviour;
- generated AST and analyzer output as navigation or advisory evidence only;
- stable
file#symbolor file-only source and evidence references; - provider-neutral author and critic workflows;
- LSP and editor behaviour;
- evidence-backed preflight, index, guard, and code-review decisions.
This review is broader than the candidate's fixed static checks, which do not replace it.
-
When skill instructions change, collect fresh held-out forward-test evidence on the supported models, including Codex. Approval in step 4 requires it, and no CI job produces it, so run the tests now and fix any failure in this change. A task stops being held out once its labels, structure, expected answer, or failure-specific wording has been copied into the skill. Keep the raw artifacts under
.research/.
From the release-prep branch:
bun install --frozen-lockfile
bun run langium:generate
bun run ast:generate
bun run changed-files
bun run format:check
bun run lint
bun run skills:check
bun test
bun run typecheck
bun run shape:ci
bun run shape:loosening
bun run docs:check
SHAPE_RELEASE_VERSION=vX.Y.Z bun run build:release
scripts/smoke-release-binary.sh \
--expected-version X.Y.Z \
dist/release/shp-linux-x64.tar.gz
bun scripts/run-release-canaries.ts --archive dist/release/shp-linux-x64.tar.gzThe last two commands are the same archive smoke test and canaries that CI runs. Commit any regenerated Langium and AST files.
Commit and push the release-prep branch, open a pull request, wait for every required check, review the complete diff, and merge it.
The Release Candidate: Skills workflow (.github/workflows/release-candidate.yml)
runs only when dispatched on master. It has four jobs:
| Job | Needs | Does |
|---|---|---|
Validate release candidate (validate) |
— | Requires master, then checks release metadata, skills:check, the regenerated Langium artifacts, ast:check, format, lint, tests, typecheck, changed-files plus shape:ci, and docs:check. Builds the release assets with SHAPE_RELEASE_VERSION set to the tag, runs the full smoke test and run-release-canaries.ts on the Linux x64 archive, and uploads the other three archives. |
Smoke packed archive (smoke-archives) |
validate |
Runs scripts/smoke-release-binary.sh --quick (version, help, check, AST generation) on native Linux ARM64, macOS ARM64, and Windows x64 runners. |
Evaluate skill conformance and behavior (skills) |
validate |
Requires Claude credentials. Uses the shared Sonnet 5.5 default to evaluate all six shipped skills against their static checks and fixture cases, or copies an approved report (see Skills report reuse). Uploads skill-release-report-<commit>. |
Skill Release Approval (approval) |
all three | Waits in the protected environment skills-release-approval for a human, then records the version, both tags, the commit, and the report name in the job summary. |
smoke-archives and skills both depend only on validate, so they run in
parallel.
-
Dispatch the candidate and find its run:
gh workflow run release-candidate.yml --ref master gh run list --workflow release-candidate.yml --branch master --limit 5
The run tests the commit that
masterpoints at when you dispatch it. Note that commit; step 5 tags exactly it.From dispatch until the Release workflow publishes, merge nothing to
master. Tagging requires the candidate's commit to be currentmaster, and publication checks it again. -
Wait until
validate, everysmoke-archivesleg, andskillspass. -
Inspect the skills report artifact and the job summary.
-
Approve
skills-release-approvalonly when every static check, fixture case, and instruction is right and, if skill instructions changed, the held-out evidence from step 1 exists. A model pass alone is not enough. Otherwise reject the deployment, fix the problem in a new pull request, merge it, and dispatch a new candidate for the new commit.
Tag only the commit that the approved candidate ran on, never a pull request
branch, a detached HEAD, or a dirty worktree. The first three lines below
check this.
git switch master && git pull --ff-only
git status --short # must print nothing
git rev-parse HEAD # must equal the approved candidate run's commit
bun scripts/check-release-metadata.ts --tag vX.Y.Z # prints: Release metadata is synchronized at vX.Y.Z.
git tag vX.Y.Z && git tag shapelang--vX.Y.Z
git push origin vX.Y.Z shapelang--vX.Y.ZBoth tags are lightweight. Push them in one command: the vX.Y.Z push starts the
Release workflow, whose first job runs
bun scripts/check-release-approval.ts --require-exact. That check stops the
release unless shapelang--vX.Y.Z already resolves to the same commit, that
commit is current master, and it has a successful, manually approved candidate
run. An approved run on an ancestor commit does not count.
The master freeze from step 4 lasts until the Release workflow has published.
The publish step repeats --require-exact immediately before
gh release create, so a merge during the run fails publication, and rerunning
the workflow on the same tag fails the same check.
Never move or replace a published release tag.
Wait for the Release run (.github/workflows/release.yml, triggered by tags
matching v*.*.*) and confirm that every job passed. The jobs run in this
order, except that the last two run in parallel:
| Job | Checks |
|---|---|
Verify skill release approval (approval) |
The tag commit is current master, shapelang--vX.Y.Z points at it, and it has a successful, approved candidate run. |
Validate (validate) |
check-release-metadata.ts --tag against the pushed tag, skills:check, the regenerated Langium artifacts, ast:check, format, lint, tests, typecheck, format:shape:check, shp check, shp obligations, shp memory, and docs:check. |
Release (release) |
Rebuilds the assets, runs the full smoke test and run-release-canaries.ts on the Linux x64 archive, repeats the approval check, and publishes the GitHub release with every asset and the notes from docs/releases/vX.Y.Z.md. |
Verify published release (verify) |
On Linux x64, Linux ARM64, macOS ARM64, and Windows x64: installs vX.Y.Z through the setup action, checks that shp --version reports X.Y.Z, and runs shp check. On Linux and Windows it also runs the release-hosted installer and checks the installed version. |
Verify published release metadata and assets (verify-publication) |
The release title equals vX.Y.Z, the release is neither a draft nor a prerelease, has exactly the seven assets, carries notes equal to docs/releases/vX.Y.Z.md, passes sha256sum --check checksums.txt, and both installers contain the tag. |
- Local validation or pull request checks fail. Fix the release-prep branch before merging.
- A candidate job fails, or the skills report is wrong. Reject the approval deployment if it is waiting, fix the problem in a new pull request, merge it, and dispatch a new candidate for the new commit.
mastermoves after the candidate. Dispatch a new candidate for the new commit. Do not tag the ancestor.- Publication fails. Fix forward from a new commit and version. Do not silently retag a version that users may already have fetched.
The skills job normally runs a fresh model evaluation. First, though, it runs
bun scripts/check-release-approval.ts --reuse-if-eligible and copies an
approved report instead when either of these holds:
- a successful, manually approved candidate run exists for the same commit;
- such a run exists for an ancestor commit, and
git diff --name-only --no-renames ANCESTOR HEADlists no path underSKILLS_RELEVANT_PATH_PREFIXESinscripts/check-release-approval.ts.
Only candidate runs dispatched on master count. A same-commit run takes
precedence over an ancestor, and among eligible runs of one kind the highest run
number wins. If the lookup fails, the job falls back to a fresh
evaluation.
SKILLS_RELEVANT_PATH_PREFIXES is the authority for the path list. An entry
ending in / matches everything under that directory, and any other entry
matches one exact path. It currently holds:
plugins/shapelang/,packages/,fixtures/.github/prompts/,.github/scripts/,.github/actions/,.github/shape-contract/.github/workflows/release-candidate.yml,.github/workflows/release.ymlscripts/,package.json,bun.lock,action.yml,install.sh,install.ps1
Because --no-renames lists both sides of a move, renaming a file out of those
paths still forces a fresh evaluation.
Reuse is not limited to docs changes: a commit whose diff from the approved
ancestor touches only paths outside the list, such as shape/*.shape,
docs-site/, docs/releases/, or experiments/, can copy the report. A
release-prep commit always changes plugins/shapelang/ (the manifest versions),
so it can never copy an ancestor's report; its first candidate always runs a
fresh evaluation.
Copying replaces only the model evaluation. The skills job still runs and still
needs Claude credentials, and the new commit still needs its own validate job,
archive smoke tests, and human approval.
These checks answer different questions, and none substitutes for another:
bun run skills:check(scripts/check-skills.ts) lints the shipped skill corpus. CI's Lint job, the candidate, and the Release workflow all run it.- The candidate JSON report is a model evaluation. It covers the fixed
static-check IDs for each skill (
.github/prompts/shape-skills-release.mdand.github/scripts/skills-release-config.mjs) and the behavioural cases infixtures/skills/cases.json, run withbun shp. The gate validates the report's schema, its skill, static-check, and case IDs, the required command evidence, and the evidence markers. The rationale text is still model-written and does not prove that every skill works on every supported model, so a human must approveskills-release-approval. - Held-out forward tests (step 1) on the supported models, including Codex, are required when skill instructions change. They are not a CI job.
run-release-canaries.ts runs the same fixture-case commands against the packed
Linux x64 archive and checks their exit codes. Those canaries and the model's
fixture cases are smoke tests, not held-out evidence.
scripts/build-release-assets.sh writes the assets to dist/release/, which Git
ignores:
shp-linux-x64.tar.gzshp-linux-arm64.tar.gzshp-darwin-arm64.tar.gzshp-windows-x64.tar.gzinstall.shinstall.ps1checksums.txt
The archive targets come from packages/shp-checker/src/tree-sitter-native-targets.ts.
Each archive holds shp (shp.exe on Windows), LICENSE, and the
tree-sitter-language-pack parser assets that shp ast source uses for
TypeScript, TSX, JavaScript/JSX, Rust, Go, Python, and Swift. checksums.txt
holds the SHA-256 of every other asset.
The builder reads the native parser packages and the archive targets from the checker before it builds anything. If either read fails, it exits with that read's status, and an empty target list exits 1, so no archive, installer, or checksum file is built from a partial list.
The builder replaces __SHAPE_DEFAULT_VERSION__ in both installers with
SHAPE_RELEASE_VERSION, or else GITHUB_REF_NAME, or else latest. The
candidate sets SHAPE_RELEASE_VERSION to the tag. The Release workflow leaves
it unset, so the builder injects GITHUB_REF_NAME, which GitHub sets to the
pushed tag, and a release-hosted installer defaults to the release it came
from. An installer taken straight from the repository has no injected version
and defaults to latest. SHAPE_VERSION overrides the default at install
time.