jscutlery/semver turns conventional commits into Nx releases
Nx plugin to automate semantic versioning and CHANGELOG generation.
At a glance
- What is it?
- The @jscutlery/semver plugin versions and generates changelogs for an Nx workspace, in either independent or synced mode, by reading conventional commits and the last git tag. Its options are the interesting part, with a dry run, a GPG-signed tag option, a tag prefix and a way to publish through post-targets.
- Who is it for?
- Adopt @jscutlery/semver if you publish several packages from one Nx workspace and want versioning driven by conventional commits rather than by hand, since the executor handles the tag, the version bump, the changelog and the publish hook in one run.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two release modes, and the choice is a policy decision
@jscutlery/semver is an Nx plugin for versioning using SemVer and for CHANGELOG generation powered by Conventional Commits.
Installation is two commands:
npm install -D @jscutlery/semver
nx g @jscutlery/semver:installThe generator sets the workspace up, and the package can then manage an Nx workspace in one of two modes, Synced or Independent.
Independent mode is the default. It allows multiple projects to be versioned independently, so you release only what you want and consumers do not get updates they do not need. The documentation frames this as allowing small, rapid and incremental adoption of your packages.
Synced mode allows multiple projects to be versioned in a synced or locked mode, automatically tying all package versions together. The stated use is when you are working with only one product. And the documentation names the cost in one sentence: a major change in any project will result in all projects having a new major version.
That is the whole design tension, stated plainly. Independent versioning is right for a library portfolio where consumers pin versions and a surprise major bump on one package is disruptive. Synced versioning is right when the packages are one product split across repositories, where a mismatch between versions would be a bug in itself.
The two modes are also not mutually exclusive across time, since `--syncVersions` is a boolean option defaulting to false, so the mode can be decided per release rather than fixed at setup.
The version executor does eight things in order
The documentation enumerates exactly what the executor does when it runs, and the order is the interesting part.
First, it optionally verifies npm registry authentication, with `--verifyNpmAuth`. Second, it retrieves the current version by looking at the last `git tag`. Third, it bumps the `package.json` version based on the commits. Fourth, it generates the CHANGELOG based on the commits, using conventional-changelog-conventionalcommits under the hood.
Fifth, it creates a new commit including the `package.json` file and the updated CHANGELOG. Sixth, it creates a new tag with the new version number. Seventh, it pushes the version to the remote repository. Eighth, it runs a post-targets hook to publish the version on NPM, GitHub or GitLab.
Two things in that list deserve attention. The current version comes from the last git tag rather than from `package.json`, which means the tag is the source of truth and the file is an output. And the changelog is generated from commits, not from a hand-written file, which is what makes the conventional commits convention a hard requirement rather than a style preference.
The commands themselves are Nx targets, so they compose with the rest of the workspace tooling. In independent mode you release a single project with `nx run my-project:version [...options]`, and you can use the affected command to only version changed packages with `nx affected --target version [...options]`. In synced mode the target is on the workspace itself, `nx run workspace:version [...options]`.
The affected-based flow is the one that scales, because it means a release touches only what changed.
The option list is where the real behaviour lives
The options table is long, and a few entries change the semantics of a release rather than just its cosmetics.
`--dryRun` runs in dry mode, and `--noVerify` skips the git hooks. Those two together are the rehearsal: run the whole thing without pushing, without committing and without hooks, and see what would have happened.
`--verifyNpmAuth` verifies npm registry authentication before versioning, which is worth turning on in a release pipeline where the last step is a publish and you would rather find out about a bad token before the commit and tag exist.
`--push` pushes the release to the remote repository, and it defaults to false. That default is the right one: the release is local until you say otherwise, so a failed run leaves a local commit and tag you can inspect or reset. `--enforceAtomicPush` enforces an atomic push without retrying a non atomic one in case of failure, which prevents the half-pushed state that a network drop mid-push can otherwise leave behind.
`--origin` selects the git remote, defaulting to 'origin', and `--baseBranch` selects the base branch, defaulting to 'main'. `--syncVersions` locks or syncs versions between projects, which is the synced mode behaviour as a per-run flag.
Then there are the output options. `--changelogHeader` sets a custom Markdown header for changelogs. `--releaseAs` specifies the level of change, `--preid` the prerelease identifier such as alpha or beta, and `--tagPrefix` the tag prefix. `--skipRootChangelog` and `--skipProjectChangelog` suppress the root or the project changelog respectively, which is what a monorepo needs when the root changelog would otherwise duplicate every project's entries.
`--postTargets` takes a list of targets to execute post-release, and `--tagSign` makes a GPG-signed git tag using `git --sign`.
Versioning from commits means the convention is the contract
The dependency on conventional commits is structural rather than cosmetic, and it is worth being explicit about what that implies for a team.
The version bump is derived from the commits, and the changelog is generated from the same commits. There is no separate configuration describing what changed, and no hand-maintained changelog to fall back on. If the commit messages do not follow the convention, the version bump is wrong and the changelog is wrong in the same way, and nothing in the tool will tell you that.
So the adoption requirement is that commits are already conventional. The repository this plugin comes from shows the practice, since it has a commitlint.config.js and a .husky/ directory with husky installed through a postinstall script, meaning commit message checks run as a git hook. The devDependencies include both @commitlint/cli and @commitlint/config-conventional.
The dependency on conventional-changelog-conventionalcommits is what produces the changelog, and it means the changelog format follows the conventional commits specification rather than a house style. The `--changelogHeader` option exists for a custom Markdown header on top of that.
One consequence worth thinking about before you enable automatic publishing. Because the version decision comes from commit text, a badly worded commit message becomes a wrong published version, and the changelog entry a user reads will say whatever the commit said. That is a reason to keep the commit convention enforced in the hook rather than trusted, and it is why the plugin has a `--dryRun` mode at all.
The plugin is one package inside a broader Nx release setup
The repository is not just this plugin. It is the tooling for an Nx workspace, with the plugin living in packages/.
The root package.json shows the shape. The scripts are Nx targets: start runs nx serve, build runs nx build, test runs jest in watch mode, and lint runs workspace-lint and then lint. There is a full set of affected scripts, one per target, including affected:apps, affected:libs, affected:build, affected:e2e, affected:test, affected:lint, affected:dep-graph and the umbrella affected. Format has write and check variants, update runs nx migrate latest, and there is a dep-graph script and a help script.
Two of those scripts are about contributors rather than releases. contributors:add and contributors:generate run the all-contributors CLI, and the repository has a .all-contributorsrc for it. And the postinstall script runs husky install, which is the mechanism that wires the git hooks into a fresh clone.
The Nx version in use is 23.1.0 across @nx/devkit, @nx/eslint-plugin, @nx/jest, @nx/js, @nx/plugin and @nx/workspace, with nx itself in the devDependencies. The Angular devkit schematics package at 19.2.19 is the other structural piece, since the plugin is generated as a schematic.
Other tooling in the tree includes a .eslintrc.json, a .prettierrc with a .prettierignore, a jest.config.ts with a jest.preset.js, an nx.json, a pnpm-workspace.yaml with a pnpm-lock.yaml, a .nvmrc and a .env, plus codecov.yml and the coverage badge.
The project is MIT licensed, TypeScript, with the last push on 2026-10-19 and releases semver-7.1.0, semver-7.0.0 and semver-6.2.0 all published in July 2026. The 7.0.0 release alongside 6.2.0 suggests the major bump came with the change of the plugin's own version line.
Where this sits against the alternatives
The realistic comparison is with the release tooling most Nx workspaces already have, and with a plain semver tool run by hand.
Nx ships its own version-related machinery, and there are standalone tools in this space as well. What @jscutlery/semver adds is a specific combination: conventional commit driven versioning, per-project independence by default, and a changelog generated in the same run.
Against running a semver CLI by hand, the difference is that the whole chain becomes one command. A manual process is usually: read the commits, decide the bump, edit the file, write the changelog entry, commit, tag, push, publish. Each step is a chance to forget one, and the two that are most often forgotten are the changelog and the tag, which are exactly the two the executor handles together with the version bump.
Against a release tool that requires a configuration file listing which packages to publish, the property-based approach here is that the affected command figures out the scope from the dependency graph. In a monorepo with many packages, the affected command is the part that makes a release affordable, and the plugin wires into it rather than replacing it.
The one thing to weigh is the coupling in synced mode. If you adopt synced versioning and your packages are actually independent, a single major change ripples to every version, which is a publishing event you then have to communicate. The mode exists for the single-product case, and the cost of using it in the wrong case is higher than the cost of the extra configuration independent mode requires.
Editorial conclusion
Adopt @jscutlery/semver if you publish several packages from one Nx workspace and want versioning driven by conventional commits rather than by hand, since the executor handles the tag, the version bump, the changelog and the publish hook in one run. Skip it if your repository has a single product with a hand-run release process, because the synced mode's coupling is a downside rather than a feature there, and if you cannot enforce a commit message convention, since the whole versioning decision comes from the commit text. Verify first that your commit history already follows conventional commits, that your npm auth is set up if you want the verify step, and that you understand the default of not pushing, since the release is local until you pass the push option.
Frequently asked questions
How do I install @jscutlery/semver in an Nx workspace?
Run `npm install -D @jscutlery/semver` and then `nx g @jscutlery/semver:install` to generate the setup. The plugin then lets you release a single project with `nx run my-project:version`, use `nx affected --target version` to version only changed packages, or in synced mode run `nx run workspace:version`.
What is the difference between independent and synced versioning?
Independent mode, the default, versions each project separately so consumers only get updates they need. Synced mode ties all package versions together, which is useful when you have one product, but a major change in any project then results in all projects having a new major version.
Does the plugin push to the remote repository?
Not by default. The `--push` option defaults to false, so the release stays local until you ask for it, and `--enforceAtomicPush` can be used to make the push atomic without retrying a non atomic one after a failure.
Can I sign the release tag and publish to a package registry?
Yes. The `--tagSign` option makes a GPG-signed git tag using `git --sign`, and `--postTargets` takes a list of targets to execute after the release, which is how publishing to NPM, GitHub or GitLab is triggered. The `--verifyNpmAuth` option verifies npm registry authentication before versioning.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/jscutlery-semver)Was this useful?