Repository navigation
242 lines (234 loc) · 10.4 KB
/
Copy pathpublish-python.yml
File metadata and controls
242 lines (234 loc) · 10.4 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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# SPDX-License-Identifier: MPL-2.0
name: Publish HMath release
on:
workflow_dispatch:
inputs:
index:
description: Package index
type: choice
options: [testpypi, pypi]
default: testpypi
permissions:
contents: read
actions: read
concurrency:
group: publish-hmath-${{ inputs.index }}
cancel-in-progress: false
jobs:
select:
# Draft releases require push access to be visible in the releases API.
permissions:
contents: write
actions: read
if: github.repository == 'wavefnd/hmath' && github.ref == 'refs/heads/master'
runs-on: ubuntu-24.04
outputs:
run_id: ${{ steps.candidate.outputs.candidate_run_id }}
version: ${{ steps.candidate.outputs.version }}
source_sha: ${{ steps.candidate.outputs.candidate_sha }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Resume a staged candidate or prepare this source
id: candidate
env:
GH_TOKEN: ${{ github.token }}
run: |
python - <<'PY'
import os
import sys
sys.path.insert(0, "tools")
import select_release_candidate as selector
repository = os.environ["GITHUB_REPOSITORY"]
commit = os.environ["GITHUB_SHA"]
version = selector.release_version()
receipt = selector._receipt_run(repository, version)
if receipt:
run_id = receipt["candidate_run_id"]
source = receipt["candidate_sha"]
run = selector.gh_read(f"repos/{repository}/actions/runs/{run_id}")
if run.get("path") == selector.WORKFLOW:
selector.validate_run(run, repository, source, run_id)
else:
if (str(run.get("id")) != run_id
or run.get("repository", {}).get("full_name", "").casefold() != repository.casefold()
or run.get("path") != ".github/workflows/publish-python.yml"
or run.get("head_sha") != source
or run.get("head_branch") != "master"
or run.get("event") != "workflow_dispatch"
or run.get("status") != "completed"):
raise ValueError("staged candidate does not belong to a completed release run")
pages = selector.gh_read(
f"repos/{repository}/actions/runs/{run_id}/jobs?filter=all&per_page=100",
paginate=True,
)
ready = [job for page in pages for job in page["jobs"]
if job["name"].split("/")[-1].strip() == "Release candidate verified"]
if not ready or not any(job["status"] == "completed" and job["conclusion"] == "success"
for job in ready):
raise ValueError("staged release never completed candidate verification")
if selector.source_version(repository, source) != version:
raise ValueError("staged candidate version differs from its source")
else:
# First publication builds and verifies its own candidate in this run.
run_id, source = "", commit
with open(os.environ["GITHUB_OUTPUT"], "a") as output:
print(f"candidate_run_id={run_id}", file=output)
print(f"candidate_sha={source}", file=output)
print(f"version={version}", file=output)
PY
preflight:
needs: select
if: needs.select.outputs.run_id == ''
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.12'
- uses: ./.github/actions/setup-haskell
with:
ghc-version: '9.10.3'
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: build-docs/cabal
key: docs-haddock-${{ hashFiles('hmath.cabal', 'cabal.project', 'src/**', 'cbits/**') }}
restore-keys: docs-haddock-
- name: Check every public API before preparing a complete release
run: |
python -m pip install -r requirements-docs.txt
python tools/generate_numeric_bindings.py --check
python tools/generate_matrix_bindings.py --check
python tools/build_docs.py
interface=$(python - <<'PY'
import json, pathlib
plan = json.loads(pathlib.Path('build-docs/cabal/cache/plan.json').read_text())
library = next(u for u in plan['install-plan'] if u.get('pkg-name') == 'hmath' and u.get('component-name') == 'lib')
print(pathlib.Path(library['dist-dir']) / 'doc/html/hmath/hmath.haddock')
PY
)
python tools/check_binding_coverage.py --interface "$interface" --require-complete
- name: Read the single project version
id: version
run: echo "version=$(python tools/release_version.py)" >> "$GITHUB_OUTPUT"
validate:
needs: preflight
uses: ./.github/workflows/validate.yml
with:
full-matrix: true
distributions:
needs: preflight
uses: ./.github/workflows/python-wheels.yml
haskell-source:
needs: distributions
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: ./.github/actions/setup-haskell
with:
ghc-version: '9.10.3'
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
name: native-distributions
path: source-artifacts
- name: Build the actual Cabal source archive
run: |
version=$(python tools/release_version.py)
mkdir "$RUNNER_TEMP/hmath-source"
tar -xzf "source-artifacts/hmath-$version.tar.gz" -C "$RUNNER_TEMP/hmath-source"
cd "$RUNNER_TEMP/hmath-source/hmath-$version"
cabal check
cabal build lib:hmath flib:hmath --offline -O0
ready:
name: Release candidate verified
needs: [preflight, validate, distributions, haskell-source]
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.12'
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
pattern: '*-distributions'
path: candidate
merge-multiple: true
- name: Record checksums and source identity
run: python tools/release_manifest.py candidate --commit "$GITHUB_SHA" --run-id "$GITHUB_RUN_ID"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-candidate
path: candidate/*
if-no-files-found: error
publish:
needs: [select, ready]
if: ${{ !cancelled() && needs.select.result == 'success' && (needs.select.outputs.run_id != '' || needs.ready.result == 'success') }}
runs-on: ubuntu-24.04
environment:
name: ${{ inputs.index }}
permissions:
id-token: write
contents: write
actions: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Download the selected immutable candidate
env:
GH_TOKEN: ${{ github.token }}
CANDIDATE_RUN_ID: ${{ needs.select.outputs.run_id || github.run_id }}
run: gh run download "$CANDIDATE_RUN_ID" --repo "$GITHUB_REPOSITORY" --name release-candidate --dir candidate
- name: Verify all artifacts and source identity
env:
SOURCE_SHA: ${{ needs.select.outputs.source_sha }}
VERSION: ${{ needs.select.outputs.version }}
RUN_ID: ${{ needs.select.outputs.run_id || github.run_id }}
run: python tools/verify_release_candidate.py candidate --commit "$SOURCE_SHA" --version "$VERSION" --run-id "$RUN_ID"
- name: Verify existing Python package hashes before external writes
id: remote
env:
VERSION: ${{ needs.select.outputs.version }}
INDEX: ${{ inputs.index }}
run: |
python - <<'PY'
import os
import sys
from pathlib import Path
sys.path.insert(0, "tools")
from check_release_remote import fetch, python_status
files = sorted(Path("candidate").glob("*.whl")) + sorted(Path("candidate").glob("hmath_python-*.tar.gz"))
if not files:
raise ValueError("missing Python distributions")
host = "pypi.org" if os.environ["INDEX"] == "pypi" else "test.pypi.org"
version = os.environ["VERSION"]
complete = python_status(files, fetch(f"https://{host}/pypi/hmath-python/{version}/json"))
with open(os.environ["GITHUB_OUTPUT"], "a") as output:
print(f"python_complete={str(complete).lower()}", file=output)
PY
- name: Verify and stage all GitHub assets before package publication
if: inputs.index == 'pypi'
env:
GH_TOKEN: ${{ github.token }}
SOURCE_SHA: ${{ needs.select.outputs.source_sha }}
run: python tools/publish_github_release.py candidate --repository "$GITHUB_REPOSITORY" --commit "$SOURCE_SHA" --stage-only
- name: Stage Python-only upload files
run: |
mkdir dist
cp candidate/*.whl candidate/hmath_python-*.tar.gz dist/
- name: Publish to TestPyPI
if: inputs.index == 'testpypi' && steps.remote.outputs.python_complete != 'true'
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # release/v1
with:
repository-url: https://test.pypi.org/legacy/
skip-existing: true
- name: Publish to PyPI
if: inputs.index == 'pypi' && steps.remote.outputs.python_complete != 'true'
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # release/v1
with:
skip-existing: true
- name: Publish the staged GitHub release
if: inputs.index == 'pypi'
env:
GH_TOKEN: ${{ github.token }}
SOURCE_SHA: ${{ needs.select.outputs.source_sha }}
run: python tools/publish_github_release.py candidate --repository "$GITHUB_REPOSITORY" --commit "$SOURCE_SHA"