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:
- 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.
- 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.
- Add a
release.conventionalCommits.ignoreAmbiguousScopes: true
opt-out.
- 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:
chore: initial scaffold (tagged @scope/code-style@1.0.0 and
@scope/cui@1.0.0).
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
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/cui → cui 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):
- 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.
- 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).
- 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.
Current Behavior
nx release --group=<group>walks the full commit range since the lastmatching tag and resolves each commit's conventional-commit scope against
the entire project graph (
findMatchingProjectsoverprojectGraph.nodes). If a scope is ambiguous against any projects inthe 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/cuiplus 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:
packages/nx/src/command-line/release/utils/shared.ts, ingetCommitsRelevantToProjects(compiled:dist/.../shared.js:344).The per-commit ambiguity check uses the global project graph, not
the
projectsargument representing the active release group.packages/nx/src/utils/find-matching-projects.ts, inaddMatchingProjectsByName. Word-boundary regex(?<![@a-zA-Z0-9-])${pattern}(?![@a-zA-Z0-9-])— slash is not inthe negative class, so scope
cuimatches both@scope/cuiand@scope/cui/forms.Expected Behavior
The cui-scoped commit is irrelevant to the
code-stylerelease 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:
A commit whose ambiguous scope only collides with projects outside
the current group should not block that group.
version.conventionalCommits.useCommitScope: falseas abypass. Today this flag only affects indirect patch bumps
(
semver.ts); it does nothing to suppress the throw.release.conventionalCommits.ignoreAmbiguousScopes: trueopt-out.
addMatchingProjectsByNameso slash-separated subpathnames 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
The workspace contains three projects:
@scope/code-style— its own single-project release group.@scope/cui— meta package, taggeddomain:cui.@scope/cui/forms— slash-style subpath, taggeddomain:cui.The
cuirelease group is["@scope/cui", "tag:domain:cui"], thecode-stylegroup is["@scope/code-style"]. Both useversion.conventionalCommits: true.Git history:
chore: initial scaffold(tagged@scope/code-style@1.0.0and@scope/cui@1.0.0).fix(cui): unrelated change— touches onlypackages/cui/src/index.ts.Releasing the
code-stylegroup is expected to be a no-op (no relevantcommits) but instead throws.
Nx Report
Failure Logs
Package Manager Version
No response
Operating System
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-stylegroup cannot publish a new patch because a singleunrelated
fix(cui): …commit lives in the tag range. Thecuigroupwill hit the same trap the next time it's released.
Why no workspace-level workaround is durable
nx releaseintoversion --specifier=patch+changelog --from=<sha>+publish— works once, but the SHA mustbe bumped manually after every new ambiguous-scope commit; not a fix.
published tag.
protection bypass, breaks every open PR.
@scope/cui→cuito short-circuitexact-match resolution in
parseStringPattern— works for scopecuionly; the next ambiguous scope (forms,auth, …) needs itsown 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):
packages/nx/src/command-line/release/utils/shared.ts,getCommitsRelevantToProjectsalready receives aprojects: 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
> 1matches.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.scopeis empty).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.