-
Notifications
You must be signed in to change notification settings - Fork 672
461 lines (424 loc) · 19.2 KB
/
Copy pathci-ecosystem-matrix.yaml
File metadata and controls
461 lines (424 loc) · 19.2 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
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
# PR gate: execute every registry pack's frontend JS against this commit's
# frontend runtime and gate on the population result.
#
# The corpus is a mirror of the frontend JS of EVERY registry pack (~5,100
# packs). Only the executable surface is kept - .js/.mjs plus the text assets
# a module can import - and binary assets are staged as empty placeholders so
# their imports still resolve. That is what makes it cacheable: ~0.3GB
# compressed against the repo's shared 10GB budget. Keeping every asset byte
# put it at 6.0GB, which evicted itself mid-run (run 31753242605: three of
# four shards found no corpus at all).
#
# Corpus freshness is a push-to-main concern, not a PR concern. main
# revalidates every pack by ETag and saves the cache; a PR restores that
# corpus and uses it as-is. Both sides of the comparison then measure the
# same packs, so a red delta is attributable to the diff rather than to a
# pack that moved overnight.
#
# The upstream static idiom scan (L1) is deliberately absent: its
# false-positive/negative rate was judged too high to act on. Packs are
# measured by executing their code - the ecosystem matrix - instead.
#
# Provenance: scripts/registry-census/ is vendored from
# Comfy-Org/ComfyUI_ECS_Compat_Check (private, migration-phase instrument
# slated for archival) - see scripts/registry-census/README.md.
name: 'CI: Ecosystem Matrix'
on:
# A frontend change is the thing this gate exists to catch, so src/** runs
# it. The corpus is restored, not refetched, so a PR run is bounded by
# execution rather than by the network.
pull_request:
paths:
- 'src/**'
- 'scripts/registry-census/**'
- 'vitest.matrix.config.mts'
- '.github/workflows/ci-ecosystem-matrix.yaml'
# Refreshes the corpus and writes the baseline every PR is measured against.
# Actions cache scoping means only a default-branch run can produce an entry
# that other branches restore.
push:
branches: [main]
paths:
- 'src/**'
- 'scripts/registry-census/**'
- 'vitest.matrix.config.mts'
- '.github/workflows/ci-ecosystem-matrix.yaml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ecosystem-matrix-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
PYTHON_VERSION: '3.11'
CORPUS_CACHE_PREFIX: registry-corpus-v3-
jobs:
corpus:
runs-on: ubuntu-latest
timeout-minutes: 30
# contents: read only. The fetch step below holds a token (for the
# codeload rate limit) while extracting untrusted archives, so this job
# must not also carry actions: write - cache pruning runs as its own job.
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
# An ubuntu-latest image bump must not silently change the interpreter
# under a verdict whose whole value is run-to-run comparability.
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
# Restore the newest corpus regardless of key: the primary key never
# hits (run-scoped), so restore-keys always serves the latest save.
- name: Restore corpus cache
id: restore
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: .census
key: ${{ env.CORPUS_CACHE_PREFIX }}${{ github.run_id }}
restore-keys: ${{ env.CORPUS_CACHE_PREFIX }}
# Re-pin the registry snapshot every run: it is the corpus's target
# list, and new packs only enter the corpus through it.
- name: Refresh registry snapshot
run: python3 scripts/registry-census/refresh_registry.py
# Authenticated codeload: anonymous fetches are rate-limited per IP, and
# a run where many packs drift at once exhausts that budget partway
# through. Run 31738365496 failed 145 of 150 attempted fetches this way
# and reported it as an ecosystem-wide outage. This job only downloads
# and unpacks archives; pack code executes in ecosystem-matrix, which
# checks out with persist-credentials: false.
#
# --revalidate only off main. On a PR the restored corpus is the
# measurement's control: refetching drifted packs there would move the
# population out from under the baseline it is being compared to.
- name: Fetch corpus
id: fetch
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
python3 scripts/registry-census/fetch_corpus.py \
${{ github.event_name == 'pull_request' && ' ' || '--revalidate' }}
# corpus.lock.json records the ETag and resolved tree of every pack this
# run measured - the identity a published figure has to be cited with.
# registry-stale.json is present only when the snapshot is a fallback.
- name: Upload corpus provenance
if: always()
uses: actions/upload-artifact@v6
with:
name: corpus-lock
path: .census/*.json
retention-days: 30
if-no-files-found: warn
# Key derived from the lockfile: unchanged corpus finds its key taken
# and the server rejects the duplicate; any pack drift writes a new
# entry. Gated on the FETCH step and a present lockfile: an early
# failure must not save a partial .census - least of all under the
# empty-hash key, which equals the restore-keys prefix and would
# become the best match for every later run.
#
# Default branch only. A PR that saved would spend the repo's shared
# cache budget on an entry no other branch can restore.
- name: Save corpus cache
if: |
github.event_name != 'pull_request'
&& github.ref_name == github.event.repository.default_branch
&& steps.fetch.outcome == 'success'
&& hashFiles('.census/corpus.lock.json') != ''
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: .census
key: ${{ env.CORPUS_CACHE_PREFIX }}${{ hashFiles('.census/corpus.lock.json') }}
# Each drift writes a new entry against the repo's shared 10GB budget; keep
# the newest only. Its own job so `corpus`, which holds a token while
# extracting untrusted archives, stays at contents: read.
prune-corpus-cache:
needs: corpus
if: |
needs.corpus.result == 'success'
&& github.event_name != 'pull_request'
&& github.repository == 'Comfy-Org/ComfyUI_frontend'
&& github.ref_name == github.event.repository.default_branch
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
actions: write
steps:
# --ref, or this enumerates every branch's entries and deletes a PR's
# corpus out from under it. --limit, because the default of 30 leaves a
# larger backlog permanently unpruned. pipefail, because the exit status
# otherwise comes from the loop, which succeeds over empty input - so a
# failing `gh cache list` would let pruning stop while the step reads
# green.
- name: Prune superseded corpus caches
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh cache list --repo "$GITHUB_REPOSITORY" \
--key "$CORPUS_CACHE_PREFIX" \
--ref "refs/heads/${GITHUB_REF_NAME}" \
--limit 100 --sort created_at --order desc \
--json id --jq '.[1:][].id' > ids.txt
while read -r id; do
gh cache delete "$id" --repo "$GITHUB_REPOSITORY" || true
done < ids.txt
# The execution rung between the corpus (every pack fetched) and the E2E
# gates (browser + backend): every JS-shipping pack's real extension code
# is imported and driven against the real frontend runtime under vitest -
# registration lifecycle, a user-operation battery, serialize/reload.
# Sharded 4 ways; vitest additionally parallelizes per core within a
# shard (~4.7s of test time per pack, measured).
#
# A shard checks harness integrity only: every built spec must write its
# per-pack row. vitest's own exit code is ignored here because pack code
# leaks unhandled rejections. The ecosystem PASS/FAIL verdict is applied
# once, over all shards combined, by matrix-verdict below.
ecosystem-matrix:
needs: corpus
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
# This job EXECUTES unreviewed third-party pack code under vitest;
# the checkout must not leave a usable git credential behind for it.
- name: Checkout repository
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
# The corpus job above saved the cache this run; same-run caches are
# visible to later jobs. Restore-only - the matrix never writes it.
- name: Restore corpus cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: .census
key: ${{ env.CORPUS_CACHE_PREFIX }}${{ github.run_id }}-matrix
restore-keys: ${{ env.CORPUS_CACHE_PREFIX }}
# node_cache: false - setup-node's post step writes the pnpm store
# AFTER this job has executed ~1,900 packs' code, and the publish
# workflows restore that same lockfile-derived key while holding
# NPM_TOKEN and id-token: write. Slower installs here; no write path
# out of the job that runs unreviewed code.
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: false
node_cache: 'false'
- name: Build shard fixture
run: python3 scripts/registry-census/build_matrix.py --shard ${{ matrix.shard }}/4
# Every built spec gets its stub row BEFORE vitest starts: no failure
# mode - worker crash, collection kill, hang - can produce a missing
# row, only an incomplete one, which the verdict counts and reports.
- name: Pre-create stub rows
run: |
python3 - <<'PY'
import json, os
manifest = json.load(open('src/__ecs_matrix__/manifest.json'))
os.makedirs('matrix-rows', exist_ok=True)
for pack, entry in manifest.items():
if 'entries' in entry:
json.dump(
{'pack': pack, 'incomplete': True},
open(os.path.join('matrix-rows', entry['safe'] + '.json'), 'w'),
)
PY
- name: Run the matrix
run: |
set -euo pipefail
shopt -s nullglob
specs=(src/__ecs_matrix__/*.matrix.test.ts)
built=${#specs[@]}
if [ "$built" -eq 0 ]; then
echo "::error::no pack specs built - corpus missing or empty"
exit 1
fi
MATRIX_OUT="$PWD/matrix-rows" pnpm exec vitest run \
--config vitest.matrix.config.mts > vitest-shard.log 2>&1 \
|| echo "vitest exited nonzero - tolerated, pack code may leak unhandled errors"
tail -25 vitest-shard.log
cp src/__ecs_matrix__/manifest.json "matrix-rows/_manifest-shard-${{ matrix.shard }}.json"
# nullglob makes an empty match an empty array rather than the
# literal pattern, so these counts stay honest when nothing ran.
spec_names=()
for f in "${specs[@]}"; do
spec_names+=("$(basename "$f" .matrix.test.ts)")
done
row_names=()
for f in matrix-rows/*.json; do
name=$(basename "$f" .json)
case "$name" in _manifest*) continue ;; esac
row_names+=("$name")
done
echo "specs=$built rows=${#row_names[@]}"
if [ "${#row_names[@]}" -ne "$built" ]; then
echo "::error::matrix wrote ${#row_names[@]} rows for $built specs - harness failure, not pack noise"
comm -23 \
<(printf '%s\n' "${spec_names[@]}" | sort) \
<(printf '%s\n' "${row_names[@]}" | sort) \
| sed 's/^/::error::missing row: /'
exit 1
fi
# The shard log is the only record of WHY a shard died. Without it a
# 06:30 failure leaves the last 25 lines of a truncated tail and nothing
# to go back to.
- name: Upload rows
if: always()
uses: actions/upload-artifact@v6
with:
name: ecosystem-matrix-shard-${{ matrix.shard }}
path: |
matrix-rows/
vitest-shard.log
retention-days: 30
if-no-files-found: warn
# The single combined result: all four shards' rows merge into one table
# and one gating verdict - this job's exit code IS the ecosystem verdict
# (0 PASS, 1 FAIL, 2 harness failure / no rows / short population).
#
# MATRIX_EXPECT_SHARDS makes a dead shard withhold rather than PASS. Every
# criterion is a ratio, so three shards produce a perfectly plausible
# number, and on PASS that partial population would be saved as the
# baseline the next run is measured against.
matrix-verdict:
needs: ecosystem-matrix
# A single failed shard must degrade to a withheld verdict over the rows
# that exist, not to a skipped check that reads as nothing happened. Not
# always(): a cancelled run would find no rows, exit 2, and manufacture a
# red out of someone pressing the stop button.
if: '!cancelled()'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
# continue-on-error: when the corpus job fails, no shard ran and no
# artifact exists - the verdict step must still run so the summarizer
# reports the documented no-rows exit (2) instead of an opaque
# download failure.
- name: Merge shard rows
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: ecosystem-matrix-shard-*
path: matrix-rows
merge-multiple: true
# A stale registry means a different denominator, and that denominator
# feeds the delta gate. refresh_registry.py records the fallback in
# registry-stale.json; the summarizer surfaces it in the report and
# stores it in metrics.json rather than leaving it in a job log.
- name: Fetch corpus provenance
continue-on-error: true
uses: actions/download-artifact@v8
with:
name: corpus-lock
path: corpus-lock
# The baseline this PR is measured against: main's last green run. The
# primary key never hits (saves carry a run_attempt suffix), so
# restore-keys serves the newest save. A re-run can restore this same
# run's earlier attempt - metrics carry runId and the summarizer skips
# the delta rather than compare a run against itself.
- name: Restore baseline metrics
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: matrix-metrics
key: matrix-metrics-${{ github.run_id }}
restore-keys: matrix-metrics-
- name: Verdict
id: verdict
env:
MATRIX_RUN_ID: ${{ github.run_id }}
MATRIX_EXPECT_SHARDS: '4'
run: |
MATRIX_OUT="$PWD/matrix-rows" \
MATRIX_PREV="$PWD/matrix-metrics/metrics.json" \
MATRIX_METRICS_OUT="$PWD/matrix-metrics/metrics.json" \
MATRIX_STALE_MARKER="$PWD/corpus-lock/registry-stale.json" \
python3 scripts/registry-census/summarize_matrix.py
# Baseline-from-health, and only from main: a PR must not publish its
# own numbers as the baseline every later PR is measured against, and a
# FAILing run must not ratchet the baseline down to its own eroded
# numbers.
- name: Save baseline metrics
if: |
github.event_name != 'pull_request'
&& github.ref_name == github.event.repository.default_branch
&& steps.verdict.outcome == 'success'
&& hashFiles('matrix-metrics/metrics.json') != ''
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: matrix-metrics
key: matrix-metrics-${{ github.run_id }}-${{ github.run_attempt }}
- name: Upload combined result
if: always()
uses: actions/upload-artifact@v6
with:
name: ecosystem-matrix-combined
path: matrix-rows/
retention-days: 30
if-no-files-found: warn
# Counter-evidence: the matrix runs over a synthetic corpus where every
# pack breaks exactly one measurement channel (detection-proof/corpus/),
# then this job requires BOTH that every channel fired with its poison
# message AND that each poison pack breaches the criterion it targets
# (verify_detection.py). A green matrix is only meaningful while this stays
# green: a runner or frontend change that blinds a channel reds THIS job
# instead of silently hollowing out the gate. No corpus/cache dependency -
# the poison fixture is checked in, so this runs on every PR that touches
# src/** even when the corpus is cold.
matrix-detection-proof:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
# Runs the poison packs (and the same harness that runs real pack
# code); same rule as the shard jobs - no persisted credential.
- name: Checkout repository
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: false
node_cache: 'false'
- name: Gate unit tests
run: python3 -m unittest discover -s scripts/registry-census -p 'test_*.py' -v
- name: Build poison fixture
run: CENSUS_ROOT=scripts/registry-census/detection-proof python3 scripts/registry-census/build_matrix.py
- name: Run the matrix over the poison corpus
run: |
MATRIX_OUT="$PWD/matrix-proof" pnpm exec vitest run \
--config vitest.matrix.config.mts 2>&1 | tail -15 \
|| echo "vitest exited nonzero - tolerated, poison packs leak by design"
- name: Every channel must fire and must gate
run: MATRIX_OUT="$PWD/matrix-proof" python3 scripts/registry-census/verify_detection.py
# Exit code must be exactly 1 (criteria FAIL): exit 2 would mean the
# harness gate withheld the verdict, which on this corpus is itself a
# detection failure.
- name: Verdict must FAIL on poison
run: |
set +e
MATRIX_OUT="$PWD/matrix-proof" python3 scripts/registry-census/summarize_matrix.py > proof-verdict.txt 2>&1
rc=$?
set -e
tail -8 proof-verdict.txt
if [ "$rc" -ne 1 ]; then
echo "::error::expected verdict FAIL (exit 1) on the poison corpus, got exit $rc"
exit 1
fi
echo "verdict correctly FAILED on the poison corpus"