chore: Restrict the sdist to an allowlist and verify it in CI - #523
Merged
Merged
Conversation
Only the wheel target was configured, so hatchling used its default for the sdist and shipped every tracked file. The published sdist carries 17 files that are of no use outside this repository: all of .github, the release-please config and manifest, CODEOWNERS, and the readthedocs and codeclimate configs. .sdk_metadata.json belongs in the same group, since release tooling reads it from a checkout and not from the package. The sdist keeps the package, the tests, the contract-test service, the docs source, the Makefile and PROVENANCE.md. Downstream redistributors build from the sdist and expect to run the test suite, so the tests stay even though the wheel correctly excludes them. PROVENANCE.md tells a consumer how to verify the artifact, so it stays too. Hatchling force-includes .gitignore so that a rebuild from the sdist applies the same exclusions. An exclude entry for it has no effect, so the list does not claim one. Verified by unpacking the built sdist in a clean directory: 262 files instead of 280, none of the excluded paths present, 1,983 tests collected, and 1,628 passed with 355 database tests skipped, the same as make test. The wheel is byte-identical to the one built from main.
jsonbailey
approved these changes
Sep 17, 2026
The exclude list had the wrong default. Every new repository file shipped unless somebody remembered to exclude it. An allowlist inverts that, but its failure mode is a needed file going missing, which nothing would notice. So the allowlist comes with a job that proves it. The job builds the sdist, unpacks it outside the checkout, and runs the unit tests, the type checks and both contract suites from the unpacked tree. The test-count check is what makes the job meaningful. An sdist that shipped half its tests would still pass the tests it did ship, so the job counts collected tests in the checkout and in the sdist and fails if the sdist collects fewer. The contract-test steps cover the other half, since a missing contract-tests directory changes no test count. One job on one Python version. The matrix already covers the checkout. The job runs the contract suites without persistence tests, because it starts no database services; the linux job still covers those. Hatchling force-includes pyproject.toml, README.md, LICENSE.txt, PKG-INFO and .gitignore whatever the file list says, so an allowlist cannot make the sdist unbuildable or uninstallable. Verified locally. The allowlist produces the same 262-file sdist as the exclude list did, 1,983 tests collect from it, and 1,628 pass with 355 database tests skipped, matching make test.
…vice start-contract-test-service depends on install-contract-tests-deps, and the -bg target backgrounds the whole chain. So the dependency sync ran in the background while the harness was already connecting to the service port. The linux job avoids this by running the install as a blocking step first, which leaves the later background start with nothing to do. The sdist job now does the same. The job passed without this, but only on timing slack. Cursor Bugbot caught the same problem in the companion change to python-eventsource.
keelerm84
marked this pull request as ready for review
September 18, 2026 13:39
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f2a735b. Configure here.
The file-data-source modules skip themselves on LD_SKIP_FLAKY_TESTS because they flake in CI. The linux and windows jobs set it; the sdist job did not, so it could fail for a reason that says nothing about whether the sdist is complete. A packaging check that reports unrelated flakes teaches people to ignore it. The skip is a module-level skipif, so pytest still collects those tests. The collected count is 1,983 with the variable and without it, which means the test-count floor in this job is not weakened by the change. Reported by Cursor Bugbot on #523. It does not apply to python-eventsource, which has no such variable.
jsonbailey
approved these changes
Sep 18, 2026
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.

Summary
pyproject.tomlconfigured only the wheel target, so hatchling fell back to its default for the sdist and shipped every git-tracked file. The published sdist carries 17 files of no use to anyone outside this repository: all of.github/,release-please-config.json,.release-please-manifest.json,CODEOWNERS,.readthedocs.yml, and.codeclimate.yml. Somebody curated the wheel and never came back for the sdist. It surfaced while fixing the identical gap in launchdarkly/python-eventsource#79.This is hygiene rather than exposure -- every one of those files is already public on a public repo.
Why an allowlist, and why it needs the CI job
The first commit here used an exclude list. That has the wrong default: every new repository file ships unless somebody remembers to exclude it. An allowlist inverts the default so a new CI config can never leak.
But the two directions fail differently. A missed exclude ships a junk file: harmless, and no test can ever catch it, because nothing breaks. A missed include drops a needed file: a real break for downstream redistributors and for
pip install --no-binary, and the kind of thing a test can catch. Switching to an allowlist without a check would trade a harmless invisible failure for a harmful one, so the second commit adds both together.One thing that shrinks the risk: hatchling force-includes
pyproject.toml,README.md,LICENSE.txt,PKG-INFOand.gitignoreregardless of what the file list says (hatchling/builders/sdist.py,get_default_build_data). An allowlist cannot make the sdist unbuildable or uninstallable. What it can lose is theMakefile,setup.cfg,PROVENANCE.md, the changelog and docs,contract-tests/, and the tests themselves. Everything under/ldclientcomes along automatically, including non-Python data files such asldclient/testing/selfsigned.{key,pem}..gitignoreis deliberately absent from both lists for the same reason:force_includebeatsexclude, so an entry for it would silently do nothing.The new
sdist (functional)jobIt builds the sdist, unpacks it outside the checkout, and from the unpacked tree runs
make test,make lint, and both contract suites.Two design points worth calling out. It runs from the unpacked tree, not install-the-sdist-then-test-the-checkout, which would test the checkout's files instead. And it enforces a test-count floor: an sdist that shipped half its tests would still pass the tests it did ship and go green having proven nothing, so the job counts collected tests in both places and fails if the sdist collects fewer. The contract-test steps cover the omission the count cannot see, since a missing
contract-tests/directory changes no test count.One job on one Python version. The existing matrix already covers the checkout on five versions across two platforms. The job starts no database services, so it runs the contract suites with
enable_persistence_tests: "false"; thelinuxjob still covers persistence. Keeping the packaging check free of Redis, Consul and DynamoDB keeps its flake surface small.It does not catch a junk file leaking back in. That is the direction that does not matter.
Judgment call worth a reviewer's eye
.sdk_metadata.jsonis not in the allowlist, so it no longer ships. It is repository metadata that release tooling reads from a checkout rather than from the built package. Say the word if something does consume it from an installed sdist and I will add it.Verification
Locally, from the unpacked sdist:
make testsetsLD_SKIP_DATABASE_TESTS=1, so the 355 skips are the Redis, DynamoDB and Consul tests that need services CI provides.main: same file list, same SHA-256 per entry.Related
launchdarkly/python-eventsource#79 is the companion change. SDK-3126 covers that one.
Note
Overview
Restricts the published sdist to an explicit Hatch allowlist instead of shipping every git-tracked file (e.g.
.github/and release config). The new[tool.hatch.build.targets.sdist]includelist coversldclient,contract-tests, docs,Makefile,setup.cfg, and selected markdown files.Adds a
sdist (functional)CI job that builds and unpacks the sdist outside the checkout, fails if pytest collects fewer tests than the repo checkout (catching allowlist omissions), then runsmake test,make lint, and sync/async contract tests from the unpacked tree (enable_persistence_tests: false,LD_SKIP_FLAKY_TESTSaligned with other jobs).Reviewed by Cursor Bugbot for commit 60a35b3. Bugbot is set up for automated code reviews on this repo. Configure here.