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`.