-
Notifications
You must be signed in to change notification settings - Fork 410
655 lines (606 loc) · 28.4 KB
/
Copy pathnextjs-deploy-suite.yml
File metadata and controls
655 lines (606 loc) · 28.4 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
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
name: Next.js Deploy Suite
on:
schedule:
# Nightly at 02:00 UTC against main
- cron: "0 2 * * *"
workflow_dispatch:
inputs:
vinext-ref:
description: vinext branch/ref to test
required: false
default: main
type: string
next-ref:
description: Next.js ref to test against
required: false
default: v16.2.6
type: string
suite-filter:
description: Which suites to run (pages, app, or all)
required: false
default: all
type: choice
options:
- app
- pages
- all
test-path:
description: Optional path filter — comma-separated, e.g. `test/e2e/edge-async-local-storage`. Empty = all.
required: false
default: ""
type: string
test-concurrency:
description: Per-shard Next.js test concurrency
required: false
default: "2"
type: string
backfill-date:
description: Optional historical date (YYYY-MM-DD) to reconstruct and publish from a full vinext commit SHA
required: false
default: ""
type: string
permissions:
contents: read
concurrency:
group: nextjs-deploy-suite-${{ inputs.vinext-ref || github.ref }}-${{ inputs.next-ref || 'v16.2.6' }}-${{ inputs.suite-filter || 'all' }}-${{ inputs.test-path || 'all' }}
cancel-in-progress: false
jobs:
build:
name: Build vinext + Next.js
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout vinext
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Backfills retain the current workflow harness in the workspace and
# check out the historical vinext source separately below.
ref: ${{ inputs.backfill-date && github.sha || inputs.vinext-ref || github.ref }}
- name: Validate backfill inputs
if: inputs.backfill-date != ''
env:
BACKFILL_DATE: ${{ inputs.backfill-date }}
VINEXT_REF: ${{ inputs.vinext-ref }}
run: |
node <<'EOF'
const { BACKFILL_DATE, VINEXT_REF } = process.env
const createdAt = Date.parse(`${BACKFILL_DATE}T02:00:00Z`)
if (!/^[0-9a-f]{40}$/i.test(VINEXT_REF || '')) {
throw new Error('Backfills require a full 40-character vinext commit SHA')
}
if (
!/^\d{4}-\d{2}-\d{2}$/.test(BACKFILL_DATE || '') ||
!Number.isFinite(createdAt) ||
new Date(createdAt).toISOString() !== `${BACKFILL_DATE}T02:00:00.000Z` ||
createdAt > Date.now()
) {
throw new Error('backfill-date must be a valid past YYYY-MM-DD date')
}
EOF
- name: Checkout historical vinext
if: inputs.backfill-date != ''
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ inputs.vinext-ref }}
path: historical-vinext
- uses: ./.github/actions/setup
with:
node-version: ${{ inputs.backfill-date && '22' || '24' }}
run-install: ${{ inputs.backfill-date == '' }}
- name: Install historical vinext
if: inputs.backfill-date != ''
working-directory: historical-vinext
run: vp install --frozen-lockfile
- name: Enable pnpm shim for Next.js scripts
run: corepack enable pnpm
- name: Checkout Next.js
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
repository: vercel/next.js
ref: ${{ inputs.next-ref || 'v16.2.6' }}
path: next.js
# Share the playwright-chromium cache with ci.yml, which keeps it hot
# on every run. Restore-only — ci.yml owns saving. Sidesteps the
# upstream Playwright extraction hang that started 2026-05-22 (runs
# 26265986264, 26279790373, 26281061094, 26281758803) by avoiding
# the download+extract path entirely on cache hit. The lockfile lives
# under next.js/ because that's where the prepare-step install pulls
# `playwright` from; if Next.js bumps Playwright before vinext does,
# the cache key won't match ci.yml's and we'd need to seed by running
# vinext's CI once on the new version first.
- name: Restore Playwright browsers
id: playwright-cache
uses: ./.github/actions/playwright-cache
with:
browser: chromium
lockfile: next.js/pnpm-lock.yaml
mode: restore
- name: Build vinext
if: inputs.backfill-date == ''
run: vp run vinext#build
- name: Build historical vinext
if: inputs.backfill-date != ''
working-directory: historical-vinext
run: corepack pnpm build
- name: Prepare Next.js checkout
timeout-minutes: 15
run: bash scripts/run-nextjs-deploy-suite.sh "$GITHUB_WORKSPACE/next.js"
env:
CI: true
VINEXT_BUILD: "0"
NEXTJS_PREPARE: "1"
NEXTJS_PREPARE_ONLY: "1"
# The Next.js workspace pulls `playwright-chromium` as a transitive
# dep, whose postinstall downloads Chrome via the Playwright CDN.
# Since 2026-05-22 that postinstall has wedged on this runner after
# the 167 MiB Chrome zip downloads but before extraction completes,
# silently consuming the whole job timeout (runs 26265986264,
# 26279790373, 26281061094). The explicit `pnpm playwright install
# chromium chromium-headless-shell` later in the prepare script is
# what actually provisions the browsers we use at test time, so
# skipping the transitive postinstall is safe.
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
- name: Generate deploy manifest
env:
TEST_PATH: ${{ inputs.test-path }}
run: |
args=(
"$GITHUB_WORKSPACE/next.js"
"$GITHUB_WORKSPACE/nextjs-deploy-manifest.json"
--filter "${{ inputs.suite-filter || 'all' }}"
)
if [ -n "${TEST_PATH:-}" ]; then
args+=(--match "${TEST_PATH}")
fi
node scripts/nextjs-deploy-manifest.mjs "${args[@]}"
# Classify every Next.js e2e test file by which router(s) its fixture
# exercises. The classification is independent of the test results —
# it describes the Next.js checkout itself — so we POST it to its own
# endpoint here in the build job and don't pass it through the test /
# report jobs. The /compatibility UI joins it onto each result row at
# query time.
- name: Classify Next.js test suites by router
env:
NEXTJS_DIR: ${{ github.workspace }}/next.js
run: |
# 1) Enumerate every *.test.{ts,tsx,js,jsx} under next.js/test/e2e/.
# 2) Run the classifier (walks each fixture dir, decides router kind).
node scripts/list-nextjs-e2e-suites.mjs "$NEXTJS_DIR" nextjs-all-suites.json
node scripts/classify-nextjs-suites.mjs \
"$NEXTJS_DIR" \
nextjs-all-suites.json \
nextjs-suite-routers.json
# Submit the classification to the worker. Decoupled from results
# ingestion so a re-classify can happen without re-running tests, and
# so a partial test run still publishes fresh classifications.
#
# Guardrails mirror the results-ingest step at the end of the report
# job: only the full suite (suite-filter=all), only against main, and
# only against the Next.js version used by the scheduled run.
# A filtered run still produces a complete classification (the
# classifier walks the whole Next.js checkout regardless of which
# tests will actually run), so the partial-data argument from the
# results endpoint doesn't apply here — but we keep the same gates
# for consistency and to avoid surprising the operator running a
# one-off workflow_dispatch.
- name: Submit suite classification to worker
if: |
(inputs.suite-filter == 'all' || inputs.suite-filter == '' || github.event_name == 'schedule')
&& (inputs.test-path == '' || github.event_name == 'schedule')
&& (inputs.next-ref == 'v16.2.6' || inputs.next-ref == '' || github.event_name == 'schedule')
&& (
(github.event_name == 'schedule' && github.ref == 'refs/heads/main')
|| (github.event_name == 'workflow_dispatch' && (inputs.vinext-ref == 'main' || inputs.vinext-ref == ''))
)
env:
COMPAT_INGEST_URL: ${{ vars.COMPAT_INGEST_URL || 'https://vinext-web.vinext.workers.dev/api/compatibility' }}
COMPAT_INGEST_SECRET: ${{ secrets.COMPAT_INGEST_SECRET }}
run: |
if [ -z "${COMPAT_INGEST_SECRET:-}" ]; then
echo "COMPAT_INGEST_SECRET not configured; skipping classification submission."
exit 0
fi
# Convert the suite → router map (object) into the endpoint's
# expected `{ classifiedAt, suites: [{ suite, router }, ...] }` shape.
# Done with node to avoid a jq dependency and to keep the JSON
# construction safely string-typed.
node -e '
const fs = require("node:fs");
const map = JSON.parse(fs.readFileSync("nextjs-suite-routers.json", "utf8"));
const body = {
classifiedAt: Date.now(),
suites: Object.entries(map).map(([suite, router]) => ({ suite, router })),
};
fs.writeFileSync("nextjs-classify-payload.json", JSON.stringify(body));
console.log("Classify payload:", body.suites.length, "suites");
'
CLASSIFY_URL="${COMPAT_INGEST_URL%/api/compatibility}/api/compatibility/classify"
echo "Submitting classifications to ${CLASSIFY_URL}"
if ! curl --silent --show-error --fail-with-body \
-X POST "${CLASSIFY_URL}" \
-H "Content-Type: application/json" \
-H "X-Compat-Secret: ${COMPAT_INGEST_SECRET}" \
--data-binary @nextjs-classify-payload.json; then
echo "::warning::Suite classification POST failed (non-fatal)."
fi
# Kept as an artifact even after submission so the classification can
# be re-submitted manually (e.g. if the live POST hiccupped) or
# inspected when debugging misclassifications.
- name: Upload suite router classification
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nextjs-suite-routers
path: |
nextjs-suite-routers.json
nextjs-classify-payload.json
if-no-files-found: ignore
retention-days: 14
- name: Save build artifacts
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
# The test job needs: vinext built dist + workspace metadata (for
# e2e-deploy.sh dependency injection), the scripts, the prepared
# Next.js checkout, Playwright browsers, and the filtered manifest.
# next.js/ is under $GITHUB_WORKSPACE so it's included in ".".
path: |
.
~/.cache/ms-playwright
nextjs-deploy-manifest.json
key: nextjs-deploy-build-${{ github.sha }}-${{ github.run_id }}
test:
name: Deploy suite (${{ matrix.group }})
needs: build
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
group:
[
1/16,
2/16,
3/16,
4/16,
5/16,
6/16,
7/16,
8/16,
9/16,
10/16,
11/16,
12/16,
13/16,
14/16,
15/16,
16/16,
]
steps:
- name: Restore build artifacts
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
.
~/.cache/ms-playwright
nextjs-deploy-manifest.json
key: nextjs-deploy-build-${{ github.sha }}-${{ github.run_id }}
- uses: ./.github/actions/setup
with:
node-version: "24"
# Skip install — the workspace is already fully built in the cache
run-install: false
- name: Enable pnpm shim for Next.js scripts
run: corepack enable pnpm
- name: Ensure Playwright browsers
working-directory: next.js
run: corepack pnpm playwright install chromium chromium-headless-shell
- name: Make scripts executable
run: chmod +x scripts/e2e-deploy.sh scripts/e2e-logs.sh scripts/e2e-cleanup.sh
- name: Run deploy shard
working-directory: next.js
env:
CI: true
NEXT_TEST_MODE: deploy
# Next.js sets this private build variable for its own dev/start
# harnesses. The deploy fixture build needs the equivalent so
# test-only client request metadata (such as fetch priority) is
# compiled into the app under test.
__NEXT_TEST_MODE: e2e
NEXT_E2E_TEST_TIMEOUT: "240000"
NEXT_EXTERNAL_TESTS_FILTERS: ${{ github.workspace }}/nextjs-deploy-manifest.json
VINEXT_DIR: ${{ inputs.backfill-date && format('{0}/historical-vinext', github.workspace) || github.workspace }}
ADAPTER_DIR: ${{ inputs.backfill-date && format('{0}/historical-vinext', github.workspace) || github.workspace }}
VINEXT_HARNESS_DIR: ${{ github.workspace }}
VINEXT_DEPLOY_DEBUG_DIR: ${{ github.workspace }}/reports/nextjs-deploy-debug
VINEXT_FALLBACK_BUILD_ID: ${{ inputs.backfill-date && inputs.vinext-ref || '' }}
# Required by the Next.js deploy test manifest — tests are keyed to
# the turbopack variant, so this must be set even though vinext uses Vite.
IS_TURBOPACK_TEST: "1"
NEXT_TEST_JOB: "1"
NEXT_TELEMETRY_DISABLED: "1"
NEXT_TEST_DEPLOY_SCRIPT_PATH: ${{ github.workspace }}/scripts/e2e-deploy.sh
NEXT_TEST_DEPLOY_LOGS_SCRIPT_PATH: ${{ github.workspace }}/scripts/e2e-logs.sh
NEXT_TEST_CLEANUP_SCRIPT_PATH: ${{ github.workspace }}/scripts/e2e-cleanup.sh
GROUP: ${{ matrix.group }}
CONCURRENCY: ${{ inputs.test-concurrency || '2' }}
run: node run-tests.js --timings --retries 0 -g "$GROUP" -c "$CONCURRENCY" --type e2e
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-${{ strategy.job-index }}
path: next.js/test/**/*.results.json
if-no-files-found: ignore
retention-days: 14
- name: Upload deploy debug logs
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: deploy-debug-${{ strategy.job-index }}
path: reports/nextjs-deploy-debug/
if-no-files-found: ignore
retention-days: 7
report:
name: Test report
needs: test
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Download all test results
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: test-results-*
path: results
merge-multiple: true
- name: Generate report
id: report
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
# Resolve the GitHub Actions expressions to env vars once, instead
# of splicing `${{ ... }}` into JS string literals further down.
# The latter is fragile — a single quote in an input value would
# break the generated JS — and not exploitable in practice but
# cleaner this way.
VINEXT_REF: ${{ inputs.vinext-ref || github.ref }}
NEXT_REF: ${{ inputs.next-ref || 'v16.2.6' }}
SUITE_FILTER: ${{ inputs.suite-filter || 'all' }}
BACKFILL_DATE: ${{ inputs.backfill-date }}
with:
script: |
const fs = require('node:fs');
const path = require('node:path');
function findResultFiles(dir) {
const files = [];
if (!fs.existsSync(dir)) return files;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...findResultFiles(full));
} else if (entry.name.endsWith('.results.json')) {
files.push(full);
}
}
return files;
}
const resultFiles = findResultFiles('results');
if (resultFiles.length === 0) {
core.summary.addHeading('Next.js Deploy Suite', 2);
core.summary.addRaw('No test result files found. All shards may have been skipped or produced no output.');
await core.summary.write();
return;
}
const passed = [];
const failed = [];
const skipped = [];
// Per-test-file counts for the /compatibility page ingest. Router
// classification is NOT attached here — it's submitted separately
// by the build job to /api/compatibility/classify, and the UI
// joins it onto each result row at query time. See the
// "Submit suite classification" step earlier in this workflow.
const fileCounts = new Map();
const backfillDate = process.env.BACKFILL_DATE;
const backfillCreatedAt = backfillDate
? Date.parse(`${backfillDate}T02:00:00Z`)
: undefined;
function bumpFile(suite, key) {
let cur = fileCounts.get(suite);
if (!cur) {
cur = { suite, total: 0, passed: 0, failed: 0, skipped: 0 };
fileCounts.set(suite, cur);
}
cur.total++;
cur[key]++;
}
// Derive a canonical Next.js test path from the result-file path.
//
// Next.js's run-tests.js does NOT populate testResults[].testFilePath
// in the JSON, so we have to recover it from where the file lives in
// the artifact tree. Each shard uploads files at their relative path
// inside next.js/test/, e.g. `e2e/app-dir/foo/foo.test.ts.results.json`.
//
// The report job uses `merge-multiple: true`, which flattens the
// contents of every artifact into `results/`. We also defensively
// strip a leading `test-results-N/` segment in case that ever
// changes and shards get nested under their artifact names.
// Result: a canonical path of the form `test/e2e/app-dir/foo/foo.test.ts`.
function deriveSuiteName(file) {
let rel = path.relative('results', file);
// Defensive: strip a leading `test-results-<n>/` shard prefix if
// the download layout ever changes.
rel = rel.replace(/^test-results-[^/]+\//, '');
const stripped = rel.replace(/\.results\.json$/, '');
if (stripped && stripped.includes('/')) {
return `test/${stripped}`;
}
// Fallback: file sits at the top level of results/ (shouldn't
// happen in practice). Use the basename so we don't break ingest.
return path.basename(file, '.results.json');
}
for (const file of resultFiles) {
try {
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
const suiteName = deriveSuiteName(file);
for (const suite of data.testResults || []) {
for (const tc of suite.assertionResults || []) {
const testName = tc.ancestorTitles
? [...tc.ancestorTitles, tc.title].join(' > ')
: tc.fullName || tc.title;
if (tc.status === 'passed') {
passed.push({ suite: suiteName, test: testName });
bumpFile(suiteName, 'passed');
} else if (tc.status === 'failed') {
const msg = (tc.failureMessages || []).join('\n').slice(0, 500);
failed.push({ suite: suiteName, test: testName, message: msg });
bumpFile(suiteName, 'failed');
} else {
skipped.push({ suite: suiteName, test: testName });
bumpFile(suiteName, 'skipped');
}
}
}
} catch (e) {
core.warning(`Failed to parse ${file}: ${e.message}`);
}
}
// Persist the per-file counts for the next step to submit to D1.
fs.mkdirSync('report', { recursive: true });
fs.writeFileSync(
'report/compat-ingest.json',
JSON.stringify(
{
kind: 'deploy',
runKey: backfillDate
? `backfill:${backfillDate}:${process.env.VINEXT_REF}`
: String(context.runId),
vinextRef: process.env.VINEXT_REF,
nextRef: process.env.NEXT_REF,
commitSha: backfillDate ? process.env.VINEXT_REF : context.sha,
...(backfillCreatedAt === undefined ? {} : { createdAt: backfillCreatedAt }),
files: Array.from(fileCounts.values()),
},
null,
2,
) + '\n',
);
const total = passed.length + failed.length + skipped.length;
const passRate = total > 0 ? ((passed.length / total) * 100).toFixed(1) : '0.0';
const statsLine = `${passed.length} passed, ${failed.length} failed, ${skipped.length} skipped (${total} total, ${passRate}% pass rate)`;
// Top-level annotation visible on the workflow run page
if (failed.length > 0) {
core.warning(`Next.js Deploy Suite: ${statsLine}`, { title: 'Next.js Deploy Suite results' });
} else {
core.notice(`Next.js Deploy Suite: ${statsLine}`, { title: 'Next.js Deploy Suite results' });
}
// Job summary
core.summary.addHeading('Next.js Deploy Suite', 2);
core.summary.addRaw(
`**${passed.length}** passed, **${failed.length}** failed, **${skipped.length}** skipped (${total} total, ${passRate}% pass rate)\n\n`
);
if (failed.length > 0) {
core.summary.addHeading('Failed tests', 3);
const rows = [
[{data: 'Suite', header: true}, {data: 'Test', header: true}, {data: 'Error', header: true}],
...failed.slice(0, 100).map(f => [
f.suite,
f.test,
f.message ? `<details><summary>Details</summary>\n\n\`\`\`\n${f.message}\n\`\`\`\n</details>` : ''
])
];
core.summary.addTable(rows);
if (failed.length > 100) {
core.summary.addRaw(`\n_...and ${failed.length - 100} more failures. Download the test-results artifacts for full details._\n`);
}
}
if (passed.length > 0) {
const suites = [...new Set(passed.map(p => p.suite))].sort();
core.summary.addHeading('Passed suites', 3);
core.summary.addDetails(
`${suites.length} suites (click to expand)`,
suites.map(s => `- \`${s}\``).join('\n')
);
}
await core.summary.write();
// Also write a JSON report as an artifact
const report = {
timestamp: new Date().toISOString(),
vinextRef: process.env.VINEXT_REF,
nextRef: process.env.NEXT_REF,
suiteFilter: process.env.SUITE_FILTER,
summary: { total, passed: passed.length, failed: failed.length, skipped: skipped.length },
failed: failed.map(f => ({ suite: f.suite, test: f.test })),
passed: passed.map(p => ({ suite: p.suite, test: p.test })),
};
fs.mkdirSync('report', { recursive: true });
fs.writeFileSync('report/deploy-suite-report.json', JSON.stringify(report, null, 2) + '\n');
if (failed.length > 0) {
core.setFailed(`${failed.length} test(s) failed`);
}
- name: Upload report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: deploy-suite-report
# Include both files: the human-readable report and the
# /compatibility ingest payload. Keeping the ingest payload
# archived lets us manually re-POST a run if the live ingest
# step fails (network blip, bad URL, expired secret, etc.)
# without re-running the 40-minute suite.
path: |
report/deploy-suite-report.json
report/compat-ingest.json
retention-days: 90
# Submit per-file results to the /compatibility page's ingest endpoint.
# Requires the COMPAT_INGEST_SECRET repo secret (matched against the
# worker's COMPAT_INGEST_SECRET set via `wrangler secret put`).
# No-op if the secret or ingest URL aren't configured.
#
# Guardrails:
# - Only the full suite (suite-filter=all, no test-path). Filtered
# runs (`app`/`pages` only, or a path-narrowed run) would publish a
# misleading partial snapshot that drags the historical pass-rate
# trend off-course.
# - Only when running against main. Scheduled runs always test main
# (github.ref == refs/heads/main). For manual workflow_dispatch we
# check that `inputs.vinext-ref` resolves to main (its default).
# Branch / PR runs are useful for spot-checks but should not pollute
# the historical record.
# - Only when testing the Next.js version used by the scheduled run.
# Manual runs against other refs are useful for investigation but
# should not replace the scheduled compatibility snapshot.
# - Historical backfills are an explicit manual exception. They must
# supply a full SHA and valid past date; the build job validates both,
# while this main-ref gate lets PR branches exercise a dry run safely.
- name: Submit results to /compatibility ingest
if: |
always()
&& (inputs.suite-filter == 'all' || inputs.suite-filter == '' || github.event_name == 'schedule')
&& (inputs.test-path == '' || github.event_name == 'schedule')
&& (inputs.next-ref == 'v16.2.6' || inputs.next-ref == '' || github.event_name == 'schedule')
&& (
(github.event_name == 'schedule' && github.ref == 'refs/heads/main')
|| (github.event_name == 'workflow_dispatch' && (inputs.vinext-ref == 'main' || inputs.vinext-ref == ''))
|| (github.event_name == 'workflow_dispatch' && inputs.backfill-date != '' && github.ref == 'refs/heads/main')
)
&& hashFiles('report/compat-ingest.json') != ''
env:
COMPAT_INGEST_URL: ${{ vars.COMPAT_INGEST_URL || 'https://vinext-web.vinext.workers.dev/api/compatibility' }}
COMPAT_INGEST_SECRET: ${{ secrets.COMPAT_INGEST_SECRET }}
run: |
if [ -z "${COMPAT_INGEST_SECRET:-}" ]; then
echo "COMPAT_INGEST_SECRET not configured; skipping ingest submission."
exit 0
fi
echo "Submitting compat results to ${COMPAT_INGEST_URL}"
# --fail-with-body keeps stdout/stderr but exits non-zero on HTTP errors.
# We still don't want the report job to fail if ingest hiccups, so trap
# the exit and just log.
if ! curl --silent --show-error --fail-with-body \
-X POST "${COMPAT_INGEST_URL}" \
-H "Content-Type: application/json" \
-H "X-Compat-Secret: ${COMPAT_INGEST_SECRET}" \
--data-binary @report/compat-ingest.json; then
echo "::warning::Compatibility ingest POST failed (non-fatal)."
fi