Deterministic versioning — intent files in, signed release out. A seam over
semver: mint owns the flow (intent
assembly, changelog, tagging, provenance) and delegates only the version
arithmetic to the proven core.
Built because tagging-by-hand drifts: a manifest left behind its release tags is a recurring bug (string-audit#42). mint makes the version a per-PR declaration and the release a single atomic step.
- Opinionated — one canonical release flow, no configurable branching.
- Strongly deterministic —
plan()is a pure function of(currentVersion, intents, date). No commit-history ordering, noDate.now(), no randomness. Same intents in → same version + changelog out (sha in → sha out). - Typed end-to-end — intents and plan validated with Zod; a verbspec CLI/MCP surface mirrors the rest of the stack.
- Owned — delegate only
semverarithmetic; own assembly, rendering, tagging, and provenance.
Each PR drops an intent in .release/ (see .release/README.md):
---
bump: minor
---
scan: promote to a verb (CLI + MCP) + shared Zod type contractsThen:
mint plan # preview: 0.6.1 → 0.7.0 (minor) + the changelog entry
mint version # apply: bump package.json + lockfile, prepend CHANGELOG.md, consume intents
mint release # cut the v<version> tag + emit release provenance (CI keyless-signs it)mint plan --json emits the machine-readable plan. --date YYYY-MM-DD pins the
changelog date (the pure core never reads the clock; the CLI injects it).
mint release cuts the annotated tag v<version> (signed when a git signing key
is configured) and emits a release provenance record — an
in-toto Statement v1 binding the three things a
release is:
tag → version plan → commit
The subject is the tag, anchored to its commit (in-toto gitCommit digest); the
predicate carries the byte-exact changelog entry (the deterministic plan()
output) and its sha256. Re-deriving the plan over the same intents reproduces
that digest — the tag-to-plan link is machine-checkable, offline.
The Statement is deterministic (releaseStatement() is pure, like plan())
and keyless-signed in CI: release.yml runs mint attest, then
cosign sign-blob binds the signature to the workflow's OIDC identity (Fulcio +
Rekor — no key material). Locally the Statement is emitted unsigned
(builder: null) so the flow degrades gracefully off-CI. This is the same
Statement / DSSE / keyless-Sigstore shape the bounded-systems sites and
@bounded-systems/verify already
produce and verify, so a mint release record verifies with the same tooling:
cosign verify-blob \
--bundle mint-release.intoto.sigstore.json \
--certificate-identity-regexp '^https://github.com/<org>/<repo>/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
mint-release.intoto.jsonanchored-chain: mint deliberately mirrors
anchored-chain's in-toto constants + Statement/DSSE shape rather than depending on it: anchored-chain's in-toto module is Phase-0 and not yet re-exported from its public surface, is bun/TypeScript with a build step, and its predicate models a derivation chain, not a release. mint stays a zero-build node ESM package and can adopt anchored-chain'sSigner/Verifieronce that surface lands — the bytes already match.
A consumer repo calls the reusable
release-provenance.yml on its tag
push (a ~10-line caller — see the header of that workflow):
# .github/workflows/release.yml
name: release
on: { push: { tags: ["v*"] } }
permissions: { contents: write, id-token: write }
jobs:
release:
uses: bounded-systems/mint/.github/workflows/release-provenance.yml@<sha>
with: { ref: <sha> }adoption.mjs --write drops both this caller and the version.yml caller into
every publishable repo — the path off hand-tagging.
mint ships from release.yml on each v* tag via OIDC trusted publishing —
no NPM_TOKEN, no JSR token, ever. The package manifests are kept in lockstep by
mint version (it bumps package.json, package-lock.json, and jsr.json).
npm uses a GitHub Environment gate — the npm-approve job is blocked by the
npm-publish environment, which requires a designated reviewer to approve in the
GitHub Actions UI before npm publish runs. Approve at:
https://github.com/bounded-systems/mint/actions
Once approved, the package publishes immediately via OIDC trusted publishing (no token).
JSR publishes immediately on the same tag (no staging concept on JSR).
JSR-readiness is verified the same way CI publishes:
npx jsr publish --dry-run --allow-slow-types # the path release.yml runs
deno publish --dry-run --allow-slow-types # equivalent, Deno-nativejsr.json carries the SPDX license and a publish.include allowlist, so the
published tarball is just the three exports + mint.mjs, README, CHANGELOG
(no tests, lockfiles, or workflows). @types/node is a dev dependency so the
JSR type-checker resolves mint's node: imports.
One-time JSR link (manual, once): before the first JSR publish, link the package on jsr.io — create
@bounded-systems/mintunder the@bounded-systemsscope and connect it to thebounded-systems/mintGitHub repo (Settings → "Link to a GitHub repository"). That GitHub link is what authorizes the keyless OIDC publish; after it, every tagged release publishes with no token.
Every fresh JSR/npm publish immediately hits Deno's
minimumDependencyAge
in any consumer running Deno 2.9+: since 2.9 it defaults to 24 hours — Deno
refuses to resolve a version published inside that window, full stop, no
config needed to trigger it. It's a real supply-chain guard (malicious
versions are usually caught/yanked within days), but it applies to first-party
@bounded-systems/* releases exactly the same as to a random third party.
Concretely: baobab@0.2.0 published, and every downstream consumer's CI
(e.g. a check-contrast.yml caller) hard-failed for the next 24h trying to
resolve it — not a flake, a guaranteed wait baked into every release
(bounded-systems/mint#11).
For one package that's a wait; for a coordinated rollout of several
interdependent @bounded-systems/* packages in one sitting, it ripples —
every consumer of every package in the batch eats the same 24h, and chains of
dependencies compound it.
The fix does NOT live in a consumer's deno.json. An earlier version of
this doc claimed a minimumDependencyAge.exclude list there would work —
that was wrong, and only looked right because it was tested against Deno
2.8.3 (which doesn't enforce the 24h default at all; the default only started
in 2.9). Verified against the real 2.9.1 binary: deno.json is never even
read when deno run's entrypoint is a bare jsr:... specifier — config-file
discovery only applies in normal "project mode." Planting invalid JSON in a
deno.json and confirming Deno never complained is what exposed this.
What actually works: the --minimum-dependency-age CLI flag on the
invocation itself. That means the fix has to live wherever the deno run
command is composed — for a reusable GitHub Actions workflow like
check-contrast.yml, that's an input on the workflow, not something a caller
can override from its own repo
(bounded-systems/baobab#7
adds minimum-dependency-age, defaulting to "0", since the only thing that
workflow ever resolves from the network is @bounded-systems/baobab itself —
a first-party package the caller already trust-bounds via its own version
range input, so the age gate adds no real protection there). Any other
reusable workflow or script that shells out to deno run jsr:.../npm:...
directly needs the same treatment: an explicit --minimum-dependency-age
(or --minimum-dependency-age=0 if every dependency it touches is
first-party) on the command, not a config file a caller drops in.
This still doesn't scale past a handful of hand-fixed workflows — #11 is the
open ask for mint to own generating this consistently across every
@bounded-systems/* reusable workflow, rather than fixing each one by hand
as it's hit.
import { plan } from "@bounded-systems/mint";
import { releaseStatement } from "@bounded-systems/mint/release";
plan({
currentVersion: "0.6.1",
intents: [{ bump: "minor", summary: "scan: promote to a verb" }],
date: "2026-06-23",
}).nextVersion; // "0.7.0"
releaseStatement({
version: "0.7.0",
tag: "v0.7.0",
commit: "0123456789abcdef0123456789abcdef01234567",
date: "2026-06-23",
changelog: "## 0.7.0 — 2026-06-23\n\n### Minor\n\n- scan: promote to a verb\n",
producer: "@bounded-systems/mint",
})._type; // "https://in-toto.io/Statement/v1"- Deterministic
plancore + Zod intent contract -
mint plan/mint version -
mint release— signed tag + in-toto release provenance, keyless-signed in CI (cosign/Sigstore; anchored-chain-shaped) - verbspec-typed CLI + MCP surface
- Reusable
workflow_callAction (version.yml+release-provenance.yml) - Publish to npm (GitHub Environment approval gate) + JSR
Tracking: bounded-systems/string-audit#43.
PolyForm-Noncommercial-1.0.0