Skip to content

Commit 6752206

Browse files
committed
ci: temporary backfill workflow for /compatibility ingest
Reuses the existing 'Generate report' + 'Submit results' steps from nextjs-deploy-suite.yml verbatim, pointing actions/download-artifact at a specific past run id instead of the current run's matrix shards. Default target is run 25925392641 (last night's nightly that failed ingest due to the bogus default URL). Workflow_dispatch accepts any other run id. Delete this file (and the branch) once the backfill is done.
1 parent 1c0bdac commit 6752206

1 file changed

Lines changed: 210 additions & 0 deletions

File tree

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# Temporary workflow: re-run the `Generate report` + `Submit results to
2+
# /compatibility ingest` steps from .github/workflows/nextjs-deploy-suite.yml
3+
# against the artifacts of a previously-completed nightly run whose ingest
4+
# submission was broken (e.g. run 25925392641, which hit a DNS error before
5+
# the URL fix landed).
6+
#
7+
# The steps below are intentionally copied byte-for-byte from
8+
# nextjs-deploy-suite.yml's `report` job. The only difference is the artifact
9+
# source: instead of `actions/download-artifact` pulling from the current
10+
# run's matrix, we use the `run-id:` field to pull from the target run.
11+
#
12+
# Delete this workflow (and its branch) once the backfill is done.
13+
14+
name: Backfill /compatibility ingest
15+
16+
on:
17+
push:
18+
branches:
19+
- opencode/backfill-compat-ingest
20+
workflow_dispatch:
21+
inputs:
22+
run-id:
23+
description: nextjs-deploy-suite run id to reprocess
24+
required: true
25+
type: string
26+
27+
permissions:
28+
contents: read
29+
actions: read
30+
31+
jobs:
32+
backfill:
33+
name: Backfill ingest for ${{ inputs.run-id || '25925392641' }}
34+
if: github.repository == 'cloudflare/vinext'
35+
runs-on: ubuntu-latest
36+
timeout-minutes: 10
37+
38+
steps:
39+
- name: Download all test results
40+
uses: actions/download-artifact@v7
41+
with:
42+
pattern: test-results-*
43+
path: results
44+
merge-multiple: true
45+
# Pull from the specified historical run instead of the current one.
46+
run-id: ${{ inputs.run-id || '25925392641' }}
47+
github-token: ${{ secrets.GITHUB_TOKEN }}
48+
49+
# Verbatim copy of the `Generate report` step from nextjs-deploy-suite.yml.
50+
# Producing `report/compat-ingest.json` is what we actually need; the
51+
# rest (job summary, failed-tests table) is harmless on a backfill.
52+
- name: Generate report
53+
id: report
54+
uses: actions/github-script@v7
55+
env:
56+
# Use the source run's id as the runKey so the upsert lands on the
57+
# same row the original submission would have produced.
58+
SOURCE_RUN_ID: ${{ inputs.run-id || '25925392641' }}
59+
VINEXT_REF: main
60+
NEXT_REF: v16.2.6
61+
SUITE_FILTER: all
62+
with:
63+
script: |
64+
const fs = require('node:fs');
65+
const path = require('node:path');
66+
67+
function findResultFiles(dir) {
68+
const files = [];
69+
if (!fs.existsSync(dir)) return files;
70+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
71+
const full = path.join(dir, entry.name);
72+
if (entry.isDirectory()) {
73+
files.push(...findResultFiles(full));
74+
} else if (entry.name.endsWith('.results.json')) {
75+
files.push(full);
76+
}
77+
}
78+
return files;
79+
}
80+
81+
const resultFiles = findResultFiles('results');
82+
if (resultFiles.length === 0) {
83+
core.summary.addHeading('Next.js Deploy Suite', 2);
84+
core.summary.addRaw('No test result files found. All shards may have been skipped or produced no output.');
85+
await core.summary.write();
86+
return;
87+
}
88+
89+
const passed = [];
90+
const failed = [];
91+
const skipped = [];
92+
// Per-test-file counts for the /compatibility page ingest.
93+
const fileCounts = new Map();
94+
function bumpFile(suite, key) {
95+
let cur = fileCounts.get(suite);
96+
if (!cur) {
97+
cur = { suite, total: 0, passed: 0, failed: 0, skipped: 0 };
98+
fileCounts.set(suite, cur);
99+
}
100+
cur.total++;
101+
cur[key]++;
102+
}
103+
104+
// Derive a canonical Next.js test path from the result-file path.
105+
//
106+
// Next.js's run-tests.js does NOT populate testResults[].testFilePath
107+
// in the JSON, so we have to recover it from where the file lives in
108+
// the artifact tree. Each shard uploads files at their relative path
109+
// inside next.js/test/, e.g. `e2e/app-dir/foo/foo.test.ts.results.json`.
110+
//
111+
// The report job uses `merge-multiple: true`, which flattens the
112+
// contents of every artifact into `results/`. We also defensively
113+
// strip a leading `test-results-N/` segment in case that ever
114+
// changes and shards get nested under their artifact names.
115+
// Result: a canonical path of the form `test/e2e/app-dir/foo/foo.test.ts`.
116+
function deriveSuiteName(file) {
117+
let rel = path.relative('results', file);
118+
// Defensive: strip a leading `test-results-<n>/` shard prefix if
119+
// the download layout ever changes.
120+
rel = rel.replace(/^test-results-[^/]+\//, '');
121+
const stripped = rel.replace(/\.results\.json$/, '');
122+
if (stripped && stripped.includes('/')) {
123+
return `test/${stripped}`;
124+
}
125+
// Fallback: file sits at the top level of results/ (shouldn't
126+
// happen in practice). Use the basename so we don't break ingest.
127+
return path.basename(file, '.results.json');
128+
}
129+
130+
for (const file of resultFiles) {
131+
try {
132+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
133+
const suiteName = deriveSuiteName(file);
134+
135+
for (const suite of data.testResults || []) {
136+
for (const tc of suite.assertionResults || []) {
137+
const testName = tc.ancestorTitles
138+
? [...tc.ancestorTitles, tc.title].join(' > ')
139+
: tc.fullName || tc.title;
140+
141+
if (tc.status === 'passed') {
142+
passed.push({ suite: suiteName, test: testName });
143+
bumpFile(suiteName, 'passed');
144+
} else if (tc.status === 'failed') {
145+
const msg = (tc.failureMessages || []).join('\n').slice(0, 500);
146+
failed.push({ suite: suiteName, test: testName, message: msg });
147+
bumpFile(suiteName, 'failed');
148+
} else {
149+
skipped.push({ suite: suiteName, test: testName });
150+
bumpFile(suiteName, 'skipped');
151+
}
152+
}
153+
}
154+
} catch (e) {
155+
core.warning(`Failed to parse ${file}: ${e.message}`);
156+
}
157+
}
158+
159+
// Persist the per-file counts for the next step to submit to D1.
160+
// Use the SOURCE run id (not this backfill workflow's run id) as
161+
// the runKey, so the row upserts onto whatever the original
162+
// submission would have written.
163+
const sourceRunId = process.env.SOURCE_RUN_ID;
164+
fs.mkdirSync('report', { recursive: true });
165+
fs.writeFileSync(
166+
'report/compat-ingest.json',
167+
JSON.stringify(
168+
{
169+
kind: 'deploy',
170+
runKey: sourceRunId,
171+
vinextRef: process.env.VINEXT_REF,
172+
nextRef: process.env.NEXT_REF,
173+
commitSha: null,
174+
files: Array.from(fileCounts.values()),
175+
},
176+
null,
177+
2,
178+
) + '\n',
179+
);
180+
181+
const total = passed.length + failed.length + skipped.length;
182+
const passRate = total > 0 ? ((passed.length / total) * 100).toFixed(1) : '0.0';
183+
core.summary.addHeading('Backfill', 2);
184+
core.summary.addRaw(
185+
`Reprocessed run \`${sourceRunId}\`: **${passed.length}** passed, **${failed.length}** failed, **${skipped.length}** skipped (${total} total, ${passRate}% pass rate).\n`
186+
);
187+
await core.summary.write();
188+
189+
# Verbatim copy of the `Submit results to /compatibility ingest` step,
190+
# minus the suite-filter/main-branch guard (we wouldn't be running this
191+
# workflow at all if the source run wasn't a main-branch full-suite run).
192+
- name: Submit results to /compatibility ingest
193+
if: hashFiles('report/compat-ingest.json') != ''
194+
env:
195+
COMPAT_INGEST_URL: ${{ vars.COMPAT_INGEST_URL || 'https://vinext-web.vinext.workers.dev/api/compatibility' }}
196+
COMPAT_INGEST_SECRET: ${{ secrets.COMPAT_INGEST_SECRET }}
197+
run: |
198+
if [ -z "${COMPAT_INGEST_SECRET:-}" ]; then
199+
echo "::error::COMPAT_INGEST_SECRET is not configured."
200+
exit 1
201+
fi
202+
echo "Submitting compat results to ${COMPAT_INGEST_URL}"
203+
if ! curl --silent --show-error --fail-with-body \
204+
-X POST "${COMPAT_INGEST_URL}" \
205+
-H "Content-Type: application/json" \
206+
-H "X-Compat-Secret: ${COMPAT_INGEST_SECRET}" \
207+
--data-binary @report/compat-ingest.json; then
208+
echo "::error::Compatibility ingest POST failed."
209+
exit 1
210+
fi

0 commit comments

Comments
 (0)