Skip to content

Commit 749ab76

Browse files
committed
fix(ci): read sdist expectations from the tarball, not the checkout
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
1 parent 9b1e498 commit 749ab76

2 files changed

Lines changed: 37 additions & 10 deletions

File tree

.github/workflows/releases.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,12 @@ jobs:
9393
# from conda-forge/zarr-feedstock, so a recipe change there would redden
9494
# unrelated PRs. On `push` to main it still catches a bad merge within
9595
# minutes, attributed to one commit, and it gates `upload_pypi` so no release
96-
# can publish an sdist conda cannot consume. Use `workflow_dispatch` to run it
97-
# against a PR branch on demand.
96+
# can publish an sdist conda cannot consume.
97+
#
98+
# To run it against a branch before merging, use `workflow_dispatch`. Note
99+
# that dispatch only offers branches of the repository it runs in, so for a
100+
# pull request from a fork that means dispatching this workflow in the fork,
101+
# not here.
98102
test_dist_conda:
99103
name: Test conda-forge can consume the sdist
100104
needs: [build_artifacts]
@@ -122,7 +126,7 @@ jobs:
122126
123127
- name: Point the recipe at the sdist we just built
124128
run: |
125-
python -m pip install pyyaml
129+
python -m pip install pyyaml==6.0.3
126130
python ci/conda_recipe_for_sdist.py \
127131
"${RUNNER_TEMP}/recipe-upstream.yaml" \
128132
"$(ls dist/*.tar.gz)" \

ci/check_sdist_contents.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,20 @@
1616
maintained for other reasons, rather than restated here. A hand-written list of
1717
"files the sdist must contain" would just be a second allowlist to forget to
1818
update, which is the problem it is meant to solve.
19+
20+
Everything is read out of the tarball, including the configuration the
21+
expectations are derived from. The check is therefore a property of the
22+
artifact alone: it gives the same answer against a release sdist downloaded
23+
from PyPI as it does against a fresh `hatch build`, with no working tree in
24+
the picture -- which matters, because a git checkout has files the tarball
25+
does not.
1926
"""
2027

2128
import sys
2229
import tarfile
2330
import tomllib
2431
from pathlib import Path
2532

26-
REPO_ROOT = Path(__file__).parent.parent.resolve()
27-
2833
# Paths no test opens, so the sdist test run in `releases.yml` cannot vouch for
2934
# them, but that packagers do need. conda-forge's recipe installs the sdist
3035
# (pyproject.toml, README.md, src/) and reads `license_file: LICENSE.txt`; the
@@ -57,17 +62,31 @@ def sdist_members(sdist: Path) -> set[str]:
5762
return {name.split("/", 1)[1] for name in names if "/" in name}
5863

5964

60-
def testpaths() -> list[str]:
61-
"""`testpaths` from pyproject.toml.
65+
def read_member(sdist: Path, path: str) -> bytes:
66+
"""Read one member of the tarball, addressed relative to its top-level directory."""
67+
with tarfile.open(sdist) as tar:
68+
roots = {name.split("/", 1)[0] for name in tar.getnames()}
69+
if len(roots) != 1:
70+
raise SystemExit(f"Expected one top-level directory in {sdist}, got {sorted(roots)}")
71+
member = tar.extractfile(f"{roots.pop()}/{path}")
72+
if member is None:
73+
raise SystemExit(f"{path} is missing from {sdist.name}")
74+
return member.read()
75+
76+
77+
def testpaths(sdist: Path) -> list[str]:
78+
"""`testpaths` from the pyproject.toml the sdist ships.
6279
6380
Derived rather than duplicated: adding a directory to `testpaths` without
6481
adding it to the sdist allowlist is exactly the mistake this catches. It is
6582
also the mistake that was already live -- `docs/user-guide` has been a
6683
testpath while `docs/` was excluded, so `pytest` on an unpacked sdist died
6784
at collection.
85+
86+
Read from the tarball rather than the working tree so the two halves of the
87+
comparison always come from the same artifact.
6888
"""
69-
with (REPO_ROOT / "pyproject.toml").open("rb") as f:
70-
config = tomllib.load(f)
89+
config = tomllib.loads(read_member(sdist, "pyproject.toml").decode())
7190
return config["tool"]["pytest"]["ini_options"]["testpaths"]
7291

7392

@@ -79,7 +98,11 @@ def check(sdist: Path) -> int:
7998

8099
missing_required = [p for p in REQUIRED_PATHS if p not in members]
81100
# A testpath is a directory; it is present if anything ships beneath it.
82-
missing_testpaths = [p for p in testpaths() if not any(m.startswith(f"{p}/") for m in members)]
101+
# Skipped when pyproject.toml itself did not ship: it is already reported as
102+
# a missing required path, and there is nothing left to read `testpaths` out
103+
# of.
104+
expected = [] if "pyproject.toml" in missing_required else testpaths(sdist)
105+
missing_testpaths = [p for p in expected if not any(m.startswith(f"{p}/") for m in members)]
83106
forbidden = sorted(m for m in members if any(m.startswith(p) for p in FORBIDDEN_PREFIXES))
84107

85108
if not (missing_required or missing_testpaths or forbidden):

0 commit comments

Comments
 (0)