Skip to main content
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.
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:
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

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

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:
Remove the markers from that file’s previous entry. Only the latest entry in each file has them.
6

Check the 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)

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.

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.
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.
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.
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/[email protected]. 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.

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:
2

Publish the placeholders

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:

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.