Skip to content

Commit 3b5f89e

Browse files
ci: tag-driven release pipeline with TestPyPI RC track
Add a Release workflow triggered by tag push. Tag pattern decides the publish target: v0.5.0-rc1, v0.5.0-rc2, ... → TestPyPI (staging / dry-run) v0.5.0 → PyPI + GitHub Release Both publish paths use Trusted Publishing (OIDC) via pypa/gh-action-pypi-publish — no long-lived API tokens. The pypi environment is the recommended place to add a "required reviewers" protection rule so real-PyPI uploads pause for manual approval. Also extend ci.yml with a `uv build` step at the end of the test job. Verifies the package builds on every PR/push so packaging bugs surface before they hit a release tag, not after. First publish from either workflow needs a one-time pending-publisher setup on TestPyPI and PyPI plus the testpypi/pypi GitHub Environments to exist; documented in the PR description.
1 parent 37e0bfd commit 3b5f89e

2 files changed

Lines changed: 149 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,10 @@ jobs:
4949

5050
- name: Run tests (pytest)
5151
run: uv run pytest -q
52+
53+
# Build the wheel + sdist so packaging bugs (missing files in the
54+
# wheel, malformed metadata, README that won't render on PyPI)
55+
# surface here rather than waiting for a release tag. No upload —
56+
# the artifact is discarded after the job ends.
57+
- name: Build package
58+
run: uv build

.github/workflows/release.yml

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
name: Release
2+
3+
# Tag conventions:
4+
# - v0.5.0-rc1, v0.5.0-rc2, ... → TestPyPI only (staging / dry-run)
5+
# - v0.5.0 → PyPI + GitHub Release
6+
#
7+
# pyproject.toml's version field MUST match the tag (minus the leading "v").
8+
# PEP 440 normalizes "0.5.0-rc1" to "0.5.0rc1" at build time, so either form
9+
# is accepted in pyproject; the tag uses the dash form for readability.
10+
on:
11+
push:
12+
tags:
13+
- "v[0-9]*.[0-9]*.[0-9]*"
14+
15+
# Read-only by default. Each publishing job opts in to id-token: write
16+
# (Trusted Publishing OIDC); the github-release job opts in to contents:
17+
# write to create the release.
18+
permissions:
19+
contents: read
20+
21+
jobs:
22+
test:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@v4
27+
with:
28+
# Conformance fixtures live in the openarmature-spec submodule.
29+
submodules: recursive
30+
31+
- name: Install uv
32+
uses: astral-sh/setup-uv@v4
33+
with:
34+
enable-cache: true
35+
36+
- name: Sync deps
37+
run: uv sync --frozen
38+
39+
- name: Lint (ruff check)
40+
run: uv run ruff check .
41+
42+
- name: Format check (ruff format --check)
43+
run: uv run ruff format --check .
44+
45+
- name: Type check (pyright)
46+
run: uv run pyright src/ tests/
47+
48+
- name: Run tests (pytest)
49+
run: uv run pytest -q
50+
51+
build:
52+
needs: test
53+
runs-on: ubuntu-latest
54+
steps:
55+
- name: Checkout
56+
uses: actions/checkout@v4
57+
with:
58+
submodules: recursive
59+
60+
- name: Install uv
61+
uses: astral-sh/setup-uv@v4
62+
with:
63+
enable-cache: true
64+
65+
- name: Sync deps
66+
run: uv sync --frozen
67+
68+
- name: Build package
69+
run: uv build
70+
71+
- name: Upload dist artifact
72+
uses: actions/upload-artifact@v4
73+
with:
74+
name: dist
75+
path: dist/
76+
77+
# Pre-release tags (vX.Y.Z-rc*) publish to TestPyPI for verification.
78+
# The job is gated on the tag's name containing "-rc"; non-RC tags skip
79+
# this job and proceed to publish-pypi instead.
80+
publish-testpypi:
81+
needs: build
82+
if: contains(github.ref_name, '-rc')
83+
runs-on: ubuntu-latest
84+
environment: testpypi
85+
permissions:
86+
id-token: write
87+
steps:
88+
- name: Download dist artifact
89+
uses: actions/download-artifact@v4
90+
with:
91+
name: dist
92+
path: dist/
93+
94+
- name: Publish to TestPyPI
95+
uses: pypa/gh-action-pypi-publish@release/v1
96+
with:
97+
repository-url: https://test.pypi.org/legacy/
98+
99+
# Real-release tags (vX.Y.Z, no rc) publish to PyPI. The pypi
100+
# environment is the recommended place to add a "required reviewers"
101+
# protection rule so the job pauses for manual approval before any
102+
# real-PyPI upload. Without that rule it auto-publishes.
103+
publish-pypi:
104+
needs: build
105+
if: ${{ !contains(github.ref_name, '-rc') }}
106+
runs-on: ubuntu-latest
107+
environment: pypi
108+
permissions:
109+
id-token: write
110+
steps:
111+
- name: Download dist artifact
112+
uses: actions/download-artifact@v4
113+
with:
114+
name: dist
115+
path: dist/
116+
117+
- name: Publish to PyPI
118+
uses: pypa/gh-action-pypi-publish@release/v1
119+
120+
# GitHub Release with auto-generated notes from commits since the last
121+
# tag. Only fires on real releases (RCs leave no GitHub Release behind).
122+
github-release:
123+
needs: publish-pypi
124+
if: ${{ !contains(github.ref_name, '-rc') }}
125+
runs-on: ubuntu-latest
126+
permissions:
127+
contents: write
128+
steps:
129+
- name: Checkout
130+
uses: actions/checkout@v4
131+
132+
- name: Download dist artifact
133+
uses: actions/download-artifact@v4
134+
with:
135+
name: dist
136+
path: dist/
137+
138+
- name: Create GitHub Release
139+
uses: softprops/action-gh-release@v2
140+
with:
141+
files: dist/*
142+
generate_release_notes: true

0 commit comments

Comments
 (0)