
Photo by Xavi Cabrera
Owning Your Version
Previously, I talked about Conventional Commit Caveats, but it’s still my preferred style of writing git commits. Since it’s such a lightweight specification, we can easily implement our own automated version management system.
First, there are many implementations already out there; plugins for IDE’s and linters, standalone scripts/binaries, and CI/CD templates to name a few. Many of which are likely more feature rich than what I will show here. If you are implementing an automated version management system for a large organization across many types of projects, there may be projects that wish to make full use of the Semantic Version specification in ways you did not foresee.
Choosing an existing implementation with a large feature set may serve you best. However, I love that the simplicity of the specification allows us to write a small script that will cover majority of our use case, should be easily understood at a glance by most developers, empowers us to support types we want, and doesn’t require any 3rd-party dependencies that we might feel obligated to vet or inventory.
Update: Socket.dev posted a report on Feb 20, 2026: SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains:
The Action also patches release toolchain configurations (.releaserc, .releaserc.json, .release-it.json, and variants) to inject @semantic-release/exec with prepareCmd: "node -e \"try{require('CARRIER')}catch(e){}\"". This ensures every subsequent npm publish from the infected repo silently executes the carrier package, creating an additional propagation path through the release pipeline.
Public GitHub Action: ci-quality/code-quality-check
If you truly don’t need the entire Semantic Versioning specification, don’t throw the kitchen-sink at it.
git log
We need to craft a git log command that will make it easy to parse for
Conventional Commit specification; therefore, the important parts are the
subject and git trailers.
git log allows us to
specify the exact format string, and even provides a
trailers placeholder
that has options to limit the git trailer key we are looking for.
The default separator is a newline, so we specify a comma using format code
“%x2c”.
Of course, the default is to print in reverse chronological order, so we must
also specify --reverse to read our Conventional Commits chronologically.
git log --reverse --no-merges \
--pretty='format:%s %(trailers:key=BREAKING-CHANGE,separator=%x2C)'
awk
Below is an awk script that supports the git trailer key “BREAKING-CHANGE” and a subject scope/type appended with an exclamation. We can also choose the exact types we want to support, along with which types should increment our semantic version.
#!/usr/bin/awk -f
BEGIN { major = minor = patch = 0 }
{
if ($0 ~ /BREAKING-CHANGE:|^[^\s:]+!:/) {
major++; minor = patch = 0
} else if (tolower($0) ~ /^(feat|build|ci|chore)[^\s:]*:/) {
minor++
} else if (tolower($0) ~ /^fix[^\s:]*:/) {
patch++
} else if (tolower($0) !~ /^(docs|style|refactor|test)[^\s:]*:|fixup!/) {
printf "Invalid type: %s\n", $0 > "/dev/stderr"
}
}
END { printf "v%d.%d.%d\n", major, minor, patch }
Don’t be tempted to adjust the awk script to support “BREAKING CHANGE” because
git will never print it using the %(trailers) placeholder since whitespace
is not a valid character for git trailer keys.
Go
How do we integrate this into a our Go project? During the build process, we can set the value of an uninitialized variable:
go build -ldflags="-X 'importpath.name=value'"
make
Finally, I like to use a Makefile with a “build” and “version” target:
build: clean ## Build binary
ifeq ($(shell git rev-parse --is-inside-work-tree 2>/dev/null),true)
@$(eval VERSION := $(shell $(MAKE) version --no-print-directory))
go build --ldflags '-X app.version.Version='$(VERSION)
else
$(error Not in a git repository)
endif
version: ## Make version
ifeq ($(shell git rev-parse --is-inside-work-tree 2>/dev/null),true)
@git log --reverse --pretty='format:%s %(trailers:key=BREAKING-CHANGE,separator=%x2C)' | \
awk 'BEGIN { major = minor = patch = 0 } { \
if ($$0 ~ /BREAKING-CHANGE:|^[^\s:]+!:/) { \
major++; minor = patch = 0 \
} else if (tolower($$0) ~ /^(feat|build|ci|chore)[^\s:]*:/) { \
minor++ \
} else if (tolower($$0) ~ /^fix[^\s:]*:/) { \
patch++ \
} else if (tolower($$0) !~ /^(docs|style|refactor|test)[^\s:]*:|fixup!/) { \
printf "Invalid type: %s\n", $$0 > "/dev/stderr" \
} \
} END { printf "v%d.%d.%d\n", major, minor, patch }'
else
$(error Not in a git repository)
endif
Conclusion
With just about ten lines of awk, we have a fairly robust implementation of an automated version management system using Conventional Commits and Semantic Versioning.