
September 10, 2026
12 min read
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.
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-releasefrom 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
mainormaster.
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.
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.
| Plugin | Role | When you need it |
|---|---|---|
@semantic-release/commit-analyzer | Reads commits, outputs release type | Always — core plugin |
@semantic-release/release-notes-generator | Builds changelog text | Always — core plugin |
@semantic-release/changelog | Writes CHANGELOG.md to disk | When consumers read changelogs in-repo |
@semantic-release/npm | Updates version and publishes to npm | Node libraries and CLI tools |
@semantic-release/git | Commits version bumps back to Git | When version files must live in the repo |
@semantic-release/github | Creates GitHub Release with notes | GitHub-hosted repos |
@semantic-release/gitlab | Creates GitLab Release | GitLab-hosted repos |
@semantic-release/exec | Runs shell commands at each step | Laravel 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.
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.
- 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: 0orGIT_DEPTH: 0. - 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.
- Competing tags from humans. A developer runs
git tag v2.0.0locally. CI later tries the same version. Protect tags and block manual tag pushes on protected branches. - Missing npm provenance or 2FA. npm orgs with 2FA and provenance attestation reject CI publishes unless
NPM_TOKENis a granular automation token. Rotate tokens on the same schedule as automated database backup credentials. - Release loops. Forgetting
[skip ci]on the bot commit retriggers the pipeline endlessly until minutes exhaust.
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: 0orfetch-depth: 0in 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-runbefore 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
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.

