Skip to content

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

Merged
hdrake merged 1 commit into
mainfrom
dynamic-version
Jul 29, 2026
Merged

Derive the version from the git tag instead of a checked-in file#27
hdrake merged 1 commit 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 regionate: the version is derived from the git tag by hatch-vcs instead of being read from a literal in regionate/version.py.

Why

regionate/version.py held __version__ = "0.5.5", which had to be bumped in its own commit before every release (engineering norm 5 said exactly that). 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 = "regionate/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 regionate/_version.py.
  • regionate/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. regionate.__version__ is unchanged for anything installed from a release.
  • .gitignore — ignores the generated regionate/_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.
  • conda/meta.yamlhatch-vcs added to host. The recipe builds with --no-build-isolation, so the backend's requirements must be installed there; I confirmed locally that without it the wheel build fails in hatchling.builders.plugin.interface.get_build_hooks. The same addition is needed on conda-forge/regionate-feedstock before the next release builds.
  • README.md — a Releasing section documenting the tag-is-the-version procedure.
  • CLAUDE.md — a Versioning section with the invariants, and norm 5 restated (there is no longer a literal to bump).

Verified locally

  • Clean tag → exact version: building at a throwaway v9.9.9 produces regionate-9.9.9.tar.gz / .whl, no suffix, with the generated regionate/_version.py inside the sdist.
  • Untagged main builds as 0.5.6.devN (last tag v0.5.5 + distance).

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. CI and Read the Docs are handled above. The one to watch is pip install git+https://github.com/<fork>/regionate.git@<branch>: if that fork has no tags it reports 0.1.devN, which can fall below a downstream regionate >= 0.5.5 floor and get silently replaced by the PyPI build. Pushing the release tags to the fork (git push <fork> --tags) fixes it; otherwise install such a branch last with --force-reinstall --no-deps, exactly as regionate's own dev branches already do for xgcm.

CI is red for an unrelated, pre-existing reason

The four build jobs fail on ValueError: Argument 'boundary' has been renamed to 'padding' from xgcm/grid.py — an xgcm 1.0 API break in
regionate/tests/test_gridded_regions.py. main is already red for the same
reason
: the last push run (Add AI Usage Policy and engineering norms to CLAUDE.md,
2026-07-13) failed identically. Nothing in this PR touches 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 regionate 0.5.6.dev9
the tag v0.5.5 plus distance, which is exactly right.

🤖 Generated with Claude Code

`regionate/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 `regionate/_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. Engineering norm 5 in CLAUDE.md said to bump
that literal, so it is restated here.

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.

conda/meta.yaml gains `hatch-vcs` in `host`: the recipe builds with
`--no-build-isolation`, so the backend's own requirements must be installed
there or the wheel build fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@hdrake
hdrake merged commit f42e91b into main Jul 29, 2026
1 of 5 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