> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deploybaseline.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Releasing

> How version bumps in one pull request become npm packages and GitHub releases.

Releases are manual, single-PR affairs. The maintainer controls the changelog voice and format. Every public package has its own version: `@deploybaseline/core`, each provider, the state stores, and `@deploybaseline/cli` are released when they change, not together.

> [!NOTE]
>
> `runbook/RELEASE.md` in the repository is the operator's walkthrough: one-time setup, every step of a release, validating one before merge, and a table of failures with their fixes. This page summarizes it.

## Preparing a release

`make release` does the mechanical steps below for you and stops before committing:

```bash theme={null}
make release PKG=stripe BUMP=patch DRY=1   # print the plan, change nothing
make release PKG=stripe BUMP=patch         # or PKG=stripe,github BUMP=minor, or PKG=cli VERSION=0.15.0
```

It refuses a dirty tree, a version already on npm, and a tag that already exists. It creates the branch, bumps each version, regenerates provider data, saves state fixtures when a provider was bumped, and opens a changelog entry for each package with a `TODO(release)` line that the release plan refuses to publish until you replace it. The steps it performs, if you prefer to do them by hand:

**1. Branch**

Create a branch such as `release-stripe-0.14.2`, or `prepare-v0.15.0` for a CLI release.

**2. Bump each package you are releasing**

Edit `version` in that package's `package.json`. Bump only the packages whose code changed, by semver: a patch for a fix, a minor for a feature. Before 1.0 a breaking change is a minor.

**3. Regenerate provider data**

```bash theme={null}
pnpm provider-metadata
```

The CLI's catalog (`packages/cli/src/catalog.generated.json`) records each provider's version, and that is the copy the CLI bundles. The script fails when a provider's version is outside the range its slot in `packages/cli/src/registry.ts` gives. A provider minor outside that range needs the range widened, and then a CLI release, before CLIs pick it up.

**4. Save state fixtures**

```bash theme={null}
pnpm state-fixtures
```

Adds `packages/cli/src/__tests__/fixtures/state/<provider>/<version>.json` for each provider version that has none yet. The CLI tests restore every fixture, and one written by a provider with a different `stateVersion` must be refused rather than half-restored. Commit the new files.

**5. Write the changelog entries**

Each package keeps `CHANGELOG.md` in its own directory; the CLI's is the root `CHANGELOG.md`. Add the entry at the top, wrapped in markers, with the version as its heading:

```md theme={null}
<!-- release:start -->
## 0.14.2

...
<!-- release:end -->
```

Remove the markers from that file's previous entry. Only the latest entry in each file has them.

**6. Check the plan**

```bash theme={null}
pnpm release-plan
```

Lists every package whose version is not on npm yet, and warns when one has no entry between the markers or a package name was never published. CI runs the same check on every pull request.

**7. Note bundle growth if needed**

`scripts/check-bundle.mjs` fails CI when the CLI tarball exceeds `packages/cli/bundle-budget.json`, unless the CLI's release entry contains a `Bundle size:` note. If the growth is intended, raise the budget and add that note.

**8. Validate packaging (optional)**

```bash theme={null}
make release-dispatch DRY=1
```

Runs the `Release` workflow on your branch in dry-run mode: build, test, pack each package that would publish, check its tarball, and `npm publish --dry-run`. Nothing is published, tagged, or released. `make release-verify` runs the tarball check locally.

**9. Open the PR and merge to main**

CI must pass as usual.

A provider patch or minor within the CLI's range reaches users without a CLI release: the CLI pulls the newest version in range when a project has no lockfile entry, and `npx @deploybaseline/cli update` moves pinned projects forward. See [Provider versions](/provider-versions.md).

## What happens after merge

The `Release` workflow (`.github/workflows/release.yml`) runs on every push to `main` and on manual dispatch. It publishes nothing unless the repository variable `RELEASE_ENABLED` is `"true"`. A dispatch that publishes must run on `main`; a dispatch with `dry_run` runs from any branch and only packs and verifies. One release runs at a time.

```mermaid theme={null}
flowchart LR
  A["check-release<br/>which versions are not on npm?"] -->|some| B["publish<br/>build, type-check, test,<br/>pack and publish each package"]
  B --> C["github-release<br/>one release per package,<br/>notes from its changelog markers"]
  A -->|none| D["stop"]
```

> **check-release**
>
> Runs `node scripts/release-packages.mjs plan`. For each public workspace package it asks npm whether that package's exact version exists, and lists tags on `origin` to find versions that reached npm without their GitHub release. It asks for the exact version rather than the `latest` dist-tag, so moving `latest` back during a rollback does not trigger a republish. The job fails when a package to release has no changelog entry for its version, or when a package name does not exist on npm at all: trusted publishing can only be configured for a package that already exists, so a new package needs a one-time bootstrap publish first.

> **publish**
>
> Runs in the protected `Release` environment with `id-token: write`. It installs with `--frozen-lockfile`, builds, type-checks, and tests, then runs `node scripts/release-packages.mjs publish`. That packs each package with `pnpm pack` (which resolves `workspace:` ranges), checks the tarball (every entry point named in `package.json` is present, the README and notices are present, no `workspace:` range is left), and publishes that exact tarball with `npm publish --provenance --access public` using npm trusted publishing over OIDC, in dependency order: core, then the providers and stores, then the CLI. Transient npm failures, including Sigstore's `TLOG_CREATE_ENTRY_ERROR`, are retried with a fresh provenance signature. Versions already on npm are skipped, so a rerun after a partial failure is safe.

> **github-release**
>
> Creates one GitHub release per published package, with the text between that package's release markers as the notes, at the commit that set the package's version. The CLI is tagged `v<version>`; every other package is tagged `<name>@<version>`, such as `@deploybaseline/stripe@0.14.2`. An existing release with that tag is left alone. Because npm goes first, a run that dies after publishing leaves only tags missing, and the next run (a rerun, a dispatch on `main`, or the next merge) creates them.

## New packages

`scripts/release-packages.mjs` discovers packages from the workspace, so a new provider is published without editing the workflow. `pnpm new:provider` creates its `CHANGELOG.md`. Its first version still needs a bootstrap publish and a trusted publisher configured on npm, as below. This is the last item in the [provider definition of done](/contributing/adding-a-provider.md#provider-definition-of-done).

## Bootstrapping package names

npm configures trusted publishing per package, and only for a package that already exists, so CI cannot publish a name that has never been on npm. `release-packages.mjs plan` stops on such a name. A maintainer publishes a placeholder for each one by hand, once, and the real version then goes out from CI with provenance, a tag, and a GitHub release like any other.

**1. Log in to npm**

Use an account that can publish to the `@deploybaseline` scope:

```bash theme={null}
npm login
npm whoami
```

**2. Publish the placeholders**

```bash theme={null}
node scripts/release-packages.mjs bootstrap --dry-run
node scripts/release-packages.mjs bootstrap
```

For each public package whose name is not on npm, this publishes version `0.0.0-bootstrap.0` under the `bootstrap` dist-tag, holding only a `package.json` and a README. A prerelease of `0.0.0` matches no range a CLI or project asks for, so nothing installs it. Names already on npm are skipped, so a rerun after a failure is safe.

**3. Add a trusted publisher to each package**

On npmjs.com, open each package's settings and add a trusted publisher: GitHub Actions, repository `deploybaseline/cli`, workflow `release.yml`, environment `Release`. `node scripts/release-packages.mjs` lists every package name.

**4. Release as usual**

`pnpm release-plan` now lists each package's real version instead of warning about missing names. Merging to `main` publishes them through the `Release` workflow.

**5. Deprecate the placeholders**

After the real versions are on npm:

```bash theme={null}
for name in $(node scripts/release-packages.mjs --json | node -e 'for (const p of JSON.parse(require("fs").readFileSync(0, "utf8"))) console.log(p.name)'); do
  npm deprecate "$name@0.0.0-bootstrap.0" "Placeholder, install a later version"
  npm dist-tag rm "$name" bootstrap
done
```

## Compatibility checks

The `Compatibility` workflow (`.github/workflows/compatibility.yml`) runs daily and on manual dispatch once the repository variable `COMPAT_ENABLED` is `"true"`. Both of its checks install packages into an empty project and start every built-in service with `--offline`:

* `node scripts/compatibility.mjs host` runs the CLI from `main` against the latest published provider in each default range. It also fails when a default range has no published version.
* `node scripts/compatibility.mjs providers` runs the providers from `main` against the oldest supported CLI, `oldestCli` in `compatibility.json`. It skips while that is `null`. Set it to the first published CLI that runs provider packages, and raise it when a CLI drops a protocol version.

CI runs `node scripts/compatibility.mjs host --packed` (`make compatibility`) on every pull request: the built CLI against the workspace's packed providers, installed from their tarballs. Each provider's saved-state fixtures in `packages/cli/src/__tests__/fixtures/state/` are restored on every test run, so a provider change that can no longer read state saved by an earlier version, without raising `stateVersion`, fails CI.

## Packaging details

* `scripts/prepare-package.mjs` runs as `prepack` in every package and copies the root `LICENSE`, `NOTICE`, and `THIRD_PARTY_NOTICES.md` in (plus `README.md` for the CLI). CI unpacks every tarball and checks these files match the root copies.
* The CLI's `files` field excludes source maps from the tarball.
* `node scripts/release-packages.mjs` prints the release order, `--json` adds each package's tag and changelog, `node scripts/release-packages.mjs notes <name>` prints a package's current notes, and `node scripts/release-packages.mjs verify [name...]` packs and checks tarballs. CI runs `verify` on every pull request.
* `scripts/release.mjs` (prepare) and `scripts/release-packages.mjs` (publish) share `scripts/lib/releases.mjs`, so they agree on the package list, publish order, changelog rules, and version arithmetic.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.