ADR-030: Versions Manifest Contract
Date: 2026-07-12
Status
Section titled “Status”Accepted
Context
Section titled “Context”External consumers (marketing sites, template catalogs, dashboards) want to display what this template ships — language, framework, and tool versions — without cloning the repo or parsing go.mod. The sibling Astro starter established the pattern: a versions.json at the repo root, fetched raw from the default branch at build time.
Such a manifest is only useful if it cannot lie. Hand-maintained version lists drift the first time a Dependabot PR merges without anyone remembering the manifest exists. The repo already solves this class of problem for AGENTS.md with a generated-artifact drift check in the quality gate (ADR-022); the same approach applies here.
Decision
Section titled “Decision”versions.jsonat the repo root is a public consumption contract. Onetemplatekey for the template’s own release version, plus one key per meaningful stack pin. Additive changes (new keys) are fine; renaming or removing a key is a breaking change for consumers and needs a deliberate decision.- Every non-
templatekey has a single in-repo source of truth, andtask versions:check(part oftask ci) fails when the manifest disagrees with it:go.mod(Go toolchain/minimum and direct dependencies),package-lock.json(Tailwind), the vendored JS bundles (htmx, Alpine), the sqlc header ininternal/database/db.go, and the workflow pins (golang-migrate, Node). A key with no wired check also fails — nothing may drift silently. Keys with no single source of truth (e.g. Postgres, which diverges between docker-compose and CI) are excluded rather than guessed. - The
templatefield is stamped by the release workflow, not by hand. On av*tag,release.ymlcommits.template = <tag>to the default branch — the ref consumers fetch — so the field cannot drift from tags. CI only enforces itsvX.Y.Zformat.
Consequences
Section titled “Consequences”- Dependency bumps that touch a manifested pin must update
versions.jsonin the same PR or CI fails — that is the point. Dependabot PRs for manifested dependencies will need a follow-up commit. - The manifest lags a release by one commit on the default branch (the stamp lands after the tag), which is acceptable for build-time consumers.
- Consumers can rely on key stability; this repo takes on the discipline cost of treating renames/removals as breaking.
Alternatives Considered
Section titled “Alternatives Considered”- Generate versions.json entirely instead of checking it. Rejected — generation needs a build step consumers can’t see run; a checked-in file with a drift gate is simpler and matches the ADR-022 pattern.
- Verify the
templatefield against git tags in CI. Rejected — every pre-release commit would fail between bumping and tagging; the stamp job owns that field.
References
Section titled “References”- Pattern source:
clownware/astro-performance-starterversions.json. - Related: ADR-021 (quality gate), ADR-022 (drift-check precedent), ADR-025 (release pipeline).
Enforcement
Section titled “Enforcement”- Testable consequences:
- TC-1: Every non-
templatekey inversions.jsonagrees with its in-repo source of truth; a key with no wired check fails. - TC-2: The
templatefield is stamped by the release workflow and only ever avX.Y.Ztag.
- TC-1: Every non-
- Checks:
- TC-1 →
task versions:checkintask ci(status: block, pre-existing) - TC-2 →
release.ymlstamp job + format assertion inversions:check(status: block, pre-existing)
- TC-1 →
- Not machine-checkable: The additive-only key discipline (rename/remove = breaking change) — semver judgment at release time.
- Graduation log: (empty)