What nx release is and how the command is structured

nx release is the Nx CLI's built-in release tooling. It splits a release into three phases that run in sequence but can also run on their own:

  1. Version: determines the next version for each releasable project, updates manifest files such as package.json, and can create a commit and git tags.
  2. Changelog: renders release notes from commit history into CHANGELOG.md files and, optionally, into GitHub or GitLab releases.
  3. Publish: pushes the built artifacts to a registry, such as npm for JavaScript packages.

Running nx release executes all three phases. Running nx release version, nx release changelog or nx release publish executes one phase. When conventional commits are enabled, commit messages (feat:, fix:, BREAKING CHANGE) determine the size of each bump. Nx's project graph determines which projects a commit touched and which dependents also need updates.

Commits → parsed as conventional commits → project graph maps changes to projects and dependents → version (manifests, release commit, git tags) → changelog (CHANGELOG.md, hosted release notes) → publish (registry)

Splitting the phases is useful in CI because each phase has different failure modes and needs different credentials. You can version and tag on a protected branch, run builds and tests against the tagged commit, and publish only after those checks pass. The publish step is the only one that needs a registry token. If it fails, you can retry it without bumping versions a second time.

The teams that benefit most are backend and platform engineers who maintain monorepos with many internal libraries, SDKs or shared configuration packages. In those repos, tracking cross-package version changes by hand regularly leads to mistakes.

#!/usr/bin/env bash
# Run all three phases locally without changing anything.
# --dry-run prints the planned version bumps, changelog entries
# and publish targets so you can inspect them first.
npx nx release --dry-run

# --- CI job 1: version + changelog on the protected branch ---
# Uses conventional commits (if enabled in nx.json) and the
# project graph to bump versions, update CHANGELOG.md, commit
# and tag. No registry token is needed for this job.
set -euo pipefail
npx nx release --skip-publish

# --- CI job 2: verify the tagged commit ---
# Build and test every project before anything is published.
npx nx run-many -t build test

# --- CI job 3: publish only ---
# Registry credentials (e.g. an npm token in .npmrc) are
# injected into this job only. If it fails, rerun this step
# alone. Versions and tags from job 1 are not bumped again.
npx nx release publish

# Individual phases can also be run on their own, for example
# to preview just the version step:
npx nx release version --dry-run

Previews a full nx release, then splits versioning and changelog, build and test, and publishing into separate CI steps so only the publish step needs registry credentials and can be retried without re-bumping versions.

Configuring releasable projects and release groups in nx.json

Nx Release reads its configuration from the release property in nx.json. The first decision is scope: which projects Nx treats as releasable. Without explicit scoping, the command can consider more projects than you intend, including applications or internal tooling that should never get a version number or a changelog entry.

Selecting releasable projects

The release.projects option takes a list of project matchers: exact project names, glob patterns such as packages/*, and tag selectors such as tag:publishable. You can exclude entries with a ! prefix. Tags are usually the most maintainable choice in a growing repo. When a new SDK package is tagged publishable in its project configuration, it joins the release automatically, and nobody has to update a central list that drifts out of date.

Defining release groups

Use release.groups when sets of projects need different release behavior. Each group is a named entry with its own projects matcher and a projectsRelationship setting, which section 3 covers. For example, you might put client SDKs in one group that ships together and shared configuration packages in another group that versions separately. Groups also let you apply different changelog or versioning settings to each set without splitting the repository. In the versions this guide was checked against, a project can belong to only one group, and top-level projects and groups are not meant to be combined.

These option names, their defaults, and the validation rules have changed between Nx major versions. Check them against the documentation for the Nx version in your lockfile before you copy any configuration.

// nx.json (Nx parses this file as JSON with comments)
{
  "release": {
    "groups": {
      // Client SDKs ship together under one shared version
      "client-sdks": {
        "projects": ["tag:sdk", "!sdk-internal-testing"],
        "projectsRelationship": "fixed"
      },
      // Shared config packages each get their own version
      "shared-config": {
        "projects": ["packages/config-*"],
        "projectsRelationship": "independent",
        "changelog": {
          // One CHANGELOG.md per package instead of one per group
          "projectChangelogs": true
        }
      }
    }
  }
}

An nx.json release config with two groups: tagged SDKs versioned together, and config packages versioned independently. Verify option names against your Nx version.

Fixed vs independent versioning: choosing a strategy

Nx Release controls this choice with the projectsRelationship setting, either fixed or independent. You can set it for the whole workspace or per release group. The setting decides more than version numbers. It also determines how many git tags you create, where changelog entries go, and what a consumer can infer from a version number.

With fixed versioning, every project in the group shares one version. A change to any project bumps all of them. Consumers get a simple rule: packages at the same version work together. The cost is noise. A package with no code changes still ships 3.4.0 → 3.5.0, so downstream teams read changelogs and run upgrades for nothing. If someone has pinned one package, they also cannot easily tell whether a bump actually affects them.

With independent versioning, each project moves according to its own changes. A version number then tells you something real about that package. The cost is volume. You get one tag per released project, typically including the project name, and one changelog per project. Consumers now have to work out which combinations of package versions are compatible.

Aspect Fixed Independent
Version numbers One shared version One per project
Git tags per release One One per released project
Changelogs Usually one, workspace-level Usually one per project
Unchanged packages Bumped anyway Left alone
Compatibility question "Same version works together" Consumer must track the version matrix

Choose fixed when packages are designed to be installed together, for example a framework and its plugins or an SDK split into modules. Choose independent when packages are consumed separately, have different owners, or change at very different rates, such as shared utility libraries used by unrelated services. If your workspace contains both kinds, put each kind in its own release group and give each group its own strategy.

How conventional commits and the project graph determine version bumps

When conventional commits are enabled for versioning, Nx reads the commit history since the last release tag. It keeps only the commits that touched files belonging to each project, then picks the highest bump those commits imply. This is why commit messages become release inputs and stop being documentation.

Commit message Bump Reason
fix: handle empty payload patch Corrects behaviour without changing the contract
feat: add retry option minor Adds capability in a backward-compatible way
feat!: drop v1 endpoint or a BREAKING CHANGE: footer major Consumers must change their code
chore:, docs:, refactor: none by default No user-facing change is declared
updated stuff none The message does not parse as a conventional commit

The last row causes most surprises. A message that does not parse does not fail the release. It is ignored, so a real fix can ship without a version bump and never appear in the changelog. Squash merges make this worse because the pull request title usually becomes the only commit message on the main branch. A title like "Address review comments" discards every well-formed commit on the branch. Lint commit messages locally with a commit-message linter, and also lint pull request titles in CI, since the title is what survives the squash.

Propagation through the project graph

Nx uses the project graph to find the dependents of a bumped project. It updates the dependency ranges in those dependents and can also bump the dependents themselves, depending on your configuration. This keeps published packages consistent. The cost is that a one-line fix: in a low-level utility library can lead to new releases of every service that depends on it, which adds publishing noise and upgrade work downstream. Before you merge changes to shared libraries, check the graph to see how far a bump will travel.

# Enable conventional-commit versioning in nx.json.
# Nx will derive each project's bump from the commits since its last release tag.
cat > nx.json <<'EOF'
{
  "release": {
    "projects": ["packages/*"],
    "version": {
      "conventionalCommits": true
    },
    "changelog": {
      "projectChangelogs": true
    }
  }
}
EOF

# A well-formed fix in a low-level library: implies a patch bump for that project.
git commit -m "fix(utils): handle empty payload"

# A squash-merge title that does not parse: Nx ignores it, so no bump and no changelog entry.
git commit --allow-empty -m "Address review comments"

# Preview the result without writing files, tagging or publishing.
# Check which projects get bumped, including dependents of 'utils' picked up through the project graph.
npx nx release version --dry-run

# Preview the full flow (version, changelog, publish) before running it for real.
npx nx release --dry-run

A minimal nx.json that enables conventional-commit versioning, plus dry-run commands to see which projects a parsed fix bumps, how an unparseable squash title is ignored, and how dependents are affected before anything is published.

Generating changelogs and previewing releases with dry-run

Nx Release can write changelogs at two levels, and they serve different readers. A workspace changelog, controlled by release.changelog.workspaceChangelog in nx.json, produces one file at the repository root that covers everything shipped in a release. It works well with fixed versioning, where the whole workspace moves together. Per-project changelogs, controlled by release.changelog.projectChangelogs, write a CHANGELOG.md inside each released project. That suits independent versioning, because consumers of one package don't have to read past entries for packages they never install. You can enable both, but most teams pick the one that matches their versioning strategy.

Either level can also set createRelease, which publishes the same notes as a release entry on your git hosting provider. This makes the notes visible where people browse tags. It also needs an API token in the environment, so plan for that in CI.

The generated text is built from commit messages, so it is only as good as those messages. A subject like "fix stuff" will appear in the changelog exactly as written. Review the output before each release and edit it where needed. This matters most for breaking changes, which usually need migration guidance that a one-line commit subject can't hold.

Run any release command with --dry-run (or -d) before doing it for real. Nx computes the new versions and prints diffs of the manifest and changelog files it would change. It also lists the git commit, tag and push operations it would perform. Nothing is written to disk or sent to a remote. That lets you catch an unexpected major bump or a malformed entry while it is still free to fix.

# Assumes nx.json enables per-project changelogs with GitHub releases, e.g.:
#   "release": {
#     "projectsRelationship": "independent",
#     "changelog": {
#       "projectChangelogs": { "createRelease": "github" }
#     }
#   }

# 1. Preview the full release: version bumps, manifest and CHANGELOG.md diffs,
#    plus the git commit, tag and push it would perform. Nothing is written.
npx nx release --dry-run

# 2. Preview only the versioning step to check for an unexpected major bump.
npx nx release version --dry-run

# 3. Show extra detail when the dry-run output looks off.
npx nx release --dry-run --verbose

# 4. After reviewing (and fixing commit messages or notes as needed),
#    run the real release. createRelease needs a hosting API token in the
#    environment; in CI, inject it from your secret store instead.
export GITHUB_TOKEN="ghp_placeholder_not_a_real_token"
npx nx release --skip-publish

# 5. Publish packages as a separate, deliberate step.
npx nx release publish

Running Nx Release with --dry-run first to preview version bumps, changelog diffs and git operations, then performing the real release with per-project changelogs and GitHub releases enabled.

Publishing and a CI workflow for safe releases

Publishing does not build anything. The publish step packs whatever already sits in each project's output directory, so a missing or stale dist folder can produce a broken package. Make the build an explicit precondition. Either run your build targets in the same job before nx release publish, or set a preVersionCommand in the release config so artifacts exist before versioning starts. Building first also means a compile failure stops the release before any tag is created.

Keep versioning and publishing as separate steps. Run nx release --skip-publish in one job and nx release publish in another. Each job then needs different privileges. The versioning job needs permission to push commits and tags to the main branch. Only the publishing job needs a registry token. Scope that token to the packages it publishes, store it as a secret available only to that job, and never expose it to pull-request builds. A token that can both push to main and publish to the registry is a single credential an attacker could use to ship arbitrary code under your name.

Trigger releases only on the main branch, and use a CI concurrency group so that two merges cannot race to compute the same version.

Partial failures usually look the same way: the tag was pushed, but publishing failed. Do not re-run versioning, because that bumps the version again. Re-run only the publish job against the existing tag. Registries such as npm reject a version that has already been published, so confirm how your publish executor handles packages that already exist on the registry before you rely on re-runs being idempotent. If it does not skip them, publish the missing packages manually from the tagged commit.

When to avoid nx release, alternatives, and impact on engineering time

Nx release works best when its inputs are reliable. It fits poorly in three situations:

  • Weak commit discipline. Version bumps and changelog entries come from commit messages. If teams squash-merge with free-form titles or skip the type prefix, releases get wrong bumps and changelogs fill with noise. You either enforce the convention with commit linting and PR title checks, or you choose a different source of release intent.
  • Non-JavaScript ecosystems with limited plugin support. Versioning and publishing need to understand each language's manifest and registry. Where that support is thin, you write and maintain the glue yourself.
  • Curated release notes. Generated changelogs list what changed. Customer-facing notes need to explain why it matters, and that still requires an editor.

Alternatives

Approach Source of release intent Good fit when Main cost
Manual versioning A person edits versions and notes Few packages, infrequent releases Human error, slow releases
Changeset-style intent files A small file committed with each PR Commit history is messy, but PRs are reviewed Contributors must remember to add the file
Custom scripts Your own rules Unusual registries or languages You own all maintenance

Engineering time

Illustrative estimate (assumed numbers, not a benchmark). These inputs are assumptions. Replace them with your own.

  • Setup: 16 hours
  • Maintenance: 2 hours per month
  • Manual release effort: 1.5 hours per release
  • Release frequency: 8 releases per month

Use this formula:

Monthly saving = (releases × manual hours) − maintenance

With the assumed inputs: (8 × 1.5) − 2 = 10 hours per month.

Break-even = setup ÷ monthly saving

With the assumed inputs: 16 ÷ 10 = 1.6 months.

If you release once a month, the saving drops to (1 × 1.5) − 2 = −0.5 hours. Automation never pays back on time alone in that case. Its remaining benefit is consistency between releases.

How OneEnv can help

Disclosure: OneEnv is the product behind this blog.

OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.

Applied to nx release, this is where it helps:

Saves time

  • Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
  • One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
  • Reviewers see the exact diff and the list of affected services up front, so approval is a decision rather than an investigation.

Saves cost

  • Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
  • Less duplicated configuration to maintain: one shared definition inherited by many services.
  • Per-service approve/reject means a bad shared value can be refused where it would break something, before it is live.
  • OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.

These are qualitative benefits, not measured results — how much you save depends on how many services share the value.

Want to try it? Start at oneenv.io.