Kamal Auto-Versioning
Kamal Auto-Versioning
When to use it: you deploy with Kamal and want a real version number that bumps itself
from what changed —feat:→ minor,fix:→ patch, breaking → major — embedded in the
binary and tagged in git, all wired through.kamal/hooks/. Keyword bait: "version bump",
"semver", "conventional commits", "kamal hook", "pre-deploy/post-deploy", "git tag on deploy",
"svu".
The model: git tags are the source of truth, bumped by svu
from Conventional Commits; Kamal keeps tagging images
by git SHA (unchanged); hooks show the pending version and create the tag only after a healthy
deploy, so a failed deploy never leaves a dangling tag.12
Bump rules
svu reads the latest vX.Y.Z tag and the commits since it. The highest bump implied wins:2
| Commit | Bump |
|---|---|
fix: … |
PATCH → X.Y.(Z+1) |
feat: … |
MINOR → X.(Y+1).0 |
feat!: … or a BREAKING CHANGE: footer (any type) |
MAJOR → (X+1).0.0 |
chore/docs/refactor/test/ci/… |
no bump (spec default) |
Setup
go install github.com/caarlos0/svu/v3@latest # or brew install svu — single Go binary, no CI needed
git tag v0.1.0 && git push origin v0.1.0 # one-time baseline if no vX.Y.Z tag exists yet
svu next # preview the next version from history
The hooks
Hooks live in .kamal/hooks/, run locally, must be executable with no file extension,
and a non-zero exit aborts the deploy. Order on kamal deploy:
pre-connect → pre-build → pre-deploy → post-deploy.3
.kamal/hooks/pre-deploy — preview + guard:
#!/usr/bin/env bash
set -euo pipefail
next=$(svu next)
echo "→ deploying ${KAMAL_SERVICE:-app} as ${next} (git ${KAMAL_VERSION:0:7}) by ${KAMAL_PERFORMER}"
[ "$(svu current)" = "$next" ] && echo "note: no release-worthy commits — deploying without a bump."
[ -z "$(git status --porcelain)" ] || { echo "refusing: uncommitted changes"; exit 1; }
.kamal/hooks/post-deploy — tag after success:
#!/usr/bin/env bash
set -euo pipefail
next=$(svu next)
echo "${KAMAL_PERFORMER} deployed ${next} to ${KAMAL_DESTINATION:-default} in ${KAMAL_RUNTIME}s"
if [ "$(svu current)" != "$next" ]; then # only tag on a real bump
git tag -a "$next" -m "release $next"
git push origin "$next"
fi
Why post-deploy, not pre-deploy: post-deploy runs only after proxy/app boot succeeds, so an
aborted deploy never creates a tag. Kamal passes KAMAL_RUNTIME (seconds) only to post-deploy;
KAMAL_VERSION is the full git SHA; KAMAL_PERFORMER is the deployer's git email.3
Embed the version in the binary (Docker build-arg + ldflags)
Kamal builds inside Docker, so pass the version as a build-arg and stamp it (target a var, never
a const, initialized to a plain string):4
ARG VERSION=dev
RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X 'main.version=${VERSION}'" -o /out/app ./cmd/app
# config/deploy.yml
builder:
args:
VERSION: <%= %x(svu current).strip %>
Also expose runtime/debug.ReadBuildInfo()'s vcs.revision/vcs.modified on a /version route
so a dirty build is visible even if a tag was forgotten.4
Make the image tag the semver too (optional)
Kamal version precedence is --version > VERSION env > git SHA. To tag the image with the
semver: VERSION=$(svu next) kamal deploy. Trade-off: you lose the SHA↔image mapping; the default
(SHA image tag + a parallel git semver tag) is usually cleaner.3
Gotchas
- Tag in post-deploy (after success);
svu nextreturns the same value in both hooks (the tag
doesn't exist yet), so pre-deploy previews and post-deploy creates it. - Executable, no extension:
.kamal/hooks/pre-deploy, not.sh/.sample.kamal init
scaffolds pre-chmod'd*.samplefiles — just drop the suffix. --skip-hooksbypasses all hooks — don't rely on one for un-skippable gating.- Conventional Commits are the input — enforce the format (commitlint /
commit-msghook) or
svucan't infer bumps; fall back tosvu major|minor|patchto force one. - Shallow clones hide tags — in CI use
fetch-depth: 0/git fetch --tags, elsesvustarts
fromv0.0.0. - No
mainbranch required —svu/tags work off the current branch (this wiki repo ships from a
feat/*branch, and that's fine).
Tooling note
svu (Go binary, actively maintained) fits a Go+Kamal repo with no heavy CI. git-cliff if you
also want a generated CHANGELOG; release-please for a PR-based GitHub-Actions flow.
standard-version is deprecated — don't adopt it; semantic-release is Node/CI-heavy overkill.1
See also
- Publishing to the Live Wiki / the repo's
deploy-wiki-to-prodskill — the actualkamal deployflow this hooks into.
-
svu— https://github.com/caarlos0/svu. ↩︎ ↩︎ -
Conventional Commits — https://www.conventionalcommits.org/en/v1.0.0/; SemVer — https://semver.org/. ↩︎ ↩︎
-
Kamal hooks (v2.x) — https://kamal-deploy.org/docs/hooks/overview/. Verified against
basecamp/kamal source: hooks run locally;KAMAL_VERSION=full SHA;VERSION/--versionoverride
the version;KAMAL_RUNTIMEis post-deploy only. ↩︎ ↩︎ ↩︎ -
go doc cmd/link(-X importpath.name=value, var-not-const) andruntime/debug.ReadBuildInfo. ↩︎ ↩︎