From a47aedbbf73e5f09877c06948a4d742c5dee974c Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 13 Aug 2026 05:52:22 +0200 Subject: [PATCH] fix(release): the package version and the tag that installs it must agree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tag 1.1.0 was cut at main on 2026-08-13 while pyproject.toml still said 1.0.0. An install from that tag reports the PREVIOUS release, and views-models#294's capability assert reads exactly that number to decide what the installed build can honour. Caught before either launcher pinned, which is the only reason re-pointing the tag is cheap. Third time in this arc a version has been declared in two places with a guard on one copy — the same shape as the appwrite_env docstring edition (#249) and the blob URL sha (#249). So this ships with the guard, not just the bump. TWO CHECKS, IN THE TWO DIRECTIONS THAT DIFFER. `test_a_release_tag_declares_the_version_the_package_declares` fires only when HEAD carries a release tag, and demands the numbers match. Silent on every untagged commit, because firing there would be the false alarm that gets a guard deleted (ADR-014 §3). `test_the_newest_release_tag_is_not_ahead_of_the_declared_version` runs everywhere and is the direction that actually bit: a tag moved and the file did not. The reverse — file ahead of tag — is an ordinary pre-release bump and stays silent. Mutation-proven both ways: reverting the file to 1.0.0 fails with the exact sentence describing what happened today; setting it to 1.2.0 passes. Suite 422 passed / 2 skipped / 40 xfailed, ruff clean. The second skip is the tag check on an untagged HEAD, which is correct here and will run on the re-pointed tag. Co-Authored-By: Claude Opus 5 (1M context) --- pyproject.toml | 2 +- tests/test_release_version.py | 81 +++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 tests/test_release_version.py diff --git a/pyproject.toml b/pyproject.toml index d76c633..043be9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "views-postprocessing" -version = "1.0.0" +version = "1.1.0" description = "" authors = [ "Dylan Pinheiro ", diff --git a/tests/test_release_version.py b/tests/test_release_version.py new file mode 100644 index 0000000..731bc69 --- /dev/null +++ b/tests/test_release_version.py @@ -0,0 +1,81 @@ +"""The package version and the git tag that installs it must agree. + +Consumers install this package by tag — `views-models`' launchers do exactly that — and +then read `importlib.metadata.version(...)` to decide what the installed build can do +(views-models#294). So the number in `pyproject.toml` is what a consumer *believes* it +installed, and the tag is what it *actually* installed. Nothing compared them. + +Measured 2026-08-13: tag `1.1.0` was cut at `main` while `pyproject.toml` still said +`1.0.0`, so an install from that tag reported the previous release. Caught before any +consumer pinned. Third time in this arc a version has been declared twice with a guard on +one copy (register C-80, C-82). +""" + +import re +import subprocess +from pathlib import Path + +import pytest + +_REPO = Path(__file__).resolve().parent.parent + + +def _declared_version() -> str: + text = (_REPO / "pyproject.toml").read_text() + match = re.search(r'^version = "([^"]+)"', text, re.M) + assert match, ( + "pyproject.toml no longer declares a version in the form this guard reads. If the " + "packaging changed, teach this test the new form — do not delete it, or the number " + "a consumer installs goes unchecked again." + ) + return match.group(1) + + +def _tags_at(commit: str) -> list[str]: + out = subprocess.run( + ["git", "-C", str(_REPO), "tag", "--points-at", commit], + capture_output=True, text=True, check=False, timeout=30, + ) + return sorted(t for t in out.stdout.split() if re.fullmatch(r"\d+\.\d+\.\d+", t)) + + +def test_a_release_tag_declares_the_version_the_package_declares(): + """If HEAD carries a release tag, `pyproject.toml` must say the same number. + + Silent off a tagged commit — most commits are not releases, and firing on them would + be the false alarm that gets a guard deleted (ADR-014 §3). + """ + tags = _tags_at("HEAD") + if not tags: + pytest.skip("HEAD carries no release tag; nothing to compare") + + declared = _declared_version() + assert tags == [declared], ( + f"HEAD is tagged {tags} but pyproject.toml declares {declared!r}. A consumer " + "installing by tag would read the wrong version from importlib.metadata, which is " + "what views-models#294's capability assert reads. Bump the file and re-point the " + "tag, or tag a commit that already carries the right number." + ) + + +def test_the_newest_release_tag_is_not_ahead_of_the_declared_version(): + """A tag newer than the file means a release was cut without bumping. + + This is the direction that actually bit: the tag moved, the file did not. Runs on + every commit, not only tagged ones, so the gap is visible the moment it opens. + """ + out = subprocess.run( + ["git", "-C", str(_REPO), "tag", "-l", "--sort=-v:refname"], + capture_output=True, text=True, check=False, timeout=30, + ) + releases = [t for t in out.stdout.split() if re.fullmatch(r"\d+\.\d+\.\d+", t)] + if not releases: + pytest.skip("no release tags in this checkout (a shallow clone, or none cut yet)") + + newest, declared = releases[0], _declared_version() + as_tuple = lambda v: tuple(int(p) for p in v.split(".")) # noqa: E731 + assert as_tuple(newest) <= as_tuple(declared), ( + f"the newest release tag is {newest} but pyproject.toml declares {declared}. A " + "release was cut without bumping the file, so an install from that tag reports a " + "version older than itself." + )