diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 572a329..dc1c7cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,6 +66,91 @@ jobs: debug_logging: "true" token: ${{ secrets.GITHUB_TOKEN }} + # Proves the sdist is complete and functional, since the sdist file list is an + # allowlist and an omission there is otherwise invisible. The test-count check + # is what makes this meaningful: without it, an sdist missing half its tests + # would still pass the tests it did ship. + sdist: + name: sdist (functional) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up uv + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + python-version: "3.13" + + - name: Count the tests in the checkout + run: | + uv sync --all-extras + count=$(uv run pytest --collect-only -q | grep -oE '[0-9]+ tests? collected' | grep -oE '^[0-9]+') + echo "The checkout collects $count tests." + echo "checkout_tests=$count" >> "$GITHUB_ENV" + + - name: Build and unpack the sdist + run: | + uv build --sdist --out-dir dist + mkdir -p "$RUNNER_TEMP/sdist" + tar -xzf dist/*.tar.gz -C "$RUNNER_TEMP/sdist" --strip-components=1 + + - name: Check that the sdist ships every test + working-directory: ${{ runner.temp }}/sdist + run: | + uv sync --all-extras + count=$(uv run pytest --collect-only -q | grep -oE '[0-9]+ tests? collected' | grep -oE '^[0-9]+') + echo "The sdist collects $count tests; the checkout collected $checkout_tests." + if [ "$count" -lt "$checkout_tests" ]; then + echo "::error::The sdist is missing tests. Add the missing paths to the include list in [tool.hatch.build.targets.sdist]." + exit 1 + fi + + - name: Run the tests from the sdist + working-directory: ${{ runner.temp }}/sdist + run: make test + + - name: Verify typehints from the sdist + working-directory: ${{ runner.temp }}/sdist + run: make lint + + # start-contract-test-service depends on install-contract-tests-deps, and + # the -bg target backgrounds the whole chain. Without a blocking install + # first, the dependency sync races the harness connecting to the service. + - name: Install the contract test dependencies from the sdist + working-directory: ${{ runner.temp }}/sdist + run: make install-contract-tests-deps + + - name: Start the SSE contract test service from the sdist + working-directory: ${{ runner.temp }}/sdist + run: make start-contract-test-service-bg + + - name: Run SSE contract tests against the sdist + uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1 + with: + repo: sse-contract-tests + branch: main + version: v2 + test_service_port: 8000 + enable_persistence_tests: "false" + debug_logging: "true" + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Start the async SSE contract test service from the sdist + working-directory: ${{ runner.temp }}/sdist + run: make start-async-contract-test-service-bg + + - name: Run async SSE contract tests against the sdist + uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1 + with: + repo: sse-contract-tests + branch: main + version: v2 + test_service_port: 8001 + enable_persistence_tests: "false" + debug_logging: "true" + token: ${{ secrets.GITHUB_TOKEN }} + windows: runs-on: windows-latest diff --git a/pyproject.toml b/pyproject.toml index 45db6eb..f9576b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,3 +74,20 @@ build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["ld_eventsource"] exclude = ["ld_eventsource/testing"] + +# An allowlist, so that a new repository-management file cannot leak into the +# sdist by default. The CI sdist job proves the list is complete: it builds the +# sdist, unpacks it, and fails if fewer tests collect there than in the +# checkout. Hatchling force-includes pyproject.toml, README.md, LICENSE, +# PKG-INFO and .gitignore whatever this says, so the sdist always builds. +[tool.hatch.build.targets.sdist] +include = [ + "/ld_eventsource", + "/contract-tests", + "/docs", + "/Makefile", + "/setup.cfg", + "/CHANGELOG.md", + "/CONTRIBUTING.md", + "/SECURITY.md", +]