Skip to content

nx release: ambiguous-scope check fires globally, blocks unrelated release groups #35744

@jmclellan-crexi

Description

@jmclellan-crexi

Current Behavior

nx release --group=<group> walks the full commit range since the last
matching tag and resolves each commit's conventional-commit scope against
the entire project graph (findMatchingProjects over
projectGraph.nodes). If a scope is ambiguous against any projects in
the workspace, Nx throws unconditionally — even when those projects are
not in the release group currently being processed.

In workspaces using slash-style subpath project names (e.g.
@scope/cui plus subpaths @scope/cui/forms, @scope/cui/select, …),
a single commit with a short-form scope such as fix(cui): …
permanently blocks every release group whose tag-range includes it.

Root-cause source pointers:

  • Throw: packages/nx/src/command-line/release/utils/shared.ts, in
    getCommitsRelevantToProjects (compiled: dist/.../shared.js:344).
    The per-commit ambiguity check uses the global project graph, not
    the projects argument representing the active release group.
  • Resolver: packages/nx/src/utils/find-matching-projects.ts, in
    addMatchingProjectsByName. Word-boundary regex
    (?<![@a-zA-Z0-9-])${pattern}(?![@a-zA-Z0-9-]) — slash is not in
    the negative class, so scope cui matches both @scope/cui and
    @scope/cui/forms.

Expected Behavior

The cui-scoped commit is irrelevant to the code-style release group;
the group should release without error. The ambiguity check should not
fire on scopes that only collide with projects outside the active
release group.

Any one of these resolutions would unblock affected workspaces:

  1. Scope the ambiguity check to projects in the active release group.
    A commit whose ambiguous scope only collides with projects outside
    the current group should not block that group.
  2. Honor version.conventionalCommits.useCommitScope: false as a
    bypass.
    Today this flag only affects indirect patch bumps
    (semver.ts); it does nothing to suppress the throw.
  3. Add a release.conventionalCommits.ignoreAmbiguousScopes: true
    opt-out.
  4. Tighten addMatchingProjectsByName so slash-separated subpath
    names aren't matched by their parent's short name (e.g., treat /
    as a name-segment separator rather than a boundary).

GitHub Repo

https://github.com/jmclellan-crexi/nx-release-scope-ambiguity-repro

Steps to Reproduce

git clone https://github.com/jmclellan-crexi/nx-release-scope-ambiguity-repro
cd nx-release-scope-ambiguity-repro
pnpm install
pnpm exec nx release --group=code-style --yes --verbose

The workspace contains three projects:

  • @scope/code-style — its own single-project release group.
  • @scope/cui — meta package, tagged domain:cui.
  • @scope/cui/forms — slash-style subpath, tagged domain:cui.

The cui release group is ["@scope/cui", "tag:domain:cui"], the
code-style group is ["@scope/code-style"]. Both use
version.conventionalCommits: true.

Git history:

  1. chore: initial scaffold (tagged @scope/code-style@1.0.0 and
    @scope/cui@1.0.0).
  2. fix(cui): unrelated change — touches only packages/cui/src/index.ts.

Releasing the code-style group is expected to be a no-op (no relevant
commits) but instead throws.

Nx Report

Node           : 24.3.0
OS             : linux-x64
Native Target  : x86_64-linux
pnpm           : 10.29.2
daemon         : Available

nx             : 22.7.2
@nx/js         : 22.7.2
@nx/workspace  : 22.7.2
@nx/devkit     : 22.7.2
typescript     : 5.9.3

Failure Logs

NX   Running release version for project: @scope/code-style

@scope/code-style 🏷️  Resolved the current version as 1.0.0 from git tag "@scope/code-style@1.0.0", based on releaseTagPattern "{projectName}@{version}"

 NX   Ambiguous scope "cui" in commit "fix(cui): unrelated change". Matches: @scope/cui/forms, @scope/cui

Error: Ambiguous scope "cui" in commit "fix(cui): unrelated change". Matches: @scope/cui/forms, @scope/cui
    at getCommitsRelevantToProjects (node_modules/nx/dist/src/command-line/release/utils/shared.js:344:27)
    at async resolveSemverSpecifierFromConventionalCommits (node_modules/nx/dist/src/command-line/release/utils/resolve-semver-specifier.js:13:29)
    at async deriveSpecifierFromConventionalCommits (node_modules/nx/dist/src/command-line/release/version/derive-specifier-from-conventional-commits.js:28:33)
    at async ReleaseGroupProcessor.determineVersionBumpForProject (node_modules/nx/dist/src/command-line/release/version/release-group-processor.js:385:30)
    at async ReleaseGroupProcessor.bumpFixedVersionGroup (node_modules/nx/dist/src/command-line/release/version/release-group-processor.js:231:88)
    at async ReleaseGroupProcessor.processGroup (node_modules/nx/dist/src/command-line/release/version/release-group-processor.js:210:9)
    at async ReleaseGroupProcessor.processGroups (node_modules/nx/dist/src/command-line/release/version/release-group-processor.js:119:13)
    at async releaseVersion (node_modules/nx/dist/src/command-line/release/version.js:163:13)
    at async release (node_modules/nx/dist/src/command-line/release/release.js:78:74)


In CI this surfaces as `ELIFECYCLE Command failed with exit code 1.`
from pnpm. Locally on Node 24, the rejected promise prints the
stacktrace but the process can still exit 0 — which is itself a
secondary issue (errors thrown inside `releaseVersion` aren't bubbling
to the CLI exit code consistently across Node versions).

Package Manager Version

No response

Operating System

  • macOS
  • Linux
  • Windows
  • Other (Please specify)

Additional Information

Production blast radius

A real-world workspace (~600 projects) using this naming convention has
been release-blocked for ~3 weeks as a result of this bug. The
@scope/code-style group cannot publish a new patch because a single
unrelated fix(cui): … commit lives in the tag range. The cui group
will hit the same trap the next time it's released.

Why no workspace-level workaround is durable

  • Splitting nx release into version --specifier=patch +
    changelog --from=<sha> + publish
    — works once, but the SHA must
    be bumped manually after every new ambiguous-scope commit; not a fix.
  • Moving the git tag forward past the offender — mutates a
    published tag.
  • Rewriting the offending commit message via force-push — branch
    protection bypass, breaks every open PR.
  • Renaming the Nx project name @scope/cuicui to short-circuit
    exact-match resolution in parseStringPattern
    — works for scope
    cui only; the next ambiguous scope (forms, auth, …) needs its
    own rename. Not a structural fix.

Willing to contribute

Happy to open a PR if a maintainer is comfortable with one of the
resolutions above. Rough sketch (option 1, smallest behavior change):

  1. In packages/nx/src/command-line/release/utils/shared.ts,
    getCommitsRelevantToProjects already receives a projects: string[]
    argument representing the active release group. Filter the
    per-pattern match set down to that projects set before counting:
    only throw when that filtered set has > 1 matches.
  2. When the scope is ambiguous against projects outside the active
    group, treat the commit as having no scope for this group's
    processing (fall through to the file-affectedness path that already
    exists when commit.scope is empty).
  3. Add a unit test covering: (a) ambiguous within the group → still
    throws, (b) ambiguous only outside the group → no throw, treated as
    non-scoped, (c) unambiguous → behavior unchanged.

Open to whichever of options 1–4 maintainers prefer; happy to switch
direction based on review.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions