Repository navigation
261 lines (229 loc) · 9.21 KB
/
Copy pathpython.yml
File metadata and controls
261 lines (229 loc) · 9.21 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
name: Python
on:
# Push only, as in the Inspector: with pull_request as well, every commit on a
# PR branch ran each workflow twice (once per event). PR checks still report
# from the push run of the head commit.
push:
# A push made with the workflow token starts no workflow, so the version PRs
# that automation opens (Version Packages, Prepare Python Release) get no
# push run. Their workflows dispatch this one on the PR's branch instead
# (#5110): `workflow_dispatch` is the one event that token can start, and its
# run's checks attach to the branch's head commit. The rollup a PR reads
# leaves dispatched checks out, so the `dispatch-status` jobs below report
# the run as a commit status instead.
workflow_dispatch:
# Superseded runs on the same ref are cancelled (e.g. rapid amend-pushes),
# except on main, where every merge keeps its own CI run
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
# Every job here only reads the repository (checkout, caches, artifacts), so
# the token is scoped in the file rather than inherited from the repository's
# default workflow permission, which could be widened in the UI with no commit
# (#5091).
permissions:
contents: read
jobs:
detect-packages:
timeout-minutes: 5
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.find-packages.outputs.packages }}
steps:
- uses: actions/checkout@v6
- name: Find Python packages
id: find-packages
working-directory: src
run: |
PACKAGES=$(find . -name pyproject.toml -exec dirname {} \; | sed 's/^\.\///' | jq -R -s -c 'split("\n")[:-1]')
echo "packages=$PACKAGES" >> $GITHUB_OUTPUT
script-tests:
timeout-minutes: 5
name: Test the Python gate script
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v7
with:
node-version: 24
- name: Run node --test
run: node --test scripts/validate-py.test.mjs scripts/coverage-py.test.mjs scripts/lib/py-coverage.test.mjs
test:
timeout-minutes: 15
needs: [detect-packages]
strategy:
matrix:
package: ${{ fromJson(needs.detect-packages.outputs.packages) }}
name: Test ${{ matrix.package }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version-file: "src/${{ matrix.package }}/.python-version"
- uses: actions/setup-node@v7
with:
node-version: 24
# The same per-server chain `npm run validate:py` runs locally: locked
# sync, ruff check, ruff format --check, pyright, pytest, uv build.
- name: Validate
run: node scripts/validate-py.mjs ${{ matrix.package }}
# The per-file coverage gate (#4855), in parallel with `test`: every file of
# each server at >= 90% on lines and on branches. The same script
# `npm run coverage:py` runs locally, one server per leg. fail-fast is off so
# one server's shortfall does not cancel the other legs' verdicts.
coverage:
timeout-minutes: 15
needs: [detect-packages]
strategy:
fail-fast: false
matrix:
package: ${{ fromJson(needs.detect-packages.outputs.packages) }}
name: Coverage ${{ matrix.package }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version-file: "src/${{ matrix.package }}/.python-version"
- uses: actions/setup-node@v7
with:
node-version: 24
- name: Coverage
run: npm run coverage:py -- ${{ matrix.package }}
# The test suites on Windows (#1149), beside the Ubuntu `test` legs. Only
# pytest: ruff, pyright and the build do not depend on the OS.
test-windows:
timeout-minutes: 20
needs: [detect-packages]
strategy:
fail-fast: false
matrix:
package: ${{ fromJson(needs.detect-packages.outputs.packages) }}
name: Test ${{ matrix.package }} (Windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version-file: "src/${{ matrix.package }}/.python-version"
- name: Install dependencies
working-directory: src/${{ matrix.package }}
run: uv sync --locked --all-extras --dev
- name: Run pytest
working-directory: src/${{ matrix.package }}
run: uv run --frozen pytest
build:
timeout-minutes: 10
needs: [detect-packages, test]
strategy:
matrix:
package: ${{ fromJson(needs.detect-packages.outputs.packages) }}
name: Build ${{ matrix.package }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version-file: "src/${{ matrix.package }}/.python-version"
- name: Install dependencies
working-directory: src/${{ matrix.package }}
run: uv sync --locked --all-extras --dev
- name: Run pyright
working-directory: src/${{ matrix.package }}
run: uv run --frozen pyright
- name: Build package
working-directory: src/${{ matrix.package }}
run: uv build
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: dist-${{ matrix.package }}
path: src/${{ matrix.package }}/dist/
# A dispatched run's checks attach to the branch's head commit but are left
# out of its status rollup, which is what a pull request reads (verified on
# #5110: 29 successful dispatched check runs, a rollup of null). So on a
# dispatch, which is how a version PR opened by automation gets CI, these two
# jobs mirror the run onto the commit as a status, which the rollup does
# include: pending at the start, then the result of every job above. A push
# run reports through its own checks and skips both.
dispatch-status-pending:
name: Report the dispatched run as pending
if: github.event_name == 'workflow_dispatch'
timeout-minutes: 5
runs-on: ubuntu-latest
permissions:
statuses: write
steps:
- name: Set the commit status to pending
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh api "repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \
-f state=pending -f context="Python (dispatched)" \
-f description="Dispatched run in progress" \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
# `always()`, so a cancelled run cannot leave the status pending. A run that
# a newer dispatch on the same commit superseded posts nothing, whatever its
# jobs' results, and the newer run reports instead; any other cancellation
# is reported as an error. A job skipped by its own `if:` counts as passed.
dispatch-status:
name: Report the dispatched run's result
if: github.event_name == 'workflow_dispatch' && always()
needs:
[
dispatch-status-pending,
detect-packages,
script-tests,
test,
coverage,
test-windows,
build,
]
timeout-minutes: 5
runs-on: ubuntu-latest
permissions:
statuses: write
# To look for a newer dispatched run of this workflow on the commit.
actions: read
steps:
- name: Set the commit status to the run's result
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NEEDS: ${{ toJSON(needs) }}
run: |
# A run superseded by a newer dispatch on the same commit posts
# nothing, whatever its jobs' results: the newer run owns the status
# (#5178). A failed lookup counts as no newer run, so this run still
# reports: a stale status is better than one left pending.
NEWER=$(gh run list --workflow python.yml --event workflow_dispatch \
--commit "$GITHUB_SHA" --json databaseId \
--jq "[.[] | select(.databaseId > $GITHUB_RUN_ID)] | length") || NEWER=0
if [ "${NEWER:-0}" -gt 0 ]; then
echo "Superseded by a newer dispatched run, which reports instead"
exit 0
fi
if jq -e 'all(.[]; .result == "success" or .result == "skipped")' <<<"$NEEDS" >/dev/null; then
STATE=success DESCRIPTION="Every job passed"
elif jq -e 'any(.[]; .result == "failure")' <<<"$NEEDS" >/dev/null; then
STATE=failure DESCRIPTION="A job failed; open the run for details"
else
STATE=error DESCRIPTION="The run was cancelled; dispatch it again"
fi
gh api "repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \
-f state="$STATE" -f context="Python (dispatched)" \
-f description="$DESCRIPTION" \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"