-
Notifications
You must be signed in to change notification settings - Fork 673
500 lines (459 loc) · 21.5 KB
/
Copy pathci-ecosystem-matrix.yaml
File metadata and controls
500 lines (459 loc) · 21.5 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
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# PR advisory: execute every registry pack's frontend JS against this commit's
# frontend runtime and report 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).
#
# Every pinned pack is fetched at the commit named in corpus.pins.json, which is
# checked in and bumped by hand. Within one exact cache, both sides of a
# comparison measure the same bytes, so a red delta is attributable to the diff.
# Packs added since the last pin bump track their registry ref when a cache is
# built. The pin-status job below leads every run with how old that snapshot is.
#
# 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 advisory 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_FAMILY: registry-corpus-
CORPUS_CACHE_PREFIX: registry-corpus-v6-
jobs:
# First job in the run, and deliberately dependency-free so its annotation
# and step summary land at the top. Stale pins are the one failure mode this
# instrument cannot detect from the inside: everything stays green while the
# ecosystem it claims to measure moves on without it.
pin-status:
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: How old is the corpus, and how do I bump it
run: python3 scripts/registry-census/pins.py
corpus:
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
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 }}
# Exact key only, deliberately no restore-keys. The key covers the pin
# set and the code that defines corpus readiness, so neither changed
# commits nor changed validation rules can restore an older cache.
- name: Restore corpus cache
id: restore
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: .census
key: ${{ env.CORPUS_CACHE_PREFIX }}${{ hashFiles('scripts/registry-census/corpus.pins.json', 'scripts/registry-census/fetch_corpus.py', 'scripts/registry-census/validate_corpus.py') }}
# A cache hit already carries the snapshot and exact corpus consumed by
# the shards. Repeating the registry crawl and missing-pack retries would
# take five minutes and be discarded because Actions caches are immutable.
- name: Refresh registry snapshot
if: steps.restore.outputs.cache-hit != 'true'
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.
#
# Fetches at the commits named in corpus.pins.json, so this only does
# real work on a cold cache or a pin bump. Prints the pin banner first.
- name: Fetch corpus
id: fetch
if: steps.restore.outputs.cache-hit != 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python3 scripts/registry-census/fetch_corpus.py
- name: Validate corpus cache
run: python3 scripts/registry-census/validate_corpus.py
# 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
.census/data/registry.json
retention-days: 30
if-no-files-found: error
include-hidden-files: true
# Same key as the restore, so concurrent runs on the same pins and
# readiness code race for one key with the loser rejected as a duplicate.
#
# Saved on any event, not just main. The shards are separate jobs and
# this cache is the only thing that carries the corpus to them: gating
# the save to the default branch meant a PR could never run the matrix
# at all, which is what reddened run 31827717441. Affordable now that
# the corpus is ~0.16GB rather than the 6.0GB that evicted itself.
#
# corpus.ready.json is written only after the fetch meets its population
# floor. A lockfile alone is not enough: it is written even when most
# targets failed so the provenance of that failure can be inspected.
- name: Save corpus cache
if: |
steps.restore.outputs.cache-hit != 'true'
&& steps.fetch.outcome == 'success'
&& hashFiles('.census/corpus.ready.json') != ''
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: .census
key: ${{ env.CORPUS_CACHE_PREFIX }}${{ hashFiles('scripts/registry-census/corpus.pins.json', 'scripts/registry-census/fetch_corpus.py', 'scripts/registry-census/validate_corpus.py') }}
# Keep only the newest corpus generation against the repo's shared 10GB
# budget. Branch-scoped, so a PR's entry is never deleted out from under it
# - those expire with the PR. 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_FAMILY" \
--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
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
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 saved this exact key; same-run caches are visible to
# later jobs. Restore-only, exact key, and fail-on-cache-miss: a prefix
# fallback here would hand the shard a corpus pinned to different
# commits, and `corpus missing` further down is a much clearer failure
# than a green verdict over the wrong packs.
- name: Restore corpus cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: .census
key: ${{ env.CORPUS_CACHE_PREFIX }}${{ hashFiles('scripts/registry-census/corpus.pins.json', 'scripts/registry-census/fetch_corpus.py', 'scripts/registry-census/validate_corpus.py') }}
fail-on-cache-miss: true
# 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 > matrix-rows/_vitest-shard.log 2>&1 \
|| echo "vitest exited nonzero - tolerated, pack code may leak unhandled errors"
tail -25 matrix-rows/_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, so it rides
# inside matrix-rows/ rather than as a second upload path: two paths
# make upload-artifact re-root the artifact at their common ancestor,
# which put every row one directory deeper than the verdict job looks
# and emptied the population (run 31829344129, 'no matrix rows'). The
# leading underscore and .log keep it out of both consumers' globs.
- name: Upload rows
if: always()
uses: actions/upload-artifact@v6
with:
name: ecosystem-matrix-shard-${{ matrix.shard }}
path: matrix-rows/
retention-days: 30
if-no-files-found: warn
# The single combined result: all four shards' rows merge into one table
# and one combined 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: [corpus, 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()
&& github.repository == 'Comfy-Org/ComfyUI_frontend'
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
if: needs.corpus.result == 'success'
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 check. 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:
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
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"