Skip to content

Releasing `crimes`

How a new crimes@X.Y.Z reaches npm and crimes.sh, written so that a collaborator who has never cut one, or an AI coding agent working on their behalf, can do it without asking. Every command is meant to be pasted as written. Facts below were verified against the repository, GitHub and npm on 4 September 2026; anything not verified says so.

The short version. One file carries the version (packages/cli/package.json). One commit, titled Prep crimes@X.Y.Z, bumps it and every surface that mirrors it. One human action, publishing a GitHub Release tagged vX.Y.Z, fires .github/workflows/release.yml, which builds, tests, smoke-tests, checks the tag against the version, and runs npm publish using npm Trusted Publishing. Nothing is published from a laptop and no secret is involved.


SurfaceMechanismTriggerHuman involvement
npm package crimes.github/workflows/release.yml, workflow name Release, job publish-npmA GitHub Release with event type published (drafts do not fire it, bare tags do not fire it)Publishing the release. That is the whole trigger.
GitHub Release page (the canonical changelog)Created by hand or with gh release create; body is docs/releases/vX.Y.Z.mdn/aWriting and approving the notes, clicking Publish
crimes.shVercel production deploy from main, building apps/website/Every push to mainNone. Note the consequence in section 9: the site shows whatever main says the version is, whether or not npm has it yet.
CI workflow (.github/workflows/ci.yml)Jobs build and publish-smokePush or pull request to mainNone. Must be green on the prep commit before the release is cut.
evals-pr-replay (.github/workflows/evals-pr.yml)Replays pinned eval resultsPush or PR to main touching detector, scoring, language, CLI or evals/ paths; also workflow_dispatchNone

Only packages/cli is published. @crimes/core, @crimes/reporter, @crimes/language-js and @crimes/language-py are workspace packages at version 0.0.0, bundled into dist/index.js by tsup, and never released on their own. packages/cli/scripts/prepack.mjs strips devDependencies and every scripts entry from the packed manifest, and postpack.mjs restores them, so the tarball declares no lifecycle script and no workspace:* reference.

There is no CHANGELOG.md, on purpose. The GitHub Release page is the changelog; docs/releases/vX.Y.Z.md is its in-repo draft and stays in the tree afterwards. .changeset/ is not wired to anything (no @changesets/cli, no config, no CI step); read .changeset/README.md before adding a file there, and delete any file whose release has shipped.

2. Who can do what (verified 4 September 2026)

Section titled “2. Who can do what (verified 4 September 2026)”
  • GitHub ortomate/crimes: andrewfantastic and timcopelandnz both hold admin. Either can push to main, create and publish a Release, and re-run a workflow. main has no branch protection (the API returns 404 for it), so nothing stops a direct push; the discipline is procedural.
  • npm crimes: the only maintainer is andrewfantastic. Because the workflow authenticates with OIDC rather than a personal token, cutting a release does not need an npm account at all. What does need one, with its 2FA: npm deprecate, npm unpublish, npm dist-tag, and changing the Trusted Publisher configuration on npmjs.com. Today only Andrew can do those.
  • Vercel: not verified from this repository. The site has deployed from main on every push through 0.27.0’s prep commit, so the wiring works; who holds the Vercel project is not recorded here.

3. One-time setup (already done; here so it can be checked or redone)

Section titled “3. One-time setup (already done; here so it can be checked or redone)”

npm Trusted Publisher. On npmjs.com, as a maintainer of crimes: Package settings, Publishing access, Trusted Publishers, Add Trusted Publisher. Provider GitHub Actions, organisation ortomate, repository crimes, workflow filename release.yml, environment blank. This is in place: every Release run from v0.22.0 through v0.26.0 succeeded, and npm view crimes@0.26.0 dist.attestations shows a SLSA provenance attestation, which only Trusted Publishing produces.

No NPM_TOKEN secret. release.yml references no secrets and asks for permissions: contents: read, id-token: write only. Do not add a token “just in case”; it widens what a tampered workflow file could do, and the workflow would not use it.

Vercel. Project linked to this repository, production branch main, build command node ./scripts/build.mjs in apps/website/, output apps/website/dist. Nothing per release.

Branch protection. Recommended and not configured. If someone turns it on, require the CI checks (install · build · typecheck · test and publish smoke (pack + install + run)) on main.

Confirm all of these before starting the prep commit.

Terminal window
cd /path/to/crimes
git switch main && git pull --ff-only
git status --porcelain # must print nothing
node --version # must match .nvmrc (26.7.0 as of 0.27.0)
pnpm --version # 10.14.0, from packageManager in package.json
gh auth status # logged in with access to ortomate/crimes
npm view crimes version # the version currently on npm
node -p "require('./packages/cli/package.json').version" # the version in the tree

The tree version and the npm version normally match here. If the tree is already ahead of npm, a prep commit has landed and not been released; go to section 7 rather than preparing a second one.

Decide the new version. Semver as this project applies it:

  • patch (X.Y.Z+1): detector bug fixes and copy changes that do not alter the JSON schema. Note that patch numbers are also consumed by eval baseline bumps between releases (evals/README.md, section “Versioning policy”), so the next release version is whatever follows the highest patch already used, not necessarily .1.
  • minor (X.Y+1.0): new detectors, new commands, additive schema fields, or any change to what a detector means (a verdict or baseline check gate would see different results). Every release since 0.16.0 has been a minor.
  • major: a breaking change to the wire format. Also bump SCHEMA_VERSION in packages/core/src/finding.ts. To date, schema bumps have shipped inside minors (0.27.0 moved schema_version to 0.8.0), because the package itself is still 0.x.

The last two prep commits, 364a333 (Prep crimes@0.26.0) and 928034c (Prep crimes@0.27.0), are the models. Reproduce their shape:

Terminal window
git show --stat 928034c

Checklist. Every box is a file the commit must touch, or a reason it need not.

  • packages/cli/package.json: "version": "X.Y.Z". The only place the version is authoritative. release.yml compares the release tag to this field and refuses to publish on a mismatch.
  • docs/releases/vX.Y.Z.md: the release notes, new file. This becomes the GitHub Release body verbatim. If the release changes fingerprints or the schema, lead with an “Upgrading” section (0.27.0 is the example). List what was planned and not done under a heading such as “Still unsettled”, so the next plan starts from the truth.
  • README.md and packages/cli/README.md: describe the current workflow and link to release notes. Keep history in docs/releases/, not a growing stack of release headlines in onboarding docs.
  • docs/roadmap.md: record the prepared version separately from the published version. Verify npm before claiming publication.
  • After building, run pnpm docs:generate. It derives docs/reference.md, apps/website/landing/llms.txt, landing versions and live output examples from the CLI and registry. pnpm verify checks for drift. Update surrounding capability copy when behavior changes, and run website verify-build.
  • docs/fixtures/messy-ts-app.json: regenerated by pnpm docs:generate from a source-only copy without Git history; the root is normalized. This keeps examples identical in shallow CI and a maintainer checkout.
  • docs/json-schema.md: only if the schema changed and the change that introduced it did not already document it.
  • evals/results/<X.Y.Z>/: the eval baseline is keyed by the version in packages/cli/package.json, so the directory moves with it. Either run pnpm run evals:ranking to seed ranking.json (as 0.26.0 did), or, when the release changed only finding identity, add a row to the carry-forward table in evals/README.md (“Identity-only bumps”) saying why no run was needed. One or the other; not neither.
  • .changeset/: delete any file describing a release that has shipped.
  • .planning/SPRINT-X.Y.md: if a sprint plan exists for this release, record the outcome and the corrections in it (see .planning/SPRINT-0.26.md sections 8 and 9 for the shape), then move it to .planning/archive/ per .planning/README.md. This can be a separate commit but should land before the release.

Do not edit PRD.md for a release. Do not touch .nvmrc, the workflows, or biome.jsonc in the prep commit; formatting settings change what crimes reports about itself (section 12).

Commit message: first line Prep crimes@X.Y.Z; body lists the files and why, records the pre-flight results with the Node version, and states that publishing is not part of this commit. 928034c is the template.

Terminal window
pnpm install --frozen-lockfile
pnpm verify # format:check, lint, build, typecheck, test
pnpm scan:example # human report against the bundled fixture
pnpm scan:example:json # JSON report against the bundled fixture
pnpm --filter crimes smoke # npm pack, install into a temp dir, exercise core commands and Python packaging
pnpm --filter @crimes/website build && node apps/website/scripts/verify-build.mjs

All six must exit 0. If format:check fails, run pnpm format and commit the result rather than hand-editing. verify-build runs against the built dist/, so it must follow the website build or it checks a stale page.

Optional tarball inspection:

Terminal window
cd packages/cli && npm pack --dry-run && cd -

Expect package.json, dist/index.js, other dist/*.js, dist/*.wasm, README.md, LICENSE. No sourcemaps, no src/, no scripts/, no workspace:* in the packed manifest.

Then push and wait:

Terminal window
git push origin main
gh run watch --repo ortomate/crimes --exit-status $(gh run list --repo ortomate/crimes --branch main --workflow CI --limit 1 --json databaseId --jq '.[0].databaseId')

Two steps, deliberately split so the irreversible one is a separate act.

7a. Draft it. This records the exact commit in target_commitish, but does not create the tag or fire the release workflow. GitHub creates the tag when the draft is published. Verify the draft’s target now and the actual tag after publication. This sequence was verified during 0.29.0.

Terminal window
git fetch origin main --tags
VERSION=$(node -p "require('./packages/cli/package.json').version")
TARGET=$(git rev-parse origin/main)
test -z "$(git ls-remote origin "refs/tags/v${VERSION}")" || exit 1
gh release create "v${VERSION}" \
--repo ortomate/crimes \
--draft \
--target "${TARGET}" \
--title "crimes v${VERSION}" \
--notes-file "docs/releases/v${VERSION}.md"
test "$(gh release view "v${VERSION}" --repo ortomate/crimes \
--json targetCommitish --jq .targetCommitish)" = "${TARGET}"

Review the draft at https://github.com/ortomate/crimes/releases. Check the tag reads vX.Y.Z with a lower-case v and matches packages/cli/package.json exactly. The command is not repeatable while the draft exists. An existing tag must be investigated before creating the draft: GitHub uses that tag’s commit even when a different target is given. To abandon an unpublished draft safely, remove it and any leftover tag with gh release delete "v${VERSION}" --repo ortomate/crimes --cleanup-tag, then start section 7 again.

7b. Publish it. A human does this. Either click Publish release on the draft, or:

Terminal window
gh release edit "v${VERSION}" --repo ortomate/crimes --draft=false
git fetch origin --tags
test "$(git rev-parse "v${VERSION}^{commit}")" = "${TARGET}"

Publishing creates the tag at the draft’s recorded target and fires release.yml. Confirm the tag points to TARGET immediately. From here there is no undo short of section 10.

Titles on past releases follow crimes vX.Y.Z optionally followed by a short theme; either is fine.

In order, from .github/workflows/release.yml:

  1. Checkout; set up pnpm (version from packageManager) and Node (from .nvmrc, registry https://registry.npmjs.org).
  2. pnpm install --frozen-lockfile=false.
  3. pnpm run build, pnpm run typecheck, pnpm run test.
  4. pnpm --filter crimes smoke.
  5. Verify github.event.release.tag_name equals v + packages/cli/package.json version. Mismatch fails the job before anything is published.
  6. Print the OIDC token claims (payload only) for debugging.
  7. npm publish --provenance --access public from packages/cli.

Past runs take a little over two minutes. Watch it:

Terminal window
gh run watch --repo ortomate/crimes --exit-status $(gh run list --repo ortomate/crimes --workflow Release --limit 1 --json databaseId --jq '.[0].databaseId')

If it fails before step 7, nothing was published: fix main, then either re-run the job (gh run rerun <id>; it checks out the tag, so a fix on main needs a new release) or delete the release and tag and start again from section 7. If it fails at step 7 itself, read the log for the npm error before doing anything; a Trusted Publisher mismatch shows up here and is fixed on npmjs.com, not in the repo.

Terminal window
npm view crimes version # X.Y.Z
npm view crimes dist-tags # latest: X.Y.Z
npm view crimes@X.Y.Z dist.attestations # provenance block present
gh release view vX.Y.Z --repo ortomate/crimes
cd "$(mktemp -d)" && npx --yes crimes@X.Y.Z --version && cd -
curl -s https://crimes.sh/ | grep -o '<span class="pill">v[0-9.]*'

The last line checks the site. Because Vercel deploys from main on push, the site advertises the new version from the moment the prep commit lands, which may be before npm has it. Keep the gap between prep and publish short for that reason.

npm allows unpublish within 72 hours and discourages it. Prefer shipping a fixed patch. All of the commands below need an npm maintainer login with 2FA; as of 4 September 2026 that is only Andrew.

Terminal window
npm deprecate crimes@X.Y.Z "Broken: install X.Y.Z+1 instead" # preferred
npm unpublish crimes@X.Y.Z # within 72h only
npm dist-tag add crimes@<good version> latest # if latest points at a bad build

Then prepare and release the fix as a new version through sections 5 to 9. Do not reuse a version number; npm will not accept it even after an unpublish.

A GitHub Release can be deleted (gh release delete vX.Y.Z --cleanup-tag), but doing so does not affect npm, and the release workflow has already run.

Read AGENTS.md first; its safety rules apply. Within them, this is the division of labour.

You may do, without asking:

  • Every item in section 5 (the prep commit) and section 6 (pre-flight), on a branch or on main when the user has asked for the prep.
  • Draft the release notes in docs/releases/vX.Y.Z.md.
  • Create the GitHub Release as a draft (section 7a). Verify its recorded target commit immediately. Saving a draft creates no tag and triggers no workflow; the draft remains removable before publication.
  • Watch workflow runs and report their result.
  • Run every read-only verification in section 9.
  • Open a pull request.

A human must do these, and you must not do them even if your credentials would allow it:

  • Publish the GitHub Release (section 7b), by clicking Publish or with gh release edit --draft=false. This is the single irreversible action in the process and it is what runs npm publish. One exception. Andrew granted the Hobbes executor release authority on crimes on 4 September 2026; the grant is recorded per product in ortomate/hobbes at docs/portfolio.toml (release = "hobbes") and shown by hobbes portfolio as release: hobbes. When you are that executor working a crimes card, you may publish the release, and you must then verify with npm view crimes version before the card is done. Every other agent, and the executor on every other product, still stops at the draft. The npm-as-a-user items below stay human for everyone.
  • Anything against the npm registry as a user: npm publish, npm deprecate, npm unpublish, npm dist-tag, npm login. These require the maintainer account and its 2FA, which you do not have and should not be given.
  • Change the Trusted Publisher configuration on npmjs.com, or add any secret to the GitHub repository. release.yml needs none.
  • git tag or git push --tags. Publishing the GitHub Release creates the tag.
  • Force-push, reset, or rewrite main.

When you have done everything you may do, stop and say exactly what remains: “Draft release vX.Y.Z is at ; publishing it will run release.yml and publish to npm.” Do not describe the release as done until npm view crimes version returns the new number.

The hero pill. apps/website/landing/index.html carries a visible vX.Y.Z pill linking to the release notes. It sat at v0.12.0 through two releases, and at 0.26.0 it still linked to v0.25.0’s notes. apps/website/scripts/verify-build.mjs now fails the build on either mistake, and on these as well, each derived from the repository rather than hard-coded:

Claim on the pageDerived from
JSON-LD softwareVersionpackages/cli/package.json
Hero pill version and its release-notes linksame, plus the built dist/docs/releases/
”N detectors across M families”the built detector registry and docs/finding-types/*.md
Every finding-type family linkeddocs/finding-types/*.md
Languages named in the prose, packages listedpackages/language-*

Adding a language pack fails the build until LANGUAGE_NAMES in verify-build.mjs and the page are both updated. That is intended.

Biome and the self-scan. large_function, large_file, exact_duplicate_block, near_duplicate_block and magic_domain_literal_scatter read line counts or line text, so lineWidth in biome.jsonc (pinned at 90) changes what crimes reports about itself. Do not change formatter settings in a release commit.

Known lint debt. apps/website/landing/index.html is excluded from the linter in biome.jsonc for 90 real a11y diagnostics in two <div role="table"> blocks. It does not block a release. Do not let the exclusion grow.

Publishing from a dirty tree. The tarball reflects the disk, not git. git status --porcelain must be empty before npm pack or smoke.

The changeset directory. Nothing consumes it and nothing empties it. Files for 0.19.0, 0.20.0, 0.21.0 and 0.25.0 were still present on 4 September 2026, all describing shipped releases.

  • Do not run npm publish locally. Trusted Publishing only works inside the workflow.
  • Do not create the tag by hand. A bare tag does not fire the workflow, and a tag that already exists when the release is created can point at the wrong commit.
  • Do not add NPM_TOKEN.
  • Do not deploy the website with the Vercel CLI; it races the automatic deploy.
  • Do not leave a prep commit on main unreleased for long. crimes.sh will advertise a version npm does not have.

The 4 September audit found a prep commit and website version ahead of publication, plus stale instruction/status files and four shipped changesets. That audit is preserved in git history. Its unverified registry state is not current release guidance. 0.28 consolidates current references and records prepared versus published status explicitly; check npm and GitHub at the time of each release rather than relying on an old snapshot.