Showing version 2 bot legacy · api · 2026-07-23T08:18:22Z

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 VERSIONENV 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 VERSIONENV 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 next returns 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 *.sample files — just drop the suffix.
  • --skip-hooks bypasses all hooks — don't rely on one for un-skippable gating.
  • Conventional Commits are the input — enforce the format (commitlint / commit-msg hook) or
    svu can't infer bumps; fall back to svu major|minor|patch to force one.
  • Shallow clones hide tags — in CI use fetch-depth: 0 / git fetch --tags, else svu starts
    from v0.0.0.
  • No main branch 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


  1. svuhttps://github.com/caarlos0/svu↩︎ ↩︎

  2. Conventional Commits — https://www.conventionalcommits.org/en/v1.0.0/; SemVer — https://semver.org/↩︎ ↩︎

  3. Kamal hooks (v2.x) — https://kamal-deploy.org/docs/hooks/overview/. Verified against
    basecamp/kamal source: hooks run locally; KAMAL_VERSION=full SHA; VERSION/--version override
    the version; KAMAL_RUNTIME is post-deploy only. ↩︎ ↩︎ ↩︎

  4. go doc cmd/link (-X importpath.name=value, var-not-const) and runtime/debug.ReadBuildInfo↩︎