Part ofCI/CD Pipelines·Step 1 of 3
DevOps & Platform

Semantic Versioning, Conventional Commits, and Release Please

Beginner40 min to complete12 min readJune 1, 2026Updated August 19, 2026

Quick answer

Eliminate version debates and manual changelogs — conventional commit messages drive automated version bumps, and Release Please creates release PRs with CHANGELOG.md entries automatically.

beginner · 40 min

Before you begin

  • Git basics — commits, branches, tags
  • A GitHub repository
Semantic Versioning
Conventional Commits
Release Please
Git
CI/CD
Automation

Semantic Versioning, Conventional Commits, and Release Please

Two recurring sources of team friction: "what version should this be?" and "who writes the changelog?" Both have well-established, automatable answers.

Semantic Versioning defines version numbers that communicate meaning. Conventional Commits structures commit messages so tooling can read that meaning. Release Please reads the commit log and creates release PRs automatically, complete with a generated CHANGELOG.


Semantic Versioning

A SemVer version has three parts: MAJOR.MINOR.PATCH

1.4.2
│ │ └── PATCH — backwards-compatible bug fix
│ └──── MINOR — new functionality, backwards compatible
└────── MAJOR — breaking change, incompatible with previous API

Rules

  • Increment PATCH for bug fixes that don't change the API
  • Increment MINOR for new features that don't break existing users (reset PATCH to 0)
  • Increment MAJOR for changes that break existing users (reset MINOR and PATCH to 0)
  • 0.x.y — initial development, anything may change at any time (no stability promise)
  • 1.0.0 — your first stable, public API

Pre-release versions

1.0.0-alpha.1        # Early development, not feature-complete
1.0.0-beta.2         # Feature-complete, may have bugs
1.0.0-rc.1           # Release candidate, bug fixes only
1.0.0                # Stable release

Pre-release versions have lower precedence than the release: 1.0.0-alpha.1 < 1.0.0-beta.1 < 1.0.0-rc.1 < 1.0.0

Build metadata

1.0.0+build.123
1.0.0+git.abc1234

Build metadata is ignored for version precedence. Two versions that differ only in build metadata are equivalent.

Version ranges in package managers

Rangenpm/semverMeaning
^1.2.3>=1.2.3 <2.0.0Compatible with 1.2.3 (same MAJOR)
~1.2.3>=1.2.3 <1.3.0Close to 1.2.3 (same MINOR)
1.2.3=1.2.3Exact version
>=1.2.0>=1.2.0At least 1.2.0

Conventional Commits

The Conventional Commits specification defines a commit message format that both humans and tools can read.

Format

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Types

TypeMeaningVersion bump
featNew featureMINOR
fixBug fixPATCH
perfPerformance improvementPATCH
refactorCode change that's not a feature or fixNo bump
choreMaintenance (deps, tooling)No bump
docsDocumentation onlyNo bump
styleFormatting, whitespace (no logic change)No bump
testAdd or correct testsNo bump
ciCI configurationNo bump
buildBuild system changesNo bump
revertRevert a previous commitPATCH

Scope (optional)

feat(auth): add OAuth2 support
fix(api): handle null response from upstream
chore(deps): bump axios from 1.6.0 to 1.7.0

Scope is a noun in parentheses identifying the area of the codebase — component, module, or subsystem.

Breaking changes — MAJOR bump

Two ways to signal a breaking change:

Option 1: Append ! after the type/scope:

feat!: redesign authentication API
feat(auth)!: remove legacy session tokens

Option 2: Add BREAKING CHANGE: in the footer:

feat(auth): add JWT-based authentication

BREAKING CHANGE: The `/api/auth` endpoint now returns a JWT token instead of a session cookie.
Clients using session-based auth must migrate to token-based auth.

Examples

feat: add support for multiple Redis clusters
fix: prevent crash when config file is missing
docs: update API reference for auth endpoints
chore(deps): bump @types/node from 20.0.0 to 22.0.0
feat(api)!: rename /users endpoint to /accounts
ci: add CodeQL security scanning to PR workflow
test(auth): add unit tests for token expiry handling
revert: "feat: add dark mode" (introduced regression in Safari)

Enforcing Conventional Commits

commitlint

commitlint checks commit messages against the conventional commits spec.

bash
1# Install
2npm install --save-dev @commitlint/cli @commitlint/config-conventional
3
4# Create config
5echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.mjs
6
7# Test manually
8echo "feat: add login page" | npx commitlint
9echo "added login page" | npx commitlint
10# ✖   subject may not be empty [subject-empty]
11# ✖   type may not be empty [type-empty]

Husky — git hooks manager

Husky runs commitlint automatically on every commit via the commit-msg git hook.

bash
1npm install --save-dev husky
2
3# Initialize Husky (creates .husky/ directory)
4npx husky init
5
6# Add the commit-msg hook
7echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg
8chmod +x .husky/commit-msg

Now any commit with a non-conventional message is rejected:

bash
git commit -m "fixed the bug"
# ⧗   input: fixed the bug
# ✖   subject may not be empty [subject-empty]
# ✖   type may not be empty [type-empty]
bash
git commit -m "fix: prevent crash when config file is missing"
# [main abc1234] fix: prevent crash when config file is missing

Enforce in CI (catch PRs that bypass hooks)

yaml
1# .github/workflows/pr-lint.yml
2name: Lint Commits
3on:
4  pull_request:
5
6jobs:
7  commitlint:
8    runs-on: ubuntu-latest
9    steps:
10      - uses: actions/checkout@v4
11        with:
12          fetch-depth: 0    # Full history needed to check all commits in the PR
13
14      - uses: actions/setup-node@v4
15        with:
16          node-version: 20
17
18      - run: npm ci
19
20      - name: Validate PR commits
21        run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose

CHANGELOG.md Structure

A conventional CHANGELOG groups commits by release and type:

markdown
1# Changelog
2
3## [2.0.0] - 2026-06-01
4
5### ⚠ BREAKING CHANGES
6
7* **auth:** remove legacy session tokens  migrate to JWT
8
9### Features
10
11* **api:** add pagination to /accounts endpoint
12* **auth:** add JWT-based authentication
13
14### Bug Fixes
15
16* prevent crash when config file is missing
17* **api:** handle null response from upstream correctly
18
19## [1.4.2] - 2026-05-15
20
21### Bug Fixes
22
23* **db:** retry connection on transient failures

This is the format Release Please generates automatically.


Release Please

Release Please is a Google-maintained GitHub Action that:

  1. Reads conventional commits since the last release
  2. Determines the next version (MAJOR/MINOR/PATCH bump)
  3. Opens a Release PR that updates CHANGELOG.md and version files
  4. When you merge the Release PR, it creates a GitHub Release and git tag

Setup

bash
# Install the CLI (optional — GitHub Actions is the primary interface)
npm install --save-dev release-please
yaml
1# .github/workflows/release-please.yml
2name: Release Please
3on:
4  push:
5    branches: [main]
6
7permissions:
8  contents: write
9  pull-requests: write
10
11jobs:
12  release-please:
13    runs-on: ubuntu-latest
14    steps:
15      - uses: googleapis/release-please-action@v4
16        id: release
17        with:
18          release-type: node    # or: python, go, java, rust, helm, simple

That's the minimal config. Release Please creates release-please-config.json and .release-please-manifest.json on first run.

Configuration: release-please-config.json

json
1{
2  "release-type": "node",
3  "bump-minor-pre-major": true,
4  "changelog-sections": [
5    { "type": "feat", "section": "Features" },
6    { "type": "fix", "section": "Bug Fixes" },
7    { "type": "perf", "section": "Performance" },
8    { "type": "deps", "section": "Dependencies" },
9    { "type": "chore", "section": "Miscellaneous", "hidden": true },
10    { "type": "docs", "section": "Documentation", "hidden": true }
11  ],
12  "extra-files": [
13    "charts/my-api/Chart.yaml"    // Also bump version in Helm chart
14  ]
15}

How Release Please works

After every merge to main:

  1. Scans commits since last release tag
  2. Computes next version based on commit types
  3. Opens (or updates) a Release PR titled "chore(main): release 2.1.0"
  4. The PR diff shows: updated CHANGELOG.md, updated package.json version field

When you merge the Release PR:

  1. Release Please creates a git tag v2.1.0
  2. Creates a GitHub Release with the changelog section as the release notes

Trigger deployments from the release

yaml
1# .github/workflows/deploy.yml
2name: Deploy on Release
3on:
4  release:
5    types: [published]      # Triggered when Release Please creates the release
6
7jobs:
8  deploy:
9    runs-on: ubuntu-latest
10    steps:
11      - uses: actions/checkout@v4
12
13      - name: Get release version
14        run: echo "VERSION=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
15
16      - name: Build and push
17        run: |
18          docker build -t ghcr.io/myorg/my-api:$VERSION .
19          docker push ghcr.io/myorg/my-api:$VERSION
20
21      - name: Deploy to production
22        run: kubectl set image deployment/my-api my-api=ghcr.io/myorg/my-api:$VERSION -n production

Monorepo Support

Release Please supports multiple independent packages in a monorepo:

json
1// release-please-config.json
2{
3  "packages": {
4    "packages/api": {
5      "release-type": "node",
6      "package-name": "@myorg/api"
7    },
8    "packages/worker": {
9      "release-type": "node",
10      "package-name": "@myorg/worker"
11    },
12    "charts/my-api": {
13      "release-type": "helm"
14    }
15  }
16}

Each package gets its own Release PR and version when commits touching its directory are merged.


The Full Workflow

Developer writes: feat(api): add rate limit headers
  → commits with Husky checking the message format
  → pushes to feature branch
  → opens PR
  → commitlint CI checks all commits in the PR
  → PR is merged to main

Release Please runs:
  → sees: feat(api): add rate limit headers (MINOR bump)
  → opens Release PR: "chore(main): release 1.5.0"
  → updates CHANGELOG.md with the feature listed
  → updates package.json version to 1.5.0

Team reviews and merges Release PR:
  → Release Please tags v1.5.0
  → GitHub Release created with CHANGELOG section as notes
  → Deploy workflow triggers on release published event
  → v1.5.0 image is built, signed, and deployed to production

Frequently Asked Questions

What actually counts as a breaking change?

Anything that requires a consumer to change something. That includes removing or renaming a field, tightening validation, changing a default, and altering behaviour a documented contract relied on. Adding an optional field is not breaking; making it required is. When in doubt, ask whether a caller doing nothing would still work.

Do I have to write commits this way for the tooling to work?

The tooling derives the version bump and changelog from commit types, so yes — inconsistent messages give an inconsistent version history. Enforce it in a commit hook and in CI rather than relying on discipline, because the one unconventional commit is the one that produces a wrong release.

How should I version pre-1.0 software?

By convention — semver itself says only that anything may change during 0.y.z — increment the minor for breaking changes and the patch for everything else, and be explicit that the API is unstable. The convention exists so consumers can pin sensibly. Staying on 0.x indefinitely to avoid committing to stability is common and worth being honest about in your README.

Should the changelog be generated or written?

Generated as a base, edited for the entries that matter. A purely generated changelog reads as a commit log and buries the two things a reader needs — what breaks and what they must do about it. Let tooling collect, then write the upgrade notes yourself.

What's Next

Next in CI/CD Pipelines

GitHub Actions: CI/CD for Containers and Kubernetes

Continue

We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.

Struggling with this in production?

We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.