Repository navigation
303 lines (272 loc) · 13.7 KB
/
Copy pathcron-python-versions.yml
File metadata and controls
303 lines (272 loc) · 13.7 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
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
name: Python Versions
# #1660: keeps .github/python-versions.toml current. `detect` recomputes the
# tiers (util/update_python_versions.py's docstring gives the rules), rewrites
# the file and the prose the audit tests pin to it, and runs that audit. `pr`
# proposes any change on `bot/python-versions` for the owner to merge. Nothing
# here pushes `main` or edits ruleset 23497679: the pull request body lists the
# required contexts to change by hand.
#
# `pr` pushes with the release App's token, not GITHUB_TOKEN, whose pushes
# start nothing, so the pull request's own checks run; none of them runs this
# workflow again. The `discussion` job posts `detect`'s report to this job's
# thread in the Discussions category REPORT_CATEGORY names (#2030); it alone
# holds `discussions: write`, and a report it cannot post is a warning.
on:
schedule:
- cron: '41 5 * * 1' # Mondays at 05:41 UTC
workflow_dispatch:
permissions: {}
# One run at a time, so two runs cannot race for the branch. A queued run is
# kept, not cancelled: it recomputes from the sources and skips what the branch
# already proposes.
concurrency:
group: python-versions
cancel-in-progress: false
# #2030: one Discussions category holds every report with no pull request to
# comment on, one thread per job.
env:
REPORT_CATEGORY: CI reports
jobs:
# No secret and a read-only token. The computation reads the network and
# writes only this checkout, and the audit is the repository's own tests.
detect:
name: Recompute the Python version tiers
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
pull-requests: read
outputs:
changed: ${{ steps.update.outputs.changed }}
publish: ${{ steps.update.outputs.publish }}
# #2030: `warning` where the run asks for a hand without failing.
status: ${{ steps.run-report.outputs.status }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# `latest` for this job, like every single-interpreter job (#1661).
- name: Read the Python versions
id: python-versions
run: python3 util/python_versions.py --github-output
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: ${{ steps.python-versions.outputs.latest }}
# PyYAML, from the `test` extra, for the workflow readers and the audit;
# neither imports pcapkit. requests fetches the sources and markdown-it-py
# reads the runner-images table (#1894); only `update` imports either, so
# `pr`'s `publish` runs without them.
- name: Install PyYAML, requests and markdown-it-py
run: python -m pip install PyYAML requests markdown-it-py
# Writes the proposal to $RUNNER_TEMP/proposal. `publish` is false when
# nothing changed, or when `bot/python-versions`' open pull request
# already proposes these tiers, so an unmerged proposal is not re-pushed,
# nor its approval asked for, every week. A branch holding commits it did
# not make that proposes other tiers fails here, before any approval, as
# `publish` would refuse it. The token reads the pull request's state.
- name: Recompute the tiers
id: update
env:
GH_TOKEN: ${{ github.token }}
run: >-
python util/update_python_versions.py update --write --out "$RUNNER_TEMP/proposal"
--proposal-branch bot/python-versions --github-output
# The modules that pin the workflows and the prose to the file, on
# the rewritten tree. A failure here does not stop the proposal: a tier
# move can need hand edits the script names, and the body lists what failed.
- name: Run the audit tests
id: audit
continue-on-error: true
shell: bash
run: >-
python -m unittest -v tests.project.test_python_versions_1660
tests.project.test_python_matrix_tiers_1633 tests.project.test_ktlo_legs_stay_green_1672
2>&1 | tee "$RUNNER_TEMP/audit.log"
- name: Write the pull request body
if: ${{ steps.update.outputs.changed == 'true' }}
env:
AUDIT_OUTCOME: ${{ steps.audit.outcome }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: >-
python util/update_python_versions.py body "$RUNNER_TEMP/proposal"
--audit-outcome "$AUDIT_OUTCOME" --audit-log "$RUNNER_TEMP/audit.log" --run-url "$RUN_URL"
# Kept for as long as an environment approval can wait. The proposal holds
# files/.github/..., which upload-artifact leaves out unless told otherwise.
- name: Upload the proposal
if: ${{ steps.update.outputs.publish == 'true' }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: python-versions-proposal
path: ${{ runner.temp }}/proposal
include-hidden-files: true
retention-days: 30
- name: Say why no pull request opens
if: ${{ steps.update.outputs.publish == 'true' && vars.RELEASE_APP_CLIENT_ID == '' }}
run: >-
echo "::warning title=No pull request without the release App::vars.RELEASE_APP_CLIENT_ID is unset,
so the pr job skips. Set up the App as docs/source/contributing/releasing.rst describes."
# With nothing changed the tree is main's, so a failing audit is main's own
# failure: the run says so rather than passing green over it.
- name: Fail on a failing audit of an unchanged tree
if: ${{ steps.update.outputs.changed != 'true' && steps.audit.outcome == 'failure' }}
run: |
echo "::error title=The audit fails on main::the tiers need no change, but the audit fails; see above."
exit 1
# #2030: what the run found, for the `discussion` job to post, from the
# plan `update` wrote: each tier that moved and why, the notes it raised,
# the audit, and what becomes of the proposal. `status=warning` where a
# note asks for a hand edit, the audit of a changed tree failed, or no
# pull request can open. `continue-on-error` on both, so a report never
# fails the run.
- name: Write the run report
id: run-report
if: ${{ !cancelled() }}
continue-on-error: true
env:
UPDATE: ${{ steps.update.outcome }}
CHANGED: ${{ steps.update.outputs.changed }}
PUBLISH: ${{ steps.update.outputs.publish }}
AUDIT: ${{ steps.audit.outcome }}
APP: ${{ vars.RELEASE_APP_CLIENT_ID != '' }}
ON_MAIN: ${{ github.ref == 'refs/heads/main' }}
run: |
python3 - "$RUNNER_TEMP/proposal/plan.json" "$RUNNER_TEMP/python-versions-report.md" <<'PY'
import json, os, pathlib, sys
env = os.environ
source, target = map(pathlib.Path, sys.argv[1:])
plan = json.loads(source.read_text(encoding='utf-8')) if source.is_file() else None
def tier(entry):
if entry is None:
return '--'
name = 'KTLO' if entry['tier'] == 'ktlo' else entry['tier']
return f"{name} on `{entry['runner']}`" if 'runner' in entry else name
lines, warned = ['### Python Versions', ''], False
if plan is None:
lines.append('- Tiers: not computed, as the job stopped first')
else:
moves = [f" - `{c['version']}`: {tier(c['before'])} -> {tier(c['after'])} ({c['reason']})"
for c in plan['changes']]
if plan['latest'][0] != plan['latest'][1]:
moves.append(f" - `latest`: {plan['latest'][0]} -> {plan['latest'][1]}")
lines += ['- Tiers moved:', *moves] if moves else ['- Tiers: unchanged']
if plan['changed_files']:
lines.append('- Regenerated: ' + ', '.join(f'`{path}`' for path in plan['changed_files']))
for kind in ('remove', 'add'):
lines += [f'- Ruleset 23497679, by hand: {kind} `{name}`' for name in plan['required'][kind]]
lines += [f"- {note['kind']}: {note['text']}" for note in plan['notes']]
warned = any(note['kind'] != 'held' for note in plan['notes'])
lines.append(f"- Audit: {env.get('AUDIT') or 'not run'}")
warned = warned or (env.get('CHANGED') == 'true' and env.get('AUDIT') != 'success')
if env.get('UPDATE') != 'success':
lines.append('- Pull request: none, as Recompute the tiers failed')
elif env.get('CHANGED') != 'true':
lines.append('- Pull request: none needed, as nothing changed')
elif env.get('PUBLISH') != 'true':
lines.append('- Pull request: none, as `bot/python-versions` already proposes these tiers')
elif env.get('APP') != 'true':
lines.append('- Pull request: none, as `vars.RELEASE_APP_CLIENT_ID` is unset')
warned = True
elif env.get('ON_MAIN') != 'true':
lines.append('- Pull request: none, as this run is not on `main`')
else:
lines.append('- Pull request: the `pr` job opens or updates it once `github-release` is approved')
target.write_text('\n'.join(lines) + '\n', encoding='utf-8')
with open(env['GITHUB_OUTPUT'], 'a', encoding='utf-8') as file:
file.write(f"status={'warning' if warned else 'success'}\n")
PY
cat "$RUNNER_TEMP/python-versions-report.md"
- name: Upload the run report
if: ${{ !cancelled() }}
continue-on-error: true
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: python-versions-report
path: ${{ runner.temp }}/python-versions-report.md
retention-days: 14
# The release App's key is a secret of the `github-release` environment,
# which needs the owner's approval, so he is asked only when there is
# something to propose. The token has no `workflows` permission, so it cannot
# push a workflow file, and `update` writes none. `publish` force-pushes
# `bot/python-versions` alone, refusing while its pull request is open if it
# holds commits it did not make; with none open it recreates it from main.
pr:
name: Open or update the pull request
needs: detect
if: >-
${{ needs.detect.outputs.publish == 'true' && vars.RELEASE_APP_CLIENT_ID != '' &&
github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest
timeout-minutes: 10
environment: github-release
# GITHUB_TOKEN only downloads `detect`'s artifact; the App's token does the rest.
permissions:
actions: read
steps:
- name: Mint the release App's token
id: app
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ vars.RELEASE_APP_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
# The commit `detect` read, with full history for the branch's merge base.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
fetch-depth: 0
token: ${{ steps.app.outputs.token }}
- name: Download the proposal
env:
GH_TOKEN: ${{ github.token }}
run: >-
gh run download "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" --name python-versions-proposal
--dir "$RUNNER_TEMP/proposal"
- name: Push the proposal and open or update its pull request
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
APP_SLUG: ${{ steps.app.outputs.app-slug }}
APP_BOT_ID: ${{ vars.RELEASE_APP_BOT_ID }}
run: python3 util/update_python_versions.py publish "$RUNNER_TEMP/proposal"
# #2030: posts `detect`'s report, on `always()` so a run failed by the audit
# or the script is posted too. Not after `pr`, which waits on the owner's
# approval of `github-release`; the pull request it opens is its own record.
# util/discussion_report.py writes a warning rather than failing;
# `continue-on-error` covers the rest.
discussion:
name: Post the Python version report
needs: [ detect ]
if: ${{ always() && needs.detect.result != 'skipped' }}
runs-on: ubuntu-latest
timeout-minutes: 10
continue-on-error: true
permissions:
contents: read
discussions: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# A run that never wrote a report uploaded none; the script then warns.
- name: Download the report
continue-on-error: true
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: python-versions-report
path: report
# `scheduled` on dispatch too: a manual run is a deliberate report. A
# green `detect` that asked for a hand reports `warning`.
- name: Post the report to Discussions
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
DETECT_RESULT: ${{ needs.detect.result }}
DETECT_STATUS: ${{ needs.detect.outputs.status }}
run: |
status="$DETECT_RESULT"
if [ "$DETECT_RESULT" = success ] && [ -n "$DETECT_STATUS" ]; then
status="$DETECT_STATUS"
fi
python3 util/discussion_report.py --category "$REPORT_CATEGORY" \
--title 'cron-python-versions: Python version update report' \
--report report/python-versions-report.md --mode scheduled --status "$status"