Semantic Release — The Version Bump That Skipped From v1.2.3 to v2.0.0
A 'fix:' commit that contained a breaking change bumped the major version.
20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.
- ✓Node.js >= 18, npm >= 9, Git, GitHub account (or other Git hosting), CI/CD platform (e.g., GitHub Actions), familiarity with Conventional Commits, basic understanding of semantic versioning (semver).
Set up semantic-release in CI: npm install -g semantic-release. Configure with .releaserc file. Push conventional commits. semantic-release: 1) analyzes commits since last release, 2) determines version bump, 3) generates changelog, 4) creates git tag, 5) publishes to npm/PyPI/etc.
Imagine a robot that reads every commit message since the last release. If it finds 'feat: add payment module', it knows the version needs a minor bump (v1.2.3 → v1.3.0). If it finds 'fix: correct tax calculation', it's a patch (v1.2.3 → v1.2.4). If it finds 'BREAKING CHANGE:', it's a major version (v1.2.3 → v2.0.0). The robot creates a changelog, tags the release, and publishes — all without human intervention.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Semantic versioning (semver) is the convention: MAJOR.MINOR.PATCH. semantic-release automates the versioning decision based on commit messages. This eliminates human error in version bumps, ensures every merge to main can produce a release, and generates changelogs automatically. The key requirement: your team must use conventional commits. This guide covers setup, the incident where a mislabeled commit would have caused a silent major version bump, and how to configure semantic-release for your project.
Why Manual Versioning Fails at Scale
Manual version bumps and changelog entries are a common source of release errors. In a monorepo with 50+ microservices, a single missed version increment can cause cascading deployment failures. Semantic Release automates this by parsing commit messages following the Conventional Commits specification to determine the next version number (major, minor, patch) and generate changelogs. This eliminates human error and ensures every release is reproducible. The tool integrates directly into CI/CD pipelines, reading commits since the last release and using plugins to publish packages and update changelogs. Teams that adopt Semantic Release report a 90% reduction in release-related incidents. The key is strict adherence to commit conventions—any deviation breaks the automation.
Setting Up Conventional Commits
Semantic Release relies on the Conventional Commits specification. Each commit message must follow the format: <type>[optional scope]: <description>. Common types are fix (patch), feat (minor), and BREAKING CHANGE (major). Scopes help organize changes in monorepos. To enforce this, use commitlint with a husky pre-commit hook. Configure commitlint to validate messages against @commitlint/config-conventional. This prevents non-compliant commits from entering the repository. Additionally, use a tool like commitizen to provide interactive prompts for developers. In CI, run commitlint on pull requests to block non-conforming messages. Without enforcement, Semantic Release will fail to parse commits and skip releases, leading to stale packages.
Configuring Semantic Release
Semantic Release is configured via a release.config.js (or .releaserc) file. The core configuration specifies branches, plugins, and dry-run options. For a standard setup, define the branches that trigger releases (e.g., main, next). Plugins handle analysis, commit parsing, changelog generation, and publishing. The default @semantic-release/commit-analyzer and @semantic-release/release-notes-generator work for most projects. Add @semantic-release/changelog to generate a CHANGELOG.md file, and @semantic-release/git to commit it back to the repository. For npm packages, include @semantic-release/npm. Always test with --dry-run before running in production. The configuration must be committed to the repository root.
CI/CD Integration and Authentication
Semantic Release runs in CI after tests pass. For GitHub Actions, use the semantic-release action or run via npx. Set environment variables for authentication: GITHUB_TOKEN for GitHub releases, NPM_TOKEN for npm publishing. Never hardcode tokens—use repository secrets. The CI job must have write permissions to push tags and commits. For GitHub Actions, set permissions: contents: write. The workflow should trigger on push to main branch. Use actions/checkout with fetch-depth: 0 to get full git history. Without full history, Semantic Release cannot determine the previous version. Also, set persist-credentials: false to avoid token conflicts.
fetch-depth: 0, causing Semantic Release to fail with 'No previous version found'. The CI pipeline ran for 30 minutes before failing, delaying the release by a day.Handling Pre-releases and Multiple Branches
Semantic Release supports pre-release branches (e.g., next, beta). Configure them with prerelease: true and a channel name. Commits on these branches produce pre-release versions like 1.0.0-next.1. This allows testing before stable release. Use separate npm dist-tags (e.g., next) to avoid polluting the latest tag. In CI, trigger the workflow on push to those branches. For monorepos, use @semantic-release/monorepo plugin or run Semantic Release per package. Pre-releases require careful coordination: merge next into main only after validation. Without proper branch management, you may accidentally release a breaking change to production.
prerelease: 'beta' to get versions like 1.0.0-beta.1. The channel name becomes the dist-tag. Consumers install with npm install package@next.next branch would have caught the issue.prerelease option to test changes before stable releases.Custom Plugins and Extending Behavior
Semantic Release is plugin-based. You can write custom plugins to modify version calculation, changelog generation, or publishing. A plugin is an object with verifyConditions, analyzeCommits, generateNotes, prepare, publish, addChannel, and success steps. For example, to add a custom changelog section, implement generateNotes. Use the context object to access commits, logger, and options. Custom plugins are useful for integrating with internal tools (e.g., Jira, Slack). However, avoid over-engineering—most needs are covered by existing plugins. If you must write one, test thoroughly with dry runs. A buggy plugin can break the entire release pipeline.
success step, causing the release to be marked as failed even though npm published successfully. The team had to manually verify.Changelog Generation and Maintenance
Semantic Release generates changelogs from commit messages. The @semantic-release/release-notes-generator plugin creates a markdown file with sections for Features, Bug Fixes, and Breaking Changes. To customize, use @semantic-release/changelog which writes to a file. You can also use @semantic-release/exec to run a custom script. Changelogs should be committed to the repository and included in releases. Avoid manual edits—they will be overwritten. For monorepos, consider generating per-package changelogs. The key is consistency: every release has an accurate changelog. Without automation, changelogs become stale or inaccurate.
note: This is a critical fix. which get included in the notes.Troubleshooting Common Failures
Semantic Release failures often stem from misconfiguration. Common issues: missing fetch-depth: 0 (no git history), incorrect token permissions, or invalid commit messages. Check CI logs for error messages. Use --dry-run locally to debug. If no release is triggered, verify branch configuration and commit types. For npm publish failures, ensure NPM_TOKEN is valid and has publish permissions. For GitHub releases, check GITHUB_TOKEN has contents: write. Another issue is plugin version mismatches—always use compatible versions. When all else fails, run with DEBUG=semantic-release:* to see verbose logs. Keep a troubleshooting checklist in your team's runbook.
git tag -d v1.0.0 && git push origin :refs/tags/v1.0.0BREAKING CHANGE in the body but not in the footer. Semantic Release interpreted it as a minor bump, causing a breaking change to be released as minor. Now they enforce footer format with commitlint.--dry-run and DEBUG=semantic-release:*; check git history, tokens, and commit messages.Monorepo Strategies
Monorepos present challenges: multiple packages with independent versions. Use @semantic-release/monorepo plugin or run Semantic Release per package. The monorepo plugin analyzes commits scoped to each package. Alternatively, use tools like Lerna or Nx with Semantic Release. For npm workspaces, configure each package's release.config.js. Ensure CI runs Semantic Release for each changed package. Use --extends to share common config. The main risk is cross-package dependencies—if package A depends on B, release B first. Use @semantic-release/exec to run build scripts in order. Without proper orchestration, you may release incompatible versions.
tagFormat to prefix tags per package (e.g., api-v1.0.0). This prevents tag conflicts and allows per-package releases.Production Rollback and Recovery
Even with automation, rollbacks happen. Semantic Release tags each release with a git tag. To rollback, revert the commit that introduced the breaking change and push a new fix commit. The next release will bump the patch version. Do not delete tags—this breaks the release history. If you must revert a published npm package, use npm unpublish only within 72 hours, or publish a new version. For GitHub releases, you can mark a release as 'Pre-release' or delete it. However, the best practice is to fix forward. Maintain a rollback runbook that includes reverting the commit and triggering a new release. Test rollback procedures in a staging environment.
Security and Access Control
Semantic Release requires tokens with write access to repositories and package registries. Treat these tokens as secrets: use CI secrets, rotate regularly, and limit scopes. For GitHub, use fine-grained tokens with minimal permissions. For npm, use granular access tokens that can only publish to specific packages. Never store tokens in code or config files. Use @semantic-release/npm with npmPublish: true and set NPM_TOKEN in CI. For private registries, configure registry in .npmrc. Also, consider using @semantic-release/exec to run security scans before publish. A compromised token can lead to malicious package releases.
Measuring Success and Adoption
Track metrics to measure the impact of Semantic Release: time from commit to production, number of failed releases, and developer satisfaction. Use CI pipeline duration and success rates. After adoption, teams typically see a 50% reduction in release time and fewer manual errors. Survey developers: do they find the commit convention burdensome? Adjust rules accordingly. Monitor the number of releases per week—automation should increase frequency. Also track the number of rollbacks. If rollbacks increase, review commit quality. Share success stories in team meetings. Continuous improvement: update plugins, refine commit rules, and automate more steps.
The 'Fix' Commit That Would Have Bumped v1.2.3 to v2.0.0
fix: rename processPayment to executePayment. 3. The commit message had no 'BREAKING CHANGE:' footer. 4. semantic-release would parse this as a patch (fix = patch). 5. The breaking API change would ship as v1.2.4. 6. All downstream packages depending on v1.2.3+ would break at next npm install. 7. The code reviewer noticed the breaking change and flagged it.feat!: rename processPayment to executePayment (the ! marks breaking change). 2. semantic-release correctly parsed this as a major version bump. 3. Added commitlint to CI to enforce conventional commit format. 4. Added a CI check: npx semantic-release --dry-run runs on every PR and shows the would-be version bump. 5. Educated the team: commit type determines version bump. Breaking changes MUST use ! or BREAKING CHANGE footer.- Commit type determines version bump: fix → patch, feat → minor, BREAKING → major.
- Use
!orBREAKING CHANGE:footer for breaking changes. - Add a CI check that shows the would-be version bump on every PR.
- Enforce conventional commit format with commitlint.
BREAKING CHANGE detection in CInpx semantic-release --dry-run to preview the version bump and changelog| File | Command / Code | Purpose |
|---|---|---|
| install.sh | npm install -g semantic-release | Why Manual Versioning Fails at Scale |
| commitlint.config.js | module.exports = { | Setting Up Conventional Commits |
| release.config.js | module.exports = { | Configuring Semantic Release |
| .github | name: Release | CI/CD Integration and Authentication |
| custom-plugin.js | module.exports = { | Custom Plugins and Extending Behavior |
| CHANGELOG.md | * add user authentication endpoint (abc1234) | Changelog Generation and Maintenance |
| debug.sh | DEBUG=semantic-release:* npx semantic-release --dry-run | Troubleshooting Common Failures |
| packages | module.exports = { | Monorepo Strategies |
| rollback.sh | git revert | Production Rollback and Recovery |
| npm-token.sh | npm token create --read-only --publish-only --cidr=10.0.0.0/8 | Security and Access Control |
| metrics.js | const releases = []; // from API | Measuring Success and Adoption |
Key takeaways
! or BREAKING CHANGE: footer to mark breaking changesFrequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.
That's Git. Mark it forged?
4 min read · try the examples if you haven't