Skip to content

Release Management

First PublishedByAtif Alam

Release management is the process of versioning, tagging, documenting, and publishing your software. Good release management makes deployments predictable, rollbacks traceable, and changelogs automatic.

The most widely adopted versioning scheme:

MAJOR.MINOR.PATCH
│ │ │
│ │ └── Bug fixes (backward compatible)
│ └──────── New features (backward compatible)
└────────────── Breaking changes (not backward compatible)
Examples:
1.0.0 → Initial release
1.1.0 → Added a new feature
1.1.1 → Fixed a bug
2.0.0 → Breaking API change
Change TypeVersion BumpExample
Bug fix, patch1.2.31.2.4Fix login timeout
New feature (backward compatible)1.2.31.3.0Add search endpoint
Breaking change1.2.32.0.0Remove deprecated API
2.0.0-alpha.1 First alpha
2.0.0-beta.1 First beta
2.0.0-rc.1 Release candidate
2.0.0 Stable release
  • Patch version MUST be incremented for backward-compatible bug fixes.
  • Minor version MUST be incremented for backward-compatible new functionality.
  • Major version MUST be incremented for any backward-incompatible changes.
  • Once a version is released, its contents MUST NOT be modified — release a new version instead.

Conventional Commits provide a structured commit message format that automation tools can parse to determine the version bump:

feat: add user search endpoint → minor bump (1.2.0 → 1.3.0)
fix: correct login timeout handling → patch bump (1.2.0 → 1.2.1)
feat!: redesign authentication API → major bump (1.2.0 → 2.0.0)
Format:
<type>(<scope>): <description>
[optional body]
[optional footer(s)]
BREAKING CHANGE: <description>
Commit TypeSemVer Bump
fix:Patch
feat:Minor
feat!: or BREAKING CHANGE: footerMajor
docs:, chore:, style:, refactor:, test:No bump (or patch, configurable)

semantic-release fully automates versioning, changelog, and publishing based on commit messages:

Commits since last release:
feat: add search API
fix: handle null response
docs: update README
semantic-release determines:
→ Minor bump (feat present)
→ 1.3.0 → 1.4.0
→ Generates CHANGELOG entry
→ Creates Git tag v1.4.0
→ Creates GitHub release
→ Publishes to npm (if configured)
Terminal window
npm install --save-dev semantic-release @semantic-release/changelog @semantic-release/git
.releaserc.json
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/github",
["@semantic-release/git", {
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]"
}]
]
}
name: Release
on:
push:
branches: [main]
permissions:
contents: write
issues: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

release-please creates a release PR that accumulates changes. When merged, it creates the tag and GitHub release:

Commits accumulate on main:
feat: add search
fix: handle null
release-please opens a PR:
"chore: release 1.4.0"
- Updates CHANGELOG.md
- Bumps version in package.json
When you merge the PR:
→ Git tag v1.4.0 created
→ GitHub release created
name: Release
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
release-type: node
publish:
needs: release-please
if: ${{ needs.release-please.outputs.release_created }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Changesets is popular in monorepos — developers add changeset files with their PRs:

Terminal window
# Developer runs this when making a change
npx changeset add
# Prompts: What packages changed? Major/minor/patch? Description?
# Creates .changeset/funny-llamas-swim.md
.changeset/funny-llamas-swim.md
---
"@myorg/api": minor
"@myorg/shared": patch
---
Add user search endpoint to the API.

When ready to release, a bot PR accumulates changesets and bumps versions.

Featuresemantic-releaserelease-pleaseChangesets
Version sourceCommit messagesCommit messagesChangeset files
Human reviewNo (fully automatic)Yes (release PR)Yes (changeset PR)
MonorepoVia pluginsYes (manifest mode)Yes (native)
ChangelogAuto-generatedAuto-generatedAuto-generated
Publishnpm, GitHub, Docker, etc.GitHub release (publish separately)npm (native)
Best forLibraries, single packagesAny project wanting review stepMonorepos

Tags mark specific commits as releases:

Terminal window
# Annotated tag (recommended for releases)
git tag -a v1.4.0 -m "Release 1.4.0: add search API, fix login timeout"
git push origin v1.4.0
# List tags
git tag -l "v1.*"
# Delete a tag
git tag -d v1.4.0
git push origin --delete v1.4.0
ConventionExampleUse Case
v prefixv1.4.0Most common for application releases
No prefix1.4.0Some package managers prefer this
Package scoped@myorg/api@1.4.0Monorepo per-package tags
Date-based2026.02.17CalVer (calendar versioning)
# GitHub Actions — run on version tags
on:
push:
tags:
- 'v*' # v1.0.0, v2.1.3, etc.
# GitLab CI
release:
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+/

Most release tools generate changelogs from commit messages:

# Changelog
## [1.4.0] - 2026-02-17
### Features
- Add user search endpoint (#123)
- Support pagination in list API (#125)
### Bug Fixes
- Fix login timeout handling (#128)
- Correct null response in profile API (#130)
## [1.3.0] - 2026-02-10
...

The Keep a Changelog convention uses these categories:

CategoryWhat Goes Here
AddedNew features
ChangedChanges to existing functionality
DeprecatedFeatures that will be removed
RemovedFeatures that were removed
FixedBug fixes
SecurityVulnerability fixes
Terminal window
# Create a release via CLI
gh release create v1.4.0 \
--title "v1.4.0" \
--notes "## What's Changed
- Add search API (#123)
- Fix login timeout (#128)" \
--target main
# Upload release assets
gh release upload v1.4.0 ./dist/myapp-linux-amd64 ./dist/myapp-darwin-amd64
# Auto-generate release notes from PRs
gh release create v1.4.0 --generate-notes

GitHub can auto-generate release notes from merged PRs — grouped by labels.

release:
stage: deploy
image: registry.gitlab.com/gitlab-org/release-cli:latest
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+/
script:
- echo "Creating release for $CI_COMMIT_TAG"
release:
tag_name: $CI_COMMIT_TAG
name: "Release $CI_COMMIT_TAG"
description: ./CHANGELOG.md
assets:
links:
- name: Linux Binary
url: https://example.com/myapp-linux-amd64
publish-npm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish-pypi:
runs-on: ubuntu-latest
permissions:
id-token: write # OIDC (Trusted Publishers)
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install build
- run: python -m build
- uses: pypa/gh-action-pypi-publish@release/v1
# Uses OIDC — no API token needed (Trusted Publisher configured on PyPI)
publish-docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.ref_name }}
ghcr.io/${{ github.repository }}:latest
main: commit ──► CI ──► tag v1.4.0 ──► publish
commit ──► CI ──► tag v1.4.1 ──► publish

Best for libraries, APIs, SaaS. Every merge to main is a potential release.

main: commit ──► commit ──► commit ──► Release cut (every 2 weeks)
──► tag v1.4.0 ──► publish

Best for products with coordinated releases, QA cycles, or marketing announcements.

main ────────●──────●──────●──────●──────●───►
\ \
▼ ▼
release/1.4 ──● ──● release/1.5 ──●
(hotfixes) (hotfixes)

Best for software that maintains multiple versions simultaneously (e.g. open-source libraries).

Some projects use date-based versioning instead of SemVer:

2026.02.17 (YYYY.MM.DD)
2026.2.1 (YYYY.M.micro)
26.2 (YY.M)
ProjectCalVer Format
UbuntuYY.MM (e.g. 24.04)
pipYY.N (e.g. 24.0)
TerraformMAJOR.MINOR.PATCH (SemVer, but frequent)
Docker DesktopMAJOR.MINOR.PATCH (SemVer)

CalVer works well for projects where compatibility is less relevant than recency (e.g. operating systems, infrastructure tools).

  • Semantic Versioning (MAJOR.MINOR.PATCH) communicates the impact of changes to consumers.
  • Conventional Commits enable automated version bumps — feat: = minor, fix: = patch, feat!: = major.
  • semantic-release is fully automatic; release-please adds a review step via PR; Changesets is best for monorepos.
  • Git tags mark release points — trigger pipelines with tag-based triggers (v*).
  • Changelogs should be auto-generated from commits or PRs — not hand-written.
  • GitHub/GitLab releases combine a tag with release notes and downloadable assets.
  • Choose a release strategy: continuous (every merge), scheduled (release train), or branched (multiple versions).