Repository navigation
244 lines (215 loc) · 10.1 KB
/
Copy pathci.yml
File metadata and controls
244 lines (215 loc) · 10.1 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
name: CI
# Every job only reads the repository; keep the token from defaulting to write.
permissions:
contents: read
on:
push:
branches: [master]
pull_request:
branches: [master]
# Manual mutation sweep (Actions -> CI -> Run workflow). Deliberately NOT on the PR path:
# each mutation re-runs its spec's whole test suite, so all 85 specs is hours, and a gate
# nobody can afford to run is worse than no gate — that is the warning in the specs job's
# own comment. Until 2026-09-13 nothing in this repo ever executed `npm run mutate`.
workflow_dispatch:
inputs:
specs:
description: 'Spec files to run (space separated). Blank = every test/mutations/*.json'
required: false
default: ''
shard:
description: 'Zero-based shard i/N (0/1 = all selected specs)'
required: false
default: '0/1'
jobs:
mutations:
name: Mutation specs (manual)
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 350
steps:
# fetch-depth 0: the mutation harness materializes its workspace with
# `git clone --local` and reads history objects, so a shallow checkout fails it.
# Measured 2026-09-13: with the default depth 1 the sweep died on the FIRST spec
# with exit 2 and produced no verdict lines at all.
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
fetch-depth: 0
- name: Setup Node 24
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '24'
- name: Install dependencies
run: npm ci
# PR/push jobs intentionally do not execute mutation specs: this is a cost boundary.
# Optional parallel form (replace the input shard with matrix.shard in Sweep):
# strategy:
# fail-fast: false
# matrix:
# shard: ['0/4', '1/4', '2/4', '3/4']
# Put strategy at the mutations job level, not inside steps. Each job has its own
# checkout, manifest and artifact; never run two shards in one owner worktree.
- name: Sweep
env:
REQUESTED_SPECS: ${{ inputs.specs }}
REQUESTED_SHARD: ${{ inputs.shard }}
run: |
args=(--shard "${REQUESTED_SHARD:-0/1}" --report-dir "$RUNNER_TEMP/mutation-report")
if [ -n "$REQUESTED_SPECS" ]; then args+=(--specs "$REQUESTED_SPECS"); fi
node scripts/mutation-sweep.mjs "${args[@]}"
- name: Assert verdict coverage
if: always()
run: node scripts/mutation-sweep.mjs --verify "$RUNNER_TEMP/mutation-report"
- name: Publish mutation evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: mutation-report-${{ github.run_id }}-${{ strategy.job-index || 0 }}
path: ${{ runner.temp }}/mutation-report/
if-no-files-found: error
syntax:
name: Node syntax check
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Setup Node 24
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '24'
- name: Check JS syntax for all src files
run: |
find src -name '*.js' -type f -print0 | xargs -0 -n1 node --check
- name: Validate package.json
run: node -e "JSON.parse(require('fs').readFileSync('package.json'))"
- name: Validate JSON configs
run: |
for f in $(find . -maxdepth 2 -name '*.json' -not -path './node_modules/*' -not -path './.git/*'); do
echo "Checking $f"
node -e "JSON.parse(require('fs').readFileSync('$f'))"
done
# Mutation-spec consistency. Three baseline/anchor drifts reached master in one week
# (#241's anchors, retry-rescue 82/87 -> 88, reasoning-continuity 289 -> 300) and the
# test suite saw none of them: it does not run specs. The anchor/shape checks read files
# only and cost ~0.3s, so they are mandatory rather than a local habit to remember.
#
# Baseline VALUE drift is measured here too, by EXECUTING the covered specs and reading
# the runner's events (scripts/spec-baseline-check.mjs) — never by scraping display text
# and never by rewriting a pin. A changed diff measures only the affected specs; a
# synchronised checkout measures all of them, which costs ~100s for the whole set (82
# specs when that was measured; 85 today). The check used to run only in the local gate,
# so a wrong pin reached CI green.
specs:
name: Mutation spec consistency
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
# Change-scoped selection diffs against origin/master, so the history has to be
# here; a shallow clone would silently widen the selection to every spec.
fetch-depth: 0
- name: Setup Node 24
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '24'
- name: Anchors unique, specs well-formed
run: node scripts/spec-static-check.mjs
- name: Measured baselines match, from executed evidence
run: node scripts/spec-baseline-check.mjs
- name: Secret scan (includes test/ — half of a typical diff lives there)
run: node scripts/secret-scan.mjs
# The `specs` job scans the WORKING TREE. A credential that was committed and then
# deleted is invisible there and still present in every clone, so this job scans every
# blob reachable from any ref plus the unreachable objects an amend / rebase /
# force-push leaves behind (the objects a rewrite was meant to hide). fetch-depth: 0 is
# the whole point — a shallow clone would scan the tip and report it clean.
#
# A finding that is not in scripts/history-secret-scan.baseline fails this job. That
# file is a reviewed list, one line per accepted historical finding with its reason;
# it is not a mute button (the scan warns about stale entries, so a fixed leak leaves a
# line someone has to delete). The scan is ~15s on a 1.9k-commit history and needs no
# network, no binary download and no token — which is why it is the gate instead of
# gitleaks; see HistorySecretScan.md for the gitleaks cross-check.
history-secrets:
name: History secret scan
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
fetch-depth: 0
- name: Setup Node 24
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '24'
- name: Scan every blob in every ref
run: node scripts/history-secret-scan.mjs
wire-identity:
name: Default-path request bytes
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
# The comparison base is a tag, so the tags have to be here.
fetch-depth: 0
- name: Setup Node 24
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '24'
- name: Select the release baseline (strict ancestor, recorded)
# `git describe --tags --abbrev=0` answers "nearest reachable tag", and at a
# tagged HEAD that is HEAD's own tag: a rerun of a release commit compared
# the tree with itself. scripts/wire-base.mjs picks the newest ANNOTATED
# release tag that is a strict ancestor of the tested commit — no
# lightweight fallback — so a rerun at a tag repeats the same comparison
# against the PREVIOUS release, the recorded SHAs never change, and a
# repository without such a tag fails closed (exit 3).
run: |
node scripts/wire-base.mjs
BASE_TAG=$(node scripts/wire-base.mjs --field tag)
BASE_SHA=$(node scripts/wire-base.mjs --field sha)
HEAD_SHA=$(git rev-parse HEAD)
if [ "$BASE_SHA" = "$HEAD_SHA" ]; then
echo "::error::refusing to compare $HEAD_SHA against itself"
exit 1
fi
git worktree add --detach "$RUNNER_TEMP/wire-base" "$BASE_SHA"
echo "comparing $HEAD_SHA against $BASE_SHA ($BASE_TAG)"
echo "WIRE_BASE_TREE=$RUNNER_TEMP/wire-base" >> "$GITHUB_ENV"
echo "WIRE_BASE_SHA=$BASE_SHA" >> "$GITHUB_ENV"
echo "WIRE_BASE_TAG=$BASE_TAG" >> "$GITHUB_ENV"
echo "WIRE_BASE_REQUIRED=1" >> "$GITHUB_ENV"
- name: Same bytes as the last release, or this fails
run: node --import ./test/setup-env.mjs --test test/wire-byte-identity.test.js
test:
name: Unit tests shard ${{ matrix.shard }}
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3]
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
# test/docs-consistency-guard.test.js resolves the handoff's tag and commit
# claims with git. The default depth-1 clone carries neither the history nor
# the tags, so shards 1 and 3 failed on "ambiguous argument '<sha>^{commit}'"
# and "no release tags reachable from HEAD" while the same suite passed
# locally. The guard fails closed by design; give it the objects, not an
# exemption. release.yml checks out full history for the same reason.
fetch-depth: 0
- name: Setup Node 24
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '24'
cache: 'npm'
# Zero npm dependencies: package.json declares none, and the JPEG/PNG decoders
# are vendored under src/vendor/. This step is a no-op today — kept so that a
# future dependency cannot silently break the shards that import it.
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:shard -- ${{ matrix.shard }} 4