Skip to content

Commit 182a821

Browse files
authored
ci: add manual PyPI trusted publishing
Add a workflow_dispatch-only PyPI publish path backed by Trusted Publishing docs and distribution policy tests.
1 parent 5ec5df8 commit 182a821

6 files changed

Lines changed: 114 additions & 3 deletions

File tree

.github/workflows/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ Steps:
1616

1717
No secrets are required for CI.
1818

19+
## `pypi.yml` — Manual PyPI Publish
20+
21+
Runs manually via `workflow_dispatch` after PyPI Trusted Publishing has been
22+
configured for this repository. It does not run on normal pushes or tags.
23+
24+
Steps:
25+
1. Validate that the requested ref is a `v*` release tag.
26+
2. Build the wheel and source distribution from that tag.
27+
3. Run `twine check`.
28+
4. Upload the checked distributions as a workflow artifact.
29+
5. Publish from the protected `pypi` environment using PyPI Trusted Publishing.
30+
31+
No PyPI token secret is required. The publish job uses GitHub OIDC with
32+
`id-token: write`, which must match the PyPI Trusted Publisher configuration:
33+
owner `saagpatel`, repository `GithubRepoAuditor`, workflow `pypi.yml`, and
34+
environment `pypi`.
35+
1936
## `audit.yml` — Manual Automated Audit
2037

2138
Runs manually via `workflow_dispatch`. The automatic weekly schedule is disabled while the repository remains private to avoid recurring GitHub Actions billing.

.github/workflows/pypi.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
ref:
7+
description: "Release tag to publish, for example v0.1.1"
8+
required: true
9+
10+
jobs:
11+
build:
12+
name: Build distributions
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: read
16+
17+
steps:
18+
- name: Validate release ref
19+
run: |
20+
case "${{ inputs.ref }}" in
21+
v*) ;;
22+
*) echo "PyPI publishes must use a PEP 440-compatible v* release tag."; exit 1 ;;
23+
esac
24+
25+
- name: Checkout release tag
26+
uses: actions/checkout@v6
27+
with:
28+
ref: ${{ inputs.ref }}
29+
fetch-depth: 0
30+
31+
- name: Set up Python
32+
uses: actions/setup-python@v6
33+
with:
34+
python-version: "3.11"
35+
36+
- name: Install build tools
37+
run: python -m pip install --upgrade build twine
38+
39+
- name: Build wheel and sdist
40+
run: python -m build
41+
42+
- name: Twine check
43+
run: python -m twine check dist/*
44+
45+
- name: Upload distributions
46+
uses: actions/upload-artifact@v7
47+
with:
48+
name: python-distributions
49+
path: dist/*
50+
if-no-files-found: error
51+
52+
publish:
53+
name: Publish distributions
54+
needs: build
55+
runs-on: ubuntu-latest
56+
environment: pypi
57+
permissions:
58+
contents: read
59+
id-token: write
60+
61+
steps:
62+
- name: Download distributions
63+
uses: actions/download-artifact@v7
64+
with:
65+
name: python-distributions
66+
path: dist
67+
68+
- name: Publish package distributions to PyPI
69+
uses: pypa/gh-action-pypi-publish@release/v1

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
88
## [Unreleased]
99

1010
### Changed
11+
- Added a manual PyPI Trusted Publishing workflow that builds a release tag and
12+
publishes from a protected `pypi` environment after PyPI is configured.
1113
- Made PyPI publishing explicitly opt-in while keeping GitHub Releases as the supported public distribution path.
1214
- Added PyPI-ready package metadata and public distribution status documentation.
1315

docs/distribution.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,21 @@ The repository is prepared for a future PyPI release:
3232
- `make dist-check` runs `twine check`
3333
- `scripts/release.sh` builds and checks artifacts by default
3434
- `scripts/release.sh --publish-pypi` is the only script path that uploads to PyPI
35+
- `.github/workflows/pypi.yml` is a manual Trusted Publishing workflow for a
36+
release tag, using the `pypi` environment and short-lived OIDC credentials
3537

3638
## Activation Checklist
3739

3840
Before the first PyPI release:
3941

4042
1. Recheck that the `github-repo-auditor` PyPI name is still available.
41-
2. Create the PyPI project through a first upload or configure Trusted Publishing.
42-
3. Prefer PyPI Trusted Publishing from GitHub Actions over long-lived API tokens.
43+
2. Configure PyPI Trusted Publishing for owner `saagpatel`, repository
44+
`GithubRepoAuditor`, workflow `pypi.yml`, and environment `pypi`.
45+
3. Protect the GitHub `pypi` environment so publishing requires intentional
46+
approval.
4347
4. Run the standard and distribution gates from [release-gates.md](release-gates.md).
44-
5. Publish the same version that is tagged on GitHub.
48+
5. Open **Actions -> Publish to PyPI -> Run workflow** and enter the release tag,
49+
for example `v0.1.1`.
4550
6. Smoke-test `pipx install github-repo-auditor` or `uv tool install github-repo-auditor`.
4651

4752
Until that checklist is complete, GitHub Releases remain the supported public

docs/release-gates.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ All three must pass before tagging:
157157
`scripts/release.sh` builds and checks artifacts by default; it uploads only when
158158
run as `scripts/release.sh --publish-pypi` with valid credentials. CI only checks
159159
and uploads to GitHub Releases.
160+
- After PyPI Trusted Publishing is configured, prefer the manual `Publish to PyPI`
161+
workflow over local token-based uploads. It builds the release tag in one job and
162+
publishes from a separate `pypi` environment job with `id-token: write`.
160163
- The `[serve]` extra is not bundled in the shiv binary by default. Users who need the
161164
web UI should install from the GitHub source with the `[serve]` extra or use a local
162165
editable clone.

tests/test_distribution_policy.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,23 @@ def test_distribution_docs_name_supported_public_channel() -> None:
3333
distribution_doc = (ROOT / "docs" / "distribution.md").read_text()
3434
readme = (ROOT / "README.md").read_text()
3535
release_gates = (ROOT / "docs" / "release-gates.md").read_text()
36+
workflows_readme = (ROOT / ".github" / "workflows" / "README.md").read_text()
3637

3738
assert "GitHub Releases remain the supported public" in distribution_doc
3839
assert "PyPI publishing is not active yet" in distribution_doc
3940
assert "docs/distribution.md" in readme
4041
assert "scripts/release.sh --publish-pypi" in release_gates
42+
assert "pypi.yml" in workflows_readme
43+
assert "id-token: write" in workflows_readme
44+
45+
46+
def test_pypi_workflow_is_manual_trusted_publishing_only() -> None:
47+
workflow = (ROOT / ".github" / "workflows" / "pypi.yml").read_text()
48+
49+
assert "workflow_dispatch:" in workflow
50+
assert "push:" not in workflow
51+
assert "environment: pypi" in workflow
52+
assert "id-token: write" in workflow
53+
assert "pypa/gh-action-pypi-publish@release/v1" in workflow
54+
assert "actions/upload-artifact" in workflow
55+
assert "actions/download-artifact" in workflow

0 commit comments

Comments
 (0)