Repository navigation
251 lines (217 loc) · 8.76 KB
/
Copy pathci-core.yml
File metadata and controls
251 lines (217 loc) · 8.76 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
name: Core CI
on:
pull_request:
push:
branches: [main]
# Cancel superseded runs on the same branch (e.g. when a PR gets a new push)
# so we don't pay for stale-commit mutation runs. The concurrency group is
# keyed on the workflow name + ref, so this is per-workflow — the LSP CI
# can run concurrently with this one without queueing.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
# Number of mutation shards. MUST equal the length of the `mutation` job's
# matrix list below (GitHub can't derive a matrix from an env var). The gate
# asserts it received exactly this many summaries, so if the two ever drift
# the gate fails loudly instead of scoring over a subset of src/.
SHARD_TOTAL: 10
jobs:
phpunit:
# 8.4 is the supported/default runtime and runs the full suite minus the
# `php85` group; a dedicated 8.5 container runs only that group, which
# covers syntax (e.g. the pipe operator `|>`) that the host-version
# parser can only tokenize on PHP 8.5.
name: PHPUnit (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- php: '8.4'
target: test/unit
- php: '8.5'
target: test/unit/php85
steps:
- uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, json, mbstring, tokenizer
coverage: none
tools: composer:v2
- name: Install dependencies
uses: ramsey/composer-install@v3
- name: Run PHPUnit
run: make ${{ matrix.target }}
phpstan:
name: PHPStan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP 8.4
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: dom, json, mbstring, tokenizer
coverage: none
tools: composer:v2
- name: Install dependencies
uses: ramsey/composer-install@v3
- name: Run PHPStan
run: make lint/phpstan
phpstan-pass:
# The `xphp check` PHPStan-integration tests (@group phpstan) shell out to a
# real phpstan binary, so they're excluded from `make test/unit` and run here.
# `composer install` provides vendor/bin/phpstan (a require-dev dependency).
name: PHPStan pass (xphp check)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP 8.4
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: dom, json, mbstring, tokenizer
coverage: none
tools: composer:v2
- name: Install dependencies
uses: ramsey/composer-install@v3
- name: Run the PHPStan-pass tests
run: make test/phpstan-pass
check:
# End-to-end self-test of the `xphp check` gate: runs the real bin/xphp
# binary against the check fixtures and asserts the 0/1/2 exit contract.
# The in-process CheckCommandTest can't observe the shipped binary's
# process exit code or its rendered stdout, so this covers that gap.
name: xphp check (self-test)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP 8.4
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: dom, json, mbstring, tokenizer
coverage: none
tools: composer:v2
- name: Install dependencies
uses: ramsey/composer-install@v3
- name: Run xphp check self-test
run: make test/check
# Mutation testing is split into three stages so the slow part -- running
# every mutant -- fans out across many machines instead of one:
#
# mutation-coverage -> mutation (x10, matrix) -> mutation-gate
#
# Stage 1 generates the code coverage once (pcov). Each stage-2 shard reuses
# it (no per-shard initial test run, no coverage driver) and mutates a
# disjoint, auto-balanced slice of src/. Stage 3 folds the shards' summaries
# into the true project-wide Covered MSI and enforces the 95% gate once.
# `mutation-gate` is the single required check to protect the branch with.
mutation-coverage:
name: Mutation coverage (generate once)
runs-on: ubuntu-latest
needs: phpunit
steps:
- uses: actions/checkout@v4
- name: Setup PHP 8.4 (with pcov for fast coverage)
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: dom, json, mbstring, tokenizer
coverage: pcov
tools: composer:v2
- name: Install dependencies
uses: ramsey/composer-install@v3
- name: Generate coverage for Infection
run: make test/mutation/coverage
- name: Upload coverage for shards
uses: actions/upload-artifact@v4
with:
name: infection-coverage
path: var/infection-coverage
retention-days: 1
mutation:
name: Mutation shard ${{ matrix.shard }}
runs-on: ubuntu-latest
needs: mutation-coverage
strategy:
# One machine per shard. Bump this list and the top-level SHARD_TOTAL
# together to add capacity; the slice packer rebalances automatically,
# and the gate fails if the two ever disagree.
fail-fast: false
matrix:
shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
steps:
- uses: actions/checkout@v4
- name: Setup PHP 8.4
# Shards reuse the coverage artifact and skip the initial test run, so
# they need no coverage driver at all. `:pcov` disables the pcov
# extension (present on the runner image): with it loaded Infection
# both prints a "running with PCOV enabled" notice and instruments
# every mutant's PHPUnit process for coverage we never collect. The
# mutant workers launch a fresh PHP that reads php.ini, so this must be
# disabled at the extension level -- a `-d pcov.enabled=0` on the
# Infection command would only reach the coordinator, not the workers.
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: dom, json, mbstring, tokenizer, :pcov
coverage: none
tools: composer:v2
# Bound each mutant worker's memory. Infection normally caps workers
# at 2x the initial test run's usage, but that measurement only
# happens during the initial run -- which shards skip
# (--skip-initial-tests) -- so no cap is applied and workers inherit
# an unlimited memory_limit. XphpSourceParser has mutants that turn a
# bounded loop unbounded; with no cap one allocates until it exhausts
# the runner's RAM and takes the runner agent down (the job reports
# "canceled / runner shutdown"). A bigger runner only delays this. A
# normal worker uses ~200M, so 1G fatals a runaway cleanly as one
# killed mutant while leaving legit mutants untouched (MSI unchanged:
# these were already being killed, just via the 120s timeout).
# Workers read php.ini directly, so it must be set here, not via a -d
# flag on the Infection command (that reaches only the coordinator).
ini-values: memory_limit=1G
- name: Install dependencies
uses: ramsey/composer-install@v3
- name: Download shared coverage
uses: actions/download-artifact@v4
with:
name: infection-coverage
path: var/infection-coverage
- name: Run mutation shard ${{ matrix.shard }}
run: make test/mutation/shard SHARD_TOTAL=${{ env.SHARD_TOTAL }} SHARD_INDEX=${{ matrix.shard }}
- name: Upload shard summary
if: always()
uses: actions/upload-artifact@v4
with:
name: infection-summary-${{ matrix.shard }}
path: var/infection-summary-${{ matrix.shard }}.json
if-no-files-found: ignore
retention-days: 14
mutation-gate:
name: Mutation gate (aggregate MSI)
runs-on: ubuntu-latest
needs: mutation
steps:
- uses: actions/checkout@v4
- name: Setup PHP 8.4
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: dom, json, mbstring, tokenizer
coverage: none
tools: composer:v2
- name: Download all shard summaries
uses: actions/download-artifact@v4
with:
pattern: infection-summary-*
path: var
merge-multiple: true
- name: Enforce aggregate Covered MSI >= 95%
# EXPECTED_SHARDS asserts every shard's summary made it here; a missing
# one fails the gate rather than scoring over a subset of src/.
run: make test/mutation/gate EXPECTED_SHARDS=${{ env.SHARD_TOTAL }}