Release Workflow
A release is a deliberate, versioned snapshot of your codebase that you ship to users. Unlike the continuous stream of commits on main, a release is a named moment in time — v2.0.0, v3.1.4 — with release notes, a version tag in the repository, and (for many projects) artifacts published to npm, PyPI, Docker Hub, or a CDN.
This guide walks through the full release lifecycle: feature freeze, testing, changelog, version bumping, tagging, publishing a GitHub Release, automating with semantic-release, and post-release cleanup.
Preparing a Release
Before cutting any tags or bumping any versions, the codebase needs to be ready. This preparation phase applies to both GitHub Flow and Gitflow.
Release preparation checklist:
Feature freeze: agree on a cutoff date — no new features after this point, only bug fixes and release-related changes.
Full test suite green: run
npm test/pytest/go test ./...locally and confirm CI is clean.No known blocking bugs: all P0/P1 issues are resolved or deferred to the next release.
Changelog drafted: every user-visible change is documented (see below).
Dependencies reviewed: lock files committed, no known security vulnerabilities (
npm audit,pip-audit).Migration guide written (if breaking changes exist): users need to know what to do before upgrading.
Documentation updated: README, API docs, and inline comments reflect the new version.
Drafting the Changelog
A changelog is the human-readable record of what changed between versions. Follow the Keep a Changelog format (keepachangelog.com):
CHANGELOG.md
# Changelog All notable changes to this project will be documented in this file. Format is based on Keep a Changelog, versioning on Semantic Versioning. ## [Unreleased] ## [2.0.0] - 2026-05-20 ### Breaking Changes - **api**: `/users` endpoint renamed to `/accounts` — update all clients - **config**: `DATABASE_URL` env var renamed to `DB_CONNECTION_STRING` ### Added - feat(auth): Google OAuth login support (#142) - feat(ui): dark mode toggle (#189) - feat(api): bulk import endpoint for CSV uploads (#201) ### Changed - perf(query): optimised user search — 4x faster on large datasets (#195) - refactor(auth): migrate from JWT to session tokens (#197) ### Fixed - fix(cart): prevent negative quantity on decrement (#209) - fix(email): correctly handle unicode in subject lines (#215) ### Deprecated - `getUserById` — use `getAccountById` instead (removed in v3.0.0) ## [1.9.4] - 2026-04-10 ### Fixed - fix(payment): add null check before charge processing (#501)
Gitflow Release Branch Approach
In Gitflow, a release/ branch is created from develop when you are ready to prepare the release. Only bug fixes, version bumps, and release docs go on this branch. Feature development continues on develop uninterrupted.
1. Create the release branch from develop
git checkout develop git pull origin develop git checkout -b release/2.0.0 # Switched to a new branch 'release/2.0.0'
2. Bump the version
npm version major --no-git-tag-version # v2.0.0 git add package.json package-lock.json git commit -m "chore(release): bump version to 2.0.0"
3. Fix any last-minute release bugs on this branch
# (Only bug fixes — no new features) git add src/api/accounts.ts git commit -m "fix(api): correct 404 message for missing account" # Update CHANGELOG.md git add CHANGELOG.md git commit -m "docs: update CHANGELOG for 2.0.0"
4. Merge into main and tag
git checkout main git pull origin main git merge --no-ff release/2.0.0 -m "Release v2.0.0" git tag -a v2.0.0 -m "Release v2.0.0" git push origin main --follow-tags
5. Merge back into develop
git checkout develop git merge --no-ff release/2.0.0 -m "Merge release/2.0.0 back into develop" git push origin develop
6. Delete the release branch
git branch -d release/2.0.0 git push origin --delete release/2.0.0
GitHub Flow Release (Tag main Directly)
In GitHub Flow there is no separate release branch — you simply tag a specific commit on main. This is simpler and works well for continuous delivery projects.
Tag a release on main
git checkout main git pull origin main # Confirm you are at the right commit git log --oneline -5 # a3f9c21 (HEAD -> main, origin/main) fix(api): correct 404 message # b1d8e34 feat(auth): add Google OAuth login # ... # Create the annotated tag git tag -a v2.0.0 -m "Release v2.0.0 ## Breaking Changes - /users endpoint renamed to /accounts - DATABASE_URL renamed to DB_CONNECTION_STRING ## What's new - Google OAuth login - Dark mode toggle - Bulk CSV import Full changelog: CHANGELOG.md" git push origin v2.0.0
Bumping the Version
Ecosystem | File(s) to update | Command |
|---|---|---|
Node.js | package.json, package-lock.json | npm version major|minor|patch --no-git-tag-version |
Python (PEP 621) | pyproject.toml | bump2version major|minor|patch |
Python (setup.py) | setup.py, version.py | bump2version major|minor|patch |
Go | version.go or go.mod | Manual edit + git tag with v prefix |
Rust | Cargo.toml | cargo set-version 2.0.0 |
Java/Maven | pom.xml | mvn versions:set -DnewVersion=2.0.0 |
Ruby | lib/project/version.rb, *.gemspec | gem bump --version major |
Node.js version bump example
# Current: 1.9.4 # Patch: 1.9.4 -> 1.9.5 npm version patch --no-git-tag-version # Minor: 1.9.4 -> 1.10.0 npm version minor --no-git-tag-version # Major: 1.9.4 -> 2.0.0 npm version major --no-git-tag-version # Specific version npm version 2.0.0 --no-git-tag-version # Then commit git add package.json package-lock.json git commit -m "chore(release): bump version to 2.0.0"
Creating a Git Tag
Annotated tag — the right way for releases
git tag -a v2.0.0 -m "Release v2.0.0 Breaking changes, new features, and bug fixes. See CHANGELOG.md for full details." # List all tags git tag # v1.9.0 # v1.9.1 # ... # v2.0.0 # Show tag details git show v2.0.0 # tag v2.0.0 # Tagger: Alex Developer <alex@example.com> # Date: Wed May 20 10:00:00 2026 +0000 # # Release v2.0.0 ... # Push the tag git push origin v2.0.0 # Push all tags at once (use carefully) git push origin --tags
Creating a GitHub Release
A GitHub Release is a wrapper around a tag that adds a title, release notes in Markdown, and optional binary file attachments. It lives on the GitHub Releases page and can trigger deployment workflows.
Option A: GitHub web UI — go to your repository → Releases → Draft a new release → choose the tag → fill in the title and notes → Publish.
Option B: GitHub CLI (gh):
Create a GitHub Release with gh CLI
# Create from existing tag gh release create v2.0.0 \ --title "v2.0.0 — Dark Mode and Google OAuth" \ --notes-file RELEASE_NOTES.md # Create release and tag at the same time gh release create v2.0.0 \ --title "v2.0.0" \ --generate-notes \ --target main # Attach a build artifact gh release create v2.0.0 \ --title "v2.0.0" \ --notes "See CHANGELOG.md" \ ./dist/myapp-linux-amd64 \ ./dist/myapp-darwin-arm64
Automated Releases with semantic-release
semantic-release automates the entire release workflow: it reads your Conventional Commits, determines the next version number, generates a changelog, creates a git tag, publishes to npm/GitHub Packages, and creates a GitHub Release — all triggered by a CI push to main.
Install semantic-release
npm install --save-dev semantic-release \ @semantic-release/changelog \ @semantic-release/git \ @semantic-release/github
.releaserc.json
{
"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": ["package.json", "CHANGELOG.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]"
}],
"@semantic-release/github"
]
}.github/workflows/release.yml
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
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: npm test
- run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}Commit type | Version bump triggered |
|---|---|
fix: | Patch (1.0.0 → 1.0.1) |
feat: | Minor (1.0.0 → 1.1.0) |
feat! or BREAKING CHANGE: | Major (1.0.0 → 2.0.0) |
chore:, docs:, test: | No release |
Post-Release Steps
After the release is live, there are a few housekeeping tasks:
Merge release branch back to develop (Gitflow): as shown above — critical to avoid losing release fixes.
Announce the release: post in your team channel, update your status page, send release emails if you have subscribers.
Publish to registries:
npm publish, push Docker image, deploy to CDN — if not already automated.Monitor the release: watch error rates, latency, and logs for 30–60 minutes after deployment.
Create the next Unreleased section in CHANGELOG.md:
git commit -m "chore: open changelog for next release"— makes it easy to add entries as work proceeds.Update documentation site: if you host versioned docs (like Docusaurus or MkDocs), publish the new version.
Milestone: close the GitHub Milestone for this version and open one for the next.