ci: add a PyPI release workflow, and modernize the license declaration - #5
Conversation
This repo had no workflows at all, so nothing was built, checked, or published automatically -- and it is not on PyPI, while its README tells readers to pip install it. The workflow builds on every push and pull request via build-and-inspect-python-package (pinned to v3.0.1, whose Twine 7 understands the packaging metadata a PEP 639 license produces; the v2 line's Twine 6 does not), and publishes only on a published GitHub release. Publishing uses PyPI trusted publishing rather than a stored token: PyPI verifies the workflow's OIDC identity, so no long-lived secret exists to leak. It also attaches a build-provenance attestation. This requires a pending publisher to be configured on PyPI for the project name before the first release -- the workflow cannot create that, and the first release will fail without it. The license declaration is modernized in the same change, deliberately, because a release writes metadata to PyPI permanently and a version cannot be re-uploaded. The deprecated license table becomes a PEP 639 SPDX expression with license-files, and the redundant "License :: OSI Approved" classifier is removed -- current backends reject carrying both. Verified by building locally: the wheel now declares License-Expression: Apache-2.0 with the LICENSE captured, and passes twine check --strict.
Two problems, both making the package claim something untrue. The classifiers stopped at 3.12. datajoint 2.3.2 classifies 3.10 through 3.14, so this package was telling PyPI it did not support the two most recent Pythons, including the current stable release. There was no upper bound. datajoint pins <3.15; this package did not, so pip would happily install it on 3.15 into an environment where its only real dependency cannot be installed at all. requires-python is now >=3.10,<3.15, matching datajoint exactly rather than approximating it -- the floor and the ceiling both come from the dependency that determines them. Verified by building: Requires-Python: <3.15,>=3.10, classifiers through 3.14, and twine check --strict passes.
|
Python range updated in a follow-up commit. Two problems, both making the package claim something untrue. Classifiers stopped at 3.12. There was no upper bound.
Verified by building rather than assuming:
One inconsistency left in the family, flagged rather than changed: |
The cap I added is unnecessary and would cause the failure mode it was meant to prevent. Removing it. My justification for it was wrong. I claimed that without a cap, pip on 3.15 would install this package into an environment where datajoint cannot be installed. It will not: this package pins datajoint>=2.0, and a dependency's requires-python is honored during resolution, so pip fails there with an error that names datajoint -- the accurate diagnosis. Verified by resolving into a 3.9 environment: `datajoint>=2.0` fails cleanly with "datajoint>=2.0.0 cannot be used", while bare `datajoint` silently resolves to 0.14.9, an ancient release predating 2.0. That silent downgrade is the argument against caps. requires-python is a resolution input, so a capped release is skipped rather than reported: on a Python the cap excludes, pip installs an older release of this package instead of saying why. And a published cap cannot be relaxed -- when 3.15 ships and works, every capped version still refuses it, and support requires a new release rather than nothing at all. The classifiers stay at 3.10 through 3.14, which is the range actually tested and claimed. Classifiers are documentation and carry no resolution behavior, so they can say what is verified without constraining what is possible.
The version was declared statically in pyproject.toml, and in this package's __init__.py as well, so two files had to agree with each other and with whatever tag a release carried. Nothing enforced that, and a release where they disagree is the kind of bug that is only visible after publishing. Now hatchling plus hatch-vcs, matching dj-zarr-codecs: the git tag is the single source of truth, the build hook writes _version.py, and the package imports it. A release is a tag, and no pull request touches a version number -- which is what we want anyway. Consequence worth stating plainly: the previously declared version is gone. Until a tag exists the build produces a development version derived from the commit distance, and the first tag is what fixes the number. That tag is a decision, not a carry-over. Also adds a .gitignore, which this repo did not have, covering the generated _version.py and the usual build artifacts.
This repo had no workflows, so nothing was built, checked, or published automatically — and it is not on PyPI, while the README tells readers to
pip install dj-figpack-codecs. That instruction currently fails, and this is the package the forthcoming blog post points readers at.The workflow
Builds on every push and PR via
build-and-inspect-python-package; publishes only on a published GitHub release.Pinned to
v3.0.1for a specific reason: its Twine 7 understands the packaging metadata a PEP 639 license declaration produces. Thev2line bundles Twine 6, which failstwine check --strictwith "'2.5' is not a valid metadata version" — the failure that haddj-zarr-codecsred, diagnosed in that repo's #9. Copying@v2here would have imported the same bug.Trusted publishing, not a stored token. PyPI verifies the workflow's OIDC identity, so there is no long-lived secret to leak or rotate. The job also attaches a build-provenance attestation.
A pending publisher must be configured on PyPI for this project name. The workflow cannot create it, and the first release will fail without it. At pypi.org publishing settings:
dj-figpack-codecsdatajointdj-figpack-codecscd.ymlpypiThe
environment: pypiin the job must match that last field.License declaration modernized in the same change
Deliberately bundled, because a release writes metadata to PyPI permanently — a version cannot be re-uploaded. A wrong first release cannot be replaced.
license = { file = "LICENSE" }→ PEP 639 SPDX expression pluslicense-filesLicense :: OSI Approvedclassifier removed — current backends reject carrying both an SPDX expression and a license classifierhatchling>=1.27for PEP 639 supportVerified by building rather than assuming: the wheel declares
License-Expression: Apache-2.0withLicense-File: LICENSEcaptured, and passestwine check --strict.Not included
No test/lint workflow. This repo does have tests (5 files), so
dj-zarr-codecs'ci.ymlis worth copying here — but it depends on a.pre-commit-config.yamlthis repo lacks, so it is a separate change rather than something to bolt on.