23. Lockstep versioning; components built separately, then combined into one image
Date: 2026-06-18
Status
Accepted. Amended 2026-09-30 — see Amendment.
Context
The system has several custom components — webapi, webui, and the CLI — plus a shared layer. They're developed in one monorepo but have different build outputs (a server, a static SPA, a CLI binary). We need a coherent release story: how they're versioned, and how they're packaged.
Decision
- Semantic versioning, all parts versioned together (lockstep). Per the existing convention (ADR 0005, ADR 0012), a single semver
vX.Y.Ztag versions the whole app — webapi, webui, CLI, shared — as one unit. There are no independently-versioned components; a release is the set. - Build components separately, then combine. Each component is built independently (webapi bundle, webui SPA
dist/, CLI binary) and released, then a final step combines them into one Docker image (the combined container that serves/api,/app, Swagger, bundled CLI download, and — Tier 3 — static docs; ADR 0002, containers.md).
Consequences
- One tag → one coordinated release; no version-skew between webapi and the clients (the OpenAPI-generated clients are regenerated at that version, ADR 0019).
- The build pipeline has two phases: (1) build + release each component, (2) assemble the combined image from those artifacts. The combine step is the only place the pieces meet.
- The CLI ships both as a released artifact (eventually per-OS binaries, cli.md) and bundled inside the image for the webui download link.
- Tag-driven releases run on GitHub Actions (ADR 0012).
Amendment: one multi-stage build, not assembled from released artifacts
2026-09-30.
Lockstep holds. Packaging differs: the root Dockerfile builds every component from source in one multi-stage build (parallel build-webui / build-docs / build-cli stages → runtime), not from released artifacts. release-cli.yml builds the CLI binaries independently from the same tag; both get the same CT_VERSION. The image already serves /app, /docs, /cli/download and Scalar at /api/docs (not Swagger).