From 0cb26bdea92985aa3cef3e86339db6efbc25ca0a Mon Sep 17 00:00:00 2001 From: "n8n-cat-bot[bot]" <283985454+n8n-cat-bot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:05:57 +0000 Subject: [PATCH] ci: Build tsc deps and surface vitest stderr in grind workflow (#31543) Co-authored-by: n8n-cat-bot[bot] Co-authored-by: Claude Opus 4.7 --- .github/scripts/grind-changed-tests.mjs | 29 +++++++++++++++++++++-- .github/workflows/grind-changed-tests.yml | 6 ++++- scripts/grind.mjs | 14 +++++++++-- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/.github/scripts/grind-changed-tests.mjs b/.github/scripts/grind-changed-tests.mjs index dac6ac1404e..6ace415e260 100644 --- a/.github/scripts/grind-changed-tests.mjs +++ b/.github/scripts/grind-changed-tests.mjs @@ -112,12 +112,14 @@ for (const file of files) { } } + const stderr = res.stderr ?? ''; + if (!parsed) { - rows.push({ file, status: 'no-result' }); + rows.push({ file, status: 'no-result', stderr }); continue; } - rows.push({ file, status: 'ran', passed: parsed.passed, total: parsed.total }); + rows.push({ file, status: 'ran', passed: parsed.passed, total: parsed.total, stderr }); } // --- Render markdown --- @@ -131,6 +133,28 @@ const renderRow = ({ file, status, passed, total }) => { return `| \`${file}\` | ${fraction} ⚠️ flaky |`; }; +const STDERR_EXCERPT_LINES = 20; + +const renderDiagnostic = ({ file, stderr }) => { + const lines = stderr.split('\n'); + const truncated = lines.length > STDERR_EXCERPT_LINES; + const excerpt = lines.slice(0, STDERR_EXCERPT_LINES).join('\n'); + const trailer = truncated ? `\n... (${lines.length - STDERR_EXCERPT_LINES} more lines)` : ''; + return [ + `
${file} — first failure stderr`, + '', + '```', + excerpt + trailer, + '```', + '', + '
', + ].join('\n'); +}; + +const diagnostics = rows + .filter((r) => r.stderr && r.stderr.trim() && (r.status === 'no-result' || (r.status === 'ran' && r.passed < r.total))) + .map(renderDiagnostic); + const body = [ '', '## Grind results — pre-merge flake detection (N=' + n + ')', @@ -139,6 +163,7 @@ const body = [ '|---|---|', ...rows.map(renderRow), '', + ...(diagnostics.length ? ['### First-failure diagnostics', '', ...diagnostics, ''] : []), '_Spawn-per-iteration mode. Catches post-teardown async flakes that `vitest --repeat` misses. See [DEVP-198](https://linear.app/n8n/issue/DEVP-198) for design notes._', '', ].join('\n'); diff --git a/.github/workflows/grind-changed-tests.yml b/.github/workflows/grind-changed-tests.yml index 033dcf146ca..3422f182c87 100644 --- a/.github/workflows/grind-changed-tests.yml +++ b/.github/workflows/grind-changed-tests.yml @@ -33,7 +33,11 @@ jobs: - name: Setup Node.js uses: ./.github/actions/setup-nodejs with: - build-command: '' + # Build editor-ui's dependencies (e.g. @n8n/api-types) but not + # editor-ui itself — Vitest transforms editor-ui sources on the fly, + # while tsc-built deps must exist on disk for import resolution. + # Turbo cache makes this near-instant on subsequent runs. + build-command: 'pnpm --filter=n8n-editor-ui^... build' - name: Install .github/scripts dependencies run: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace diff --git a/scripts/grind.mjs b/scripts/grind.mjs index 7d8c0ee5ba2..d60585166cb 100644 --- a/scripts/grind.mjs +++ b/scripts/grind.mjs @@ -98,12 +98,22 @@ const runnerArgs = : ['jest', fileRelToPkg, '--colors=false']; let passed = 0; +let firstFailureLogged = false; for (let i = 0; i < n; i++) { const res = spawnSync('pnpm', runnerArgs, { cwd: pkgRoot, - stdio: values.json ? ['ignore', 'ignore', 'ignore'] : ['ignore', 'inherit', 'inherit'], + stdio: values.json ? ['ignore', 'ignore', 'pipe'] : ['ignore', 'inherit', 'inherit'], + encoding: 'utf8', }); - if (res.status === 0) passed++; + if (res.status === 0) { + passed++; + } else if (values.json && !firstFailureLogged) { + // Without this, JSON-mode invocations (used in CI) swallow every + // vitest error message and leave authors with no diagnostic trail. + firstFailureLogged = true; + process.stderr.write(`\n[grind] first failing iteration for ${fileRelToPkg}:\n`); + if (res.stderr) process.stderr.write(res.stderr); + } if (!values.json) process.stdout.write(res.status === 0 ? '.' : 'F'); }