Skip to content

Commit 3fb557f

Browse files
authored
chore: add publishing workflow and pin action SHA (#22)
1 parent 7878ce1 commit 3fb557f

2 files changed

Lines changed: 125 additions & 7 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ on:
55
branches: [master]
66
pull_request:
77
workflow_dispatch:
8+
# Lets the release workflow run this exact gate (lint + test matrix +
9+
# package) before publishing, instead of duplicating it. workflow_call adds
10+
# no automatic runs of its own.
11+
workflow_call:
812

913
concurrency:
1014
group: ${{ github.workflow }}-${{ github.ref }}
@@ -27,13 +31,13 @@ jobs:
2731
name: Lint and type-check
2832
runs-on: ubuntu-latest
2933
steps:
30-
- uses: actions/checkout@v4
34+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
3135
with:
3236
# Don't leave GITHUB_TOKEN in .git/config for later steps to reach.
3337
persist-credentials: false
3438

3539
- name: Install uv
36-
uses: astral-sh/setup-uv@v5
40+
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
3741
with:
3842
enable-cache: true
3943

@@ -69,13 +73,13 @@ jobs:
6973
matrix:
7074
python-version: ["3.11", "3.12", "3.13"]
7175
steps:
72-
- uses: actions/checkout@v4
76+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
7377
with:
7478
# Don't leave GITHUB_TOKEN in .git/config for later steps to reach.
7579
persist-credentials: false
7680

7781
- name: Install uv
78-
uses: astral-sh/setup-uv@v5
82+
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
7983
with:
8084
enable-cache: true
8185
python-version: ${{ matrix.python-version }}
@@ -93,13 +97,13 @@ jobs:
9397
name: Build and verify distributions
9498
runs-on: ubuntu-latest
9599
steps:
96-
- uses: actions/checkout@v4
100+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
97101
with:
98102
# Don't leave GITHUB_TOKEN in .git/config for later steps to reach.
99103
persist-credentials: false
100104

101105
- name: Install uv
102-
uses: astral-sh/setup-uv@v5
106+
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
103107
with:
104108
enable-cache: true
105109

@@ -116,7 +120,7 @@ jobs:
116120
- name: Verify distribution contents
117121
run: uv run --no-project python scripts/check_dist.py
118122

119-
- uses: actions/upload-artifact@v4
123+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
120124
with:
121125
name: distributions
122126
path: dist/

‎.github/workflows/release.yml‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
name: Release
2+
3+
# Publishes to PyPI via Trusted Publishing (OIDC)
4+
# Two paths:
5+
#
6+
# * Push a tag `vX.Y.Z` -> validate, build, verify, publish to PyPI.
7+
# * Run manually (workflow_dispatch) -> validate, build, verify, publish to
8+
# TestPyPI, for a dry run of the whole pipeline against a throwaway index.
9+
#
10+
# A pending publisher must be registered on the target index (PyPI and/or
11+
# TestPyPI) pointing at this repo + release.yml + the matching environment.
12+
# That is a one-time web step and cannot be done here.
13+
#
14+
# Third-party actions are pinned to full commit SHAs (version in a comment).
15+
# This job graph carries `id-token: write`, so a moving tag on a compromised
16+
# action could exfiltrate the OIDC token or tamper with the artifact.
17+
18+
on:
19+
push:
20+
tags:
21+
- "v*"
22+
workflow_dispatch:
23+
24+
permissions:
25+
contents: read
26+
27+
jobs:
28+
# Run the full PR gate -- lint, type-check, the 3-version test matrix, and
29+
# the packaging check -- before anything is published. Publishing is
30+
# irreversible per version, and a tag can point at any commit, including one
31+
# that never went through CI. Reused from ci.yml rather than duplicated, so
32+
# the release gate cannot drift from the everyday one.
33+
checks:
34+
uses: ./.github/workflows/ci.yml
35+
permissions:
36+
contents: read
37+
38+
build:
39+
name: Build and verify distributions
40+
needs: checks
41+
runs-on: ubuntu-latest
42+
steps:
43+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
44+
with:
45+
persist-credentials: false
46+
47+
- name: Install uv
48+
# No build cache: this build becomes an unremovable public artifact, so
49+
# it is produced from a clean environment rather than a restored cache.
50+
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
51+
52+
# Guard against the one mistake tags make: a vX.Y.Z tag whose number does
53+
# not match pyproject. PyPI would publish whatever pyproject says, under a
54+
# version the tag does not describe -- and it could never be corrected.
55+
- name: Tag matches package version
56+
if: startsWith(github.ref, 'refs/tags/v')
57+
run: |
58+
tag="${GITHUB_REF_NAME#v}"
59+
pkg="$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")"
60+
echo "tag=$tag pyproject=$pkg"
61+
if [ "$tag" != "$pkg" ]; then
62+
echo "::error::tag v$tag does not match pyproject version $pkg"
63+
exit 1
64+
fi
65+
66+
- name: Build sdist and wheel
67+
run: uv build
68+
69+
# Same guard CI runs on every PR: required data ships, no test fixtures
70+
# or signing material leak. Doubly worth it here -- this build is the one
71+
# that becomes an unremovable public artifact.
72+
- name: Verify distribution contents
73+
run: uv run --no-project python scripts/check_dist.py
74+
75+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
76+
with:
77+
name: release-dist
78+
path: dist/
79+
80+
publish-testpypi:
81+
name: Publish to TestPyPI (dry run)
82+
if: github.event_name == 'workflow_dispatch'
83+
needs: build
84+
runs-on: ubuntu-latest
85+
environment: testpypi
86+
permissions:
87+
id-token: write # OIDC token for Trusted Publishing; no stored secret.
88+
steps:
89+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
90+
with:
91+
name: release-dist
92+
path: dist/
93+
94+
- name: Publish
95+
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # release/v1
96+
with:
97+
repository-url: https://test.pypi.org/legacy/
98+
99+
publish-pypi:
100+
name: Publish to PyPI
101+
if: startsWith(github.ref, 'refs/tags/v')
102+
needs: build
103+
runs-on: ubuntu-latest
104+
environment: pypi
105+
permissions:
106+
id-token: write
107+
steps:
108+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
109+
with:
110+
name: release-dist
111+
path: dist/
112+
113+
- name: Publish
114+
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # release/v1

0 commit comments

Comments
 (0)