-
Notifications
You must be signed in to change notification settings - Fork 78
190 lines (174 loc) · 7.85 KB
/
Copy pathpublish.yml
File metadata and controls
190 lines (174 loc) · 7.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
name: Publish
# Publishing to PyPI uses a Trusted Publisher (OIDC), configured on PyPI as:
#
# repository: MicroPyramid/django-mfa
# workflow: publish.yml <- this file's NAME is part of the trust
# environment: pypi <- and so is the environment below
#
# There is no API token anywhere: PyPI trades this workflow's short-lived OIDC
# token for an upload token, scoped to this project, valid for minutes. Renaming
# this file, or the environment, silently breaks publishing -- PyPI will reject
# the token because the claims no longer match. Change either one on PyPI first.
#
# The `pypi` environment additionally restricts deployment to tags matching
# `v*`, so a release cut from a branch, or tagged `4.0.0` without the `v`,
# never reaches the publish job at all.
#
# Two ways in, and both arrive on a TAG ref, which is what the version guard
# and the environment rule both key off:
#
# 1. workflow_dispatch, from tag-release.yml, against the tag it just made.
# This is the normal path. It exists because a Release created by
# tag-release.yml's GITHUB_TOKEN does NOT fire the `release` trigger
# below -- GitHub suppresses workflow runs from events its own token
# caused, so workflows cannot loop. workflow_dispatch is one of the two
# documented exceptions, which is the only reason this chain works.
# 2. release: published, for a Release a HUMAN publishes from the GitHub UI.
# A person's token is not suppressed, so this fires normally.
#
# Exactly one of the two fires per release; they do not double-publish.
#
# Note that ci.yml's `push` trigger is branch-scoped, so pushing a tag runs NO
# tests of its own -- that is why this workflow re-runs the corners of the test
# matrix before it builds anything.
#
# GitHub resolves both triggers against the DEFAULT branch, so this file does
# nothing until it is on master -- having it on a feature branch is not enough,
# and there is no warning to that effect anywhere.
on:
release:
types: [published]
workflow_dispatch:
# Read-only by default. Only the `publish` job gets id-token: write, and it is
# the only job that can reach the `pypi` environment.
permissions:
contents: read
concurrency:
group: publish-${{ github.ref }}
# Never cancel a run that may be mid-upload.
cancel-in-progress: false
jobs:
test:
name: py${{ matrix.python-version }} / Django ${{ matrix.django }}
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
# The corners of ci.yml's grid: the oldest supported combination and
# the newest. A release that breaks either must not reach PyPI, and a
# release is exactly the moment nobody is watching CI.
include:
- python-version: "3.10"
django: "4.2"
- python-version: "3.13"
django: "6.1"
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v10.0.0
with:
# No cache anywhere in this workflow -- see the build job below.
enable-cache: false
- name: Run the test suite
run: >
uv run
--python ${{ matrix.python-version }}
--with "django~=${{ matrix.django }}.0"
python test_runner.py
build:
name: Build and install-smoke
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v10.0.0
with:
# Deliberately off, and not just for speed reasons.
#
# The GitHub Actions cache is writable from less-trusted contexts
# (a pull_request run can poison an entry a later run restores), so
# a cache is an input to whatever this job builds -- and what this
# job builds is what gets published to PyPI under the project's
# name. setup-uv v10 made exactly this its headline change,
# disabling caching for release, tag-push, pull_request_target and
# workflow_run events when `enable-cache: auto`.
#
# `auto` is not enough here: this workflow's normal entry point is
# workflow_dispatch on a tag, which is not in that list. And an
# explicit `true` skips the check entirely -- setup-uv reads it as
# `return enableCacheInput === "true"`, before any event test. Only
# an explicit `false` is actually off. ci.yml still caches; it
# publishes nothing.
enable-cache: false
- name: Refuse to publish if the tag and the packaged version disagree
# Nothing else connects the two. hatchling reads the version from
# pyproject.toml and never looks at the tag, so a release tagged
# v4.0.0 cut from a tree still saying 4.0.0a1 would publish 4.0.0a1
# and report success -- the mismatch would surface only to users, and
# PyPI does not allow re-uploading a version to fix it. This has
# already happened once, on the v4.0.0 release of 2026-08-13.
#
# Deliberately NOT conditioned on the event name. An earlier version
# guarded this with `if: github.event_name == 'release'`, which meant
# the workflow_dispatch path skipped the check entirely and reported a
# green tick for having done nothing. A skipped guard reads exactly
# like a passing one.
run: |
case "$GITHUB_REF" in
refs/tags/*) ;;
*) echo "::error::publish must run on a tag ref, got $GITHUB_REF"
exit 1 ;;
esac
tag="${GITHUB_REF_NAME#v}"
version=$(python3 -c "import tomllib, pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
if [ "$tag" != "$version" ]; then
echo "::error::release tag ${GITHUB_REF_NAME} does not match pyproject.toml version ${version}"
exit 1
fi
echo "publishing django-mfa ${version}"
- name: Build sdist and wheel
run: uv build
- name: Check the README renders on PyPI
# PyPI's renderer is not Sphinx, and a README it cannot render is
# published as an EMPTY project page rather than an error. `--strict`
# makes warnings fail here instead.
run: uv run --with "twine" --with "readme-renderer[md]" twine check --strict dist/*
- name: Install the wheel into a clean environment
run: |
uv venv /tmp/smoke
VIRTUAL_ENV=/tmp/smoke uv pip install dist/*.whl
- name: Start Django against the installed package
# Runs from /tmp so the checkout is not on sys.path: this is the job
# that catches a wheel missing a subpackage. See the script's docstring.
working-directory: /tmp
run: /tmp/smoke/bin/python "$GITHUB_WORKSPACE/.github/scripts/smoke_installed_wheel.py"
- name: Upload the distributions for the publish job
uses: actions/upload-artifact@v7
with:
name: distributions
path: dist/
publish:
name: Publish to PyPI
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/django-mfa
permissions:
# Mints the OIDC token PyPI trades for a scoped upload token. This is
# the only job in the repository that has it.
id-token: write
steps:
- name: Download the distributions built above
# Deliberately not a rebuild: what gets uploaded is byte-for-byte the
# artifact the install-smoke actually exercised.
uses: actions/download-artifact@v8
with:
name: distributions
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
# No `with:` block on purpose -- no user, no password, no api-token.
# `skip-existing` stays off: re-running a release whose version is
# already on PyPI should fail loudly, not report a green success.