Skip to content

Releasing

One tag ships three packages and one container image. nx release versions the projects nx.json lists under release.projects in lock step, because projectsRelationship there is fixed. Fixed means one version and one git tag for the whole set, under the default tag pattern v{version}. So both release workflows wait for a tag matching v*, or for a run you start by hand from the Actions tab. Pushing to main starts neither. publish.yml sends the three tarballs to npm, and image.yml builds the daemon image from the same commit for the two architectures the operator guide names.

Both workflows read the RELEASE_DRY_RUN repository variable, and until it holds false a tag ships no artefact. GitHub ignores case when it compares strings, so False and FALSE do the same work. Every other step still runs. pnpm verify runs on the tagged commit, nx release publish runs with --dry-run, and the image job builds both architectures and pushes neither. An unset variable returns an empty string, and an empty string is not false, so a repository nobody has configured stays in rehearsal. Set it under Settings → Secrets and variables → Actions → Variables:

RELEASE_DRY_RUN = false

Once set, it stays set. A deployment protection rule on the npm environment would stop the publish job until a reviewer approved it or a timer expired. Environments, environment secrets and deployment branches on a private repository need GitHub Pro, Team or Enterprise. But on the Free, Pro and Team plans a wait timer or a required reviewer is available for a public repository alone. So this repository has the npm environment with the secret on it, and no approval gate in front of the publish job at all. Three things cover that gap instead: this variable, the tag check below, and the gate running green on the tagged commit. The variable stops the first tag somebody pushes without meaning to publish, and every tag after that one publishes. Clearing it, or giving it a value other than false, puts the next tag back into rehearsal, and that is the move if a release has to be stopped between tags.

Version and tag on your own machine. Then you can read the commit and the tag before either one leaves it. --skip-publish stops nx after versioning and changelog generation, and it leaves behind a commit and a v tag in the local repository. Add --first-release to the very first release you cut, because there is no earlier tag for the changelog to compare against:

Terminal window
pnpm nx release --skip-publish --dry-run # read what it would do
pnpm nx release --skip-publish # version, changelog, commit, tag
git push --follow-tags

--follow-tags is the half that starts the release, and pushing the commit alone leaves the tag behind and triggers neither workflow. nx release publish then runs inside the Publish job, where the credential is. It publishes the version each manifest declares, not the version the tag names, and a tag typed by hand can disagree with them. So the publish job compares the two before it runs the gate, and a mismatch fails the run naming the manifest and both versions.

Cut one release at a time. Each workflow puts every run of itself in one concurrency group, so two tags pushed close together go out one after the other, not at once. But GitHub keeps one run pending per group and cancels that pending run when a third arrives. So with one release running and a second already waiting, a third tag takes the second one’s place. No status reports the loss, and the version that run carried never ships.

Start either workflow from the Actions tab against any ref. Publish takes a dryRun checkbox, ticked by default; Image takes a push checkbox, unticked by default. Run with those defaults and the whole path executes while both registries stay untouched. Unticking dryRun or ticking push does not override RELEASE_DRY_RUN, because each workflow reads the variable as well as the box. One step is missing from a run started against a branch: the tag check reads github.ref and runs on a tag alone, so that run publishes whatever the manifests declare. Read them before you untick dryRun, or dispatch against the tag and let the check run.

Rehearse after every change to either workflow file, because a release is the one path with no other rehearsal. A rehearsal costs a full run. Image’s gate job runs under no if at all, so a dispatch that ships nothing still runs pnpm verify and then builds both architectures, with the arm64 leg under emulation. timeout-minutes in image.yml says how long each of those two may take. So rehearse a change confined to publish.yml with Publish alone, and pay for the image only when the change reaches it.

NPM_TOKEN lives on the npm environment and not on the repository. Environment secrets are readable only by a job that names that environment, so the publish job is the only job here that can read it. It is a granular npm token with read and write access to the scope the manifests publish under. image.yml needs no secret of its own: it signs in to the container registry with the run’s own GITHUB_TOKEN, under the packages: write scope that workflow requests.

npm generates no provenance for a private repository, even when the package itself is public, and a package published with one needs its repository field to name the public repository it is published from. So the publish job derives NPM_CONFIG_PROVENANCE from github.event.repository.private and asks for no statement while this repository is private. That switch belongs to the workflow and not to npm. Without it the job would ask for a statement npm cannot build here. Artifact attestations follow a similar rule: every current plan grants them on a public repository, and a private one needs GitHub Enterprise Cloud. So the image job skips its attestation. Both workflows read github.event.repository.private to decide, and opening the repository turns both on with no workflow edit.

Two failures behave differently, and the difference decides what you do next. A failure inside pnpm verify stops both workflows before either registry is touched, and that version is still free: delete the tag on the remote and locally, commit the fix, and tag the same version again. But a run that breaks partway through publishing has already sent whatever went out before it stopped, and npm never lets a version be used twice, unpublished or not. Recover with a new version. Image is a separate workflow, so a failed build there leaves the published tarballs alone. Start it again from the Actions tab against the tag, with push ticked.

pages.yml deploys the site on a push to main, under an allow-list paths:. A change that reaches the site by a path nobody listed leaves the live deployment where it is and reports no failure, and the Actions tab redeploys it by hand. No tag starts that workflow. So a documentation fix ships without a release, and a release never waits for the site. Tooling covers it beside the rest of continuous integration.