What you will achieve
You will set up automated version bumping and changelog generation driven by Conventional Commits. Push a feature, get a minor version bump and changelog entry; push a fix, get a patch bump - all without manual intervention.
The mechanism
Tooling reads commit messages following the Conventional Commits format (feat:, fix:, chore:) and decides what kind of release to cut. BREAKING CHANGE: footers trigger major bumps. The result: a strict mapping from commit history to release version.
Step 1: adopt Conventional Commits
git commit -m "feat(checkout): add Apple Pay support"
git commit -m "fix(auth): handle expired refresh tokens"
git commit -m "chore(deps): bump axios to 1.7.0"
git commit -m "feat!: drop Node 16 support
BREAKING CHANGE: minimum Node version is now 18."
Common types: feat, fix, chore, docs, refactor, test, perf, style.
Step 2: enforce the format
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky
# commitlint.config.js
module.exports = { extends: ['@commitlint/config-conventional'] };
# .husky/commit-msg
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
npx --no -- commitlint --edit $1
Now badly formatted commits are rejected at commit time.
Step 3: install release tooling
Common choices: semantic-release, release-please, changesets, standard-version. We use release-please:
# .github/workflows/release-please.yml
name: release-please
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
release-type: node
package-name: my-app
Step 4: how release-please works
On every push to main, release-please:
- Reads commits since the last release tag.
- Categorises by type and decides the next version.
- Updates
CHANGELOG.mdandpackage.jsonversion. - Opens (or updates) a "release PR" with these changes.
When you merge the release PR, release-please:
- Tags the merge commit (
v1.4.0). - Creates a GitHub release.
- Triggers any tag-based publish workflow.
Step 5: trigger the publish workflow
# .github/workflows/publish.yml
name: publish
on:
push:
tags: ['v*.*.*']
jobs:
publish:
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 run build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Step 6: alternative - changesets
For monorepos, changesets works well:
npm install --save-dev @changesets/cli
npx changeset init
Each PR runs npx changeset, which produces a markdown file describing the change and its impact. On merge, a "Version packages" PR rolls up changesets into version bumps and changelog entries; merging that PR cuts the release.
Step 7: alternative - semantic-release
npm install --save-dev semantic-release
# .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"
]
}
semantic-release is fully automated - merging to main triggers the release pipeline directly, no separate release PR. Tradeoff: less human review of the version bump.
Step 8: pre-releases
For betas:
git checkout -b beta
git push -u origin beta
# release-please configured to handle beta channel
Or with semantic-release:
{
"branches": ["main", { "name": "beta", "prerelease": true }]
}
Step 9: write good commit messages
The system is only as good as its inputs:
feat(checkout): add Apple Pay support
Implements Apple Pay via the Web Payments API. Falls back to the
existing card flow if Apple Pay is unavailable. Closes #1234.
Step 10: handle squashes
If you squash-merge, the squash commit message becomes the entry release-please reads. Configure GitHub to use the PR title as the squash subject - then the PR title needs to follow Conventional Commits format. commitlint on PR titles in CI helps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
The result
Versions and changelogs maintain themselves. Releases are predictable and auditable. The discipline pays off the first time someone asks "when did this regression appear?" and the answer is in the auto-generated changelog.