Skip to content

Commit efc2c1e

Browse files
committed
ci(publish): add SHA-pinned OIDC PyPI publish workflow
Triggers: - push to any `v*` tag auto-publishes. - workflow_dispatch lets you (re)publish an existing tag manually (e.g. backfilling v0.2.1 which shipped before this workflow). Details: - Trusted Publisher (OIDC) via pypa/gh-action-pypi-publish — no long-lived PyPI token stored in GitHub. All third-party actions SHA-pinned. - Build step sanity-checks that pyproject.toml's version matches the tag when triggered by a tag push. Skipped for workflow_dispatch. - Uses `environment: pypi` so PyPI's environment-scoped Trusted Publisher binding works (see TODOS.md for the one-time PyPI setup instructions). No PyPI upload happens on this commit — the workflow only runs on tag push or manual dispatch.
1 parent d2cd280 commit efc2c1e

2 files changed

Lines changed: 97 additions & 3 deletions

File tree

.github/workflows/publish.yml

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
name: Publish to PyPI
2+
3+
# Triggers:
4+
# - Pushing any tag matching `v*` auto-publishes that ref to PyPI.
5+
# - `workflow_dispatch` lets you (re)publish an existing tag manually,
6+
# useful for backfilling a tag that shipped before this workflow
7+
# existed (e.g. v0.2.1) or for retrying a failed publish.
8+
9+
on:
10+
push:
11+
tags:
12+
- "v*"
13+
workflow_dispatch:
14+
inputs:
15+
tag:
16+
description: "Existing tag to publish (e.g. v0.2.1). Leave blank to use the branch HEAD."
17+
required: false
18+
type: string
19+
default: ""
20+
21+
jobs:
22+
build:
23+
name: Build sdist + wheel
24+
runs-on: ubuntu-latest
25+
steps:
26+
- name: Checkout
27+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
28+
with:
29+
# workflow_dispatch may override with a specific tag; otherwise
30+
# use the ref that triggered the workflow (tag push = that tag).
31+
ref: ${{ github.event.inputs.tag != '' && github.event.inputs.tag || github.ref }}
32+
33+
- name: Set up uv
34+
uses: astral-sh/setup-uv@e4db8464a088ece1b920f60402e813ea4de65b8f # v4
35+
36+
- name: Build distributions
37+
run: uv build
38+
39+
- name: Sanity-check version matches tag
40+
# When triggered by a tag push, ensure pyproject.toml version matches
41+
# the tag name. Skipped for workflow_dispatch (where we assume the
42+
# invoker knows what they're doing).
43+
if: github.event_name == 'push'
44+
run: |
45+
TAG_VERSION="${GITHUB_REF_NAME#v}"
46+
PKG_VERSION=$(grep -E '^version = ' pyproject.toml | head -1 | sed -E 's/version = "([^"]+)"/\1/')
47+
echo "Tag: v${TAG_VERSION} pyproject: ${PKG_VERSION}"
48+
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
49+
echo "::error::Tag v${TAG_VERSION} does not match pyproject version ${PKG_VERSION}"
50+
exit 1
51+
fi
52+
53+
- name: Upload artifacts
54+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
55+
with:
56+
name: dist
57+
path: dist/
58+
if-no-files-found: error
59+
retention-days: 7
60+
61+
publish:
62+
name: Publish to PyPI (OIDC)
63+
needs: build
64+
runs-on: ubuntu-latest
65+
# `environment` is required by PyPI's Trusted Publisher if one was
66+
# configured with an environment name. We use `pypi` as the
67+
# convention. If you set a different name on PyPI, update it here.
68+
environment:
69+
name: pypi
70+
url: https://pypi.org/project/seeklink/
71+
permissions:
72+
# Required for PyPI Trusted Publishing (OIDC) — the action exchanges
73+
# this for a short-lived PyPI upload token, so no long-lived API
74+
# secret is ever stored in GitHub.
75+
id-token: write
76+
steps:
77+
- name: Download artifacts
78+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
79+
with:
80+
name: dist
81+
path: dist/
82+
83+
- name: Publish to PyPI
84+
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
85+
# No `password:` input — Trusted Publishing via OIDC.
86+
with:
87+
packages-dir: dist/

TODOS.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,16 @@ Currently freshness warnings only appear in the cold-start CLI path (`seeklink s
3333

3434
## Infrastructure
3535

36-
### PyPI v0.2 publishing
37-
Publish v0.2.0 to PyPI. v0.1.0 is already there (manually published).
38-
When setting this up, prefer a [Trusted Publisher (OIDC)](https://docs.pypi.org/trusted-publishers/) flow via a SHA-pinned `pypa/gh-action-pypi-publish` action instead of a long-lived API token. One-time config; removes the "what if the token leaks" supply-chain class entirely.
36+
### PyPI publishing (in progress)
37+
`.github/workflows/publish.yml` (v0.2.1+) ships an OIDC-based Trusted Publisher flow via a SHA-pinned `pypa/gh-action-pypi-publish@v1.14.0`. Future tags matching `v*` will auto-publish once the Trusted Publisher has been registered on PyPI. One-time PyPI-side setup:
38+
39+
1. Log in to https://pypi.org/manage/project/seeklink/settings/publishing/
40+
2. Add a new **trusted publisher** with:
41+
- Owner: `simonsysun`
42+
- Repository name: `seeklink`
43+
- Workflow filename: `publish.yml`
44+
- Environment name: `pypi`
45+
3. Trigger the workflow once for v0.2.1 via `gh workflow run publish.yml -f tag=v0.2.1` (or via the GitHub Actions UI with the `tag` input set to `v0.2.1`).
3946

4047
### Multi-vault daemon support
4148
Current daemon binds to a single socket (`~/.rhizome/seeklink.sock`) regardless of vault. If multiple vaults need concurrent daemons, hash the vault path into the socket name. Deferred until multi-vault is a real use case.

0 commit comments

Comments
 (0)