Back to Thoughts

TIL: Your Commit Messages Are Release Inputs, Not Notes to Yourself

Feb 18, 2026

4 min read

View raw

I set up automated releases for this portfolio recently. semantic-release on every push to main — versions, tags, changelogs, GitHub releases, all automated.

The setup itself wasn't the insight. The insight was this: commit messages are release inputs, not notes to yourself.

When you write fix(feed): correct RSS item dates, you're not just describing a change. You're telling the release pipeline to bump the patch version, write a changelog entry, and generate a GitHub release. The message is configuration.

That reframing changes how you think about commits.


What semantic-release Does

Push to main. The pipeline runs. It looks at every commit since the last tag and decides what happens next:

  • Any fix: commits → bump patch (1.2.3 → 1.2.4)
  • Any feat: commits → bump minor (1.2.3 → 1.3.0)
  • Any breaking changes → bump major (1.2.3 → 2.0.0)

Then it updates CHANGELOG.md, creates a Git tag, publishes a GitHub Release, and commits the artifacts back to the repo.

If no releasable commits exist (only chore:, docs:, ci:), nothing happens. No release.

Zero manual steps. No version bump PRs. No "forgot to update the changelog" commits.


Conventional Commits: The Format

<type>(optional-scope): <short summary>

Examples:

feat(search): add keyboard navigation
fix(feed): correct RSS item dates
chore(ci): update pnpm cache strategy
docs(readme): add setup instructions

Breaking changes:

feat(api)!: remove legacy endpoint

# or with footer:
BREAKING CHANGE: /v1 endpoint removed

The types that matter for releases:

  • fix: → patch
  • feat: → minor
  • ! or BREAKING CHANGE: → major

Everything else (chore:, docs:, ci:, refactor:, style:, test:) → no release.


The Highest Bump Wins

This tripped me up initially. I assumed many fix: commits would "add up" somehow.

They don't. The pipeline picks the highest-priority bump from all commits since the last tag:

2 feat + 4 fix + 3 chore    minor (one feat is enough)
10 fix + 0 feat               patch
1 breaking + anything else    major

SemVer is about communicating impact, not commit volume. One feature changes the API surface more than ten fixes. That's the rule.


Tags Are the Source of Truth

semantic-release starts from the latest Git tag, analyzes all commits after it, and creates exactly one next version.

This means tags aren't decorative. They're checkpoints. The pipeline's entire history lives in tags.

If you've never tagged your repo, semantic-release uses the first commit as the baseline. If your tags are inconsistent or manual, the analysis breaks down.

Lesson: Don't create tags manually once you're on this workflow. Let the pipeline own them.


The Changelog Is Generated, Not Written

Before this setup, changelog entries were either missing, vague ("various improvements"), or written retrospectively from memory.

Now CHANGELOG.md is generated from commit metadata. Each release section is built from actual commit subjects, grouped by type. The quality of the changelog is a direct function of commit quality.

Write a vague commit: fix: stuff → changelog entry: "stuff"

Write a specific commit: fix(og): correct missing title in dynamic OG image → changelog entry that actually means something.


What Breaks This

A few things will silently break the pipeline:

Non-conventional commits

# These produce no release:
"Updated stuff"
"WIP"
"fixed the thing"

You can push all day with messages like this and semantic-release will never trigger. Not an error — just silence.

Inconsistent squash merges

If you squash PRs before merging to main, the squash commit message is what the pipeline reads. If your squash message is feat: add user dashboard — great. If it's Squash and merge (#42) — nothing happens.

Manually bumping package.json

Don't. The pipeline manages version. If you bump it manually and push, the pipeline will ignore it on the next commit and overwrite it with its own determination. Pick one: manual or automated.


Good Standards That Actually Help

After running this for a few weeks, a few habits made a real difference:

Use scope when it's not obvious

fix: correct date is vague. fix(feed): correct RFC 2822 date in RSS is searchable, useful in a changelog, and immediately locatable.

Keep subjects short and specific

The subject line is the changelog entry. Write it like you're writing a changelog entry — because you are.

Mark breaking changes explicitly

Add ! after the type: feat(api)!: remove v1 routes. Don't bury breaking changes in commit bodies. The automation won't find them there.

One logical change per commit

Two unrelated fixes in one commit means they get one changelog entry and one version bump — together. Separate them if they're separate things.


Local Dry Run Before Pushing

Before you trust the pipeline on an unfamiliar repo:

GITHUB_TOKEN=<token> pnpm exec semantic-release --dry-run --no-ci

Shows you exactly what version would be published and what the release notes would say — without actually publishing anything.

Useful for sanity-checking after a batch of commits.


The Mental Shift

Before this setup, a commit was a checkpoint. "Here's where I am."

After: a commit is a declaration. "Here's what this changes and why it matters."

The automation doesn't change the code. It just makes the cost of sloppy commits visible — a broken pipeline, a missing release, a useless changelog entry.

Which is probably the right incentive structure anyway.


Gopal Khadka