# Publish a stable release

Sonik is published manually from a developer machine. GitLab CI does **not** publish — `.gitlab-ci.yml` only runs Vitest and (manually) deploys the VitePress docs site to GitLab Pages, with `APP_NOT_DEPLOYABLE: "true"`. Cutting a release is a local `npm publish`.

For pre-release / WIP work see [PUBLISH_BETA.md](./PUBLISH_BETA.md) instead.

## Prerequisites

- An npm account that's a member of the `@loophq` org with publish rights on the [`@loophq/sonik`](https://www.npmjs.com/package/@loophq/sonik) package.
- Logged in to npm: run `npm whoami` and confirm; if not, `npm login`.
- A 2FA OTP available — `@loophq` requires it for publish, so be ready with `--otp <code>` or your authenticator app.
- Node version per `.nvmrc`. `nvm use` will pick it up.
- A clean working tree on the **release branch** (the branch behind the MR you're about to merge). Publishing from the branch — not from `main` — means the version bump, changelog entry, build verification, and publish all live in the same MR.

## Versioning

Versioning policy is summarized at the top of [`docs/CHANGELOG.md`](./CHANGELOG.md):

- **patch** — bugfixes and non-functional changes (including internal dependency refreshes that don't change the public API)
- **minor** — new components
- **major** — large sweeping breaking changes only

## Release steps

These all happen on the release branch, **before** merging the MR.

1. **Update the changelog.** Add the new version and notes to [`docs/CHANGELOG.md`](./CHANGELOG.md). (The root `CHANGELOG.md` is auto-generated by the tag/release workflow — leave it alone.)

2. **Bump the package version.**

   ```sh
   npm version <patch|minor|major>
   ```

   This updates `package.json` + `package-lock.json` and creates a tagged commit (`vX.Y.Z`) on the current branch. If `package.json` is already at the target version (e.g., the version was set manually earlier in the branch), tag it explicitly instead:

   ```sh
   git tag -a vX.Y.Z -m "Release X.Y.Z"
   ```

3. **Build.**

   ```sh
   npm run build
   ```

   This runs `type-check` → `build:types` → `build:exports` → `build:theme`. Note that the package ships `lib/**/*.ts` directly (see `main` / `exports` in `package.json`) — there is no bundled JS output; consumers' bundlers compile Sonik's source. Sanity check the tarball with `npm pack --dry-run` if anything about the file layout changed.

4. **Publish.**

   ```sh
   npm publish
   # or, if 2FA prompts don't auto-show:
   npm publish --otp <code>
   ```

5. **Push the branch and tag.**

   ```sh
   git push --follow-tags
   ```

6. **Merge the MR.** Use a **merge commit** (not squash) so the tagged commit stays reachable from `main`. If your project requires squash-merge, the tag will still work — npm has the tarball and the tag still points to the right tree — but `git log` on `main` won't directly contain the published commit. Re-tag the squashed merge commit on `main` after the fact if you want exact correspondence.

## Post-publish sanity checks

- Verify the new version is live:

  ```sh
  npm view @loophq/sonik version
  npm view @loophq/sonik dist-tags
  ```

  Or check [npmjs.com/package/@loophq/sonik](https://www.npmjs.com/package/@loophq/sonik).

- Bump in a host app (e.g. `mx-frontend` or `loop-returns-app/frontend-v2`) and confirm `npm install` resolves and the app builds.

## Recovery

If you accidentally published a beta as `latest`, see ["If beta is pushed as latest by accident"](./PUBLISH_BETA.md#if-beta-is-pushed-as-latest-by-accident) in the beta doc — the `npm dist-tag` recipe applies here too.
