How can a CI job derive the next version number and write the changelog automatically from commit messages, without a human editing a version file?
answer
- decision captured at commit time
- types map to bump levels
- last tag is the current version
- no version-bump commit, no loop
- squash merge rewrites the subject
basics
~20 sConventional Commits give each commit a type — fix, feat, or a breaking marker. A release job reads every commit since the last release tag, derives the SemVer bump from the highest-ranked type it finds, generates the changelog from the same messages, then tags and publishes.
solid answer
~50 sThe convention is Conventional Commits: each subject reads `type(scope): description`, with `feat` and `fix` as the two types that carry release meaning, and a breaking change signalled either by a `!` after the type or a `BREAKING CHANGE:` footer. A release tool such as semantic-release then runs on the default branch after merge: it finds the last release tag, collects the commits since it, maps `fix` to a PATCH, `feat` to a MINOR and any breaking marker to a MAJOR, takes the highest, and derives the next version. The same commits become the changelog entry, grouped by type. Crucially the version lives in the Git tag, not in a committed file, so the release does not push a version-bump commit that retriggers the pipeline. The convention has to be enforced — commitlint on commits, or a lint on the pull-request title when the merge is squashed, since that title becomes the commit message.
code
bash · 4 linesgit commit -m "fix(auth): reject refresh tokens past their expiry"
git commit -m "feat(api): add cursor pagination to the list endpoint"
git commit -m "feat(api)!: remove the v1 response envelope"
git commit -m "chore(deps): bump the linter"go deeper
Know that the commit message carries a type such as feat or fix, and that a tool reads those types after merge to pick the next version and write the changelog for you.
Explain the mapping from type to bump level, where the current version comes from, and why the release job should not push a version-bump commit back to the branch.
Demonstrate that you would enforce the convention where it actually lands — the pull-request title under squash merge — and be able to walk a failure where the wrong bump shipped, from message to analyser to published artifact.
Choose the model for the organisation: publish-on-merge, release-PR, or intent-file. Argue it from release cadence, the need for a human gate, and how many consumers a mislabelled major would disrupt.
## The problem being solved Hand-cut releases fail in predictable ways: someone forgets to bump the version, the changelog is written from memory the day of the release, and the tag does not match what was published. Commit-driven release automation removes the human from the mechanical part — the *decision* about the bump is still made by engineers, but they make it at commit time, in the commit message, when they still remember what the change did. ## The commit grammar Conventional Commits 1.0.0 specifies a subject line of the form: ``` <type>[optional scope][!]: <description> [optional body] [optional footer(s)] ``` `feat` and `fix` are the two types the specification gives release meaning. Other types in common use — `docs`, `refactor`, `test`, `build`, `ci`, `chore`, `perf`, `style` — carry no release meaning by default, though tools let you remap them. A breaking change is signalled two ways: an exclamation mark before the colon (`feat(api)!: drop the v1 envelope`), or a `BREAKING CHANGE: <explanation>` footer in the body. Either one forces a MAJOR. ## How the bump is computed A release run does roughly this: 1. Find the last release tag reachable on this branch — that is the current version, and the repository holds no other copy of it. 2. Collect the commits between that tag and HEAD. 3. Map each commit's type to a bump level, take the maximum, and apply it to the current version. 4. If nothing maps to a release (only `chore` and `docs`, say), publish nothing and exit successfully. This is a feature, not a failure. 5. Generate release notes by grouping the qualifying commits under headings — Features, Bug Fixes, BREAKING CHANGES. 6. Publish the artifact, create the tag, and record the notes. semantic-release splits exactly these responsibilities across plugins — `@semantic-release/commit-analyzer` for step 3, `@semantic-release/release-notes-generator` for step 5, then publishing plugins for step 6 — which is worth knowing because it is where you customise behaviour. ## Why the tag, not a version file, is the source of truth If the release job bumps a version field in a committed file, it must push that commit back to the branch. That creates two hazards: a push loop where the release commit triggers another pipeline run, and a race where a human merge lands between the read and the push. Deriving the current version from the last tag avoids both — the release job pushes a tag and an artifact, and never a source commit. Tools that do write a version file (many ecosystems require the manifest to contain the real version) usually make that an opt-in plugin and skip CI for the resulting commit. Note a related trap: several CI platforms deliberately suppress pipeline triggers for pushes made with the pipeline's own credentials, precisely to prevent loops. So a release job that pushes a tag may find that the tag pipeline never runs. Verify that behaviour on your platform rather than assuming either way. ## Enforcing the convention Automation is only as good as the messages. Two enforcement points: - **commitlint** (with `@commitlint/config-conventional`) run as a local hook and in CI, rejecting non-conforming subjects. - **The pull-request title**, when the repository squash-merges. Under squash merge the individual commit messages are discarded and the PR title becomes the commit subject — so linting commits alone silently protects nothing, and the title is what must be linted. ## Failure modes worth naming - A squash merge collapses one `feat` and one breaking change into a subject typed `chore`, and the release is silently skipped or under-bumped. - Someone writes the words "BREAKING CHANGE" in a commit body as prose; the footer parser sees it and publishes an unintended major. - A revert lands but is not typed as a revert, so the changelog claims a feature exists that was withdrawn. - The release job runs on a fork pull request and either fails for want of credentials or, worse, is granted them. ## The alternatives Two other models are common and worth being able to contrast: - **Release-PR tools** (release-please) accumulate changes into a standing "release" pull request whose diff *is* the version bump and changelog. Merging that PR cuts the release. The advantage is a human approval point and a visible preview; the cost is an extra merge in the loop. - **Changesets** ask the author to add an intent file describing the bump in the pull request itself, decoupling the release note from the commit grammar. This suits repositories publishing several packages, because each change declares which packages it affects. All three share the same principle: the release decision is captured as data at authoring time, and the pipeline merely executes it.
- Your team squash-merges every pull request. What does that do to commit-driven release automation?It discards the individual commit messages and replaces them with the pull-request title, so that title is the only message the analyser sees. Linting commits locally then protects nothing. The fix is to lint the PR title against the same convention and to make sure a breaking change in any squashed commit is restated in the title or the merge-commit body.
- A pull request contains only refactoring and documentation commits. What should the release job do when it merges?Nothing — no version bump, no tag, no publish, and it should exit successfully. Types with no release meaning are supposed to produce no release; treating that as a failure trains people to type everything as `fix` to make the pipeline green, which destroys the signal the convention exists to carry.
- How would you contrast semantic-release with a release-PR tool such as release-please?semantic-release publishes directly from the default branch on merge: fastest path, no human gate, and the release is a side effect of merging. release-please instead maintains a standing pull request holding the computed version bump and changelog; merging it cuts the release. You trade immediacy for a reviewable preview and an explicit approval moment, which suits teams that batch releases.
saying these in an interview costs you the question
- Storing the current version in a committed file
- Letting the release job push a bump commit
- Linting commits while squash-merging pull requests
- Treating a no-release run as a pipeline failure
- Believing the tool decides the bump on its own