Introduction to Semantic Release
Semantic Release is an automated version management and package publishing tool that follows the principles of Semantic Versioning (SemVer). Instead of manually bumping version numbers, updating changelogs, and tagging releases, Semantic Release analyzes your commit messages and determines the next version number automatically, generates release notes, and publishes your package — all without human intervention.
The tool is built around the Conventional Commits specification, a standardized format for commit messages that encodes the intent of each change. By parsing these messages, Semantic Release can decide whether a release should be a patch, minor, or major version bump, or whether no release is needed at all.
Why Automated Releases Matter
Manual release processes are error-prone and tedious. Developers frequently forget to update the changelog, mistype version numbers, or skip tagging releases entirely. Semantic Release eliminates these issues by enforcing a consistent, repeatable workflow. The benefits include:
- Consistency: Every release follows the same rules and format.
- Reduced cognitive load: Developers focus on writing code, not on release mechanics.
- Accurate changelogs: Release notes are generated directly from commit history.
- Continuous delivery: Releases happen automatically when code is merged, enabling true continuous deployment.
- Traceability: Each release is tied to specific commits, making it easy to understand what changed and why.
Understanding Conventional Commits
Before configuring Semantic Release, you must understand the commit message format it relies on. The Conventional Commits specification defines a structured format:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
The type field is the most important, as it directly influences versioning. The common types are:
feat: A new feature — triggers a minor version bump.fix: A bug fix — triggers a patch version bump.BREAKING CHANGEin the footer or!after the type/scope — triggers a major version bump.chore,docs,style,refactor,test,perf: Do not trigger a release by default.
Here are some examples of valid conventional commit messages:
feat(auth): add OAuth2 login support
fix(parser): handle empty input without crashing
feat(api)!: remove deprecated v1 endpoints
This is a breaking change that removes the legacy API.
chore: update dependencies
Installing Semantic Release
Semantic Release is distributed as an npm package. Install it as a development dependency in your project:
npm install --save-dev semantic-release
For projects using Yarn:
yarn add --dev semantic-release
Semantic Release also requires a Node.js environment and access to your Git repository. Additionally, depending on your platform, you will need plugins for npm, GitHub, GitLab, or other services. These plugins are installed separately, which we will cover in the configuration section.
Basic Configuration
Semantic Release can be configured in several ways: through a .releaserc file, a release key in package.json, or a .releaserc.js JavaScript file for dynamic configuration. The most common approach is using a .releaserc.json file at the root of your project.
Here is a minimal configuration for a project hosted on GitHub and published to npm:
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/github",
"@semantic-release/git"
]
}
Let us break down what each plugin does:
@semantic-release/commit-analyzer: Parses commit messages and determines the type of version bump.@semantic-release/release-notes-generator: Generates release notes from commit history.@semantic-release/changelog: Updates theCHANGELOG.mdfile with each release.@semantic-release/npm: Publishes the package to the npm registry and updatespackage.jsonwith the new version.@semantic-release/github: Creates a GitHub release and optionally comments on related issues and pull requests.@semantic-release/git: Commits the updatedCHANGELOG.mdandpackage.jsonback to the repository.
Install the required plugins:
npm install --save-dev @semantic-release/commit-analyzer \
@semantic-release/release-notes-generator \
@semantic-release/changelog \
@semantic-release/npm \
@semantic-release/github \
@semantic-release/git
Configuring Branches
The branches option defines which Git branches should trigger releases. This is critical for managing release channels such as stable, beta, and next. A simple configuration releases only from the main branch:
{
"branches": ["main"]
}
For more complex workflows, you can define named release channels with specific versioning rules:
{
"branches": [
"main",
{
"name": "next",
"prerelease": "next"
},
{
"name": "beta",
"prerelease": "beta"
}
]
}
With this configuration, merges to main produce stable releases like 1.2.0, while merges to next produce pre-releases like 1.2.0-next.1. This is useful for testing features before they reach the stable channel.
Plugin Configuration in Depth
Commit Analyzer
The commit analyzer plugin can be customized to change how commit types map to version bumps. For example, you might want perf commits to trigger a patch release:
{
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "angular",
"releaseRules": [
{ "type": "perf", "release": "patch" },
{ "type": "refactor", "release": "patch" },
{ "type": "style", "release": false }
]
}
]
]
}
Release Notes Generator
This plugin controls the format of your release notes. You can customize the preset and add custom writers:
{
"plugins": [
[
"@semantic-release/release-notes-generator",
{
"preset": "angular",
"presetConfig": {
"types": [
{ "type": "feat", "section": "Features" },
{ "type": "fix", "section": "Bug Fixes" },
{ "type": "perf", "section": "Performance Improvements" },
{ "type": "refactor", "section": "Code Refactoring" },
{ "type": "docs", "section": "Documentation" }
]
}
}
]
]
}
Changelog Plugin
The changelog plugin writes release notes to a file. By default, it uses CHANGELOG.md, but you can change this:
{
"plugins": [
[
"@semantic-release/changelog",
{
"changelogFile": "docs/CHANGELOG.md",
"changelogTitle": "# Changelog\n\nAll notable changes to this project are documented here."
}
]
]
}
Git Plugin
The git plugin commits the modified files back to your repository. You must specify which files to include and provide a commit message template:
{
"plugins": [
[
"@semantic-release/git",
{
"assets": ["package.json", "package-lock.json", "docs/CHANGELOG.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}
The [skip ci] token in the commit message is important — it prevents your CI pipeline from triggering another build for this automated commit, which would create an infinite loop.
Setting Up CI/CD Integration
Semantic Release is designed to run in a CI environment. It should execute after your tests pass and code is merged into a release branch. Below are configurations for popular CI platforms.
GitHub Actions
Create a workflow file at .github/workflows/release.yml:
name: Release
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release
Note the fetch-depth: 0 setting — Semantic Release needs the full Git history to analyze all commits since the last release. The GITHUB_TOKEN is automatically provided by GitHub Actions, but you must create an NPM_TOKEN manually and add it as a repository secret.
GitLab CI
Add the following to your .gitlab-ci.yml:
stages:
- test
- release
test:
stage: test
image: node:20
script:
- npm ci
- npm test
release:
stage: release
image: node:20
rules:
- if: $CI_COMMIT_BRANCH == "main"
script:
- npm ci
- npx semantic-release
variables:
GITLAB_TOKEN: $CI_JOB_TOKEN
NPM_TOKEN: $NPM_TOKEN
Authentication Setup
Semantic Release needs authentication tokens to publish packages and create releases. The required tokens depend on your platform:
- npm: Create an automation token at
npmjs.comand store it asNPM_TOKEN. - GitHub: Use the built-in
GITHUB_TOKENor create a personal access token withrepopermissions. - GitLab: Use the
CI_JOB_TOKENor a project access token stored asGITLAB_TOKEN.
For npm publishing, add the following to your .npmrc file:
registry=https://registry.npmjs.org/
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
This ensures that Semantic Release can authenticate with the npm registry using the environment variable.
Advanced Configuration with JavaScript
For dynamic configurations, use a .releaserc.js file. This allows you to use environment variables, conditional logic, and shared utilities:
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',
{
assets: ['package.json', 'CHANGELOG.md'],
message: 'chore(release): ${nextRelease.version} [skip ci]'
}
],
process.env.GITHUB_TOKEN && '@semantic-release/github'
].filter(Boolean)
};
This pattern is useful when you want to conditionally enable plugins based on the environment, such as disabling the GitHub plugin in local testing.
Dry Runs and Debugging
Before running Semantic Release in production, you should test your configuration with a dry run. This simulates the entire release process without actually publishing anything:
npx semantic-release --dry-run
To get detailed debug output, set the DEBUG environment variable:
DEBUG=semantic-release:* npx semantic-release --dry-run
This will print verbose logs showing how commits are analyzed, which version is determined, and what each plugin would do. It is invaluable for troubleshooting configuration issues.
Best Practices
Enforce Conventional Commits
Semantic Release only works well if your team consistently follows the Conventional Commits format. Use tools like commitlint and commitizen to enforce and guide commit messages:
npm install --save-dev @commitlint/cli @commitlint/config-conventional
Create a commitlint.config.js file:
module.exports = {
extends: ['@commitlint/config-conventional']
};
Add a Husky hook to validate commits before they are created:
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit $1'
Keep Releases Atomic
Each commit should represent a single, logical change. This makes the generated changelog more readable and ensures that version bumps accurately reflect the scope of changes. Avoid mixing features, fixes, and chores in a single commit.
Use Scope Consistently
The optional scope in commit messages helps organize changelogs. Agree on a set of scopes with your team and use them consistently. For example, in a monorepo, scopes might correspond to package names.
Protect Release Branches
Configure branch protection rules on your release branches to require pull request reviews and passing CI checks before merging. This prevents broken or unreviewed code from triggering a release.
Pin Plugin Versions
In production environments, pin Semantic Release and its plugins to specific versions to avoid unexpected behavior from breaking changes in new releases. Use exact versions in your package.json rather than caret or tilde ranges.
Handle Monorepos Carefully
For monorepos, consider using semantic-release-monorepo or tools like Lerna that integrate with Semantic Release. These tools allow you to release individual packages independently while sharing a single commit history.
Common Pitfalls and Solutions
No Release Is Triggered
If Semantic Release reports "There are no relevant changes, so no new version is released," check that your commits use the correct types. Only feat and fix trigger releases by default. If you need other types to trigger releases, configure releaseRules in the commit analyzer plugin.
Authentication Failures
Ensure that your tokens are correctly set as environment variables in your CI configuration. For npm, verify that the token has publish permissions and that the .npmrc file references the correct environment variable name.
Infinite CI Loops
If your CI pipeline keeps triggering after a release commit, ensure that your release commit message includes [skip ci] and that your CI platform is configured to respect this token. Most platforms support it natively, but some may require additional configuration.
Conclusion
Semantic Release transforms the release process from a manual, error-prone chore into a fully automated, reliable workflow. By combining Conventional Commits with a well-configured CI pipeline, you ensure that every release is consistent, well-documented, and traceable. While the initial setup requires careful attention to configuration and authentication, the long-term payoff is significant: your team can ship with confidence, knowing that version numbers, changelogs, and package publishing are handled automatically. Start with a dry run, enforce commit conventions with commitlint, and gradually refine your configuration as your project evolves. With these practices in place, Semantic Release becomes an indispensable part of a modern continuous delivery pipeline.