Fixing semantic-release Not Publishing a Release
semantic-release decides whether to publish by reading commit messages since the last release tag. When it concludes "no release", the job still succeeds — so the first sign of a problem is often a merged fix that never reaches npm. The causes are specific and diagnosable: the branch is not configured as a release branch, the run is a dry run or a pull request build, the clone is too shallow to see tags, the commits do not match the configured convention, or previous tags are missing or wrong. This guide reads the logs that identify each cause and gives the fix for each.
Exact symptoms and log messages
The most common outcome is a clean exit with a "no release" message:
[semantic-release] › ℹ Found git tag v2.4.1 associated with version 2.4.1 on branch main
[semantic-release] › ℹ Found 3 commits since last release
[semantic-release] [@semantic-release/commit-analyzer] › ℹ Analyzing commit: update deps
[semantic-release] [@semantic-release/commit-analyzer] › ℹ The commit should not trigger a release
[semantic-release] [@semantic-release/commit-analyzer] › ℹ Analyzing commit: fixed the date parsing bug
[semantic-release] [@semantic-release/commit-analyzer] › ℹ The commit should not trigger a release
[semantic-release] › ℹ There are no relevant changes, so no new version is released.
Other runs stop earlier:
[semantic-release] › ℹ This test run was triggered on the branch fix/date-parsing, while semantic-release is configured to only publish from main, therefore a new version won't be published.
[semantic-release] › ℹ This run was triggered by a pull request and therefore a new version won't be published.
[semantic-release] › ⚠ Run automated release from branch main on repository https://github.com/acme/lib in dry-run mode
And shallow clones produce misleading version calculations:
[semantic-release] › ℹ No git tag version found on branch main
[semantic-release] › ℹ No previous release found, retrieving all commits
[semantic-release] › ✘ EINVALIDNEXTVERSION The release `1.0.0` on branch `main` cannot be published as it is out of range.
Root cause analysis
semantic-release runs a fixed sequence: verify conditions (branch, CI, pull request, credentials), find the last release from git tags, collect commits since that tag, ask the commit analyzer what bump they imply, and only then generate notes and publish. A "no release" can come from any step. The conventions it relies on are covered in Configuring Conventional Commits and semantic-release.
Diagnosing and fixing each cause
Branch configuration
semantic-release publishes only from branches listed in branches (default: main, master, next, next-major, maintenance branches like 1.x, and prerelease branches beta and alpha). If your default branch has another name, or you release from develop, configure it:
{
"branches": [
"+([0-9])?(.{+([0-9]),x}).x",
"main",
{ "name": "next", "channel": "next", "prerelease": false },
{ "name": "beta", "prerelease": true }
]
}
Pull requests and dry runs
semantic-release detects pull request builds through CI environment variables and refuses to publish from them, by design. Make sure the release job runs on push to the release branch, not on pull_request. Also check that no --dry-run flag or "dryRun": true setting was left in configuration after testing.
Shallow clones and missing tags
semantic-release needs the full commit history and all tags to find the last release. Most CI checkouts are shallow by default:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history and tags
persist-credentials: false
If tags were pushed but never created as git tags (for example, releases published manually from a laptop), semantic-release cannot see the last version. Create the missing tag on the commit that was released (git tag v2.4.1 <sha> && git push origin v2.4.1) so the next run starts from the right place.
Commit messages that do not trigger releases
With the default Angular-style preset, only feat: (minor), fix: and perf: (patch) and commits with a BREAKING CHANGE: footer or ! (major) trigger releases. chore:, docs:, refactor:, test: and free-form messages such as "fixed the date parsing bug" do not. Two fixes:
- Enforce the convention on commit messages or pull request titles with commitlint, and use squash merges so the pull request title becomes the commit message.
- Adjust
releaseRulesif your team wants other types to release:
{
"plugins": [
["@semantic-release/commit-analyzer", {
"preset": "conventionalcommits",
"releaseRules": [
{ "type": "refactor", "release": "patch" },
{ "type": "deps", "release": "patch" }
]
}],
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"@semantic-release/github"
]
}
To publish the pending fix immediately, push a new commit with a correct message — for example, an empty commit git commit --allow-empty -m "fix: publish date parsing fix" — rather than rewriting history on the release branch.
Authentication and plugin failures that look like skips
Not every missing release is a "no release" decision. Some runs fail during verification or publishing, and in CI setups that mark the job as allowed to fail, or that swallow the exit code in a wrapper script, those failures look like skipped releases.
Missing npm credentials produce ENONPMTOKEN No npm token specified or, with an invalid token, EINVALIDNPMTOKEN. With npm trusted publishing, the @semantic-release/npm plugin needs a version that supports OIDC and the workflow needs id-token: write; without them the plugin falls back to looking for a token and fails.
Missing GitHub credentials produce ENOGHTOKEN from @semantic-release/github, which also needs permission to create releases and comment on issues (contents: write, issues: write, pull-requests: write in GitHub Actions).
Tag push failures — branch protection that rejects the release tag or the changelog commit pushed by @semantic-release/git — produce EGITNOPERMISSION. Either allow the release identity to push, or drop the git plugin and keep changelogs in GitHub releases only, which avoids pushing commits back to a protected branch.
Version already published produces EPUBLISHCONFLICT or npm's cannot publish over the previously published versions, typically after a tag was deleted locally but the version exists on the registry. Recreate the tag at the released commit so semantic-release's history matches the registry.
Check the job's exit code and the last error line before assuming the commit analyzer is to blame.
Maintenance and prerelease branches
Releases from branches other than main follow extra rules that can also produce "no release". Maintenance branches such as 2.x may only publish versions within their range; a feat: commit on 2.x that would produce 2.5.0 when main has already released 2.5.0 fails with EINVALIDNEXTVERSION, because the version is out of the branch's allowed range. Prerelease branches (beta, alpha) publish only when the branch is configured with prerelease: true, and they publish to their own dist-tag. When a release from one of these branches is missing, compare the computed next version in a dry run with the branch's configured range. Release channels are covered in Release Channels and Dist-Tags.
Verifying before the real run
Run semantic-release in dry-run mode locally or in CI to see what it would do:
GITHUB_TOKEN=... NPM_TOKEN=... npx semantic-release --dry-run --no-ci --branches main
The output lists the last release, the commits analysed, the release type and the next version, without publishing or tagging. Add --debug for plugin-level detail.
Worked example: fixes that sat unreleased for two weeks
A library's maintainers noticed that a bug fix merged two weeks earlier had not been published. The release job was green every time. The logs showed "The commit should not trigger a release" for every commit: the team had switched from squash merges to merge commits, so commit subjects became "Merge pull request #212 from acme/fix-dates" instead of the pull request titles, which followed the convention. They switched back to squash merging, added a CI check that validates pull request titles with commitlint, and pushed an empty fix: commit to publish the pending changes immediately.
Prevention and guardrails
- Validate pull request titles (or commit messages) against the convention in CI.
- Use squash merges so the validated title is what semantic-release reads.
- Always check out with full history in the release job.
- Alert on long gaps: a scheduled job that warns when
mainhasfix:orfeat:commits newer than the latest tag catches silent skips early.
Frequently Asked Questions
Why does semantic-release exit successfully when it does not publish? Because "no relevant changes" is a valid outcome. Treat the log line, not the exit code, as the signal — or add the scheduled gap check above.
Can I force a release without a qualifying commit? Not through a flag; semantic-release derives versions from commits. Push a commit with the appropriate type, even an empty one, so the history records why the release happened.
Does it work in monorepos? Only with community plugins that analyse commits per package. For monorepos, Changesets is usually a better fit, as discussed in Choosing Fixed vs Independent Versioning in a Monorepo.
Related
- Semantic Versioning and Release Automation covers release automation options.
- Configuring Conventional Commits and semantic-release sets up the conventions semantic-release reads.
- Fixing Changesets Not Detecting a Version Bump is the Changesets equivalent of this problem.
- Publishing from CI with npm Trusted Publishing replaces the npm token semantic-release needs.