← Back to DevBytes

Semantic Release: Complete Configuration Guide

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:

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:

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:

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:

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles