Semantic Versioning in CI
Semantic versioning in CI automates version bumps from conventional commits, tags releases in git, and wires tag events to publish pipelines — connecting human-readable versions…
Introduction
Semantic versioning in CI automates version bumps from conventional commits, tags releases in git, and wires tag events to publish pipelines — connecting human-readable versions to immutable artifacts.
The story
Release day chaos: three engineers manually edited package.json to "2.4.0", forgot to tag, and npm publish ran from main without changelog. Customers on 2.3.x never got security patch notes. Platform introduced semantic-release: commits on main trigger analyze → bump → tag → publish; git tag v2.4.1 triggers prod container rebuild from same commit. Release notes became automatic; rollback = deploy previous tag's artifact digest.
Understanding the topic
Semver (MAJOR.MINOR.PATCH) signals compatibility. CI connects commit message conventions to version bumps and release automation.
- Semver rules: MAJOR = breaking API; MINOR = backward-compatible feature; PATCH = backward-compatible fix.
- Conventional Commits:
feat:→ minor,fix:→ patch,feat!:orBREAKING CHANGE:footer → major. - Release tags: git tag
v1.2.3marks release point; CIon: push: tags: ['v*']triggers publish/deploy. - Pre-release:
1.2.0-beta.1for RC channels; separate npm dist-tag or container tag. - Monorepo versioning: independent per package (lerna/changesets) vs unified repo version.
Internal architecture
Commit → version → artifact → deploy automation flow:
Conventional commit on mainfix(auth): handle token expiry → PATCH bumpfeat(api): add export endpoint → MINOR bumpfeat!: drop v1 endpoints → MAJOR bump↓semantic-release / changesets / release-please├─ compute next version from commits since last tag├─ update CHANGELOG.md + package.json / Chart.yaml├─ git tag v2.4.1 + GitHub Release└─ trigger publish job (npm, PyPI, OCI)↓CI on tag v*├─ build artifact (if not already from main merge)└─ deploy prod with semver tag + digest metadata↓Consumers pin ^2.4.0 or exact 2.4.1 per policy
Visual explanation
Two diagrams show where Semantic Versioning in CI lives in the delivery path and how teams implement it in production.
Step-by-step explanation
- Adopt Conventional Commits — enforce with commitlint in CI on PR title and commits.
- Choose release tool: semantic-release (JS), release-please (multi-lang), changesets (monorepo), standard-version (manual trigger).
- Configure bot/service account with permission to push tags and CHANGELOG to main (branch protection exception or release branch).
- Wire tag push workflow: build, sign, publish to registry with semver tag AND git SHA tag.
- Document consumer pinning policy: libraries use semver ranges; services deploy exact digests with semver labels.
Production implementation
semantic-release + GitHub Actions with tag-triggered container publish:
- commitlint.config.js: extends ['@commitlint/config-conventional'].
- For monorepos use @changesets/cli — PR adds changeset file, merge triggers version PR.
- Avoid double-build: main merge builds artifact; tag workflow promotes existing digest with semver label.
# .github/workflows/release.ymlname: Releaseon:push:branches: [main]jobs:release:runs-on: ubuntu-latestpermissions:contents: writeissues: writepull-request: writesteps:- uses: actions/checkout@v4with: { fetch-depth: 0 }- uses: actions/setup-node@v4with: { node-version: 20 }- run: npm ci- run: npx semantic-releaseenv:GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}NPM_TOKEN: ${{ secrets.NPM_TOKEN }}# Separate workflow — deploy on tag# .github/workflows/deploy-release.ymlon:push:tags: ['v*']jobs:deploy-prod:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- run: echo "Deploy version ${GITHUB_REF_NAME} — use artifact from merge SHA"
Execution workflow
Enforce commit format
commitlint on PR; squash merge uses PR title as commit.
Real-world use
Conventional Commits spec powers semantic-release, Angular commit format, and GitHub release-please (used by Google Cloud client libraries). CalVer (calendar versioning) competes for continuously deployed SaaS — but semver dominates library ecosystems and API products.
Enterprise use cases
Kubernetes itself uses semver tags. Terraform providers publish semver to registry. Internal platforms often wrap release-please with approval on major bumps. Banks may require human sign-off on MAJOR — CI opens release PR, architect approves, bot tags.
- Library teams: npm semver ranges; breaking change = major with migration guide in CHANGELOG.
- Service teams: semver for communication; deploy unit remains image digest.
- Mobile: store version decoupled from semver but CI tags map builds to git.
Production case study
An open-core company published npm SDK and Docker API. Manual versioning caused 3 semver mistakes in one quarter (duplicate 1.4.0, skipped 1.3.2 patch). Adopted changesets + release-please.
- Process: PR must include changeset markdown; commitlint on PR title.
- Automation: release-please opens "release: sdk v2.1.0" PR; merge creates tag.
- CI: tag triggers npm publish and GHCR push with semver + digest.
- Outcome: zero manual version edits; support can correlate customer report to exact tag and digest.
Trade-offs
- Automated semver: consistent releases; requires commit discipline and bot write access to main.
- Manual versioning: human judgment on major; error-prone and skips changelog automation.
- CalVer: clear time ordering; poor signal for API compatibility.
- 0.x.y semantics: minor can break in 0.x — document policy for pre-1.0 services.
Security implications
Release automation bots are high-value targets — compromised token publishes malicious semver.
- Release bot uses fine-grained PAT or GitHub App with minimal permissions (contents, pull requests only).
- Tag protection restricts who can push
v*— prefer bot-only tag creation. - Verify npm/PyPI publish with provenance attestations (npm trusted publishing, OIDC).
- Audit MAJOR releases — breaking changes may remove security controls if changelog ignored.
Scalability analysis
High commit velocity on main can create release noise or version inflation.
- Batch releases with changesets (daily release PR) vs per-commit semantic-release.
- Monorepo: 50 packages × independent semver — changeset aggregation prevents 50 npm publishes per merge.
- Changelog size grows — link to GitHub Releases API for programmatic consumption.
- Tag explosion in git — retention policy for old pre-release tags.
Staff engineer insights
- Semver for libraries, digests for deploys — don't conflate the two in pipeline design.
- If commits aren't conventional, automation fails silently or mis-bumps — invest in commitlint early.
- Major bumps need migration docs — automation should fail if BREAKING CHANGE lacks body in CHANGELOG.
- Release-please's release PR is the human gate for regulated orgs uncomfortable with fully bot tagging.
Best practices
- Squash merge PRs with conventional title — one commit per PR simplifies semver analysis.
- Keep CHANGELOG in git; link releases to git compare view.
- Publish pre-releases to separate npm dist-tag (beta) or container tag suffix.
- Document 0.x breaking change policy for pre-stable APIs.
Common mistakes
- Running semantic-release on feature branches — accidental prerelease tags.
- Manual tag push bypassing CI — untested artifact labeled v1.0.0.
- Using semver ranges in prod deploy manifests instead of digests — float risk remains.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1AdvancedQuestionHow do conventional commits map to semver bumps in CI?+
Answer
Follow-up
2AdvancedQuestionDesign release automation for monorepo with 20 npm packages.+
Answer
Follow-up
3AdvancedQuestionTag-triggered deploy vs main-triggered deploy with semver label?+
Answer
Follow-up
4AdvancedQuestionCustomer reports bug on v2.3.1 — how do you locate code?+
Answer
Follow-up
5AdvancedQuestionWhen would you choose CalVer over semver?+
Answer
Follow-up
Hands-on exercise
Set up commitlint and create a conventional commit flow that would bump PATCH vs MINOR.
- Intentionally write bad commit message — confirm CI fails.
- Draft CHANGELOG entry format for each bump type.
- Map tag v1.0.0 to hypothetical deploy workflow trigger.
npm install -D @commitlint/cli @commitlint/config-conventionalecho "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js# Valid commits:git commit -m "fix(api): correct null pointer in handler" # PATCHgit commit -m "feat(ui): add dark mode toggle" # MINORgit commit -m "feat(auth)!: remove legacy token format" # MAJORnpx commitlint --from HEAD~1 --to HEAD --verbose
Summary
You can wire semver in CI through conventional commits, automated tagging, and publish pipelines — connecting commit messages to version bumps, changelogs, and release-triggered deploys without manual package.json edits.