Repository navigation
321 lines (298 loc) · 13.8 KB
/
Copy pathci-unit.yml
File metadata and controls
321 lines (298 loc) · 13.8 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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
name: ci-unit
# THE UNIT AND C++ E2E TESTS OF ONE HOST, BUILT ONCE AND RUN IN SHARDS (design
# 2026-11-14). Called twice by ci.yml, as two stages:
#
# archive every test built once by the bootstrap mcpp (`mcpp test --no-run
# --no-programs --archive`, SPEC-015 §4), with the measured history
# of main's last run beside it: artifact `mcpp-unit-<host>`. The
# package's program is not built here: the tests run build.yml's.
# It needs no build of this commit, so it runs beside build.yml.
# run the host's gate, then the shards: each runs its part of the
# archive (`mcpp test --archive --shard I/N --split auto`) against
# this commit's mcpp as build.yml built it (MCPP_BIN_MCPP), the tests
# assigned longest first by that one history, so the shards together
# run each test once; a re-run of a failed shard runs what it failed.
#
# Measured on 2026-11-14 (Linux x86_64, GCC): the gate held the build job 18.6
# minutes after the binary was built, every Linux job waited for it, and the
# one job that then built and ran every test passed its 45-minute limit. In the
# archive stage the gate and the archive built the package's program again
# (1118 s of a 38.5-minute stage), and the shards tested that build.
on:
workflow_call:
inputs:
stage:
description: archive | run
type: string
required: true
host:
description: the host of build.yml's artifact (linux-x86_64)
type: string
required: true
name:
description: the host's name in the stores' artifact names (linux)
type: string
required: true
runs-on:
type: string
required: true
gate:
description: the tests that must pass before the run goes on (build.yml `gate`)
type: string
default: e2e/basics
tests:
description: the tests written as `mcpp-tests-<host>` for the jobs that run named cases (e2e-cases)
type: string
default: ''
shards:
description: the run stage's shards, a JSON list of 1..N
type: string
default: '[1]'
archived:
description: the number of tests the archive stage built; 0 starts no shard
type: string
default: '1'
cases:
description: the run stage's cases of `mcpp-tests-<host>` that need the MinGW cross toolchain, one per line
type: string
default: ''
outputs:
archived:
description: the number of tests the archive holds
value: ${{ jobs.archive.outputs.archived }}
env:
MCPP_HOME: /home/runner/.mcpp
# Verbose every mcpp invocation for richer CI diagnostics (src/cli.cppm). The
# e2e cases read mcpp's default output; the case support removes it.
MCPP_VERBOSE: "1"
jobs:
archive:
name: unit tests, built (${{ inputs.host }})
if: inputs.stage == 'archive'
runs-on: ${{ inputs.runs-on }}
timeout-minutes: 60
outputs:
archived: ${{ steps.archive.outputs.archived }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/bootstrap-mcpp
# THE PACKAGE'S PROGRAM IS BUILD.YML'S. `--no-programs` (2026.11.13.1)
# plans the package's programs for their names and neither builds nor
# carries them; the run takes each from MCPP_BIN_<NAME>. A bootstrap
# without the option builds and carries the program, as before it, and
# the tests run that build of this commit's sources.
- name: The archive's programs
id: programs
shell: bash
run: |
if "$MCPP" test --help 2>/dev/null | grep -q -- '--no-programs'; then
echo "flag=--no-programs" >> "$GITHUB_OUTPUT"
else
echo "the bootstrap $("$MCPP" --version 2>/dev/null | head -1) builds the package's program into the archive"
fi
# MAIN'S TEST BUILD, REUSED (design 2026-11-14 §6, W1). The archive of
# main's last successful run carries its build directory and the blob of
# every tracked file it was built from. A file whose blob is the same is
# given a time older than every output; a changed one keeps the
# checkout's, so only what changed and what imports it is built again.
# The bootstrap mcpp is pinned, so the configuration is main's; a pin
# that moved builds whole, as a run without the artifact does.
- name: Main's test build
shell: bash
env:
GH_TOKEN: ${{ github.token }}
HOST: ${{ inputs.host }}
run: |
set -uo pipefail
run=$(gh run list -R "$GITHUB_REPOSITORY" --workflow ci.yml --branch main --status success --limit 1 \
--json databaseId -q '.[0].databaseId' 2>/dev/null || true)
[ -n "$run" ] || { echo "no successful run of main: the tests are built whole"; exit 0; }
if ! gh run download "$run" -R "$GITHUB_REPOSITORY" -n "mcpp-unit-build-$HOST" -D "$RUNNER_TEMP/build" 2>/dev/null; then
echo "main's run $run kept no test build for $HOST: the tests are built whole"; exit 0
fi
tar --zstd -xf "$RUNNER_TEMP/build/target.tar.zst" && rm -f "$RUNNER_TEMP/build/target.tar.zst"
python3 .github/tools/source_mtimes.py restore target
echo "main's run $run: its test build restored"
# EVERY TEST, ON EVERY SET. The bootstrap mcpp that builds them selects
# by change without the rule that a test running the package's program
# is affected by the program's sources (2026.11.13.1), so it would leave
# the e2e programs out of a pull request that changes src/; with that
# rule nearly every change selects them all.
- name: The tests, built once
id: archive
shell: bash
run: |
set -euo pipefail
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
mkdir -p "$RUNNER_TEMP/unit"
# shellcheck disable=SC2086 -- the flag is one word or none
# The profile `ci` (mcpp.toml): -O0 and no debug information. The
# default profile of this package is release (-O2); the tests built
# with it took 8988 s of compiler time, and 4183 s with `ci`.
"$MCPP" test --profile ci --no-run ${{ steps.programs.outputs.flag }} --archive "$RUNNER_TEMP/unit/unit.tar.gz" 2>&1 | tee "$RUNNER_TEMP/archive.log"
n=$(sed -n 's/.*Archived \([0-9][0-9]*\) test.*/\1/p' "$RUNNER_TEMP/archive.log" | tail -1)
[ -f "$RUNNER_TEMP/unit/unit.tar.gz" ] || n=0
echo "archived=${n:-0}" >> "$GITHUB_OUTPUT"
echo "the archive holds ${n:-0} test(s)"
# One history for every shard: the shards of one run assign the tests
# from it, so a run of main that finishes while they start changes none
# of their assignments.
- name: The measured history of main's last CI run
shell: bash
env:
GH_TOKEN: ${{ github.token }}
KEY: ${{ inputs.name }}
run: |
set -uo pipefail
mkdir -p "$RUNNER_TEMP/unit/history"
run=$(gh run list -R "$GITHUB_REPOSITORY" --workflow ci.yml --branch main --status success --limit 1 \
--json databaseId -q '.[0].databaseId' 2>/dev/null || true)
if [ -z "$run" ]; then echo "no successful run of main: the shards divide by count"; exit 0; fi
d="$RUNNER_TEMP/unit-stores"
if ! gh run download "$run" -R "$GITHUB_REPOSITORY" -p "mtest-unit-$KEY*" -D "$d" 2>/dev/null; then
echo "main's run $run kept no store for $KEY: the shards divide by count"; exit 0
fi
python3 .github/tools/test_history.py take "$RUNNER_TEMP/unit/history" "$d" "$d"/*/
- name: Upload the tests and their history
if: steps.archive.outputs.archived != '0'
timeout-minutes: 10
uses: actions/upload-artifact@v4
with:
name: mcpp-unit-${{ inputs.host }}
path: ${{ runner.temp }}/unit
if-no-files-found: error
retention-days: 3
# THE TEST BUILD FOR THE NEXT RUN: main's only, so a pull request
# starts from the build of the branch it merges into.
- name: Keep the test build
id: keep
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.archive.outputs.archived != '0'
shell: bash
run: |
set -euo pipefail
python3 .github/tools/source_mtimes.py record target
tar --zstd -cf "$RUNNER_TEMP/target.tar.zst" target
du -h "$RUNNER_TEMP/target.tar.zst"
- name: Upload the test build
if: steps.keep.outcome == 'success'
timeout-minutes: 10
uses: actions/upload-artifact@v4
with:
name: mcpp-unit-build-${{ inputs.host }}
path: ${{ runner.temp }}/target.tar.zst
compression-level: 0
retention-days: 7
# The tests a job that runs named cases takes (e2e-cases), built here
# beside the others; the job names the program they test.
- name: Write the tests other jobs run
if: inputs.tests != ''
shell: bash
run: |
set -euo pipefail
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
# shellcheck disable=SC2086 -- the input is a list of test paths
"$MCPP" test ${{ inputs.tests }} --profile ci --no-run ${{ steps.programs.outputs.flag }} --archive "dist/mcpp-tests.tar.gz"
- name: Upload the tests other jobs run
if: inputs.tests != ''
timeout-minutes: 5
uses: actions/upload-artifact@v4
with:
name: mcpp-tests-${{ inputs.host }}
path: dist/mcpp-tests.tar.gz
if-no-files-found: error
retention-days: 7
shard:
name: unit tests ${{ matrix.shard }}/${{ strategy.job-total }} (${{ inputs.host }}, self-host)
if: inputs.stage == 'run' && inputs.archived != '0'
runs-on: ${{ inputs.runs-on }}
# The caller grants `actions: write`: a failed gate cancels the run. A job
# of a called workflow that asks for more than its caller grants fails the
# whole run at its start, the stage that does not run it included.
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
shard: ${{ fromJson(inputs.shards) }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/bootstrap-mcpp
- uses: ./.github/actions/use-built-mcpp
with:
host: ${{ inputs.host }}
- name: The tests of this run
uses: actions/download-artifact@v4
with:
name: mcpp-unit-${{ inputs.host }}
path: ${{ runner.temp }}/unit
# The program the tests run is this commit's, as build.yml built it: an
# archive written with --no-programs takes it from MCPP_BIN_MCPP.
- name: The program under test
shell: bash
run: |
echo "MCPP_VENDORED_XLINGS=$XLINGS_BIN" >> "$GITHUB_ENV"
echo "MCPP_BIN_MCPP=$MCPP" >> "$GITHUB_ENV"
# THE HOST'S GATE (build.yml): a new program scaffolded, built and run by
# this commit's mcpp, seconds of running, from the archive. A red commit
# stops here, as the shards start, and the run is cancelled. The first
# shard runs it; the gate's tests run again in their own shard.
- name: The host's gate
id: gate
if: inputs.gate != '' && matrix.shard == 1
shell: bash
run: |
set -euo pipefail
# shellcheck disable=SC2086 -- the input is a list of test paths
"$MCPP" test --archive "$RUNNER_TEMP/unit/unit.tar.gz" ${{ inputs.gate }}
- name: A failed gate cancels the run
if: ${{ failure() && steps.gate.outcome == 'failure' }}
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
echo "::error::the gate of ${{ inputs.host }} failed: the run is cancelled"
gh run cancel "$GITHUB_RUN_ID" -R "$GITHUB_REPOSITORY" \
|| echo "::warning::the run could not be cancelled (the token's actions permission)"
# One history for every shard, taken after the gate: the gate's run is
# not the shard's, and a record of it in the store would be the newest,
# sizing this shard's tests unlike the others' and answering a re-run's
# `--failed` in place of the attempt that failed.
- name: The history the shards share
shell: bash
run: |
rm -rf -- "$GITHUB_WORKSPACE/target/mtest"
python3 .github/tools/test_history.py take target/mtest "$RUNNER_TEMP/unit/history"
- name: Unit + integration tests via `mcpp test`
uses: ./.github/actions/mcpp-unit
with:
name: ${{ inputs.name }}-${{ matrix.shard }}
archive: ${{ runner.temp }}/unit/unit.tar.gz
shard: ${{ matrix.shard }}/${{ strategy.job-total }}
# The Windows cases only a job with the MinGW cross toolchain runs, from the
# tests the archive stage wrote (no build here); each case is held to its
# `ok` line, so a skip cannot pass as a run.
cases:
name: Windows cases from ${{ inputs.host }} (mingw-cross)
if: inputs.stage == 'run' && inputs.cases != ''
runs-on: ${{ inputs.runs-on }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/bootstrap-mcpp
- uses: ./.github/actions/use-built-mcpp
with:
host: ${{ inputs.host }}
# llvm beside it: the case that pairs clang with the MinGW payload reads
# the program's resources with the payload's llvm-readobj (`llvm`).
- name: Install mingw-cross toolchain
shell: bash
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$MCPP" toolchain install mingw-cross 16.1.0
"$MCPP" toolchain install llvm 23.1.3
echo "MCPP_VENDORED_XLINGS=$XLINGS_BIN" >> "$GITHUB_ENV"
- uses: ./.github/actions/e2e-cases
with:
mcpp: ${{ env.MCPP }}
under-test: ${{ env.MCPP }}
host: ${{ inputs.host }}
cases: ${{ inputs.cases }}