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 built
artifact and tagged in git, all wired through.kamal/hooks/. Framework-agnostic: the
hooks/semver flow is identical for Rails, Java, Go, Node, etc.; only version-embedding differs
per stack. 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 into the artifact (framework-agnostic)
The svu + hooks + tagging flow above is identical for every language. Only embedding the
version differs. Kamal builds inside Docker, so the universal pattern works for any stack:
pass the version as a build-arg, expose it as an env var the app reads.
# config/deploy.yml — feed svu's version into every build
builder:
args:
VERSION: <%= %x(svu current).strip %>
# Dockerfile — the universal fallback (any language)
ARG VERSION=dev
ENV APP_VERSION=${VERSION} # read APP_VERSION at runtime; surface on /health or /version
Language-native options on top of (or instead of) the env var:4
| Stack | Bake it in | Read it at runtime |
|---|---|---|
| Go | go build -ldflags "-X 'main.version=$VERSION'" — target a var, not a const, no initializer call |
the version var; plus runtime/debug.ReadBuildInfo() → vcs.revision/vcs.modified for dirty-build detection |
| Ruby / Rails | ARG VERSION → ENV APP_VERSION (Kamal is Rails-native) |
ENV["APP_VERSION"] in config/application.rb/an initializer; expose on your deep /health |
| Java (Spring Boot) | Maven spring-boot:build-info / Gradle springBoot { buildInfo() } → META-INF/build-info.properties; or a ${revision} POM version + -Drevision=$VERSION |
/actuator/info via BuildProperties, or getClass().getPackage().getImplementationVersion() from MANIFEST.MF |
| Node | ARG VERSION → ENV APP_VERSION, or write it into package.json's version |
process.env.APP_VERSION (or require('./package.json').version) |
| Any | ENV APP_VERSION=$VERSION (universal pattern above) |
read the env var; log it at boot |
Whatever the stack, surface version + git SHA on a /health or /version endpoint so a
running instance self-reports what shipped — and prefer VCS-stamped dirty-state where the
toolchain offers it (Go's vcs.modified) so an untagged/dirty build is visible.
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 is a single self-contained binary (written in Go) that versions any Kamal repo from git
tags + Conventional Commits — no language runtime or heavy CI needed. git-cliff if you also want
a generated CHANGELOG; release-please for a PR-based GitHub-Actions flow (native Ruby/Java/Node/Go
support). standard-version is deprecated — don't adopt it; semantic-release is Node/CI-heavy
overkill for a hook-driven Kamal deploy.1
See also
- Rails Production Playbook — production release discipline this plugs into (the "Versioning & releases" section links back here).
- 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. ↩︎