Skip to content

chore: Restrict the sdist to an allowlist and verify it in CI - #523

Merged
keelerm84 merged 4 commits into
mainfrom
mk/sdk-3127/sdist-excludes
Sep 18, 2026
Merged

keelerm84 merged 4 commits into
mainfrom
mk/sdk-3127/sdist-excludes

Conversation

@keelerm84

@keelerm84 keelerm84 commented Sep 17, 2026

Copy link
Copy Markdown
Member

Summary

pyproject.toml configured 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-INFO and .gitignore regardless 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 the Makefile, setup.cfg, PROVENANCE.md, the changelog and docs, contract-tests/, and the tests themselves. Everything under /ldclient comes along automatically, including non-Python data files such as ldclient/testing/selfsigned.{key,pem}.

.gitignore is deliberately absent from both lists for the same reason: force_include beats exclude, so an entry for it would silently do nothing.

The new sdist (functional) job

It 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"; the linux job 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.json is 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:

$ uv build --sdist && tar -xzf dist/*.tar.gz -C /tmp/sim --strip-components=1
$ cd /tmp/sim && ls -a
.gitignore  CHANGELOG.md  CONTRIBUTING.md  LICENSE.txt  Makefile  PKG-INFO
PROVENANCE.md  README.md  SECURITY.md  contract-tests  docs  ldclient
pyproject.toml  setup.cfg
$ uv run pytest --collect-only -q | tail -1  -> 1983 tests collected
$ make test                                  -> 1628 passed, 355 skipped in 54.30s
$ make lint                                  -> Success: no issues found in 228 source files

make test sets LD_SKIP_DATABASE_TESTS=1, so the 355 skips are the Redis, DynamoDB and Consul tests that need services CI provides.

  • The allowlist produces the same 262-file sdist the exclude list did, down from 280.
  • 1,983 tests collect from the sdist, the same as the checkout.
  • The wheel is byte-identical to one built from main: same file list, same SHA-256 per entry.
  • The guard was checked against a deliberately broken sdist in the companion repo, where it correctly failed rather than passing vacuously.

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] include list covers ldclient, 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 runs make test, make lint, and sync/async contract tests from the unpacked tree (enable_persistence_tests: false, LD_SKIP_FLAKY_TESTS aligned with other jobs).

Reviewed by Cursor Bugbot for commit 60a35b3. Bugbot is set up for automated code reviews on this repo. Configure here.

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.
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.
@keelerm84 keelerm84 changed the title chore: Exclude repository-management files from the sdist chore: Restrict the sdist to an allowlist and verify it in CI Sep 17, 2026
…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
keelerm84 marked this pull request as ready for review September 18, 2026 13:39
@keelerm84
keelerm84 requested a review from a team as a code owner September 18, 2026 13:39

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread .github/workflows/ci.yml
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.
@keelerm84
keelerm84 merged commit 5d1213e into main Sep 18, 2026
16 checks passed
@keelerm84
keelerm84 deleted the mk/sdk-3127/sdist-excludes branch September 18, 2026 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants