Skip to content

Commit 251cdc7

Browse files
sklinglernvclaude
andcommitted
ci: drop third-party actions from the publish workflow
This org enforces a GitHub Actions allowlist, and a disallowed action fails the *entire workflow* at startup rather than just its own job — that is what left CI dead for eight days (see PR #50). publish.yml referenced two non-GitHub actions, `astral-sh/setup-uv` and `pypa/gh-action-pypi-publish`, either of which could have produced a `startup_failure`. A publish workflow that cannot start is one that silently never ships, and this one would only be exercised at the moment of an actual release. Every `uses:` is now an `actions/*` action. uv is installed from a pinned, versioned install script and performs the upload itself. `--trusted-publishing always` rather than `automatic`, so a broken publisher config fails loudly instead of falling back to hunting for a token. `--check-url` makes a re-run idempotent, skipping already-uploaded files — which matters because `fail-fast: false` means a partial publish is a state we can land in and need to resume from. The tradeoff is losing PEP 740 attestations: uv uploads them but does not generate them, and the action that does may not be allowlisted. Provenance is worth nothing if the workflow cannot start. Documented in RELEASING.md as something to revisit. Verified locally: the pinned install script yields exactly uv 0.11.4, and `uv publish --trusted-publishing always` outside an OIDC environment exits 2 with "No OIDC token discovered" rather than hanging or falling back. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 05dc0de commit 251cdc7

2 files changed

Lines changed: 63 additions & 13 deletions

File tree

.github/workflows/publish.yml

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
# Auth: PyPI Trusted Publishing (OIDC). No API tokens, no repo secrets.
66
#
77
# `workflow_dispatch` runs the same build against TestPyPI for a dry run.
8+
#
9+
# Every `uses:` here is an `actions/*` action, i.e. GitHub-created. That is
10+
# deliberate: this org enforces an Actions allowlist, and a disallowed action
11+
# fails the *entire workflow* at startup (see PR #50 — one blocked action left
12+
# CI dead for 8 days). A publish workflow that cannot start is a publish
13+
# workflow that silently never ships. uv is installed from a pinned, versioned
14+
# install script and does the upload itself via `uv publish`.
815
name: Publish
916

1017
on:
@@ -20,6 +27,11 @@ on:
2027

2128
permissions: {}
2229

30+
env:
31+
# Pinned: the install script is fetched at runtime, so an unversioned URL
32+
# would make every run depend on whatever uv ships that day.
33+
UV_VERSION: "0.11.4"
34+
2335
jobs:
2436
build:
2537
runs-on: ubuntu-latest
@@ -29,9 +41,10 @@ jobs:
2941
fetch-depth: 0 # tags needed for uv-dynamic-versioning
3042

3143
- name: Install uv
32-
uses: astral-sh/setup-uv@v5
33-
with:
34-
python-version: "3.12"
44+
run: |
45+
set -euo pipefail
46+
curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh" | sh
47+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
3548
3649
# --no-sources: build with `tool.uv.sources` disabled, so the build is
3750
# exercised the way a non-uv consumer's build backend would see it. Without
@@ -125,10 +138,20 @@ jobs:
125138
cp dist/"${PKG//-/_}"-* upload/
126139
test "$(ls upload | wc -l)" -eq 2 # exactly one wheel + one sdist
127140
ls -l upload
128-
- uses: pypa/gh-action-pypi-publish@release/v1
129-
with:
130-
packages-dir: upload/
131-
repository-url: https://test.pypi.org/legacy/
141+
- name: Install uv
142+
run: |
143+
set -euo pipefail
144+
curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh" | sh
145+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
146+
# --trusted-publishing always: never silently fall back to looking for a
147+
# token if the OIDC exchange fails. Fail instead, so a broken publisher
148+
# config surfaces as an error rather than an auth prompt.
149+
- name: Publish to TestPyPI
150+
run: |
151+
uv publish --trusted-publishing always \
152+
--publish-url https://test.pypi.org/legacy/ \
153+
--check-url https://test.pypi.org/simple/ \
154+
upload/*
132155
133156
publish-pypi:
134157
needs: build
@@ -160,9 +183,23 @@ jobs:
160183
cp dist/"${PKG//-/_}"-* upload/
161184
test "$(ls upload | wc -l)" -eq 2 # exactly one wheel + one sdist
162185
ls -l upload
163-
- uses: pypa/gh-action-pypi-publish@release/v1
164-
with:
165-
packages-dir: upload/
186+
- name: Install uv
187+
run: |
188+
set -euo pipefail
189+
curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh" | sh
190+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
191+
# --trusted-publishing always: never silently fall back to looking for a
192+
# token if the OIDC exchange fails. Fail instead, so a broken publisher
193+
# config surfaces as an error rather than an auth prompt.
194+
#
195+
# --check-url makes a re-run idempotent: already-uploaded files are
196+
# skipped rather than erroring. Matters because `fail-fast: false` means
197+
# a partial publish is a state you can land in and need to resume from.
198+
- name: Publish to PyPI
199+
run: |
200+
uv publish --trusted-publishing always \
201+
--check-url https://pypi.org/simple/ \
202+
upload/*
166203
167204
# Attach the built wheels to the GitHub Release so `pip install <url>` and
168205
# air-gapped consumers get the exact artifacts that went to PyPI.

RELEASING.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,24 @@ Publishing the release triggers the workflow, which:
5151
2. Fails the run if the built version does not match the tag, or is a `.devN`
5252
version (which means the tag was not reachable from the checked-out commit).
5353
3. Smoke-tests the wheels in a clean venv (imports + `nooa --version`).
54-
4. Uploads to PyPI via **Trusted Publishing** — no API tokens.
54+
4. Uploads to PyPI via **Trusted Publishing** (`uv publish`) — no API tokens.
5555
5. Attaches the wheels and sdists to the GitHub Release.
5656

57-
The upload waits on the `pypi` GitHub Environment, so you can require a manual
58-
approval there if you want a second pair of eyes before the irreversible step.
57+
Each upload waits on its `pypi-<package>` GitHub Environment, so a required
58+
reviewer there gives a second pair of eyes before the irreversible step.
59+
60+
> **Why no third-party actions.** Every `uses:` in `publish.yml` is an
61+
> `actions/*` action. This org enforces a GitHub Actions allowlist, and a
62+
> disallowed action fails the *entire workflow* at startup — that is what left
63+
> CI dead for eight days (PR #50). A publish workflow that cannot start is one
64+
> that silently never ships, so uv is installed from a pinned install script
65+
> and does the upload itself.
66+
>
67+
> The tradeoff is **no PEP 740 attestations**: `uv publish` uploads them but
68+
> [does not generate them](https://docs.astral.sh/uv/guides/package/), and the
69+
> action that does (`pypa/gh-action-pypi-publish`) may not be allowlisted.
70+
> Worth revisiting if it is added to the allowlist, or once uv can generate
71+
> them. Trusted Publishing itself is unaffected.
5972
6073
### Dry run against TestPyPI
6174

0 commit comments

Comments
 (0)