Cut a new npm release of rift-ds. Bump the version, dry-run, publish via the Release workflow, then tag the published commit. Use when asked to cut a release, publish a new version, or ship the package to npm.
Installs into .claude/skills of the current project.
Sponsors
SSkills DirectorySkills Directory
Know which skills are safe — weekly.
SSkills DirectorySkills Directory
Is that skill safe to install?
Are you the author of Release?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/robritacca-dotcom-release)
---
name: release
description: Cut a new npm release of rift-ds. Bump the version, dry-run, publish via the Release workflow, then tag the published commit. Use when asked to cut a release, publish a new version, or ship the package to npm.
icon: rocket_launch
displayDescription: "Cuts an npm release of the component library: bumps the single source-of-truth version, runs the Release workflow in dry-run to prove a real consumer can install and build the tarball, publishes with signed provenance, then tags and writes the GitHub Release against the exact commit that shipped. Knows the two things that bite on release day: a version number can never be reused, and the registry lags a green publish by minutes."
invoke: ["cut a release","publish a new version","ship the package to npm","release [version]"]
---
# release
Cut a new npm release of `rift-ds` — bump, dry-run, publish, tag.
## When invoked
Use this skill when asked to cut a release, publish a new version, or ship the package to npm — phrases like "cut a release", "publish the next version", "ship the package".
**This is the one workflow in this repo where a mistake is permanent.** npm never lets a version number be reused, even after unpublishing, so a botched publish burns that version forever. Read the guardrails before starting.
## Instructions
### 1. Decide the version
Read `PACKAGE_VERSION` in `scripts/package-manifest.mjs` — that constant is the *authoritative* version; two other files mirror it (see step 3) and `validate-package-exports.mjs` fails the build when they disagree (the root package.json version is a hand-maintained mirror; `dist/package.json` is generated from the manifest). Then pick the next version from what actually changed since the last release:
- **patch** — bug fixes, internal refactors, docs
- **minor** — new components, new exports, new tokens (additive)
- **major** — a renamed/removed prop, export, or token; anything a consumer must edit code for
Check what shipped since the last tag to justify the choice, and confirm it with the owner before bumping:
```bash
git log $(git describe --tags --abbrev=0)..HEAD --oneline
```
`node scripts/validate-api-compat.mjs` answers half of this mechanically (the published schema files and meta-schema properties under `website/public/schema/` included): it compares the working tree's props, export names, subpaths, deep-import files and bin with the last tag, and fails on anything removed or narrowed. A clean run means nothing published has gone, which rules out a major for shape reasons (a moved default is behaviour, and still needs a reader). A deliberate break ships only as a major, with a `SANCTIONED_BREAKS` row in that script naming the version and the reason. The one other kind of row is a correction to the baseline's own record, where the published types never changed and only the generated API misreported them (its reason starts "not a break" and says so); that rides in a minor. Either way, after the tag in step 7 the row goes stale and the next run fails until it is deleted.
Be deliberate about breaking changes: components are exported both from the barrel and from `./components/*` deep paths, so a renamed component folder breaks consumers even if the barrel still exports the old name. Every other subpath in `SUBPATHS` in `scripts/package-manifest.mjs` (the chat widget's, `./format`, the published behaviours and the token modules) is a public path on the same terms, and the scaffold templates under `src/cli/templates/` (one folder per `init` subcommand: `chat`, `app` and `blocks`) ride in the tarball, so a renamed export they import breaks files the bin has already written into consumer apps.
### 2. Pre-flight
- **Working tree must be clean and pushed.** The workflow builds from the repo, not your disk — anything uncommitted will not be in the release. `git status --short` and `git log origin/main..HEAD` should both be empty.
- **CI on `main` must be green.** A release from a red main ships known-broken code.
- **Confirm the Trusted Publishing registration on npmjs.com still covers THIS repo** before a real publish. It is keyed to repo + workflow filename, and was registered for `robritacca-dotcom/rift-ds` + `release.yml` on 2026-09-26. Renaming either breaks publishing until the registration is updated, and the owner owns anything that changes on npmjs.com.
- Run `npm run verify` if anything at all is uncommitted or you haven't verified since the last change.
### 3. Bump and commit
**Three files carry the version — edit the first two, regenerate the third:**
1. `PACKAGE_VERSION` in `scripts/package-manifest.mjs` — the source of truth for what ships.
2. `"version"` in the root `package.json` — **must be kept in sync by hand.** No generator writes it. The root package.json stays `private: true` forever; only the generated `dist/package.json` is published, but the parity check still gates every build.
3. `package-lock.json` — npm records the version there too. Refresh it with `npm install --package-lock-only` and commit it with the bump; a stale lockfile dirties every later plain `npm install` (the 0.3.0 bump shipped without this and broke the worktree-based skills' cleanup).
`scripts/validate-package-exports.mjs` fails the build when any of the three disagree.
**Per-version facts (the version, its date, what shipped) have one home: the release log** (`website/src/data/release-log.json`, written in step 8). Never restate them in prose anywhere else, a doc or a workflow comment: nothing validates a hand copy, so it drifts the first release it is forgotten.
Then:
```bash
npm run validate-registry
```
This re-runs the three-way version parity check and regenerates the derived surfaces — and several generators stamp `PACKAGE_VERSION` into their output, so the bump rewrites every per-component markdown page under `website/public/components/`, every component schema and the index under `website/public/schema/` (their `version` field) and every DTCG token file there (its `$extensions` block), the consumer agent skills' folders under `website/public/skill/` (`AGENT_SKILLS` in `scripts/brand.mjs` owns the names), the agent starter (`website/public/AGENTS.md` and its module, `website/src/data/agent-starter.generated.ts`) and the site corpus that carries the starter's text. Whatever the regeneration touches is the list. The diff is large by design. Commit all of it together as the bump commit (`chore(release): <version>`) and push — a bump pushed without the restamped files fails CI's drift guard (that exact split produced the 0.15.0 cleanup commit), and the commit you push here is the commit that will be published and tagged.
### 4. Dry run — never skip this
```bash
gh workflow run release.yml -f dry_run=true
```
Watch it to completion — but confirm you are watching the run you just dispatched, not the previous one. Registration lags the dispatch by a few seconds, so `--limit 1` immediately after `gh workflow run` can return the prior run (at step 5 that prior run is the green dry run, and watching it reads as a successful publish):
```bash
sleep 10
gh run list --workflow=release.yml --limit 3 --json databaseId,createdAt,displayTitle # newest first — check createdAt is after your dispatch
gh run watch <the-new-databaseId> --exit-status
```
The dry run does everything except upload: builds `dist/`, packs the tarball, installs it into two scratch Vite + React apps — one that **builds with none of the optional peers installed** (the regression this catches: a peer leaking into the main barrel), and one that installs every peer, runs the packed bin's `init chat`, `init app` and `init block` for every block against an app-router layout, and type-checks and builds the scaffolds — then prints the publish preview. It needs no npm token, so it is free to run as often as you like. Read the preview's file count and package size and sanity-check them against the previous release; a sudden jump means something got swept into the tarball. Expect `LICENSE`, `README.md`, the init bin under `bin/` (named by `BIN_NAME` in `scripts/brand.mjs`, origin-stamped and executable) and its scaffold templates (every folder of `src/cli/templates/`, one per `init` subcommand) under `bin/templates/` alongside the build output; anything else new is not deliberate.
### 5. Publish
```bash
gh workflow run release.yml -f dry_run=false
```
Watch it the same way (the same watch-the-right-run caution applies, and matters more here). The publish step runs `npm publish --access public --provenance --loglevel verbose` from `dist/` — the verbose flag is deliberate, because its log lines are the only visible evidence of the OIDC token exchange the auth guardrails below tell you to look for — and signs a provenance attestation tying the tarball to this repo, commit, and workflow run.
### 6. Verify — and do not panic at a 404
**The registry lags a successful publish by several minutes.** A `404` from `npm view` right after a green workflow is propagation, not failure. Confirm the workflow's publish step actually ran (`gh run view <id> --json jobs`) and look for `+ rift-ds@<version>` in its log — if that line is there, it published. **Never re-run the workflow on a 404**; the version is already consumed and the rerun will fail with `EPUBLISHCONFLICT`.
Once it propagates, confirm the registry serves the new version:
```bash
npm view rift-ds version --prefer-online
```
The real consumer-shaped check already ran before publish: `scripts/smoke-consumer.mjs` packs the tarball into two scratch Vite apps and builds them: one with no optional peer installed, importing the barrel, and one with every optional peer that also runs the packed bin's `init chat`, `init app` and `init block` for every block, and builds the scaffolds (bare `node` can't import the barrel because components import their own CSS; that needs a bundler). To repeat it against the *published* artifact rather than the local tarball, scaffold a scratch Vite app, `npm install rift-ds@<version>`, import the barrel plus `tokens/tokens.css`, and run its build; for the chat, install every optional peer in a scratch Next.js app router project, run `npx rift-ds@<version> init chat` there, and build it; in the same project, run `npx rift-ds@<version> init app`, add the lines it prints to the root layout (importing `tokens/base.css` among them), and build again. The smoke consumer is a Vite app with `next/link` stubbed, so this is `init app`'s only check against a real Next.js app.
### 7. Tag the published commit
Tag the commit that was **published**, not necessarily current HEAD — the provenance attestation names that commit, so the tag, attestation, and tarball should all agree:
```bash
git tag -a v<version> <published-sha> -m "v<version> — <one-line summary>"
git push origin v<version>
gh release create v<version> --verify-tag --title "v<version> — <short title>" --notes-file <notes>
```
Tagging moves the compatibility gate's baseline, so any `SANCTIONED_BREAKS` row this release carried now matches nothing. Delete it in the next commit.
Write the notes for a consumer, not a maintainer: what's new, anything breaking with the migration step spelled out, and the install snippet. The previous release is the format reference; the prose follows `content-design.md`.
### 8. Append the release-log entry
The `/releases` page is 1:1 with npm by rule: every publish gets exactly one entry in `website/src/data/release-log.json`, written now, as part of this ritual — never later, never speculatively. Add the new entry at the TOP of `releases` (newest first) with the published `version`, the publish `date`, a plain descriptive `title`, and `body` paragraphs written for a consumer (the same story as the GitHub release notes, condensed — `content-design.md`'s release-log register row owns the voice). `scripts/validate-release-log.mjs` holds the structure. The site corpus carries the log, so run `npm run validate-registry` after adding the entry and commit the regenerated corpus with it; commit both with the version-bump follow-ups so the site and the registry never tell different stories.
### 9. Report
Version published, the npm URL, the tagged commit, the release URL, and anything a consumer must do to upgrade.
## Guardrails
- **Never** publish from a local machine (`npm publish` by hand) — releases go through the workflow so every release is provenance-signed and smoke-tested
- **Never** re-run the publish workflow after a successful publish, even if the registry 404s
- The version's three homes must agree — step 3 above is the procedure and `CLAUDE.md`'s Release section owns the fact. Never bump `package.json` alone — the manifest is what ships
- Never publish from a dirty tree, an unpushed commit, or a red CI
- **Auth is Trusted Publishing (OIDC) — there is no npm token to expire or rotate.** The one exception is a package's first-ever publish: a trusted publisher can only be attached to a package that already exists, so a brand-new name must be created with the workflow's `bootstrap_token` input (off by default, `NPM_TOKEN` secret, revoked immediately after). npm's tell for that case is specific — `404 ... OIDC token exchange error - package not found` from the exchange endpoint. Otherwise, if the publish step fails to authenticate, the cause is one of: `actions/setup-node` was given a **`registry-url:`** (see below); the `id-token: write` permission was dropped from `release.yml`; the workflow file was **renamed or moved** (the trusted-publisher registration on npmjs.com is keyed to the filename `release.yml`); the registration on npmjs.com names a different repository — it is keyed to repo + workflow filename, so check the pre-flight above for what is registered today; the npm CLI on the runner is older than 11.5.1; or the registration itself was removed. The owner owns anything that has to change on npmjs.com.
- **A `404` on `PUT` during publish is an AUTH failure, not a missing package.** npm returns 404 instead of 403 so it doesn't leak whether a package exists — the message even says "or you do not have permission to access it". Do not go hunting for a missing package; check the auth path.
- **Never add `registry-url:` to `actions/setup-node` in this workflow.** It writes an `.npmrc` with `_authToken=${NODE_AUTH_TOKEN}` *and* exports a placeholder `NODE_AUTH_TOKEN`, so npm thinks it is already authenticated, skips the OIDC exchange entirely, and gets rejected. The tell in the log: no mention of `oidc`/`trusted` anywhere, and `NODE_AUTH_TOKEN: XXXXX-XXXXX-XXXXX-XXXXX` in a step's env block. Provenance signing still succeeds in this state — that is GitHub→Sigstore and proves nothing about npm accepting the token.
- **A dry run never authenticates**, so it cannot prove OIDC is working — it exercises the build and the tarball, nothing else. After any change to `release.yml`'s auth, permissions, or filename, the only real proof is a genuine publish. Treat that as a reason to make the *next* release a small patch, not a reason to skip the dry run.
- The npm README is the package README — if the release changes install or usage, fix `README.md` in the same release, since it ships inside the tarball. Fix `src/stories/Configure.mdx` too: it carries the same install snippet on the Storybook landing page, deploys separately, and is where people evaluating a component arrive. `generate-readme-content.mjs` enforces that both mention the package name, but it cannot tell whether the *snippet* is still correct. The README banner image (`.github/readme-banner.jpg`) has no generator: it is a screenshot of the site hero and states its headline and subtitle in pixels. If either moved since the last re-shoot, run `node scripts/capture-readme-banner.mjs` and commit the image in the same release