Home DevOps Semantic Release — The Version Bump That Skipped From v1.2.3 to v2.0.0
Advanced 4 min · July 12, 2026
Semantic Release: Automated Versioning and Changelogs

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.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Production
production tested
July 12, 2026
last updated
2,406
articles · all by Naren
Before you start⏱ 20 min
  • 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).
 ● Production Incident
Quick Answer

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.

✦ Definition~90s read
What is Semantic Release?

semantic-release is an automated tool that determines the next version number, generates changelogs, and publishes releases based on conventional commit messages. feat → minor, fix → patch, BREAKING CHANGE → major.

Imagine a robot that reads every commit message since the last release.
Plain-English First

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.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

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.

install.shBASH
1
2
3
npm install -g semantic-release
# Or use npx: npx semantic-release
# Requires Node.js >= 18 and a CI environment
Output
+ semantic-release@24.0.0
added 1 package in 2s
⚠ Commit Convention Enforcement
Without a commit linting step (e.g., commitlint), a single 'fix: typo' commit can trigger a patch release when you intended a feature. Always validate commits before they reach the main branch.
📊 Production Insight
In a production incident at a fintech company, a missing 'BREAKING CHANGE' footer caused a major version bump to be skipped, leading to a silent API contract violation that took down payment processing for 4 hours.
🎯 Key Takeaway
Semantic Release automates versioning and changelogs by parsing structured commit messages, eliminating manual errors.
semantic-release THECODEFORGE.IO Semantic Release System Layers Component hierarchy for automated version management Source Control GitHub Repository | Branch Protection Rules CI/CD Pipeline GitHub Actions | Jenkins | CircleCI Release Orchestrator semantic-release CLI | Plugins | Config File Versioning Engine Commit Analyzer | Release Notes Generator | Version Calculator Publishing Targets npm Registry | GitHub Releases | Changelog THECODEFORGE.IO
thecodeforge.io
Semantic Release

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.

commitlint.config.jsJAVASCRIPT
1
2
3
4
5
6
7
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'scope-enum': [2, 'always', ['api', 'web', 'shared']],
    'subject-case': [2, 'always', 'sentence-case']
  }
};
Output
// Valid: feat(api): add user endpoint
// Invalid: added new feature
Try it live
💡Automate Commit Hooks
Use husky to run commitlint on commit-msg hook. This catches errors before push. Example: npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'
📊 Production Insight
A team skipped commitlint setup and a developer pushed 'fix: minor bug' with a typo. Semantic Release ignored it, and the bug fix never made it to production until the next release, causing a customer-facing outage.
🎯 Key Takeaway
Enforce Conventional Commits with commitlint and husky to ensure Semantic Release can parse every commit.

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.

release.config.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
module.exports = {
  branches: ['main', { name: 'next', prerelease: true }],
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    '@semantic-release/changelog',
    '@semantic-release/npm',
    '@semantic-release/git',
    '@semantic-release/github'
  ]
};
Output
// Dry run: npx semantic-release --dry-run
// Output shows next version and changelog without publishing
Try it live
🔥Plugin Order Matters
Plugins execute in order. Place changelog before git to ensure the changelog file is updated before committing. npm plugin should run before git to update package.json version.
📊 Production Insight
A misconfigured plugin order caused the changelog to be generated after the git commit, resulting in an outdated changelog in the release tag. Users saw incorrect release notes for weeks.
🎯 Key Takeaway
Configure Semantic Release with plugins for commit analysis, changelog generation, and publishing; always dry-run first.
Manual vs Semantic Release Comparing version bump approaches in CI/CD Manual Release Semantic Release Version Bump Trigger Human decision Commit message analysis Consistency Prone to errors Automated and predictable Release Notes Manually written Auto-generated from commits CI Integration Manual steps required Fully automated pipeline Rollback Handling Manual revert Automatic patch on fix commits THECODEFORGE.IO
thecodeforge.io
Semantic Release

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.

.github/workflows/release.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
name: Release
on:
  push:
    branches: [main]
permissions:
  contents: write
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          persist-credentials: false
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx semantic-release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Output
// Successful run creates a GitHub Release and publishes npm package
⚠ Token Scopes
GITHUB_TOKEN automatically scoped to the repository. For npm, generate a granular access token with publish permissions. Never use a legacy token with full access.
📊 Production Insight
A team forgot to set 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.
🎯 Key Takeaway
Integrate Semantic Release in CI with full git history and proper token permissions; use fetch-depth: 0.

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.

release.config.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
  branches: [
    'main',
    { name: 'next', prerelease: 'next' },
    { name: 'beta', prerelease: 'beta' }
  ],
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    '@semantic-release/changelog',
    ['@semantic-release/npm', { npmPublish: true, distTag: 'next' }],
    '@semantic-release/git',
    '@semantic-release/github'
  ]
};
Output
// Commit on next branch: feat: new feature
// Results in 1.0.0-next.1 published with dist-tag 'next'
Try it live
💡Pre-release Versioning
Use 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.
📊 Production Insight
A team merged a breaking change into main without a pre-release. The next release bumped major version, breaking all consumers. Using a next branch would have caught the issue.
🎯 Key Takeaway
Configure pre-release branches with 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.

custom-plugin.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
module.exports = {
  generateNotes: async (pluginConfig, context) => {
    const { commits, logger } = context;
    const notes = commits
      .filter(c => c.type === 'feat')
      .map(c => `- ${c.subject} (${c.hash.slice(0, 7)})`)
      .join('\n');
    logger.log('Generated custom notes');
    return `## Features\n${notes}`;
  }
};
Output
// In release.config.js:
// plugins: [..., ['@semantic-release/exec', { generateNotesCmd: 'node custom-plugin.js' }]]
Try it live
🔥Plugin Lifecycle
Each step runs in order: verifyConditions, analyzeCommits, generateNotes, prepare, publish, success. Failure in any step aborts the release.
📊 Production Insight
A custom plugin that posted to Slack had a bug that threw an exception in success step, causing the release to be marked as failed even though npm published successfully. The team had to manually verify.
🎯 Key Takeaway
Extend Semantic Release with custom plugins for specialized workflows, but prefer existing plugins to reduce maintenance.

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.

CHANGELOG.mdMARKDOWN
1
2
3
4
5
6
7
8
## [1.2.0] - 2026-07-12
### Features
* add user authentication endpoint (abc1234)
* implement rate limiting (def5678)
### Bug Fixes
* fix token expiration check (ghi9012)
### Breaking Changes
* remove deprecated v1 API (jkl3456)
Output
// Generated automatically by Semantic Release
💡Changelog Hygiene
Do not manually edit CHANGELOG.md. If you need to add context, use commit footers like note: This is a critical fix. which get included in the notes.
📊 Production Insight
A team manually added a changelog entry for a hotfix, but the next automated release overwrote it, causing confusion. They now use commit footers for additional context.
🎯 Key Takeaway
Automated changelogs ensure accuracy and consistency; never manually edit them.

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.

debug.shBASH
1
2
DEBUG=semantic-release:* npx semantic-release --dry-run
# Output includes plugin steps, commit analysis, and version calculation
Output
semantic-release:plugins Running plugin @semantic-release/commit-analyzer
semantic-release:commit-analyzer Analyzing commits: [ 'fix: ...', 'feat: ...' ]
semantic-release:version Next version: 1.2.0
⚠ Common Pitfall: Git Tags
If a git tag for the version already exists (e.g., from a previous failed run), Semantic Release will skip. Delete the tag locally and remotely: git tag -d v1.0.0 && git push origin :refs/tags/v1.0.0
📊 Production Insight
A production release failed because a developer pushed a commit with BREAKING 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.
🎯 Key Takeaway
Debug with --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.

packages/api/release.config.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
module.exports = {
  extends: '../../release.config.js',
  branches: ['main'],
  tagFormat: 'api-v${version}',
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    ['@semantic-release/npm', { pkgRoot: '.' }],
    '@semantic-release/github'
  ]
};
Output
// Tag example: api-v1.2.3
Try it live
🔥Independent Versioning
Use tagFormat to prefix tags per package (e.g., api-v1.0.0). This prevents tag conflicts and allows per-package releases.
📊 Production Insight
A monorepo team released a new version of a shared library without updating the dependent package's version range, causing runtime errors. They now enforce release order with a CI pipeline.
🎯 Key Takeaway
For monorepos, use per-package configuration with unique tag formats to manage independent 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.

rollback.shBASH
1
2
3
4
5
# Revert the breaking commit
git revert <commit-hash>
git push origin main
# Semantic Release will create a new patch version
# Example: 2.0.0 -> 2.0.1
Output
// New release: 2.0.1 with the revert commit
💡Fix Forward
Avoid deleting tags or unpublishing packages. Revert the change and let Semantic Release create a new version. This maintains a clean audit trail.
📊 Production Insight
A team deleted a git tag to 'undo' a release, but the npm package was already downloaded by users. They had to publish a new version with a revert, causing confusion. Now they always fix forward.
🎯 Key Takeaway
Rollback by reverting the commit and letting Semantic Release create a new patch version; never delete tags.

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.

npm-token.shBASH
1
2
3
# Generate a granular token on npmjs.com with 'Read and Publish' for specific package
npm token create --read-only --publish-only --cidr=10.0.0.0/8
# Set as secret in GitHub: NPM_TOKEN
Output
npm password:
Token: npm_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
⚠ Token Rotation
Rotate tokens every 90 days. Use GitHub's secret scanning to detect leaked tokens. If a token is exposed, revoke immediately and generate a new one.
📊 Production Insight
A leaked NPM_TOKEN in a public repository allowed an attacker to publish a malicious version of a popular package. The team had to revoke the token and audit all releases.
🎯 Key Takeaway
Use scoped, short-lived tokens for Semantic Release; never hardcode secrets.

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.

metrics.jsJAVASCRIPT
1
2
3
4
5
// Example: Track release frequency
const releases = []; // from API
const weeklyAvg = releases.length / 4; // last 4 weeks
console.log(`Weekly releases: ${weeklyAvg}`);
// Compare to pre-automation baseline
Output
Weekly releases: 12 (was 3 before automation)
Try it live
🔥Adoption Tips
Start with a single repository and a pilot team. Provide training on Conventional Commits. Use commitizen to simplify. Gradually expand to other teams.
📊 Production Insight
A team tracked a 70% reduction in release-related incidents after adopting Semantic Release. They also saw a 3x increase in release frequency, enabling faster feature delivery.
🎯 Key Takeaway
Measure release frequency, failure rate, and developer satisfaction to demonstrate the value of automation.
● Production incidentPOST-MORTEMseverity: high

The 'Fix' Commit That Would Have Bumped v1.2.3 to v2.0.0

Symptom
A developer included a breaking API change in a commit with type 'fix:'. semantic-release correctly parsed the commit message, found no 'BREAKING CHANGE:' footer, and would have bumped the patch version (v1.2.4). The breaking change would have been released as a patch, silently breaking all downstream consumers. The code reviewer caught the mistake and updated the commit to 'feat:' with a 'BREAKING CHANGE:' footer before merge.
Assumption
The developer assumed the commit type ('fix:') was just a label and wouldn't affect versioning. They didn't realize semantic-release uses the type to determine the version bump.
Root cause
1. Developer made a breaking API change (renamed a function, removed a parameter). 2. They committed with 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.
Fix
1. Changed the commit message to 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.
Key lesson
  • Commit type determines version bump: fix → patch, feat → minor, BREAKING → major.
  • Use ! or BREAKING 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.
Production debug guideUse this when production is on fire3 entries
Symptom · 01
I want to automate version bumps and releases
Fix
Set up semantic-release in CI with conventional commits
Symptom · 02
I need to prevent accidental major version bumps
Fix
Use commitlint to enforce conventional commit format, and add BREAKING CHANGE detection in CI
Symptom · 03
I want to see what version the next release will be
Fix
Run npx semantic-release --dry-run to preview the version bump and changelog
⚙ Quick Reference
11 commands from this guide
FileCommand / CodePurpose
install.shnpm install -g semantic-releaseWhy Manual Versioning Fails at Scale
commitlint.config.jsmodule.exports = {Setting Up Conventional Commits
release.config.jsmodule.exports = {Configuring Semantic Release
.githubworkflowsrelease.ymlname: ReleaseCI/CD Integration and Authentication
custom-plugin.jsmodule.exports = {Custom Plugins and Extending Behavior
CHANGELOG.md* add user authentication endpoint (abc1234)Changelog Generation and Maintenance
debug.shDEBUG=semantic-release:* npx semantic-release --dry-runTroubleshooting Common Failures
packagesapirelease.config.jsmodule.exports = {Monorepo Strategies
rollback.shgit revert Production Rollback and Recovery
npm-token.shnpm token create --read-only --publish-only --cidr=10.0.0.0/8Security and Access Control
metrics.jsconst releases = []; // from APIMeasuring Success and Adoption

Key takeaways

1
semantic-release automates version bumps from commit messages
2
fix → patch, feat → minor, BREAKING CHANGE → major
3
Use ! or BREAKING CHANGE: footer to mark breaking changes
4
Always dry-run before actual release to verify the version bump
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
What happens if a commit message doesn't follow Conventional Commits?
02
Can Semantic Release work with private npm registries?
03
How do I handle breaking changes in a pre-release branch?
04
Why is Semantic Release not creating a release even though I pushed a valid commit?
05
Can I customize the changelog format?
06
How do I rollback a release without breaking the automation?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
July 12, 2026
last updated
2,406
articles · all by Naren
🔥

That's Git. Mark it forged?

4 min read · try the examples if you haven't

Previous
Trunk-Based Development: Merge to Main Multiple Times Daily
39 / 47 · Git
Next
Branch Protection Rules: Secure Your Git Workflow