Case study
Go Release Manager
A Go command-line tool that decides a software's next version from its commit messages and creates the tag. It runs in production: it versions sdd-kit.
- v1.3.0 in production, versioning sdd-kit
- 97% test coverage
- 3 operating systems in CI: Linux, macOS and Windows
Figures from release v1.3.0, measured on 2026-10-15 (96.9% coverage). Everything can be checked in the public repository.
See the repository →Read the docs →Download the releases →
$ git log --oneline
fa4ed96 docs: usage examples
1be5e0c feat(cli)!: drop --legacy
57bc93a fix(parser): handle empty input
43d79e8 (tag: v1.3.0) feat: first command
$ go-release-manager create --dry-run --explain
No .go-releaserc.yml found. Using the default rules (feat/fix).
Latest stable version: v1.3.0
Analyzing 3 commit(s) since tag v1.3.0...
Analysis done. Highest increment: Major
Increment: Major. Next version: v2.0.0
--- DRY RUN ---
Latest tag: v1.3.0
Commits analyzed: 3
Increment: Major
Decided by:
Major feat(cli)!: drop --legacy ("!" in the header)
Tag that would be created: v2.0.0
--- END OF DRY RUN ---
$ go-release-manager check-pr --title "feat: drop --legacy" \
--body "BREAKING CHANGE: --legacy was removed"
Pull request: feat: drop --legacy
Breaking change announced in: PR description
✗ [breaking-not-in-title] the pull request announces a breaking change (PR description), but the title has no "!", and the title is what a squash merge keeps: use "feat!: drop --legacy"
1 error, 0 warningsThe problem
Deciding the next version is often done by hand: someone reads the commits, picks patch, minor or major and creates the tag. It is slow, relies on memory and goes wrong easily, above all with pre-releases and breaking changes.
Tools such as semantic-release solve it, but bring Node.js, plugins and configuration to projects that use none of them.
The solution
A single binary with no dependencies that reads the commits since the latest stable version, applies Conventional Commits and computes the next SemVer tag. The tool does one thing: building, release notes and publishing stay with GoReleaser.
- The next command prints only the version, for scripts; the JSON output gives the full result.
- The dry run (--dry-run) shows everything without writing anything or asking for credentials.
- Pre-releases per channel (beta, rc) with a continuous counter, a forced version when the plan calls for one, and tagging a specific commit.
- A ready-made GitHub Action that downloads the release binary and verifies its checksum before running.
- --explain lists the commits that decided the increment and the rule behind each one, so nobody has to guess why the version came out major.
Breaking changes in squash merges
After the launch, a reader asked what happens in a squash merge, where only the final commit message decides the version. If the BREAKING CHANGE notice lives only in the PR description or in a commit in the middle, and the title has no "!", the release comes out minor when it should be major, and users upgrade without knowing something broke.
v1.3.0 closes that gap in layers, all with the same detection rule as the version calculation:
- The check-pr command fails the PR when a break announced anywhere is missing from the title, and suggests the right title; it also warns about a misplaced footer and a misspelled "BREAKING CHANGES".
- A check-pr Action for CI, which reads the title and description from the event file, not from the script, so a hostile title cannot inject code.
- A post-merge safety net that compares the commit that landed on main with its PR and opens an issue if the break was lost.
- A best-practices guide, in Portuguese and English, covering what GitHub keeps in each squash setting and how to write the PR.
How quality is enforced
The project follows the same process I use at work:
- Spec-driven development: every versioning rule has acceptance criteria, and every criterion points to the test that proves it.
- Unit and end-to-end tests, with 97% code coverage; CI requires at least 80%.
- CI on every pull request: lint, tests with race detection, build, the README commands, the Action running on Linux, macOS and Windows, and check-pr itself checking the title.
- Automated releases: the tool computes its own version, and GoReleaser publishes binaries for five platforms with SHA-256 checksums.
- One branch and one pull request per task, with Conventional Commits and a CHANGELOG.
Technical decisions
- The base is always the highest reachable stable tag, so a pre-release never becomes the base by mistake.
- On 0.x versions, a breaking change bumps the minor, as SemVer says.
- Diagnostics go to stderr and the result to stdout: scripts read the output without filtering anything.
- The tool speaks English, and the documentation is a complete guide in Portuguese and English.
Outcome
The tool computes the versions of sdd-kit, my spec-driven development kit, at every phase closing. In the production review, new tests raised coverage of the whole codebase from 7.9% to 97.1%, and a defect in the published binary's --version was found and fixed before release to the public.
Limits
Real-world use is still sdd-kit and my own projects; validation by other teams is missing. check-pr only catches declared breaks (with "!" or BREAKING CHANGE): a break nobody announced would need a comparison of the public API between versions. There is no published Docker image, and on purpose the tool neither writes the changelog nor publishes the release by itself.
Need someone who delivers like this?
I work in QA and development: specs, automated tests, CI and delivery all the way to production, with documentation for the people who will use it.
Get in touch →