diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3e1efe07..97340dbf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,7 +3,10 @@ name: Publish on: push: tags: ['v*'] - workflow_dispatch: {} + # Packaging check for every PR targeting the release branch: build the + # sdist/wheel and publish to TestPyPI before a release tag is cut. + pull_request: + branches: [master] permissions: contents: write @@ -44,7 +47,7 @@ jobs: publish-testpypi: needs: build - if: github.event_name == 'workflow_dispatch' + if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - name: Download dist @@ -53,11 +56,15 @@ jobs: name: dist path: dist/ - - name: Publish to TestPyPI + - name: Publish to TestPyPI (Trusted Publishing / OIDC) uses: pypa/gh-action-pypi-publish@release/v1 with: - repository-url: https://test.pypi.org/ - password: ${{ secrets.TEST_PYPI_API_TOKEN }} + repository-url: https://test.pypi.org/legacy/ + # No `password`: uses OIDC Trusted Publishing configured on + # TestPyPI for the pull_request subject claim. + # Concurrent PRs share the same package version and TestPyPI + # rejects re-uploads of existing files; skip instead of failing. + skip-existing: true publish-pypi: needs: build diff --git a/CHANGELOG.md b/CHANGELOG.md index 964743fa..d424de1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- The publish workflow now runs on every pull request targeting `master`: it + builds the sdist and wheel and publishes them to TestPyPI (`skip-existing`), + validating packaging before a release tag is cut. + ## [5.0.0] - 2026-09-16 ### Added diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..e534d459 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include requirements/*.txt diff --git a/docs/release.rst b/docs/release.rst index 7eb35198..8a27d913 100644 --- a/docs/release.rst +++ b/docs/release.rst @@ -35,8 +35,14 @@ The ``publish`` workflow (``.github/workflows/publish.yml``) runs when a ``PYPI_API_TOKEN`` secret is supported as a fallback. 4. Creates a GitHub Release with the matching changelog section. -A manual **TestPyPI** run is available from *Actions → Publish → Run workflow* -(uses a ``TEST_PYPI_API_TOKEN`` secret) to validate before a real release. +On every pull request targeting ``master``, the same build step runs and the +artifacts are published to **TestPyPI** via Trusted Publishing (OIDC — the +publisher's subject claim must be +``repo:FormalLanguageConstrainedPathQuerying/CFPQ_Data:pull_request``), with +``skip-existing`` so concurrent PRs sharing a version do not collide. This is +a packaging check that fails the PR before a release tag is cut; since a tag +always points at a merged PR's head, it validates exactly the code that will +be released. Prerequisites (one-time, owner action) -------------------------------------- diff --git a/tasks/detailed_plan.md b/tasks/detailed_plan.md index 48f341c0..0e60697e 100644 --- a/tasks/detailed_plan.md +++ b/tasks/detailed_plan.md @@ -110,3 +110,39 @@ a docs defect. fails — no suppression. - Note the behavior in the "Docs build and deploy" section of `docs/developer.rst`. + +### S7: Fix sdist packaging so `python -m build` succeeds + +Added 2026-09-16 at the publish gate. The first real `python -m build` run +(publish workflow on tag `v5.0.0`) failed: `setup.py` reads +`requirements/*.txt`, but no MANIFEST.in existed, so the files were missing +from the sdist and the sdist→wheel step raised FileNotFoundError. This is the +first packaging build in the project's history — it was never exercised +before. + +**Code:** `MANIFEST.in` (new: `include requirements/*.txt`) +**Tests:** skip — verified with an isolated `python -m build` (clean venv, +same as CI); sdist contains all four requirements files and the wheel builds. +**Docs:** none + +### S8: Publish to TestPyPI on every PR targeting master + +Added 2026-09-16 at the merge gate (user request: validate publishing +automatically before the human merge). The `publish` workflow gains a +`pull_request: branches: [master]` trigger; the `publish-testpypi` job runs on +PRs with `skip-existing: true` because concurrent PRs share one package +version and TestPyPI rejects re-uploads of existing files. + +Final design (after the first PR run failed): TestPyPI uses **OIDC Trusted +Publishing**, not a token — the first run proved the pypi-publish action +silently falls back to OIDC when no token secret exists, and the user prefers +no long-lived secrets (consistent with the PyPI setup). The publisher's +subject claim is `repo:FormalLanguageConstrainedPathQuerying/CFPQ_Data:pull_request` +(the documented sub for pull_request events; confirmed in the failed run's +claims dump). The manual `workflow_dispatch` TestPyPI path was removed: its +sub claim cannot match the PR publisher, and the PR check fully covers +pre-release validation (a tag always points at a merged PR's head). + +**Code:** `.github/workflows/publish.yml` +**Tests:** skip — workflow-only; the build step is the check itself +**Docs:** `docs/release.rst` (Package publishing section), `CHANGELOG.md`