> ## Documentation Index
> Fetch the complete documentation index at: https://dev.haico.gr/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR-0007: Build identity from the release tag

> The running version is stamped into both images from the release tag and asserted after deploy; the prod version is inherited from the dev tag; the OpenAPI version tracks the contract, not the build.

# 0007. Stamp the build identity from the release tag and keep it apart from the API contract version

## Context and problem statement

Until now nothing running in a deployment could say which release it was. The backend advertised a hard-coded `FastAPI(version="0.1.0")` and `frontend/package.json` carried a stale `0.1.0` that nothing read, so support could not ask "which version are you on?", a dev deployment was indistinguishable from prod in the browser, and the deploy job had no way to confirm that the containers it had just restarted were the ones it had just built. Two related drifts compounded this. The prod Release Drafter draft guessed its version from PR labels, while the maintainer promotes the exact commit validated on dev under the same number (`v1.3.0` and `dev-v1.3.0` point at one commit), so the draft's title was renamed by hand and its body kept the guessed number. And the only version string the OpenAPI document had was the same hard-coded one, so wiring the release version into it would have rewritten the committed `docs/openapi.json` on every tag without a single endpoint changing. The tension is between one honest, verifiable version for the running artefact and a spec version that only moves when the contract does.

## Decision drivers

* **One source of truth.** The tag the maintainer pushes (see [ADR-0001](/docs/adr/0001-two-branch-model)) is the release; every surface must derive from it, never from a file someone forgot to bump.
* **Verifiable after deploy.** CI must be able to assert that the stack actually serving traffic is the build it just shipped.
* **No plausible fakes.** A local or unstamped build must render as such, never as a made-up semver.
* **A stable committed spec.** `docs/openapi.json` is regenerated by CI and asserted equal to the served schema; it must not churn per release.
* **Nothing new to configure.** No new secrets, variables or `.env` keys for operators.

## Considered options

1. **Stamp at build time from the tag.** `ci.yml` passes `APP_VERSION`, `APP_CHANNEL`, `APP_COMMIT` and `APP_BUILT_AT` as build-args into both Dockerfiles; the backend reads them from the image environment, the frontend inlines them as `NEXT_PUBLIC_APP_*` at `next build`.
2. **Read the version from the manifests.** Bump `frontend/package.json` and `FastAPI(version=...)` on every release and read them at runtime.
3. **Configure the version at runtime.** Add `APP_VERSION` to the deployment `.env` files and let the server report whatever it is told.
4. **Keep resolving the prod version from labels.** Leave the Release Drafter resolver as the source of the prod number.

## Decision outcome

**Chosen option: stamp at build time from the tag**, with two companion rules, because it is the only option where the reported version cannot disagree with the code in the image. The design:

* **One value fans out.** `resolve-release.outputs.version` (the tag with its prefix stripped) becomes `APP_VERSION` for both images; the environment name becomes `APP_CHANNEL`, `github.sha` becomes `APP_COMMIT`, and a UTC ISO-8601 stamp becomes `APP_BUILT_AT`. Defaults (`0.0.0-dev`, `local`, `unknown`, empty) make an unstamped build report exactly that. The backend serves them at `GET /api/version` and on `X-App-Version` / `X-App-Commit` headers; the frontend renders them as `data-app-*` attributes on `<html>`, in the footer and navbar, and as a `[DEV]` title prefix off prod.
* **Asserted post-deploy.** After `deploy.sh` returns, `ci.yml` polls `/api/version` and the served HTML on the target host until both report the tag's version and commit, and fails the run otherwise. The version-only layers sit after the expensive dependency layers in both Dockerfiles, so a rebuild for a new tag reuses the cache.
* **The prod version is inherited from the dev tag.** The prod drafter workflow finds the `dev-vX.Y.Z` tag pointing at the promoted commit and passes `X.Y.Z` as the drafter's `version` input, so tag, title and body agree with what was validated on dev; only when no such tag exists does the label-based resolver guess, with a warning.
* **The spec version is the contract version.** `FastAPI(version=...)` reads `API_CONTRACT_VERSION` from `app/core/version.py`, bumped by hand when an endpoint or field changes. `GET /api/version` reports it as `api_contract` next to the build fields, so a client can see both.

### Positive consequences

* Every deployment answers "which release, which commit, built when" without a login, and a mismatch between what CI built and what runs is a red pipeline, not a support ticket.
* Prod release notes carry the number the maintainer validated on dev, with no manual retitling.
* `docs/openapi.json` stays byte-stable across releases; the contract version moves only when the contract does.
* `frontend/package.json` stays at `0.0.0` and stops pretending to be a version.

### Negative consequences

* Two version strings exist (build and contract), which needs one sentence of explanation wherever they meet; both are documented at `/api/version`.
* `API_CONTRACT_VERSION` is bumped by hand, so it can be forgotten. Mitigated by the schema tests and the OpenAPI sync workflow, which surface a changed spec in review.
* A locally built image always reads as `dev build` on the `local` channel, which is intended but occasionally surprising.

## Pros and cons of the options

### Option 1: stamp at build time from the tag

* **+** The version is a property of the artefact, so it cannot be misconfigured or forgotten.
* **+** Post-deploy assertion becomes possible.
* **−** Four more build-args in CI, the Dockerfiles, `docker-compose.yml` and the deploy scripts to keep aligned.

### Option 2: read the version from the manifests

* **+** Conventional; no build plumbing.
* **−** Requires a bump commit on every release, in two files, that the tag can silently disagree with.
* **−** Ties the OpenAPI version to the release, churning the committed spec.

### Option 3: configure the version at runtime

* **+** Trivial to change without a rebuild.
* **−** That is the flaw: an operator can run image X while the server claims version Y, which defeats the post-deploy check and support diagnosis.

### Option 4: keep resolving the prod version from labels

* **+** No workflow change.
* **−** Cannot know the dev number the maintainer chose; produced wrong bodies for `v1.2.1` and `v1.3.0`, and since the release-drafter v7 bump proposed `patch` for everything.

## Links

* Related ADRs: [0001-two-branch-model](/docs/adr/0001-two-branch-model)
* Docs: [Maintainers handbook](/docs/maintainers-handbook), [Endpoint groups](/docs/api-guide/endpoint-groups)
* Code: `.github/workflows/ci.yml`, `.github/workflows/release-drafter-prod.yml`, `backend/Dockerfile`, `frontend/Dockerfile`, `backend/app/core/version.py`, `backend/app/core/build_headers.py`, `backend/app/routers/version.py`, `frontend/src/lib/build-info.ts`
