Diff v1 → v2
v1: bot legacy · 2026-07-23T07:52:40Z
v2: bot legacy · 2026-07-23T08:18:22Z
# 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". + > 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`](https://github.com/caarlos0/svu) from [Conventional Commits](https://www.conventionalcommits.org/); 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.[^svu][^cc] ## Bump rules `svu` reads the latest `vX.Y.Z` tag and the commits since it. The highest bump implied wins:[^cc] | 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 ```bash 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`.[^hooks] `.kamal/hooks/pre-deploy` — preview + guard: ```bash #!/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: ```bash #!/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.[^hooks] - ## Embed the version in the binary (Docker build-arg + ldflags) + ## Embed the version into the artifact (framework-agnostic) - 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):[^ldflags] + 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. - ```dockerfile - ARG VERSION=dev - RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X 'main.version=${VERSION}'" -o /out/app ./cmd/app - ``` ```yaml - # config/deploy.yml + # config/deploy.yml — feed svu's version into every build 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.[^ldflags] + ```dockerfile + # 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:[^ldflags] + + | 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.[^hooks] ## 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` (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.[^svu] + `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.[^svu] ## See also + - [[rails-production-playbook|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-prod` skill — the actual `kamal deploy` flow this hooks into. [^svu]: `svu` — <https://github.com/caarlos0/svu>. [^cc]: Conventional Commits — <https://www.conventionalcommits.org/en/v1.0.0/>; SemVer — <https://semver.org/>. [^hooks]: 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. [^ldflags]: `go doc cmd/link` (`-X importpath.name=value`, var-not-const) and `runtime/debug.ReadBuildInfo`.