# ADR-007: Product versioning and releases - **Status:** ACCEPTED - **Date:** 2026-07-10 - **Deciders:** cproudlock - **Supersedes:** none ## Context ADR-002 established semantic versioning for the plugin contract via `__contract_version__` in `shopdb/__init__.py`. That number answers one question only: is a given plugin compatible with this platform build. It says nothing about the state of the product as a whole. A sister site standing up its own instance needs a different answer: which release am I running, and what changed since the last one. Until now the product had no version, no tags, no changelog, and no CI. ADR-002's pinning model implicitly assumes that a downstream site can pin a known-good build, but there were no tags to pin to. Adopters had no release record to read before upgrading, and no automated gate confirming that a given commit builds and passes tests. ## Decision The product carries its own release version, separate from the plugin contract version. 1. **Product version** (`shopdb/__init__.py`): a single `__version__` constant. This is the version of the shopdb-flask product as a whole. It follows semantic versioning of the product's user-visible and operator-visible behavior. It is deliberately NOT part of the `shopdb.api` contract surface, so it is never re-exported through `shopdb.api`; exporting it would itself be a contract change under ADR-002. 2. **Plugin contract version** (`__contract_version__`): unchanged from ADR-002. It moves only when the plugin contract surface changes, per the major/minor/patch rules in ADR-002. The two are distinct series with independent bump rules. They happen to coincide at `0.5.0` for this release; that is a coincidence of timing, not a coupling. A product release that changes no contract surface bumps `__version__` while leaving `__contract_version__` fixed, and vice versa. 3. **Release record** (`CHANGELOG.md`, repo root): the canonical, human-readable record of what changed in each release, in Keep-a-Changelog format. Every release has an entry; work in flight accumulates under `## [Unreleased]`. 4. **Git tags**: each product release is tagged `vX.Y.Z`, where `X.Y.Z` matches `__version__` at the tagged commit. Tags are what ADR-002's downstream-pinning model pins to. 5. **Frontend version** (`frontend/package.json`): kept in lock-step with `__version__` so the shipped single-page app reports the same product version as the backend that serves it. ### Release procedure To cut release `X.Y.Z`: 1. Bump `__version__` in `shopdb/__init__.py` to `X.Y.Z`. 2. Bump `version` in `frontend/package.json` to the same `X.Y.Z`. 3. Move the accumulated `## [Unreleased]` notes in `CHANGELOG.md` into a new `## [X.Y.Z] - YYYY-MM-DD` section, leaving a fresh empty `## [Unreleased]` above it. 4. If the plugin contract surface changed this release, bump `__contract_version__` per ADR-002 (independently of `__version__`). 5. Commit, then tag: `git tag -a vX.Y.Z -m "shopdb-flask X.Y.Z"`. 6. Push the commit and the tag. ## Consequences ### Positive - Adopters have a real release to name in bug reports and a changelog to read before upgrading. - ADR-002's pinning story finally has tags to pin to. - Product and contract can evolve at their own pace without one dragging the other into a misleading bump. ### Negative / cost - Two version numbers to keep straight. The comment in `shopdb/__init__.py` and this ADR exist to keep the distinction clear. - Release discipline: the changelog and both version constants must be updated together, or the tagged build misreports itself. ### Neutral - CI (`.gitea/workflows/ci.yml`) runs the backend tests, the naming/style gate, and the frontend build on push and PR. It is best-effort: Gitea Actions availability on the host is unverified, so the workflow is config-only until a runner is confirmed. ## Alternatives considered 1. **Reuse `__contract_version__` as the product version.** Conflates two independent concerns; a docs-only or UI-only release would either falsely bump the contract or leave the product looking unchanged. Rejected. 2. **Calendar versioning for the product** (e.g. `2026.07.0`). Easy to bump but poor at signaling breaking operator-facing changes. Rejected for the same reasons ADR-002 rejected it for the contract. 3. **No product version, rely on git SHAs.** Opaque to adopters and unpinnable in any human-meaningful way. Rejected. ## References - ADR-001 (defines the contract surface) - ADR-002 (plugin contract versioning; `__contract_version__` bump rules) - `shopdb/__init__.py` (`__version__`, `__contract_version__`) - `CHANGELOG.md` (release record) - `frontend/package.json` (frontend version, kept in lock-step) - `.gitea/workflows/ci.yml` (CI gate)