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

# Release

> Changesets per member, the version-packages pull request, tags and recovery.

> [!NOTE]
>
> Status: partly live, partly target. `@changesets/cli` is configured (`.changeset/config.json`) and
> `pnpm changeset`, `pnpm changeset:status` and `pnpm changeset:version` exist.
> `.github/workflows/release.yml` opens the version pull request and, on its merge, runs `changeset
>     publish` to `npm.figentra.com`. Not built: the Infisical release identity, a packed-artifact smoke
> test and signed artifacts. Today every package is still `"private": true`, so nothing publishes
> yet. Policy: [0025](/adr/0025-packages-publish-to-private-registry) (supersedes 0013 decision 3),
> [0024](/adr/0024-mit-license-with-exclusions) (MIT),
> [0119](/adr/0119-merge-policy-and-release-authority) (the owner merges the version pull request).

## The rules

* **Every change to a member ships with a changeset.** `pnpm changeset` records it; `pnpm changeset:status` fails when a changed member has none (target: chained into `pnpm verify`).
* **`CHANGELOG.md` files are generated** by changesets and never hand-edited.
* **Versions are bumped by the release job on `main`**, not on a feature branch. `guard-bash` denies `changeset version` locally for that reason.
* **Merging the version pull request is the release decision.** Claude prepares it; the owner merges it ([0119](/adr/0119-merge-policy-and-release-authority) D7).
* **Published or tagged versions are permanent.** A version is never republished or silently replaced; roll forward with a new patch.
* **Only packages are versioned.** Apps and services deploy by commit SHA and get no version or tag (`privatePackages: { version: false, tag: false }`, [0025](/adr/0025-packages-publish-to-private-registry) decision 4).
* **A package publishes when it flips to `"private": false`.** It then builds to `dist/*.mjs` + `.d.mts` and publishes to `npm.figentra.com` (`@stackra` scope).

## A normal release

**1. Land changesets with the work**

In each pull request that changes a member: `pnpm changeset`, pick the members, pick the bump
(patch, minor, major) and write one user-facing line. Commit the generated file under
`.changeset/`.

**2. Let CI open the version pull request**

After a push to `main` the release job runs `pnpm changeset:version` (which also refreshes the
lockfile) and opens or updates one pull request titled `chore(release): version packages`.

**3. Review it**

Read the `CHANGELOG.md` diffs and the version bumps. Check the bump matches the change. The
release job pushes and opens it as `stackra-bot` (a GitHub App token from
`actions/create-github-app-token`, repo secrets `STACKRA_BOT_APP_ID` and
`STACKRA_BOT_PRIVATE_KEY`), so the Quality Gate runs on it without a close and reopen.

**4. Merge it**

The merge to `main` is the release. The job builds, publishes every non-private package to
`npm.figentra.com`, tags it and creates the release notes.

**5. Verify**

`git ls-remote --tags origin` shows the new tags on the merge commit, and each member's
`CHANGELOG.md` contains the entry.

## Before the first release (once, by a human)

* Branch protection on `main` requires the `Quality Gate` check ([CI](/runbooks/ci)).
* A tag ruleset on the version tag pattern blocks deletion and non-fast-forward updates for everyone. Version tags are immutable.
* The host setting that lets the workflow token create pull requests is on.
* The release job runs in a protected environment restricted to `main`, so a branch that edits the workflow cannot obtain a credential.

## Publishing to npm.figentra.com

[0025](/adr/0025-packages-publish-to-private-registry) records the decision. Before the first package flips to `"private": false`, a human:

* creates an Infisical machine identity for the release job (OIDC, trusting only this repository's `release` environment on `main`) with read access to `/ci` in `prod`;
* stores the registry publish token in `/ci` as `NPM_FIGENTRA_TOKEN`;
* sets the repository variables `INFISICAL_RELEASE_IDENTITY_ID` and `INFISICAL_PROJECT_ID` (names and ids, never a token). Until they are set the release job skips the token step.

The procedure the job follows, some of it still target:

* **The publish token comes only from Infisical `/ci` over OIDC** ([0009](/adr/0009-secrets-two-mechanisms)); no token sits in `.npmrc`, a repository secret or a file.
* Build the tarball and an SBOM with no privileges, then sign both keylessly in the one job that holds `id-token: write`.
* Publish is idempotent: the same bytes already on the registry are not published again, so re-running failed jobs is safe; different bytes fail the job.
* Read access for consumers is a read-only token held in Infisical `/ci` and fetched through OIDC ([Secrets](/runbooks/secrets)).
* A prerelease publishes under a `next` dist-tag and moves no floating tag.

## When something fails

Re-run only the failed jobs of the same run unless a row says otherwise.

| Failed step | State | Do |
| - | - | - |
| Version pull request not opened | Nothing released | Check the host setting for pull requests from workflows, then push any commit to `main` or re-run |
| `verify` on the version pull request | Nothing released | Close and reopen the pull request; if it is red, fix on `main` and let the job update it |
| Tagging | Merged, not tagged | Re-run the failed job; it tags over an existing release safely |
| Wrong bump merged | Tagged | Roll forward: add a changeset and release the correction |

## Revert

* **Before merging:** close the version pull request. The next push recreates it from the remaining changesets.
* **After merging:** roll forward. For a bad release that was published, deprecate it (`npm deprecate <pkg>@<bad> "<reason; use <good>>"`), move the `latest` dist-tag back to the last good version with `npm dist-tag add` if needed, and ship a fixed patch. Prefer deprecate over unpublish.
* **A bad release record on the host:** mark it a prerelease or delete the release page; leave the tag.
* **The workflow itself:** revert the workflow file on `main`; nothing else depends on it.

## Checks that back this page

| Rule | Check | State |
| - | - | - |
| Changeset per changed member | `pnpm changeset:status` | Script live; not in `verify` |
| No local `changeset version` | `guard-bash` | Live |
| Conventional commits and generated changelogs | commitlint, changesets | Live |
| Release workflow, tag ruleset | CI | Not built |

See also [Mobile release](/runbooks/mobile-release), [Deploys](/runbooks/deploys) and [Commits and releases](/guides/workflow).


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