ci: check the sdist can actually be consumed - #291
Open
d-v-b wants to merge 3 commits into
Open
Conversation
7 tasks
…ubpackages (zarr-developers#4261) * fix(build): use an sdist allowlist so the zarr sdist stops shipping subpackages The root sdist config was a blocklist naming /.github, /bench and /docs, so every release shipped whatever else happened to sit in the repository root. That included the whole packages/ tree — the zarr-indexing, zarr-metadata and zarr-http-server sources, which are released as their own distributions — plus ci/, design/, towncrier fragments and other repo furniture. 2.9M of the 1.4M sdist was other people's packages. Replace it with an explicit allowlist, matching what packages/zarr-indexing and packages/zarr-metadata already do. Including /docs also fixes a second problem: tests/test_docs.py walks docs/ and testpaths collects docs/user-guide, so with docs/ excluded the shipped test suite died at collection with 'Not a file or directory'. tests/test_docs.py now runs green from an unpacked sdist (61 passed, 2 skipped), and full collection finds 7581 tests with no errors. Assisted-by: ClaudeCode:claude-opus-5 * docs(build): trim the sdist allowlist comment Drop the narration of the blocklist this replaced -- that history lives in git -- and keep only the durable rationale and the reason each entry is on the list. Assisted-by: ClaudeCode:claude-opus-5
The sdist contents are defined by an allowlist in pyproject.toml. That is the safe default -- a new subpackage under packages/ stays out of the zarr sdist unless someone opts it in -- but it inverts the failure mode. A blocklist ships too much silently; an allowlist can ship too little, by adding a file consumers need without a matching allowlist entry. The 'Test distribution artifacts' job previously ran 'ls dist' and asserted nothing. Two checks now replace it, split by what they cost, both inspecting the built tarball rather than the working tree since a git checkout has files the sdist does not. test_dist_pypi runs ci/check_sdist_contents.py: every 'testpaths' entry from pyproject.toml ships, packages/ does not, and the handful of paths packagers read but no test opens (LICENSE.txt, PKG-INFO, py.typed, the hatch-vcs version file) are present. The testpaths check is derived from config maintained for other reasons rather than restated, so it cannot drift into being a second allowlist to forget. Seconds and hermetic, so it runs on pull requests too. Against the pre-allowlist sdist it reports both defects that were live: docs/user-guide missing, and 370 packages/ paths. test_dist_conda builds the sdist with the real conda-forge recipe, fetched from the feedstock at run time and repointed at the tarball we just built by ci/conda_recipe_for_sdist.py. conda-forge is the consumer most exposed to a missing file: it builds from the sdist rather than the wheel and runs our suite from a directory holding only what its recipe copies out of the tarball. Fetching beats reproducing -- a copy of that recipe would go stale silently, which is the same class of bug this job exists to catch. Note the recipe's requirements are already regenerated from our pyproject.toml by grayskull on each version bump (bot: inspection: update-grayskull in the feedstock's conda-forge.yml); the tests block is hand-maintained there. That job is skipped on pull_request. It runs the whole suite from an unpacked sdist, measured at 591.95s of a 613s run, so conda's build and solve are about 22 seconds of it and there is no cheaper variant of the same coverage. On push to main it still catches a bad merge within minutes and attributes it to one commit, and it gates upload_pypi so no release can publish an sdist conda cannot consume. workflow_dispatch runs it against a branch on demand. Keeping it off pull requests also stops a recipe change in the feedstock from reddening unrelated PRs. A url source rather than a path one, deliberately: path sources honour .gitignore, and src/zarr/_version.py is gitignored. It is written into the sdist by the hatch-vcs hook precisely because an unpacked sdist has no git history, so a path source would drop it and break the build. Verified on CI before the trigger was narrowed: imports test passed, pip check passed, 6884 passed / 475 skipped / 4 xfailed. Assisted-by: ClaudeCode:claude-opus-5
check_sdist_contents.py derived `testpaths` from the working tree's pyproject.toml while comparing it against the tarball's contents, so the two halves of the comparison could come from different trees -- and the job comment claims the opposite, that these checks inspect the artifact rather than the checkout. Read pyproject.toml out of the tarball instead, which makes the check a property of the artifact alone: the same answer against a release sdist downloaded from PyPI as against a fresh `hatch build`. Also pin pyyaml, matching how hatch is installed in test.yml, and correct the workflow_dispatch note: dispatch only offers branches of the repository it runs in, so for a fork PR that means dispatching in the fork. Assisted-by: ClaudeCode:claude-opus-5
d-v-b
force-pushed
the
claude/zarr-sdist-ci-checks
branch
from
August 13, 2026 14:33
22cf142 to
749ab76
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 AI text below 🤖
Split out of the sdist allowlist PR (#289 / zarr-developers#4261) so the packaging change and the CI change can be reviewed separately.
Stacked on
claude/zarr-sdist-allowlist-414698, notmain—check_sdist_contents.pyassertspackages/is absent from the sdist, which is only true once the allowlist lands. Based onmainthis PR would be red by construction. Merge the allowlist first; the diff here is CI-only either way.Why
The allowlist is the safe default — a new subpackage under
packages/stays out of thezarrsdist unless someone opts it in — but it inverts the failure mode. A blocklist ships too much silently; an allowlist can ship too little, by adding a file consumers need without a matching entry.Test distribution artifactspreviously ranls distand asserted nothing. Two checks replace it, split by cost.test_dist_pypi— seconds, runs on PRsci/check_sdist_contents.pyasserts:testpathsentry frompyproject.tomlshipspackages/does notLICENSE.txt,PKG-INFO,py.typed, and the hatch-vcs_version.pyThe
testpathsassertion is the one that earns its keep — it's derived from config maintained for other reasons, not restated, so it can't rot into a second allowlist to forget. Pointed at the pre-allowlist sdist it reports both defects that were actually live:test_dist_conda— ~10 min, not on PRsBuilds the sdist with the real conda-forge recipe, fetched from the feedstock at run time and repointed at our freshly built tarball by
ci/conda_recipe_for_sdist.py. conda-forge is the consumer most exposed to a missing file: it builds from the sdist rather than the wheel, and runs our suite from a directory holding only what its recipe copies out of the tarball.Fetching beats reproducing. A vendored copy of that recipe would go stale the moment the feedstock changed, and silently — the same class of bug this job exists to catch. Only the fields naming which tarball to build are rewritten;
requirements,tests,pip_checkandlicense_fileexecute as conda-forge wrote them.Worth knowing: the recipe's
requirementsare already regenerated from ourpyproject.tomlby grayskull on each version bump (bot: inspection: update-grayskullin the feedstock'sconda-forge.yml) — the 3.3.0 bump movedtyping_extensions >=4.13 → >=4.14with no human involved. Thetests:block is hand-maintained there, which is why running it beats reproducing it.Why it's skipped on
pull_requestIt runs the whole suite from an unpacked sdist. Measured on CI: 591.95s of a 613s job — conda's build and env solve are ~22 seconds of that. The cost is inherent to testing an sdist, so there's no cheaper variant of the same coverage.
On push to
mainit still catches a bad merge within ~10 minutes, attributed to one commit, and it gatesupload_pypi, so no release can publish an sdist conda cannot consume.workflow_dispatchruns it against a branch on demand. Keeping it off PRs also stops a recipe change in the feedstock from reddening unrelated PRs.Release gating is unchanged in effect: on a
releaseevent neither job is skipped, soupload_pypistill requires both.One trap worth flagging
The generated recipe uses a
url:source, notpath:.path:sources honour.gitignore, andsrc/zarr/_version.pyis gitignored (.gitignore:69). It's written into the sdist by the hatch-vcs hook precisely because an unpacked sdist has no git history to derive a version from — so apath:source silently drops it and the build fails. Noted in a comment in the script.Verification
Ran on Linux CI before the trigger was narrowed, with the recipe fetched live:
No
changes/fragment: CI-only, nothing user-visible for the release notes.