Cutting a release
A release is cut from main, by CI, and never from a laptop. That is not ceremony: a
release a person can produce locally is a release whose provenance attestation certifies
nothing, because the attestation only says “GitHub Actions built this from that commit” if
GitHub Actions actually did.
The compatibility contract — what counts as a break, what gets a patch, what gets a minor —
is in VERSIONING.md. This page
is the mechanics.
What triggers it
Section titled “What triggers it”.github/workflows/release.yml runs on a pushed tag matching v*.*.*, and on nothing else.
There is deliberately no workflow_dispatch with a version input.
# after CHANGELOG.md and both package versions are updated in one commit on maingit tag v0.1.0git push origin v0.1.0The workflow has four jobs, in order: verify → npm + PyPI → smoke.
verify
Section titled “verify”Re-runs every gate on the tagged commit. A green CI run on main is not evidence about
a tag, because a tag need not point at the commit that was tested.
It also checks three things a human typed:
- the tag,
packages/tx402/package.json, andpyproject.tomlall carry the same version; - the bundled manifest’s signature verifies;
- the manifest is not signed by
tx402-release-1, the development key.
That last check is why the release key matters, and it is a hard failure rather than a warning.
npm and PyPI
Section titled “npm and PyPI”Both publish through OIDC trusted publishing. Neither job has a registry token, because no long-lived registry token exists — the one that did was revoked and deliberately never replaced. The workflow’s own identity is exchanged for a short-lived credential at publish time.
npm publishes with provenance. The --no-provenance flag that the 0.0.0 placeholder was
published with must never appear here: provenance requires CI OIDC, and this is that CI.
Installs tx402 from both registries into a clean environment, runs each CLI, imports each
package, and checks that npm actually recorded a provenance attestation. Everything before
this job verified the repository; this is the only job that verifies the registry.
One-time setup
Section titled “One-time setup”Two things must be configured by an account owner before the first real release. Neither can
be done from this repository, and both are why verify fails loudly rather than publishing
something unattested.
1. npm trusted publisher
Section titled “1. npm trusted publisher”On npmjs.com → Settings → Trusted publisher, link:
| Field | Value |
|---|---|
| Repository | the published repository |
| Workflow filename | release.yml |
| Environment | release |
2. PyPI trusted publisher
Section titled “2. PyPI trusted publisher”On pypi.org → Publishing → Add a new publisher → GitHub:
| Field | Value |
|---|---|
| Owner / repository | the published repository |
| Workflow name | release.yml |
| Environment name | release |
The release signing key
Section titled “The release signing key”The bundled manifest is signed with an Ed25519 key whose public half is compiled into both
packages. tx402-release-1 is a development key: it was generated on a workstation, and
anything generated on a workstation should be assumed to have been readable there.
A published release must be signed by a key that was never on a developer’s machine.
Generating it
Section titled “Generating it”Run this somewhere you control, not in this repository’s tooling and not through an agent. The private key must not pass through a terminal that is being recorded, a shell history, or a coding assistant’s context.
node tools/manifest-signer/index.js keygen --key-id tx402-release-2That writes two files:
| File | Handling |
|---|---|
core-spec/manifests/keys/tx402-release-2.pub.json |
Commit it. It is public by design. |
core-spec/manifests/keys/tx402-release-2.private.pem |
Never commit it. Gitignored. |
Storing it
Section titled “Storing it”Put the private PEM in a secret manager, or in a GitHub Actions secret scoped to the
release environment — SPEC §13 requires “CI OIDC or secret manager; never repository
variables in plaintext.” Then delete the local copy. Verify you can still sign before you
delete it, not after.
Rotating to it
Section titled “Rotating to it”- Add the new public key to the trusted set compiled into both packages
(
packages/tx402/src/core/trusted-keys.tsand the Python mirror) alongside the old one, and release that. Clients must be able to verify the new key before anything is signed with it, or an upgrade breaks construction for everyone still on the old version. - Re-sign the manifest with the new key and embed it.
- Remove
tx402-release-1from the trusted set in a later release.
Doing steps 1 and 2 in the same release is the mistake worth naming: a client that has not yet upgraded would reject the new signature, and manifest verification failure is a construction failure, not a warning.
Verifying
Section titled “Verifying”pnpm manifest:verifyReports the key id, the network count, and the expiry. The release workflow runs the same
command and additionally refuses tx402-release-1.
Supply-chain artifacts
Section titled “Supply-chain artifacts”pnpm sbom emits CycloneDX SBOMs for both packages plus a licence report, scoped to
dependencies that actually ship — the development tree is not distributed and is excluded.
CI uploads them as build artifacts on every run.
pnpm sbom # sbom/tx402-{npm,pypi}-{core,evm,solana,all}.cdx.json + LICENSES.mdpnpm supply-chain # licence policy + vulnerability gatepnpm reproducible # two clean builds must be byte-identicalThe licence gate blocks on anything outside a permissive allowlist, evaluating SPDX OR and
AND expressions rather than comparing strings — Apache-2.0 OR BSD-3-Clause is a choice of
two acceptable licences and passes. The vulnerability gate blocks on critical and high
findings in shipped dependencies only; a development-tree advisory is reported and does
not fail the build, because a gate nobody can keep green is a gate that gets disabled.