Skip to content

Derive the version from the git tag instead of a checked-in file - #42

Merged
hdrake merged 2 commits into
mainfrom
dynamic-version
Jul 29, 2026
Merged

Derive the version from the git tag instead of a checked-in file#42
hdrake merged 2 commits into
mainfrom
dynamic-version

Conversation

@hdrake

@hdrake hdrake commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Ports the scheme from hdrake/xeos#10 to xwmb: the version is derived from the git tag by hatch-vcs instead of being read from a literal in xwmb/version.py.

Why

xwmb/version.py held __version__ = "0.6.0", which had to be bumped in its own commit before every release. Nothing tied that commit to the tag the release was actually cut from, so the two could disagree — and when they did, the symptom was a 400 File already exists from PyPI at the very end of the release. That is exactly how the xeos v0.2.1 release failed: the tag was placed one commit before the bump, the workflow built the previous version, and PyPI rejected it.

With the tag as the source of truth, tagging is the bump. There is no second commit to remember and no way for the tag and the artifact to disagree.

What changed

  • pyproject.tomlhatch-vcs added to build-system.requires; [tool.hatch.version] switches from path = "xwmb/version.py" to source = "vcs"; local_scheme = "no-local-version" so untagged builds are X.Y.Z.devN rather than PEP 440 local versions that indexes refuse; a build hook writes the resolved version to xwmb/_version.py.
  • xwmb/version.py — now a shim that imports from the generated _version.py, with a 0.0.0+unknown fallback for a checkout that has never been built or installed. xwmb.__version__ is unchanged for anything installed from a release.
  • .gitignore — ignores the generated xwmb/_version.py.
  • .github/workflows/publish-to-pypi.ymlfetch-depth: 0 (a shallow clone has no tag, so the build would silently produce a .devN artifact), plus a step asserting the built version matches the release tag. checkout/setup-python bumped off the long-EOL v2.
  • .github/workflows/ci.ymlfetch-depth: 0 on the checkout, since the job installs the package.
  • .readthedocs.yamlpost_checkout unshallows and fetches tags. docs/source/conf.py titles the pages with the installed version, so without this they would read .devN.
  • README.md — a Releasing section documenting the tag-is-the-version procedure and the two invariants it implies.

Verified locally

  • Clean tag → exact version: building at a throwaway v9.9.9 produces xwmb-9.9.9.tar.gz / .whl, no suffix, with the generated xwmb/_version.py inside the sdist.
  • main is exactly at v0.6.0, so today's build there is 0.6.0 — identical to what the literal produced.

Follow-up needed on the feedstock

conda-forge builds with --no-build-isolation, so the backend's requirements must be present in host. conda-forge/xwmb-feedstock's recipe currently lists only hatchling; I confirmed locally that building the sdist that way with no hatch-vcs installed fails in hatchling.builders.plugin.interface.get_build_hooks. hatch-vcs must be added next to hatchling in the feedstock's host requirements before the next release builds.

Note on untagged checkouts

A checkout without tags — a shallow clone, or a fork that never fetched them — now resolves a .devN version rather than the release line. That is why every checkout in CI uses fetch-depth: 0, and it is worth pushing the release tags to any fork you install from with pip install git+https://….

CI is red for an unrelated, pre-existing reason

The three build jobs fail at collection with
ImportError: cannot import name 'flatten_lol' from 'xbudget', raised from the
installed PyPI xwmt 0.2.0: that release imports a helper xbudget removed in
0.7. It is a dependency-compatibility problem in the environment, not something this
branch touches — main has not run CI since February 2026 and would fail the same
way today. Nothing here changes Python outside version.py.

What this PR is responsible for did pass in those same runs: pip install -e .
resolved the version from the tag, and conda list reports xwmb 0.6.1.dev2 (tag
v0.6.0 plus the PR's merge commits). On main itself, which sits exactly on the
tag, the build is 0.6.0.

🤖 Generated with Claude Code

hdrake and others added 2 commits July 28, 2026 08:46
`xwmb/version.py` held a literal that had to be bumped in its own commit before
every release, and nothing tied that commit to the tag the release was actually
cut from. The two could disagree, and when they did the symptom was a 400 from
PyPI at the very end of the release.

hatch-vcs derives the version from the tag at build time and writes it to a
generated `xwmb/_version.py`, so tagging *is* the bump. `version.py` becomes a
shim over the generated file, with a `0.0.0+unknown` fallback for a checkout
that has never been built.

The tag has to be visible for that to work, so the CI and publish checkouts use
`fetch-depth: 0` and Read the Docs unshallows in `post_checkout`; without it the
build quietly produces a `.devN` artifact. The publish workflow also asserts
that the version it built matches the tag it was fired from, which is the check
that would have caught the failure mode described above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@hdrake
hdrake merged commit fd6f50c into main Jul 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant