ADR-NNN: The published version is derived from the release tag
Status Active · Filed 2026-08-16
Context
#93 diagnosed the 0.4.0 release
failure and named the remedy ladder explicitly: guarding the property is rung
2, deriving the version is rung 1, and rung 1 was deferred because switching to
hatch-vcs without also fetching tags would have failed the release a second
time.
This is rung 1, with the tag fetch that makes it safe.
The underlying fault is DP-5. The version lived in pyproject.toml and in the
git tag with nothing making them agree, and it drifted at the first
opportunity. DP-5’s own ladder puts derive the projection above guard the
property, and rung 2 alone leaves the two copies in place — it just makes
their disagreement loud.
A downstream data point for why the quiet version matters: an adopting project
was running pip install luria (0.3.0) while its CI pinned
dmarx/luria/actions/generate@main. Local lint and CI lint were executing
different code, and nothing in either reported it, because
pip install --upgrade luria is a no-op while the release is missing.
Decision
pyproject.toml declares dynamic = ["version"], adds hatch-vcs to the
build requirements, and sets [tool.hatch.version] source = "vcs". The build
backend reads git describe, so a release build is stamped with the tag that
triggered it and no field needs touching.
Two details are load-bearing.
fetch-depth: 0 on the publish checkout. git describe needs tags; the
default shallow checkout has none. Without this the derived version silently
becomes a 0.1.dev… fallback — which would replace a loud drift with a quiet
one, and is exactly why #93 declined to bundle this.
No generated version file. [tool.hatch.build.hooks.vcs] can write the
resolved version into the package; it is deliberately not configured, because
nothing in luria reads its own version and a written-out file is precisely
the hand-maintained copy this removes.
#93’s assertion stays. Deriving the version prevents the drift; it does not make a recurrence visible, and the two remedies are complementary rather than alternatives.
Alternatives considered
- Rung 2 alone, as merged in #93. Correct and insufficient by DP-5’s own ordering: it leaves two copies of one fact and catches their disagreement after the fact. It was the right thing to ship first, since it needs no build change and it fails before the environment gate.
- Keep the field, bump it by hand, add it to a release checklist. The option that already failed. A checklist is itself a hand-maintained projection of the release process.
- A bot commit writing the version before tagging. Keeps the number greppable in the tree, which is a real benefit. But it makes the tree’s version authoritative between releases when it is meaningless, adds a commit per release, and reintroduces the two-copies shape one indirection out.
setuptools_scm. The same mechanism under a different backend, and it would mean changing backends.hatch-vcsis the hatchling-native equivalent.
Consequences
Builds from a working tree now produce versions like
0.4.1.dev0+gf61282e8c.d20260816 rather than a round number. That is correct —
an untagged build is not a release — and the +g… local segment means such a
wheel cannot be uploaded to PyPI even by accident.
Verified before merging: a clean checkout on the 0.4.0 tag builds
luria-0.4.0-py3-none-any.whl, with no .devN and no local segment. All tests
pass and pip install -e . still works, so the dynamic version does not break
local development installs.
Anything wanting the version at runtime must ask
importlib.metadata.version("luria") rather than reading a constant. Nothing
does, and this decision is the reason to keep it that way.