Lesson 16 of 30
No one types a version number by hand. The pipeline reads your commit messages, decides whether that's a patch, minor, or major bump, and writes the changelog for you.
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Bumping a version number by hand doesn't scale: someone has to remember whether the last change was breaking, someone has to write the changelog, someone has to actually run npm version and tag it, and all of that is exactly the kind of judgment call that gets skipped under deadline pressure. Semantic-release automates the whole decision by reading your commit history since the last release and computing the next version from it — no human opinion required, because the commit messages already encode the opinion.
That only works if commit messages follow a predictable shape. Conventional Commits is that shape: type(scope): description, where type is one of a small fixed set — feat, fix, docs, chore, refactor, and a few others. semantic-release maps types to version bumps: fix -> patch, feat -> minor, and a BREAKING CHANGE marker (either ! after the type, or a BREAKING CHANGE: footer in the commit body) -> major. Everything else, like docs or chore, triggers no release at all.
Four commits land on main since the last release, v2.3.0:
docs: fix typo in README
fix(auth): correct token expiry off-by-one
feat(auth): add refresh token rotation
feat(api)!: remove deprecated /v1/users endpoint
BREAKING CHANGE: /v1/users is gone, use /v2/users instead
semantic-release scans all four, ignores the docs commit (no release-worthy content), and finds a fix, a feat, and a feat with a breaking-change marker. The breaking change wins: next version is v3.0.0, not v2.4.0 or v2.3.1. It writes a CHANGELOG.md entry grouping the commits under "Features," "Bug Fixes," and "BREAKING CHANGES," creates the git tag, and publishes a GitHub Release — all from a job that runs after your existing test/build job passes:
release:
needs: verify
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Fix login bug instead of fix: login bug and semantic-release simply doesn't see a release-worthy commit — no error, no release, silence. Teams that rely on this enforce the format with a commit-linter (like commitlint) in CI, rejecting the commit before it ever lands, rather than trusting everyone to remember the format.main, semantic-release only ever sees that one final message — the twelve original messages don't exist on main and don't count. The squash commit's own type is what decides the bump, so it has to be written correctly, not the twelve commits behind it.! right after the type/scope and a BREAKING CHANGE: footer are both valid, independently. If half the team only knows about one of them, some real breaking changes silently ship as minor bumps.Whatever version semantic-release computes still ends up as a real git tag, pushed to the repo, exactly like one you'd create yourself. The mechanics of what that tag actually is — a fixed pointer to one commit, annotated with a message, versus a plain moving reference — are the Git track's tags-and-releases lesson. This lesson is just about who's typing the tag command: here, it's a bot in CI reading your commit history, not a person deciding by hand.