Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ jobs:
with:
access_token: ${{ github.token }}
- name: Checkout source
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
# `pip install -e .` resolves the version from the git tag via
# hatch-vcs; a shallow clone has no tag, so the installed package
# would report `0.1.devN` instead of the release line.
fetch-depth: 0

- name: Conda setup
uses: conda-incubator/setup-miniconda@v2
Expand Down
22 changes: 20 additions & 2 deletions .github/workflows/publish-to-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
# hatch-vcs derives the version from the git tag, which a shallow clone
# does not have. Without this the build silently produces a
# `0.0.0.dev...` artifact instead of the release version.
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
Expand All @@ -19,6 +24,19 @@ jobs:
pip install build twine
- name: Build package
run: python -m build
# Deriving the version from the tag should make this unfalsifiable, but it
# is the last point at which a mismatch is cheap to see: the alternative
# symptom is a 400 from PyPI at the very end of the release, and only when
# that version already exists.
- name: Built version must match the release tag
run: |
built=$(ls dist/*.tar.gz | sed -E 's|.*/sectionate-(.*)\.tar\.gz|\1|')
tag="${GITHUB_REF_NAME#v}"
echo "tag=$tag built=$built"
if [ "$built" != "$tag" ]; then
echo "::error::Release tag $GITHUB_REF_NAME implies version '$tag', but the build produced '$built'."
exit 1
fi
- name: Publish package
env:
TWINE_USERNAME: __token__
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,6 @@ venv.bak/

# mypy
.mypy_cache/

# Version file generated from the git tag by hatch-vcs at build time.
sectionate/_version.py
9 changes: 9 additions & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ build:
os: ubuntu-24.04
tools:
python: "miniforge3-latest"
jobs:
post_checkout:
# RTD clones shallow, but installing sectionate resolves its version from
# the git tag via hatch-vcs, and the docs are titled with it
# (`docs/source/conf.py` reads it from the installed metadata). Without
# the tag they would say `.devN`. Tolerate failure: an already-complete
# clone makes `--unshallow` an error, which must not fail the build.
- git fetch --unshallow || true
- git fetch --tags || true

# Build documentation in the "docs/" directory with Sphinx
sphinx:
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,37 @@ pip install -e .
python -m ipykernel install --user --name docs_env_sectionate --display-name "docs_env_sectionate"
jupyter-lab
```

Releasing
---------

**The git tag is the version.** `sectionate` has no version string checked into
the source tree: `hatch-vcs` derives it from the tag at build time and writes
`sectionate/_version.py` (gitignored, but shipped inside the sdist and wheel).
To cut a release you tag; there is no file to bump and nothing to keep in sync.

1. Make sure `master` is green and has everything you want in the release.
2. **Publish a GitHub Release** whose tag is `vX.Y.Z`, targeting the commit you
want to ship:
```bash
gh release create vX.Y.Z --target "$(git rev-parse origin/master)" \
--title vX.Y.Z --generate-notes
```
Publishing it (not merely pushing a tag) is what fires the workflow. Target
the commit you actually want: a tag placed *before* the commit you meant to
release builds the previous version, and PyPI rejects it as a duplicate.
3. The **Publish to PyPI** workflow builds from that tag and uploads. It checks
out with `fetch-depth: 0` so the tag is visible to `hatch-vcs`, and asserts
that the built version matches the tag before publishing.
4. Verify: <https://pypi.org/project/sectionate/>.
5. conda-forge builds from the PyPI sdist and lags by design; the autotick bot
opens the version-bump PR. Its recipe must list `hatch-vcs` alongside
`hatchling` in `host`, since it builds with `--no-build-isolation`.

Two things follow from the tag being the version. Never add a version literal
back to the tree — `sectionate/version.py` is a shim over the generated file, and
a `0.0.0+unknown` from it means the package was imported without being built or
installed, not that a number is missing. And any checkout that installs the
package needs its tags: a shallow clone, or a fork that never fetched them,
resolves a `.devN` version instead of the release line. That is why CI checks out
with `fetch-depth: 0` and Read the Docs unshallows in `post_checkout`.
5 changes: 5 additions & 0 deletions conda/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ requirements:
- python {{ python_min }}
- pip
- hatchling
# The build backend resolves the version through hatch-vcs. The build above
# is `--no-build-isolation`, so it has to be installed here or the wheel
# build fails outright; the sdist carries the generated `_version.py`, so
# git itself is not needed.
- hatch-vcs
run:
- python >={{ python_min }}
- dask
Expand Down
21 changes: 19 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,28 @@ dependencies = [
"Sibling package" = "https://github.com/hdrake/regionate"

[build-system]
requires = ["hatchling"]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

# The version comes from the git tag, not from a file in the tree. Releasing is
# then a single act -- tag `vX.Y.Z` and publish the GitHub Release -- with no
# second commit to remember, and no way for the tag and the built artifact to
# disagree about which version they are.
[tool.hatch.version]
path = "sectionate/version.py"
source = "vcs"

# Drop the `+g<sha>` local segment. On a clean tag the version is exactly X.Y.Z
# either way, but an untagged build would otherwise carry a PEP 440 *local*
# version, which package indexes refuse outright. Untagged builds are
# `X.Y.Z.devN` instead, which an index will accept.
[tool.hatch.version.raw-options]
local_scheme = "no-local-version"

# Building writes the resolved version to `sectionate/_version.py`, which is
# gitignored but *is* included in the sdist -- so `pip install` from the sdist,
# and conda-forge's build from the PyPI sdist, both work without git present.
[tool.hatch.build.hooks.vcs]
version-file = "sectionate/_version.py"

[tool.hatch.build]
exclude = ["examples/**", "data/**"]
19 changes: 17 additions & 2 deletions sectionate/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
"""sectionate: version information"""
"""sectionate: version information.

__version__ = "0.3.3"
The version is derived from the git tag at build time by ``hatch-vcs``, which
writes the resolved value to ``sectionate/_version.py``. That file is generated
rather than checked in: there is deliberately no version string in the source
tree that could drift out of step with the tag it is supposed to describe.

``_version.py`` ships in every built artifact -- wheel, sdist, and editable
install -- so the fallback below only fires when ``sectionate`` is imported
straight from a source checkout that has never been built or installed.
"""

try:
from sectionate._version import __version__
except ImportError: # pragma: no cover - un-built source checkout
__version__ = "0.0.0+unknown"

__all__ = ["__version__"]
Loading