---
url: /release.md
---
# Release & Publishing

Silvery uses two GitHub Actions workflows for releases:

* `verify.yml` — runs on every push to `main` and every PR. Pre-publish gate.
* `release.yml` — runs on tag push (`v*`). Publishes to npm + GitHub Release.

Both run the `verify-publishable` revision pinned in `package.json` and `bun.lock`.

## Pre-publish gate (verify-publishable)

Pre-publish bugs are expensive — once a broken `silvery@0.19.X` reaches the npm registry it is forever-pinned in users' lockfiles. The gate catches three classes of bug *before* the tag push:

1. **Wrong `publishConfig.exports`** — the `exports` field reachable from a published tarball does not actually map to a file in `dist/`. Silvery 0.19.0 shipped this way; consumers got `Cannot find module ./src/index.ts`.
2. **Empty tarball / missing dist** — `tsdown` crashed silently or ran from the wrong cwd; the package ships without its build output.
3. **EPRIVATE on accidentally-listed public package** — a package that is supposed to publish (e.g. `@silvery/color`) still has `private: true` in `package.json`. `npm publish` would refuse, halting the release midway.

The gate builds all workspaces, packs their exact tarballs, checks packed manifest targets, Publint and ATTW, then publishes them to an isolated local registry. It installs each of the ten public packages into a fresh Node 24 consumer and probes their imports and bins. The `verifyPublishable.public` list in `package.json` asserts the release set.

The legacy verify workflow ran `npm install <packed-tarball.tgz>` directly, which always failed during release windows because the tarball's transitive deps reference `@silvery/<dep>@<thisversion>` that wasn't on the public registry yet (chicken-and-egg). Verdaccio breaks the loop by hosting every cross-dep itself.

## Run locally

```bash
bun run verify-publishable
```

This runs the pinned local binary. It builds first, then runs the local registry and fresh-consumer checks.

Useful flags:

* `--no-build` — use artifacts already built from this checkout.
* `--keep` — retain the verifier's scratch directory for inspection.
* `--output-dir <empty-dir>` — retain verified tarballs and their SHA-512 digests for a publisher.

The gate normally takes 1-2 minutes (build dominates; the verdaccio cycle is ~30s).

## What gets probed

The package manifest asserts these public names:

* `@silvery/ansi`
* `@silvery/color`
* `@silvery/command`
* `@silvery/commander`
* `@silvery/config`
* `@silvery/scope`
* `@silvery/selection`
* `@silvery/signals`
* `@silvery/syntax`
* `silvery` (root barrel)

Private workspaces are published only to the isolated registry so internal dependencies resolve. They are not consumer-probed. To add a public package, update `verifyPublishable.public` and the release workflow in the same change.

## Adding a public package

1. Create the package under `packages/<name>/` with `tsdown` build + `publishConfig.exports`.
2. Add its exact npm name to `verifyPublishable.public` in the root `package.json`.
3. Add a publish step to `release.yml` in the right dependency-order layer.
4. Run `bun run verify-publishable` locally to confirm the gate is happy.

## Release flow

Once `verify-publishable` is green on `main`:

1. Bump `version` in every workspace `package.json` (keep them in lockstep — `release.yml` publishes in dependency order).
2. Commit + tag: `git tag v<version> && git push --tags`.
3. `release.yml` runs: build → verify-publishable gate → publish in dep order → smoke test → GitHub Release.

If the gate fails on a release tag, no packages reach npm — fix the issue, retag.
