Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Automate Releases with semantic-release

By Kokil Thapa | Last reviewed: September 2026

Manual release days waste engineering time. You bump a version, write a changelog, tag Git, publish an artefact, and hope nobody forgot a step. When you Automate Releases with semantic-release, the pipeline reads your commit history and handles the rest. This guide walks through a production-ready setup on Conventional Commits and semantic versioning, with configs you can paste into GitLab CI or GitHub Actions today.

What happens when you Automate Releases with semantic-release?

semantic-release is a Node.js tool that turns commit messages into release artefacts. It does not guess versions from gut feel. It parses commits since the last release tag and applies semver rules automatically.

A fix: commit triggers a patch bump. A feat: commit triggers a minor bump. A breaking change—marked with BREAKING CHANGE: in the footer or a ! after the type—triggers a major bump. If every commit since the last tag is only docs: or chore:, no release runs at all.

That last point matters. Many teams assume CI always publishes. semantic-release publishes only when the commit analysis says a user-facing change landed. Your main branch stays clean, and npm or GitHub Releases update only when something actually changed for consumers.

Automate Releases with semantic-releaseGit CommitsConventionalAnalyzesemver rulesVersionpackage.jsonPublishnpm + Git tagGenerate NotesCHANGELOG.mdCreate Git Tagv2.4.0 formatNo releasable commits = pipeline exits cleanlydocs and chore only since last tag
How semantic-release Automate Releases from Conventional Commits through analysis, versioning, and publish steps

The official project describes this as Continuous Delivery for version numbers. You stop debating whether a change is 1.3.1 or 1.4.0 in a Slack thread. The commit message already decided it before the merge landed.

On real client projects I maintain with GitLab CI and Deployer, semantic-release often lives upstream of application deploy. The npm library or shared frontend package gets tagged first. The Laravel app then pins that version in composer.json or package.json. Release automation and deploy automation stay separate concerns, and that separation keeps rollbacks predictable.

Prerequisites you cannot skip

Three foundations must exist before semantic-release works reliably:

  • Conventional Commits on every merge. Squash merges should produce one clean message. Train the team or enforce format with Git hooks that automate checks before commit and push.
  • CI-only releases. Never run npx semantic-release from a laptop against production branches. Local runs race with CI and can double-publish.
  • Protected default branch. Only CI should push version commits and tags back to main or master.

Without Conventional Commits, semantic-release either skips releases silently or mis-bumps versions. Fix the commit discipline first. The tooling second.

How do you configure semantic-release in CI/CD?

Install semantic-release as a dev dependency in any Node.js project. Node.js 26 LTS and npm 12 are solid choices in 2026. PHP-only repos can still use semantic-release in CI by installing Node in the job image—no runtime Node required on your Ubuntu server.

Step 1: Add the package and config file

npm install --save-dev semantic-release \
  @semantic-release/changelog \
  @semantic-release/git \
  @semantic-release/github \
  @semantic-release/npm

Create release.config.cjs at the repository root:

/** @type {import('semantic-release').GlobalConfig} */
module.exports = {
  branches: ['main'],
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    ['@semantic-release/changelog', { changelogFile: 'CHANGELOG.md' }],
    '@semantic-release/npm',
    ['@semantic-release/git', {
      assets: ['CHANGELOG.md', 'package.json', 'package-lock.json'],
      message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
    }],
    '@semantic-release/github',
  ],
};

The [skip ci] token in the release commit message prevents infinite CI loops. GitLab and GitHub both honour common skip patterns when configured.

Step 2: Wire GitLab CI

I use GitLab CI on several production sites. A minimal release stage looks like this:

release:
  stage: release
  image: node:26-bookworm
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  before_script:
    - npm ci
  script:
    - npx semantic-release
  variables:
    GIT_DEPTH: 0
    GL_TOKEN: $GITLAB_TOKEN

GIT_DEPTH: 0 fetches full history. Shallow clones break tag detection. That single variable causes more failed first-time setups than any other misconfiguration.

Store GITLAB_TOKEN or GH_TOKEN as a masked CI variable with permission to push tags and write packages. GitHub Actions uses GITHUB_TOKEN by default, but fine-grained PATs are safer for org repos with branch protection.

CI Pipeline Release StageMerge to mainsquashed commitLint + Testmust pass firstsemantic-releaseNode 26 jobTag pushProtected Branch RulesCI bot can push tags + version commitsDevelopers cannot push directly to mainGIT_DEPTH: 0 for full tag historyRelease commit includes [skip ci]
GitLab CI or GitHub Actions release stage placement after tests on a protected default branch

Step 3: GitHub Actions equivalent

name: Release
on:
  push:
    branches: [main]
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 26
          cache: npm
      - run: npm ci
      - run: npx semantic-release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

For monorepos, add the official semantic-release documentation guidance on multi-package plugins or use independent per-package configs. Start simple with one package before splitting release paths.

Which semantic-release plugins should you enable?

semantic-release uses a plugin pipeline. Each plugin handles one step. You compose only what your project needs.

PluginRoleWhen you need it
@semantic-release/commit-analyzerReads commits, outputs release typeAlways — core plugin
@semantic-release/release-notes-generatorBuilds changelog textAlways — core plugin
@semantic-release/changelogWrites CHANGELOG.md to diskWhen consumers read changelogs in-repo
@semantic-release/npmUpdates version and publishes to npmNode libraries and CLI tools
@semantic-release/gitCommits version bumps back to GitWhen version files must live in the repo
@semantic-release/githubCreates GitHub Release with notesGitHub-hosted repos
@semantic-release/gitlabCreates GitLab ReleaseGitLab-hosted repos
@semantic-release/execRuns shell commands at each stepLaravel deploy hooks, Slack notify

For a Laravel + Vite frontend that publishes a private npm scope, I often skip @semantic-release/npm public publish and use @semantic-release/exec to trigger a downstream deploy job instead. The version tag still lands in Git. Deployer or GitLab downstream pipelines pick it up.

Validate your config shape with the JSON formatter tool when exporting plugin options to JSON. A trailing comma in a hand-edited config breaks the release job at the worst moment—Friday evening before a client demo.

Custom release rules for hotfix branches

Production hotfixes sometimes need releases from a hotfix/* branch. Extend the branches array:

branches: [
  'main',
  { name: 'hotfix/*', channel: 'hotfix', prerelease: '${name.replace(/^hotfix\\//, "")}' },
]

Prerelease channels publish tagged npm dist-tags like 1.2.3-hotfix.1. Document the channel naming for your team. Ambiguous branch patterns create duplicate version collisions.

How does semantic-release compare to manual release workflows?

Teams without automation usually assign one person as "release captain." That person reads merged PRs, edits CHANGELOG.md, runs npm version, pushes tags, and publishes. It works until that person is on leave or the team ships daily.

Manual vs Automated ReleasesManual ReleaseHuman reads PR listDebate semver in chatEdit CHANGELOG by handForgotten tag = drift30–90 min per releaseBus factor risksemantic-releaseCommits define semverChangelog auto-generatedTag always matches npmRuns in 2–5 min in CISame rules every timeRequires commit disciplineswitch
Manual release workflows versus Automate Releases with semantic-release on speed, consistency, and team dependency

The trade-off is upfront discipline. Manual releases forgive messy commit messages because a human interprets intent. semantic-release treats the message as the contract. A fix: that actually breaks API compatibility ships as a patch unless someone marks the breaking change correctly.

For agencies shipping custom software development projects, I recommend semantic-release on shared internal packages first. Client-facing Laravel apps can keep calendar versioning if the client expects it. Mixing models in one repo creates confusion—pick one strategy per artefact.

Related reading: semantic-release automated versioning deep dive, toil reduction by automating boring ops tasks, and Google Cloud Build for container release pipelines.

What are common semantic-release mistakes in production?

Most failures I troubleshoot are configuration or process problems, not bugs in the tool itself.

  1. Shallow Git clones. CI fetches depth 50, semantic-release cannot find the last tag, and it tries to release every commit since repo birth. Set fetch-depth: 0 or GIT_DEPTH: 0.
  2. Squash merge messages that lose scope. GitHub default squash title is the PR title, not the Conventional Commit body. Configure squash templates or enforce PR title format with a regex check—test patterns in a regex tester before adding them to CI.
  3. Competing tags from humans. A developer runs git tag v2.0.0 locally. CI later tries the same version. Protect tags and block manual tag pushes on protected branches.
  4. Missing npm provenance or 2FA. npm orgs with 2FA and provenance attestation reject CI publishes unless NPM_TOKEN is a granular automation token. Rotate tokens on the same schedule as automated database backup credentials.
  5. Release loops. Forgetting [skip ci] on the bot commit retriggers the pipeline endlessly until minutes exhaust.
Release Failure Decision TreeCI release failed?No release madecheck commit typesError thrownread CI logsOnly docs/choreexpected skipAuth or Git depthFix token / depthSuccess pathtag + changelog + publish
Troubleshooting decision tree when semantic-release skips or fails inside CI pipelines

Laravel and PHP projects without a public npm package

Most Laravel apps I ship—booking portals, legal-tech sites like Adventure Third Pole Trek, or Mijar Law Associates—are not npm libraries. semantic-release still adds value for:

  • Shared JavaScript component libraries consumed by multiple Blade + Vite frontends
  • Internal CLI tools published to a private GitLab npm registry
  • Open-source Composer packages that also ship frontend assets

For deploy-only PHP repos, tag releases with @semantic-release/exec calling your Deployer recipe after version analysis. Keep application version in config/app.php synced via a small script in the exec hook. Do not force npm publish where no package exists.

Pair release automation with support and maintenance contracts so clients understand semver bumps may arrive without a formal "release meeting." Patch fixes ship when CI greenlights them.

Security and compliance notes

Release bots need write access. Scope tokens minimally: tag create, contents write, packages write—nothing else. Audit token usage quarterly.

Follow the Conventional Commits specification for message format. Pair it with Semantic Versioning 2.0.0 rules so consumers trust the version numbers your pipeline emits.

On shared EC2 infrastructure where several sister sites run Deployer 7 + GitLab CI, one compromised release token can affect multiple properties. Store secrets in GitLab masked variables, not in .env files committed to application repos.

How do you roll out semantic-release without breaking existing consumers?

Start on a low-risk package or an internal tool. Run semantic-release in dry-run mode first:

npx semantic-release --dry-run --no-ci

Dry-run prints the next version, changelog preview, and publish targets without mutating Git or npm. Fix commit history gaps before enabling live mode.

If your repo already has manual tags, ensure the highest tag matches the current package.json version. semantic-release uses the latest semver tag as its baseline. A mismatch causes an unexpected major jump or a skipped release.

Communicate the switch to downstream teams. API consumers should watch GitHub Releases or GitLab release pages instead of waiting for a human email. Document the change in your internal wiki and link to DevOps automation examples for teams still adapting workflows.

Enterprise clients often ask whether automated releases violate change-control policies. Frame semantic-release as enforced change classification: every commit is categorized before merge, and the pipeline merely executes the agreed semver outcome. That argument satisfies most audit questionnaires I've seen on enterprise application development engagements.

Key Takeaways

  • Automate Releases with semantic-release only after Conventional Commits are consistent on your default branch.
  • Set GIT_DEPTH: 0 or fetch-depth: 0 in CI—shallow clones are the most common first-run failure.
  • Compose plugins for your artefact: npm publish, GitLab Release, changelog file, or exec hooks for Laravel deploys.
  • Add [skip ci] to bot commit messages to prevent infinite pipeline loops.
  • Run --dry-run before enabling live publishes on packages with existing downstream dependents.
  • Scope CI tokens narrowly and rotate them on the same schedule as other production credentials.

People Also Ask

Does semantic-release work with private npm registries?

Yes. Configure @semantic-release/npm with npmPublish options pointing at your GitLab Package Registry or Verdaccio instance. Set NPM_TOKEN to a registry-specific token. The plugin updates package.json version and publishes to the configured registry URL, not only the public npmjs.org.

Can semantic-release run on pull requests?

Not for real releases. Use pull request pipelines for dry-run validation only. Actual releases should execute on merges to release branches. Some teams add a PR comment bot showing the predicted next version—that requires custom scripting or third-party GitHub Apps, not the default semantic-release behaviour.

What happens if two features merge at the same time?

CI serializes releases on the default branch. The first pipeline completes and pushes a tag. The second run sees the new tag baseline and bumps from there. If two pipelines start simultaneously before either finishes, one may fail on tag collision. Use concurrency controls in GitLab (resource_group) or GitHub Actions (concurrency) to queue release jobs.

Is semantic-release only for JavaScript projects?

The tool is Node-based, but CI runs it against any repo that benefits from semver tags and changelogs. PHP, Python, and Docker repos use @semantic-release/exec to bump version files and trigger builds. The commit analysis logic stays the same regardless of the primary language.

Ship releases on every merge, not every meeting

When you Automate Releases with semantic-release, version numbers become a function of engineering discipline rather than calendar politics. Your team merges with intent, CI publishes with consistency, and consumers trust the semver contract.

Start with one package, full Git history in CI, and a protected main branch. Expand to exec hooks and deploy integration once the first ten releases land without manual intervention.

Need help wiring semantic-release into GitLab CI, a Laravel monorepo, or a multi-site Deployer pipeline? Contact us for a practical review. Browse portfolio projects that run automated deploy pipelines, or read more on the blog about Ansible-based server automation and Linux system administration for the infrastructure layer beneath your release jobs.

Frequently Asked Questions

semantic-release is a Node.js tool that turns commit messages into release artefacts. It parses commits since the last release tag and applies semver rules automatically. A fix: commit triggers a patch bump, feat: triggers a minor bump, and BREAKING CHANGE: in the footer or a ! after the type triggers a major bump. The pipeline generates changelogs, creates Git tags, and publishes packages without a human debating version numbers in Slack.

If every commit since the last tag is only docs: or chore:, no release runs at all. Many teams assume CI always publishes, but semantic-release publishes only when commit analysis finds a user-facing change.

Node.js 26 LTS and npm 12 are solid choices in 2026. PHP-only Laravel repos can still use semantic-release by installing Node only in the CI job image—no Node runtime is required on your Ubuntu production server.

Three foundations: Conventional Commits on every merge, with squash merges producing one clean message; CI-only releases, never npx semantic-release from a laptop against production branches; and a protected default branch where only CI pushes version commits and tags. Without commit discipline, semantic-release either skips releases silently or mis-bumps versions. Fix the commit format first, then add the tooling. Train the team or enforce format with Git hooks that automate checks before commit and push.

Install semantic-release and plugins as dev dependencies, create release.config.cjs at the repository root with your plugin pipeline, then add a release stage after tests on the protected default branch. GitLab needs GIT_DEPTH: 0 for full history and a masked GL_TOKEN with permission to push tags and write packages. GitHub Actions needs fetch-depth: 0 on checkout, Node 26, npm ci, and GITHUB_TOKEN plus NPM_TOKEN env vars. The release commit message must include [skip ci] to prevent infinite pipeline loops.

Always include commit-analyzer and release-notes-generator. Add changelog to write CHANGELOG.md, npm when publishing Node libraries, git to commit version bumps back to the repo, github or gitlab for platform releases, and exec for Laravel deploy hooks or Slack notifications. On a Laravel plus Vite frontend with a private npm scope, I often skip public npm publish and use exec to trigger a downstream Deployer job instead—the version tag still lands in Git and downstream pipelines pick it up. Compose only what your artefact needs.

Shallow clones are the most common first-run failure. If CI fetches depth 50, semantic-release cannot find the last tag and tries to release every commit since repo birth. Set GIT_DEPTH: 0 in GitLab CI or fetch-depth: 0 in GitHub Actions checkout. Full history is required for tag detection—this single misconfiguration causes more failed setups than almost anything else in production pipelines I troubleshoot.

The @semantic-release/git plugin commit message must include [skip ci]. GitLab and GitHub both honour common skip patterns when configured. Without it, the bot commit that bumps CHANGELOG.md and package.json retriggers the pipeline endlessly until CI minutes exhaust. Add it to the message template in release.config.cjs alongside the release version and notes. Forgetting this token is one of the most painful Friday-evening failures before a client demo.

Most Laravel apps I ship—booking portals, legal-tech sites, client portals—are not npm libraries, but semantic-release still adds value for shared JavaScript component libraries consumed by multiple Blade plus Vite frontends, internal CLI tools, or open-source Composer packages that also ship frontend assets. For deploy-only PHP repos, use @semantic-release/exec to call your Deployer recipe after version analysis and sync config/app.php via a small script in the exec hook. Do not force npm publish where no package exists.

Manual releases assign one person as release captain who reads merged PRs, edits CHANGELOG.md, runs npm version, pushes tags, and publishes. It works until that person is on leave or the team ships daily. semantic-release trades upfront commit discipline for speed and consistency—the commit message is the contract. Manual releases forgive messy messages because a human interprets intent; semantic-release treats them as semver law. I recommend starting on shared internal packages first while client-facing Laravel apps can keep calendar versioning if the client expects it.

Yes. Configure @semantic-release/npm with npmPublish options pointing at your GitLab Package Registry or Verdaccio instance. Set NPM_TOKEN to a registry-specific token. The plugin updates package.json version and publishes to the configured registry URL, not only public npmjs.org. npm orgs with 2FA and provenance attestation reject CI publishes unless NPM_TOKEN is a granular automation token—rotate tokens on the same schedule as other production credentials.

Not for real releases. Use pull request pipelines for dry-run validation only. Actual releases should execute on merges to release branches such as main. Some teams add a PR comment bot showing the predicted next version, but that requires custom scripting or third-party GitHub Apps, not default semantic-release behaviour. Pair PR dry-runs with squash merge templates so the final merge message still follows Conventional Commits format.

CI serializes releases on the default branch in normal operation. The first pipeline completes and pushes a tag; the second run sees the new tag baseline and bumps from there. If two pipelines start simultaneously before either finishes, one may fail on tag collision. Use concurrency controls in GitLab via resource_group or in GitHub Actions via concurrency settings to queue release jobs. Also protect tags and block manual tag pushes on protected branches to avoid competing versions from developers.

Start on a low-risk package or internal tool. Run npx semantic-release --dry-run --no-ci first—it prints the next version, changelog preview, and publish targets without mutating Git or npm. If your repo already has manual tags, ensure the highest tag matches the current package.json version; a mismatch causes an unexpected major jump or a skipped release. Communicate the switch so downstream teams watch GitHub Releases or GitLab release pages instead of waiting for a human email. Document the change in your internal wiki.

Release bots need write access, so scope tokens minimally: tag create, contents write, packages write—nothing else. Audit token usage quarterly. Store GITLAB_TOKEN or GH_TOKEN as masked CI variables, not in .env files committed to application repos. On shared EC2 infrastructure where several sister sites run Deployer 7 plus GitLab CI, one compromised release token can affect multiple properties. Fine-grained PATs are safer than default tokens for org repos with branch protection. Follow Conventional Commits and Semantic Versioning 2.0.0 so consumers trust the version numbers your pipeline emits.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: