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-vcs is 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.