# Publishing a release > **Project** (non-normative maintainer notes) ยท **For:** maintainers shipping Cotal Cotal releases with pnpm, Changesets, and one GitHub Actions workflow, `changesets.yml`. The fixed group in [`.changeset/config.json`](../.changeset/config.json) versions the public packages together. Examples are private and never publish. ## Prepare the version Add a changeset with each package change. These packages are 0.x, so a breaking change takes a minor bump. Write its first sentence as a headline a user can understand on its own. The GitHub Release and Discord announcement lead with that sentence; put the mechanism after it. Every push to `main` that leaves changesets there runs the Changesets workflow. The workflow opens or updates the version PR and packs its head with all native binaries. That pack is the release. Unchanged native inputs can reuse a complete, unexpired artifact set from a successful `main` push of this repository's Changesets workflow. Source files, SDK pins, build scripts, install inputs and build instructions must match; the packing checkout checks them again. The original builder commit stays in each receipt, and the existing architecture, binary-hash and source-hash checks still run. Lockfile matching covers the seat and workspace importers, linked workspace importers, and all reachable package and snapshot entries, including peer and optional dependencies. Shared resolution settings still match in full. Unrelated importer or package changes do not require new native builds. Unsupported lockfile versions, dependency forms or incomplete dependency graphs refuse reuse. No receipt is rewritten to name the application release commit. Changed inputs or missing artifacts require fresh native builds; an API or validation failure refuses rather than using unverified bytes. Before you run the publish command, have the builds and a focused upgrade check ready. A fresh install that succeeds does not prove that an existing installation upgrades. CI, review rounds, and the broad native test matrix are not required to publish unless the operator asks for them. ## Publish Run one command that names the `main` commit the release must include: ```bash pnpm release:cut ``` The command refuses a commit that is not on `main`, and a commit that carries no changeset. It then waits until the version PR carries the commit, sits on the head of `main`, and has its pack. Then it merges the version PR by its head commit. That merge publishes the release. The workflow run for the merge finds the run for the merge's parent commit, checks that the parent run built its pack from the same tree, and uploads that pack. Nothing is built after the command. A merge of the version PR by any other means publishes the same way. A run that finds unpublished versions on `main` but no pack for its tree fails and publishes nothing. The command prints one UTC line for each step: `command`, `ready`, `merged`, `run`, `published`, `visible`, and `run rc`. The budget is ten minutes from `command` to `visible`. `visible` is the moment that `npm view cotal-ai@ version` answers. The upload ends about two minutes after the merge, and npm has then taken four to seven minutes to serve the new `cotal-ai` version. A command that you run after the pack is ready fits the budget. A command that you run right after a merge also waits for that merge's pack. Measure from the original `command` time. The target is verified availability within ten minutes. If you miss it, report the miss. Don't reset the clock, and don't call a queued job published. ## Recover from a failed publish npm accepts an upload but waits several minutes before it serves the version, and it refuses a second upload of that version. The patch in `patches/` makes Changesets treat that refusal as an already published version. A re-run of a failed publish job therefore skips what npm already holds and publishes the rest. A transparency-log conflict (`TLOG_CREATE_ENTRY_ERROR`, HTTP 409) fails an upload, and the same upload passes on a re-run. For that reason, `pnpm release:cut` re-runs a failed publish job up to twice. If the job still fails, inspect the registry before you retry again. To publish again from a dispatch, run: ```bash gh workflow run changesets.yml --ref main ``` A dispatch publishes only while the head of `main` is the version PR merge. A dispatch while unreleased changesets remain on `main` fails with `Merge the Changesets version PR before publishing.` and publishes nothing. Apart from these re-runs of a failed publish job, never repeat an upload whose outcome is unknown. Inspect the registry and the published-package output first. ## Verify the result The publish action reports the packages it published and pushes one git tag per package. A following job then runs direct-mode verification. It checks every exact package version, installs the fixed closure from npm into an isolated prefix, and runs the CLI. Direct publishing, the default, updates `latest` during the upload, before these checks run. A verification failure stops the GitHub Release, but it cannot undo those tag updates. An accepted upload and a successful install are separate observations. After an upload, npm can return cached missing-package responses for a while. The large `cotal-ai` tarball has taken up to twenty minutes to become installable, so the install job waits up to twenty-five minutes for the install to succeed. The closure census before it waits as long, so a late `cotal-ai` does not fail the job and skip the Release. The install check uses a fresh cache and `--prefer-online` to ask npm to revalidate metadata. That does not force npm's upstream caches to converge. The next job cuts one GitHub Release for the version, tagged `v`. Its notes open with the install command and a restart reminder, then `## What changed`: one sentence per change, major and minor changes before patches. Full summaries follow in a collapsed block, then the merged pull requests. The summaries cover versions since the previous Release, including versions that never received a Release, and are deduplicated across packages. Both layers keep whole changes within the remaining release-body byte budget and name any omissions. The job installs the Markdown parser from the workspace lockfile. If that installation fails, a version has no changelog section or summaries, or the detail script fails otherwise, the job keeps the diagnostic, emits a warning, and creates the release without the optional summaries. These steps do not add a publication gate. The Discord announcement carries only the `## What changed` bullets and an upgrade command, links to the Release, and drops whole bullets to stay within the embed limit. It refuses a body without that section. The request reads the webhook credential in-process, never from a command argument, and prints no request error that could contain the credential. Redirects are refused and failed requests are not retried automatically; check Discord before retrying an unknown delivery. Before you report a fix as delivered, check the upgrade from an existing installation that the fix affects. ## How the workflow publishes The workflow uses the maintained Changesets actions to select the release, pack its public packages, and publish the packed artifacts directly to `latest` with the existing publishing authority. There is no separate qualification or promotion workflow. - GitHub Actions pins each run to one commit. - One upload runs at a time. - Direct-mode verification runs in a later job outside the publisher queue, so its wait does not hold later uploads. Native build jobs produce Cotal's native binaries: - The seat binding, on Linux for x64 and arm64. - The workspace binding, on Windows and macOS for x64 and arm64. The pack job assembles those binaries and builds the packages. It has no npm publishing authority. Only the publish job has the npm trusted-publishing permission, and it uploads the packed output without rebuilding it. pnpm caches its package store, keyed by the lockfile, and never caches built output. The publisher installs only the root tooling. The root `ci:publish` command is the standard `pnpm changeset publish`. Maintainers publish through the workflow instead, so that the release has the configured identity and includes the native binaries. ## Configure npm Every public npm package needs this trusted publisher: - Repository owner and repository: this repository. - Workflow filename: `changesets.yml`. - Environment: `npm-publish`, allowing `main`. - Allowed action: direct `npm publish`. The default workflow needs no grant to manage dist-tags. The workflow authenticates with GitHub OIDC and never falls back to a long-lived npm token. A missing package or a missing publisher authorization is an access problem. It is not permission to borrow other credentials. ## Staged mode Staged mode is optional, off by default, and not yet qualified for use. It is on only when the repository variable `COTAL_STAGED_RELEASE` is `true`. The run that prepares a pack freezes the mode and records it with the pack, so a later edit of the variable does not change a prepared release. In staged mode, the workflow validates the complete fixed group and packs it under the `cotal-staged` dist-tag, using the standard Changesets plan. After the closure and install checks pass, ordinary `npm dist-tag add` commands promote the packages in dependency order, with `cotal-ai` last. - A failed verification changes no `latest` tag. - A failed promotion stops before the later packages and creates no GitHub Release. - Each tag is a separate registry write, so promotion is not an atomic transaction. - Staged-mode verification holds the publisher queue through installation and promotion, so an older delayed staged release cannot overwrite the tags of a newer release. Before anyone sets `COTAL_STAGED_RELEASE=true`, the publisher owner must grant `manageDistTags` on every package's trusted publisher. That grant is separate from `createPackage`, which direct publishing uses, as npm's [allowed-action documentation](https://docs.npmjs.com/trusted-publishers/#configuring-allowed-actions) explains. The registry checks the grant on each dist-tag command, so no separate read of the trust settings is needed. An HTTP 401 on a settings read shows neither that a write is allowed nor that it is denied. Don't enable the variable until the owner has granted `manageDistTags`. npm's own [staged publishing](https://docs.npmjs.com/staged-publishing/) feature does not replace staged mode. npm keeps staged contents private until someone approves them, so they cannot pass the closure install check against the public registry first. `npm stage approve` also requires maintainer 2FA, and an OIDC exchange token cannot run it, as the [npm stage command reference](https://docs.npmjs.com/cli/v12/commands/npm-stage) documents. ## Correct release notes Add a `**Correction:**` paragraph under the affected changelog bullet, and keep the original text. Use the same correction wording in each affected package, so that the release notes can deduplicate it. ## Earlier section links These links lead to the current guidance for sections renamed in earlier versions. ### Publication deadline [#publication-deadline] See [Publish](#publish). ### CI policy [#ci-policy] See [Prepare the version](#prepare-the-version). ### Trusted publishing [#trusted-publishing] See [Configure npm](#configure-npm). ### Adding a publishable package [#adding-a-publishable-package] See [Configure npm](#configure-npm). ### Preparing a release [#preparing-a-release] See [Prepare the version](#prepare-the-version). ### Publishing the prepared candidate [#publishing-the-prepared-candidate] See [Publish](#publish). ### Publication controls [#publication-controls] See [Publish](#publish). ### Verifying availability [#verifying-availability] See [Verify the result](#verify-the-result). ### Recovering late registry visibility [#recovering-late-registry-visibility] See [Verify the result](#verify-the-result). ### Correcting a released changelog entry [#correcting-a-released-changelog-entry] See [Correct release notes](#correct-release-notes). ### 0.11 runtime migration [#011-runtime-migration] See [Prepare the version](#prepare-the-version).