What this usually means
The most common root cause is that semantic-release cannot determine that the current commit should trigger a release. This happens when commit messages don't follow the conventional commits format (e.g., 'fix: ...' or 'feat: ...'), or the CI environment variable like GITHUB_REF doesn't point to the main branch. Another frequent cause is a misconfigured or expired GitHub token that has no write access to the repository. semantic-release often swallows these failures and exits gracefully, which is why engineers don't notice until someone asks why the version didn't bump.
The first ten minutes — establish facts before touching code.
- 1Run `npx semantic-release --dry-run` locally or in CI to preview what would happen. Look for 'This run was not triggered by a push to' messages.
- 2Check the CI logs for the line 'No commits since last release' – that means no relevant commits exist since the last tag.
- 3Verify the GitHub token: `echo $GITHUB_TOKEN | gh auth status` or check if the token has 'repo' scope.
- 4Inspect the commit message of the latest commit: `git log -1 --pretty=%B`. Ensure it starts with 'fix:', 'feat:', 'BREAKING CHANGE:', or a valid scope like 'fix(scope):'.
- 5Confirm the CI branch is the main branch: `git rev-parse --abbrev-ref HEAD` should return 'main' or 'master' unless you configured custom branches.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchCI pipeline logs – search for 'semantic-release' or 'No release'
- searchGitHub repository Settings -> Secrets and variables -> Actions (check GITHUB_TOKEN or custom token)
- searchLocal terminal: run `npx semantic-release --dry-run` after `export GITHUB_TOKEN=your_token`
- searchPackage.json `release` configuration block for `branches`, `plugins`, and `preset`
- search`.releaserc` or `release.config.js` file in repository root
- searchGitHub Releases page to see if any tags exist (tags are created even if release fails)
- searchNPM package page (if publishing) – check if version matches last tag
Practical causes, not theory. These are the things you will actually find.
- warningGitHub token lacking repo scope or expired. Token must have `public_repo` or full `repo` scope.
- warningCommit message does not follow conventional commits: missing 'fix:' or 'feat:' prefix.
- warningCI is running on a pull_request event instead of push to main – semantic-release ignores PRs by default.
- warningBranch name is not in the `branches` configuration (default: ['+([0-9])?(.{+([0-9]),x}).x', 'main', 'master', 'next', 'next-major', {name: 'beta', prerelease: true}, {name: 'alpha', prerelease: true}]).
- warningPrevious release tag was deleted or not created – semantic-release uses git tags to determine last release.
- warning`GITHUB_REF` environment variable points to refs/heads/feature-branch instead of refs/heads/main.
- warningPlugin misconfiguration: missing @semantic-release/npm or @semantic-release/github in plugins list.
Concrete fix directions. Pick the one that matches your root cause.
- buildRegenerate GitHub token with full `repo` scope and update CI secret. Use a personal access token (classic) not a fine-grained token unless you configure repository access.
- buildEnforce conventional commits with a commitlint hook or PR title check in CI. Add `@commitlint/config-conventional` and a CI step to validate.
- buildChange CI trigger: update GitHub Actions workflow to `on: push: branches: [main]` instead of `pull_request`.
- buildAdd the current branch to `branches` in release config: e.g., `"branches": ["main", "next"]`.
- buildIf tags were deleted, push a dummy commit to main to force semantic-release to re-evaluate: `git commit --allow-empty -m "fix: reset release history" && git push`.
- buildSet `GITHUB_TOKEN` environment variable explicitly in CI step: `env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}`. The default token may be insufficient; use a personal token.
A fix you cannot prove is a guess. Close the loop.
- verifiedRun `npx semantic-release --dry-run --no-ci` locally with proper token. It should output 'Create release 1.2.3 on GitHub'.
- verifiedPush a commit with message `fix: test release trigger` to main and watch CI logs for 'Published release 1.2.3'.
- verifiedCheck that a new git tag appears: `git tag --list` should include vX.Y.Z.
- verifiedVisit GitHub Releases page – the new release should appear with release notes.
- verifiedIf publishing to npm, run `npm view <package> version` and compare to expected version.
- verifiedInspect the CI environment: add a step `run: echo $GITHUB_REF` to confirm it's `refs/heads/main`.
Things that make this bug worse or harder to find.
- warningUsing the default GITHUB_TOKEN from GitHub Actions without adding the 'contents: write' permission in the workflow YAML.
- warningAssuming semantic-release errors are always fatal – it often exits 0 even when it does nothing.
- warningDeleting git tags to 'reset' state – this breaks semantic-release's version tracking and may cause version jumps.
- warningForgetting to include `@semantic-release/github` plugin when publishing to GitHub Releases.
- warningRunning semantic-release on every branch without configuring `branches` – it will error on non-release branches.
- warningUsing a fine-grained GitHub token without granting read/write access to contents and pull requests.
The Silent Merge: Three Days of No Releases
Timeline
- 09:00Team merges a fix PR to main. CI passes green. No release created.
- 09:30First engineer runs semantic-release locally (dry-run) – it says 'This run was not triggered by a push to main'. Confused because they are on main.
- 10:00Second engineer checks GITHUB_REF in CI logs – it shows refs/heads/main correctly. Token is set.
- 10:30Someone recalls that the previous release tag was accidentally deleted during a cleanup two days ago.
- 11:00Check git log: no commits since deletion. semantic-release thinks the last release is the very first commit.
- 11:30Push an empty commit with message 'fix: reset release tracking'. CI runs, now semantic-release creates a release from scratch – version jumps from 1.2.3 to 1.0.0.
- 12:00Revert the tag, manually set version to 1.2.4, push tag. Finally back to normal.
I walked in to a Slack thread: 'Why hasn't my fix been released? It's been three days.' The CI logs showed green checkmarks but no new version on npm. My team had been merging PRs to main and every CI run exited cleanly. I ran semantic-release locally with --dry-run and got the puzzling message: 'This run was not triggered by a push to main'. But I was on main. That's when I knew this wasn't a simple token issue.
I checked git tags and saw only one tag from months ago. 'git log --oneline --decorate' showed no tags on recent commits. Someone had run a cleanup script that deleted all tags. semantic-release uses tags to know the last release; without a tag on the latest release commit, it scans the entire history and finds no new conventional commits since the beginning. It then assumes no release is needed.
The fix was to push an empty commit with a conventional message to create a new baseline. After that, semantic-release correctly identified the commit as a fix and created version 1.0.0. We then manually adjusted the version to continue from 1.2.4. The lesson: never delete tags, and always run semantic-release in dry-run mode on CI to catch these silent failures.
Root cause
Git tags were deleted, causing semantic-release to lose track of the last release; it then found no commits since the initial commit and did nothing.
The fix
Pushed an empty commit with message 'fix: reset release tracking' to force a new release. Then manually corrected the version tag to match the previous series.
The lesson
Tags are the source of truth for semantic-release. Deleting them breaks versioning. Always run dry-run on CI and alert if no release is created.
semantic-release determines whether to create a new version by scanning git history from the latest tag (or the first commit if no tag exists). It looks for commits that match the conventional commits spec (Angular convention). If it finds at least one commit with `fix:`, `feat:`, or `BREAKING CHANGE:`, it increments the version accordingly. If no such commits exist between the last tag and HEAD, it prints 'No commits since last release' and exits 0.
The key environment variable is `GITHUB_REF` (or equivalent in other CI). It must be a push event to a branch listed in the `branches` configuration. If the CI runs on a pull_request event, `GITHUB_REF` points to `refs/pull/.../merge`, which is not a release branch, so semantic-release skips. Many engineers miss this because the CI log doesn't show an error.
The default `GITHUB_TOKEN` in GitHub Actions has read-only permissions by default unless you explicitly grant write access in the workflow YAML using `permissions: contents: write`. Even then, that token can only interact with the repository that triggered the workflow. For publishing to npm, you must use an `NPM_TOKEN` secret. Many guides forget to mention that the token needs `repo` scope for private repos and `public_repo` for public repos.
I've debugged cases where the token was expired or had insufficient scopes, but semantic-release didn't fail immediately. It only fails when trying to push the tag or create the release. Check the CI logs for lines like 'Failed to create git tag' or 'Error: Not Found' from the GitHub API. Always test token access with `curl -H 'Authorization: token $GITHUB_TOKEN' https://api.github.com/user`.
By default, semantic-release uses the `@semantic-release/commit-analyzer` and `@semantic-release/release-notes-generator` with the Angular preset. If you have a custom `preset` in your config, ensure it produces valid release types. Some presets (like eslint) only recognize specific keywords. If your commits use 'upgrade' but the preset expects 'chore', no release will happen.
Another common mistake is omitting `@semantic-release/github` from the plugins array. Without it, semantic-release won't create a GitHub release, only a git tag. The npm plugin `@semantic-release/npm` publishes to npm but also requires a token. I list plugins in order: `['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator', '@semantic-release/npm', '@semantic-release/github']`.
Start with a local dry run: `npx semantic-release --dry-run --no-ci`. This simulates a release without actually publishing. It will tell you the next version and what it would do. If it says 'No release', check the commit history: `git log --oneline $(git rev-list --max-parents=0 HEAD)..HEAD`. Count how many conventional commits exist.
To verify the token works, run: `GITHUB_TOKEN=your_token npx semantic-release --dry-run`. If you get authentication errors, the token is bad. Also check the repository branch: `git rev-parse --abbrev-ref HEAD` and ensure it's in the config. You can also add verbose logging by setting `DEBUG=semantic-release:*` environment variable.
Frequently asked questions
semantic-release says 'This run was not triggered by a push to main' but I am pushing to main. Why?
Check the CI trigger configuration. If you have a GitHub Actions workflow that runs on both push and pull_request, the pull_request event sets GITHUB_REF to a merge branch like refs/pull/42/merge, not refs/heads/main. Fix by separating workflows or adding a condition to run semantic-release only on push events: `if: github.event_name == 'push'`.
How do I see what version semantic-release would create without actually publishing?
Run `npx semantic-release --dry-run`. It will output the next version and list the steps it would take without making changes. You can also pass `--no-ci` to skip CI environment checks.
Can I use semantic-release with a monorepo?
Yes, but you need additional plugins like `@semantic-release/monorepo` or use a tool like `lerna`. Configure each package with its own .releaserc, or use the `--extends` option. Be careful with shared tags – you may need to prefix tags by package (e.g., `my-package-v1.0.0`).
Why does semantic-release exit 0 even when it fails to publish to npm?
By default, semantic-release continues even if one plugin fails unless you set `fail: true` in that plugin's options. If npm publish fails (e.g., authentication error), the GitHub release may still be created. Check the logs for plugin errors. To hard-fail on any plugin error, use the `@semantic-release/error` plugin or set `failComment` in the GitHub plugin.