Skip to content

Release from Your Workstation

llms.txt lists every page for an agent
Optional: hand this page to your coding agentThe steps work by hand too.
Show the whole prompt
Read https://intentius.io/chant/guide/release-from-workstation/.
Add the publish workflow the page shows for this repository's forge, triggered by release/** tags, in a pull request.
Run `npx chant release cut <version>` without `--yes` on a clean checkout of a green trunk commit, and show me what it prints; do not run it with `--yes`, push a tag, or run `chant release publish` yourself.
Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.

This page cuts a release on your machine and ships it from CI (#4027). The split follows from what each side holds. Your machine holds the decision and the signing key. CI holds the OIDC token that gives an uploaded package its provenance, which npm trusted publishing needs.

A release is the compiled output of one green commit, cut once and promoted through every environment (ws-104). Only chant release cut cuts one; a green tick never does (ws-105). The commands below run unchanged in CI, so you can move the cut there later without changing what it records.

You need a project whose commits get ci/green/<sha> tags from chant ci, and whose chant.config.ts lists its environments. You also need an ssh key that .chant/allowed_signers at base lists for you, with no namespaces option or one that includes chant-release: chant release publish refuses a release whose signature does not verify (#4049). Under a release gate in identity.gates the cut itself needs it. See who may cut.

The cut refuses a dirty tree, a commit that is not on trunk and pushed, and a commit with no green tag. Trunk is the branch ci.green.branch names, else the one origin/HEAD names, else main. Fetch the branch and the tags, then check out the newest green commit:

Terminal window
git fetch origin main --tags
git checkout "$(chant ci last-green)"
git status --short # prints nothing on a clean tree

An untracked file that .gitignore does not cover counts as dirty, since the compile would read it.

Without --yes, chant release cut compiles every environment and prints what it would do. It writes nothing.

Terminal window
chant release cut v1.4.0 --build "npm run build" --sign

Read three parts of the output before you go on:

  • The actor line names who the cut will record. With --sign it is the principal the signers file lists for your key, otherwise git’s user.email. Pass --actor <name> to name someone else.
  • The tag line names the tag the cut makes, release/v1.4.0.
  • The lines under What will be published where list each component’s publish-* steps with the environments whose deploy runs them, and what the release command would upload.

The publish targets come from release.publish in chant.config.ts. See Publish Targets to declare npm packages, an image for an OCI registry, or forge release assets. A component’s publish-image and publish-artifact steps are separate: they run when an environment deploys the release with chant components promote --release.

Run the same command with --yes:

Terminal window
chant release cut v1.4.0 --build "npm run build" --sign --yes

The cut builds the artifacts once, stores the release on chant/lifecycle, and appends a cut line with the actor and a run id minted on your machine (local-<epoch ms>). It pushes chant/lifecycle when there is a remote, then tags the commit release/v1.4.0 in your clone. It ends by naming the next command:

Next: git push origin release/v1.4.0
or: chant release publish v1.4.0
or: chant components promote --release v1.4.0 --to dev

chant never pushes the tag. Push it yourself when you are ready for CI to take over:

Terminal window
git push origin release/v1.4.0

The workflow below runs on a release/** tag and ships the release the tag names. The job gets an OIDC token from id-token: write, so what it uploads carries provenance. The job checks out the tag, so the checkout is the release’s commit, as a publish with targets requires. A publish never builds, so build what the targets upload before it runs, into paths .gitignore covers.

.github/workflows/release-publish.yml
name: release publish
on:
push:
tags: ["release/**"]
permissions:
contents: write
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm run build
- run: git fetch origin 'refs/tags/ci/*:refs/tags/ci/*'
- run: git fetch origin chant/lifecycle:chant/lifecycle
- run: npx chant release verify "${GITHUB_REF_NAME#release/}"
- run: npx chant release publish "${GITHUB_REF_NAME#release/}" --require-provenance --yes

setup-node with registry-url writes an .npmrc that reads NODE_AUTH_TOKEN; with npm trusted publishing set up for the package, the OIDC token is enough. chant release publish fetches chant/lifecycle itself. It refuses a release whose commit was revoked after the cut, and one whose signature does not verify against the signers file at base. chant release verify checks the same signature offline against the signers file at the release’s commit. The --require-provenance flag makes a job with no OIDC token fail instead of publishing without provenance. When the token is there, the npm target runs npm audit signatures over what it published. Every target attaches an SBOM and the release’s provenance statement (see SBOMs and provenance). The job needs contents: write because the publish pushes its record to chant/lifecycle, and a forge target makes a release. When a publish fails part way, re-run the job: it resumes from that record and publishes nothing again (see staging and resuming). On GitLab, the job declares an ID token instead:

release-publish:
rules:
- if: $CI_COMMIT_TAG =~ /^release\//
id_tokens:
SIGSTORE_ID_TOKEN:
aud: sigstore
script:
- npm ci
- npm run build
- git fetch origin 'refs/tags/ci/*:refs/tags/ci/*'
- npx chant release publish "${CI_COMMIT_TAG#release/}" --require-provenance --yes

A project with no CI can run chant release publish v1.4.0 on the workstation to see the plan, then again with --yes to publish. The targets read their credentials from your shell, such as OCI_PASSWORD or GITHUB_TOKEN, and npm uses your npm login. Before it uploads anything it warns that the run has no OIDC token, so nothing it uploads carries OIDC provenance. The warning does not stop it.

What a workstation publishes carries the release’s signature and no OIDC provenance. npm records no provenance for the packages, and npm audit signatures finds none to check. The provenance statement on an image or a forge release says its provenance is the signature, and carries it. Anyone with the repository checks that signature with chant release verify v1.4.0, against .chant/allowed_signers at the release’s commit. Each target still attaches its SBOM.

The cut is the same command in a CI job, with --yes. There the cut line records the CI run’s id and origin and the CI’s actor, and the next command it prints is chant release publish. A cut of one commit on a workstation and one in CI store the same release files, and their cut lines differ only in runId, runOrigin, actor and timestamp. See what —yes records.

chant