diff --git a/.claude/commands/pr-status.md b/.claude/commands/pr-status.md index 1c2018d01431..583574cbcd49 100644 --- a/.claude/commands/pr-status.md +++ b/.claude/commands/pr-status.md @@ -95,6 +95,15 @@ Analyze PR status including CI failures and review comments. - **MEDIUM priority**: Identify root cause pattern, address open suggestions - **LOW priority**: Mark as likely flaky/transient, note resolved/nitpick comments +8. When proposing local repro commands, **always include the exact env vars from the CI job** (shown in the "Job Environment Variables" section of index.md). Key variables that change behavior: + - `IS_WEBPACK_TEST=1` forces webpack (turbopack is default locally) + - `NEXT_SKIP_ISOLATE=1` skips packing next.js into a separate project (hides module resolution failures) + - Feature flags like `__NEXT_USE_NODE_STREAMS=true`, `__NEXT_CACHE_COMPONENTS=true` change build-time DefinePlugin replacements + - Example: a failure in "test node streams prod" needs `IS_WEBPACK_TEST=1 __NEXT_USE_NODE_STREAMS=true __NEXT_CACHE_COMPONENTS=true __NEXT_EXPERIMENTAL_DEBUG_CHANNEL=true NEXT_TEST_MODE=start` + - Never use `NEXT_SKIP_ISOLATE=1` when verifying module resolution or build-time compilation fixes + +9. The script automatically checks the last 3 main branch CI runs for known flaky tests. Check the **"Known Flaky Tests"** section in index.md and the `flaky-tests.json` file. Tests listed there also fail on main and are likely pre-existing flakes, not caused by the PR. Mark them as **FLAKY (pre-existing)** in your summary table. Use `--skip-flaky-check` to skip this step if it's too slow. + - Do not try to fix these failures or address review comments without user confirmation. - If failures would require complex analysis and there are multiple problems, only do some basic analysis and point out that further investigation is needed and could be performed when requested. diff --git a/AGENTS.md b/AGENTS.md index 43cfa193820c..be6fd880b27d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,7 @@ # Next.js Development Guide +> **Note:** `CLAUDE.md` is a symlink to `AGENTS.md`. They are the same file. + ## Codebase structure ### Monorepo Overview @@ -54,12 +56,12 @@ pnpm --filter=next exec taskr ## Fast Local Development -For iterative development, use watch mode + fast test execution: +For iterative development, always use watch mode + skip-isolate (not full builds): **1. Start watch build in background:** ```bash -# Runs taskr in watch mode - auto-rebuilds on file changes +# Auto-rebuilds on file changes (~1-2s per change vs ~60s full build) # Use Bash(run_in_background=true) to keep working while it runs pnpm --filter=next dev ``` @@ -67,14 +69,16 @@ pnpm --filter=next dev **2. Run tests fast (no isolation, no packing):** ```bash -# NEXT_SKIP_ISOLATE=1 - skip packing Next.js for each test (much faster) +# NEXT_SKIP_ISOLATE=1 - skip packing Next.js for each test (~100s faster) # testonly - runs with --runInBand (no worker isolation overhead) NEXT_SKIP_ISOLATE=1 NEXT_TEST_MODE=dev pnpm testonly test/path/to/test.ts ``` **3. When done, kill the background watch process.** -Only use full `pnpm --filter=next build` for one-off builds (after branch switch, before CI push). +**For type errors only:** Use `pnpm --filter=next types` (~10s) instead of full `pnpm --filter=next build` (~60s). + +Only use full `pnpm --filter=next build` for: branch switches, before CI push, or runtime bundle changes (taskfile.js / next-runtime.webpack-config.js). **Always rebuild after switching branches:** @@ -83,6 +87,19 @@ git checkout pnpm build # Required before running tests (Turborepo dedupes if unchanged) ``` +**When NOT to use NEXT_SKIP_ISOLATE:** Drop it when testing module resolution changes (new require() paths, new exports from entry-base.ts, edge route imports). Without isolation, the test uses local dist/ directly, hiding resolution failures that occur when Next.js is packed as a real npm package. + +## Bundler Selection + +Turbopack is the default bundler for both `next dev` and `next build`. To force webpack: + +```bash +next build --webpack # Production build with webpack +next dev --webpack # Dev server with webpack +``` + +There is no `--no-turbopack` flag. + ## Testing ```bash @@ -103,6 +120,12 @@ pnpm test-dev-turbo test/development/ - `pnpm test-start-turbo` - Production build+start with Turbopack - `pnpm test-start-webpack` - Production build+start with Webpack +**Run tests headless** (no browser window): Set `HEADLESS=true` when running e2e tests unless you need visual browser debugging: + +```bash +HEADLESS=true pnpm test-dev-turbo test/path/to/test.ts +``` + **Other test commands:** - `pnpm test-unit` - Run unit tests only (fast, no browser) @@ -123,6 +146,20 @@ Generating tests using `pnpm new-test` is mandatory. pnpm new-test --args true my-feature e2e ``` +**Analyzing test output efficiently:** + +Never re-run the same test suite with different grep filters. Capture output once to a file, then read from it: + +```bash +# Run once, save everything +HEADLESS=true pnpm test-dev-turbo test/path/to/test.ts > /tmp/test-output.log 2>&1 + +# Then analyze without re-running +grep "●" /tmp/test-output.log # Failed test names +grep -A5 "Error:" /tmp/test-output.log # Error details +tail -5 /tmp/test-output.log # Summary +``` + ## Writing Tests **Test writing expectations:** @@ -196,6 +233,7 @@ This fetches CI workflow runs, failed jobs, logs, and PR review comments, genera **CI Analysis Tips:** +- **Assume test failures are NOT flaky by default.** Investigate every failure as if it is caused by the current changes until proven otherwise. However, check the **Known Flaky Tests** section in pr-status output for historical data before deep-diving into a failure. - Prioritize blocking jobs first: build, lint, types, then test jobs - Prioritize CI failures over review comments @@ -235,6 +273,29 @@ See [Codebase structure](#codebase-structure) above for detailed explanations. - Use `DEBUG=next:*` for debug logging - Use `NEXT_TELEMETRY_DISABLED=1` when testing locally +## Context-Efficient Workflows + +**Reading large files** (>500 lines, e.g. `app-render.tsx`): + +- Grep first to find relevant line numbers, then read targeted ranges with `offset`/`limit` +- Never re-read the same section of a file without code changes in between +- For generated files (`dist/`, `node_modules/`, `.next/`): search only, don't read + +**Build & test output:** + +- Capture to file once, then analyze: `pnpm build 2>&1 | tee /tmp/build.log` +- Don't re-run the same test command without code changes; re-analyze saved output instead + +**Batch edits before building:** + +- Group related edits across files, then run one build, not build-per-edit +- Use `pnpm --filter=next types` (~10s) to check type errors without full rebuild + +**External API calls (gh, curl):** + +- Save response to variable or file: `JOBS=$(gh api ...) && echo "$JOBS" | jq '...'` +- Don't re-fetch the same API data to analyze from different angles + ## Commit and PR Style - Do NOT add "Generated with Claude Code" or co-author footers to commits or PRs @@ -249,6 +310,14 @@ See [Codebase structure](#codebase-structure) above for detailed explanations. - **Choose the right verification method for each change.** This may include running unit tests, integration tests, type checking, linting, building the project, or inspecting runtime behavior depending on what was changed. - **When unclear how to verify a change, ask the user.** If there is no obvious test or verification method for a particular change, ask the user how they would like it verified before moving on. +**Pre-validate before committing** to avoid slow lint-staged failures (~2 min each): + +```bash +# Run exactly what the pre-commit hook runs on your changed files: +pnpm prettier --with-node-modules --ignore-path .prettierignore --write +npx eslint --config eslint.config.mjs --fix +``` + ## Rebuilding Before Running Tests When running Next.js integration tests, you must rebuild if source files have changed: @@ -259,10 +328,30 @@ When running Next.js integration tests, you must rebuild if source files have ch ## Development Anti-Patterns +### Adding Experimental Flags + +All flags need: `config-shared.ts` (type) → `config-schema.ts` (zod). If the flag is consumed in user-bundled code (client components, edge routes, `app-page.ts` template), also add it to `define-env.ts` for build-time injection. Runtime-only flags consumed exclusively in pre-compiled bundles can skip `define-env.ts`. + +Beyond that, it depends on where the flag is consumed: + +**Client/bundled code only** (e.g. `__NEXT_PPR` in client components): `define-env.ts` is sufficient. Webpack/Turbopack replaces `process.env.X` at the user's build time. + +**Pre-compiled runtime bundles** (e.g. code in `app-render.tsx`): The flag must also be set as a real `process.env` var at runtime, because `app-render.tsx` runs from pre-compiled bundles where `define-env.ts` doesn't reach. Two approaches: + +- **Runtime env var**: Set in `next-server.ts` + `export/worker.ts`. Both code paths stay in one bundle. Simple but increases bundle size. +- **Separate bundle variant**: Add DefinePlugin entry in `next-runtime.webpack-config.js` (scoped to `bundleType === 'app'`), new taskfile tasks, update `module.compiled.js` selector, and still set env var in `next-server.ts` + `export/worker.ts` for bundle selection. Eliminates dead code but adds build complexity. + +For runtime flags, also add the field to the `NextConfigRuntime` Pick type in `config-shared.ts`. + ### Test Gotchas +- **Cache components enables PPR by default**: When `__NEXT_CACHE_COMPONENTS=true`, most app-dir pages use PPR implicitly. Dedicated `ppr-full/` and `ppr/` test suites are mostly `describe.skip` (migrating to cache components). To test PPR codepaths, run normal app-dir e2e tests with `__NEXT_CACHE_COMPONENTS=true` rather than looking for explicit PPR test suites. +- **Quick smoke testing with toy apps**: For fast feedback, generate a minimal test fixture with `pnpm new-test --args true e2e`, then run the dev server directly with `node packages/next/dist/bin/next dev --port ` and `curl --max-time 10`. This avoids the overhead of the full test harness and gives immediate feedback on hangs/crashes. - Mode-specific tests need `skipStart: true` + manual `next.start()` in `beforeAll` after mode check - Don't rely on exact log messages - filter by content patterns, find sequences not positions +- **Snapshot tests vary by env flags**: Tests with inline snapshots can produce different output depending on env flags. When updating snapshots, always run the test with the exact env flags the CI job uses (check `.github/workflows/build_and_test.yml` `afterBuild:` sections). Turbopack resolves `react-dom/server.edge` (no Node APIs like `renderToPipeableStream`), while webpack resolves the `.node` build (has them). +- **`app-page.ts` is a build template compiled by the user's bundler**: Any `require()` in this file is traced by webpack/turbopack at `next build` time. You cannot require internal modules with relative paths because they won't be resolvable from the user's project. Instead, export new helpers from `entry-base.ts` (which is part of the pre-compiled runtime bundle where relative requires work) and access them via `entryBase.*` in the template. +- **Reproducing CI failures locally**: Always match the exact CI env vars (check `pr-status` output for "Job Environment Variables"). Key differences: `IS_WEBPACK_TEST=1` forces webpack (turbopack is default), `NEXT_SKIP_ISOLATE=1` skips packing next.js (hides module resolution failures). Always run without `NEXT_SKIP_ISOLATE` when verifying module resolution fixes. ### Rust/Cargo @@ -275,6 +364,42 @@ When running Next.js integration tests, you must rebuild if source files have ch - Source map paths vary (webpack: `./src/`, tsc: `src/`) - try multiple formats - `process.cwd()` in stack trace formatting produces different paths in tests vs production +### Pre-compiled Runtime Bundles + +Server rendering code (`app-render.tsx`, route modules) is NOT bundled during the user's `next build`. It runs from pre-compiled runtime bundles at `dist/compiled/next-server/app-page*.runtime.*.js`. + +- **Built by**: `next-runtime.webpack-config.js` (rspack) via `taskfile.js` bundle tasks +- **Selected at runtime by**: `src/server/route-modules/app-page/module.compiled.js` based on `process.env` vars +- **Variants**: `{turbo/webpack} × {experimental/stable/nodestreams/experimental-nodestreams} × {dev/prod}` = up to 16 bundles per route type +- **Key implication**: `process.env.X` checks in `app-render.tsx` are either replaced by DefinePlugin at runtime-bundle-build time, or read as actual env vars at server startup. They are NOT affected by the user's webpack/Turbopack defines from `define-env.ts`. +- **Adding a new feature flag**: Either leave it as a runtime env var (both code paths in bundle), or create new bundle variants (separate DefinePlugin value per variant, new taskfile tasks, updated `module.compiled.js` selector) +- **Gotcha**: DefinePlugin entries in `next-runtime.webpack-config.js` must be scoped to the correct `bundleType` (e.g. `app` only, not `server`) to avoid replacing assignment targets in `next-server.ts` +- **Edge runtime is different**: Edge routes do NOT use pre-compiled runtime bundles. They are compiled by the user's webpack/Turbopack, so `define-env.ts` controls DCE. Feature flags that gate `node:*` imports must be forced to `false` for edge builds in `define-env.ts` (`isEdgeServer ? false : flagValue`), otherwise webpack will try to resolve `node:stream` etc. and fail. +- **Webpack DCE for `require()` calls**: Webpack only DCEs a `require()` when it sits inside the dead branch of an `if/else` whose condition DefinePlugin can evaluate at compile time. An early-return or `throw` before the `require()` does NOT work: webpack doesn't do control-flow analysis for throws/returns, so the `require()` is still traced. The correct pattern is `if (dead) { ... } else { require('node:stream') }`. Bare `if (live) { require(...) }` without else works for inline `node:*` specifiers but NOT for `require('./some-module')` that pulls a new file into the module graph. Always test edge changes with `pnpm test-start-webpack` on `test/e2e/app-dir/app/standalone.test.ts` (has edge routes), not with `NEXT_SKIP_ISOLATE=1` which skips the full webpack compilation. +- **TypeScript + DCE interaction**: Use `if/else` (not two independent `if` blocks) when assigning a variable conditionally on `process.env.X`. TypeScript cannot prove exhaustiveness across `if (flag) { x = a }; if (!flag) { x = b }` and will error with "variable used before being assigned". The `if/else` pattern satisfies both TypeScript (definite assignment) and webpack DCE (DefinePlugin replaces the condition, making one branch dead code). + +### Stale Native Binary + +If Turbopack produces unexpected errors after switching branches or pulling, check if `packages/next-swc/native/*.node` is stale. Delete it and run `pnpm install` to get the npm-published binary instead of a locally-built one. + +### Vendored React Packages & Types + +React is NOT resolved from `node_modules` for **App Router**. It's vendored into `packages/next/src/compiled/` during `pnpm build` (task: `copy_vendor_react()` in `taskfile.js`). Pages Router resolves React from `node_modules` normally. + +- **Type declarations**: `packages/next/types/$$compiled.internal.d.ts` contains `declare module` blocks for vendored React packages. When adding new APIs (e.g. `renderToPipeableStream`, `prerenderToNodeStream`), you must add type declarations here. The bare specifier types (e.g. `declare module 'react-server-dom-webpack/server'`) are what source code in `src/` imports against. +- **Two channels**: stable (`compiled/react/`) and experimental (`compiled/react-experimental/`). The runtime bundle webpack config (`next-runtime.webpack-config.js`) aliases to the correct channel via `makeAppAliases({ experimental })`. +- **Turbopack remap**: `react-server-dom-webpack/*` is silently remapped to `react-server-dom-turbopack/*` by Turbopack's import map. Code says "webpack" everywhere but Turbopack gets its own bindings. +- **Adding Node.js-only React APIs** (e.g. `renderToPipeableStream`): These exist in `.node` builds but not in the type definitions. Use dynamic `require()` behind a `process.env` guard. Add type declarations to `$$compiled.internal.d.ts`. For the `import/no-extraneous-dependencies` eslint rule, use block-level disable/enable (safest for multi-line patterns): + + ```typescript + /* eslint-disable import/no-extraneous-dependencies */ + const ComponentMod = + require('react-server-dom-webpack/server.node') as typeof import('react-server-dom-webpack/server.node') + /* eslint-enable import/no-extraneous-dependencies */ + ``` + + If using `eslint-disable-next-line`, the comment must be on the line immediately before the `require()` call, NOT before the `const` declaration. When the `const` and `require()` are on different lines, this is error-prone. Prefer block-level disable/enable. + ### Documentation Code Blocks - When adding `highlight={...}` attributes to code blocks, carefully count the actual line numbers within the code block diff --git a/scripts/pr-status.js b/scripts/pr-status.js index c84c13a5a410..a776a1c90ce3 100644 --- a/scripts/pr-status.js +++ b/scripts/pr-status.js @@ -83,6 +83,60 @@ function isBot(username) { return username.endsWith('-bot') || username.endsWith('[bot]') } +/** + * Parses the build_and_test.yml workflow to extract env vars from afterBuild + * sections. Returns a map of job display name prefix → env var list. + */ +function getJobEnvVarsFromWorkflow() { + const workflowPath = path.join( + __dirname, + '..', + '.github', + 'workflows', + 'build_and_test.yml' + ) + try { + const content = require('fs').readFileSync(workflowPath, 'utf8') + const envMap = {} + // Match job blocks: " job-id:\n name: display name\n" ... "afterBuild: |" + const jobRegex = + /^ {2}([\w-]+):\s*\n\s+name:\s*(.+)\n[\s\S]*?afterBuild:\s*\|\n([\s\S]*?)(?=\n\s+stepName:)/gm + let match + while ((match = jobRegex.exec(content)) !== null) { + const displayName = match[2].trim() + const afterBuild = match[3] + const exports = [] + for (const line of afterBuild.split('\n')) { + const exportMatch = line.match( + /^\s*export\s+([\w]+)=["']?([^"'\s]+)["']?/ + ) + if (exportMatch) { + exports.push(`${exportMatch[1]}=${exportMatch[2]}`) + } + } + if (exports.length > 0) { + envMap[displayName] = exports + } + } + return envMap + } catch { + return {} + } +} + +/** + * Given a job name like "test node streams prod (4/7) / build" and the env map, + * returns the relevant env vars or null. + */ +function getEnvVarsForJob(jobName, envMap) { + for (const [prefix, vars] of Object.entries(envMap)) { + if (jobName.startsWith(prefix)) { + return vars + } + } + return null +} + // ============================================================================ // Data Fetching Functions // ============================================================================ @@ -413,7 +467,9 @@ function generateIndexMd( runMetadata, categorizedJobs, jobTestCounts, - reviewData + reviewData, + jobEnvMap, + flakyTests ) { const { failed, inProgress, queued, succeeded, cancelled, skipped } = categorizedJobs @@ -493,6 +549,41 @@ function generateIndexMd( ) } lines.push('') + + // Show env vars for failed jobs if they differ from defaults + if (jobEnvMap && Object.keys(jobEnvMap).length > 0) { + const jobEnvGroups = new Map() + for (const job of failed) { + const envVars = getEnvVarsForJob(job.name, jobEnvMap) + if (envVars) { + const key = envVars.join(', ') + if (!jobEnvGroups.has(key)) { + jobEnvGroups.set(key, []) + } + jobEnvGroups.get(key).push(job.name) + } + } + if (jobEnvGroups.size > 0) { + lines.push('### Job Environment Variables', '') + for (const [envStr, jobNames] of jobEnvGroups) { + const prefix = jobNames[0].replace(/ \(.*/, '') + lines.push(`**${prefix}**: \`${envStr}\``, '') + } + } + } + + // Known flaky tests section + if (flakyTests && flakyTests.size > 0) { + lines.push('### Known Flaky Tests (failing on 2+ branches)', '') + lines.push( + 'These tests also failed in recent CI runs across multiple different branches and are likely pre-existing flakes, not caused by this PR:', + '' + ) + for (const testPath of [...flakyTests].sort()) { + lines.push(`- \`${testPath}\``) + } + lines.push('') + } } // In-progress jobs section (only when CI is running) @@ -838,6 +929,146 @@ function generateThreadMd(thread, index) { return lines.join('\n') } +// ============================================================================ +// Flaky Test Detection +// ============================================================================ + +/** + * Fetches recent failed CI runs across all branches and identifies tests that + * fail on multiple different branches (indicating flakiness, not branch-specific bugs). + * Excludes the current PR's branch to avoid self-matching. + * Returns a Set of test file paths that are likely flaky. + */ +async function getFlakyTests(currentBranch, runsToCheck = 5) { + console.log( + `Checking last ${runsToCheck} failed CI runs across all branches for known flaky tests...` + ) + + // Get recent failed build-and-test runs across ALL branches + const jqQuery = `.workflow_runs[] | select(.conclusion == "failure") | {id, head_branch}` + let output + try { + output = exec( + `gh api "repos/vercel/next.js/actions/workflows/57419851/runs?status=completed&per_page=30" --jq '${jqQuery}'` + ) + } catch { + console.log(' Could not fetch CI runs, skipping flaky check') + return new Set() + } + + if (!output.trim()) { + console.log(' No failed runs found') + return new Set() + } + + // Filter out the current branch and take up to runsToCheck + const allRuns = output + .split('\n') + .filter((line) => line.trim()) + .map((line) => JSON.parse(line)) + .filter((run) => run.head_branch !== currentBranch) + .slice(0, runsToCheck) + + if (allRuns.length === 0) { + console.log(' No failed runs from other branches found') + return new Set() + } + + const branchCount = new Set(allRuns.map((r) => r.head_branch)).size + console.log( + ` Checking ${allRuns.length} runs from ${branchCount} different branches...` + ) + + // Fetch failed jobs for all runs in parallel + const runJobResults = await Promise.all( + allRuns.map(async (run) => { + try { + const jobsJq = '.jobs[] | select(.conclusion == "failure") | {id, name}' + const jobsOutput = exec( + `gh api "repos/vercel/next.js/actions/runs/${run.id}/jobs?per_page=100" --jq '${jobsJq}'` + ) + if (!jobsOutput.trim()) return { run, jobs: [] } + const jobs = jobsOutput + .split('\n') + .filter((line) => line.trim()) + .map((line) => JSON.parse(line)) + // Skip runs with 20+ failed jobs (likely systemic, not flaky) + if (jobs.length > 20) return { run, jobs: [] } + return { run, jobs } + } catch { + return { run, jobs: [] } + } + }) + ) + + // Collect all (job, branch) pairs, then fetch logs in parallel (batch of 5) + const jobBranchPairs = [] + for (const { run, jobs } of runJobResults) { + for (const job of jobs) { + jobBranchPairs.push({ job, branch: run.head_branch }) + } + } + + console.log(` Fetching logs for ${jobBranchPairs.length} failed jobs...`) + + // Map: testPath → Set of branches where it failed + const testFailBranches = new Map() + + // Process in batches of 5 to avoid overwhelming the API + const BATCH_SIZE = 5 + for (let i = 0; i < jobBranchPairs.length; i += BATCH_SIZE) { + const batch = jobBranchPairs.slice(i, i + BATCH_SIZE) + const results = await Promise.all( + batch.map(async ({ job, branch }) => { + try { + const logs = exec( + `gh api "repos/vercel/next.js/actions/jobs/${job.id}/logs"` + ) + return { logs, branch } + } catch { + return { logs: null, branch } + } + }) + ) + + for (const { logs, branch } of results) { + if (!logs) continue + const testResults = extractTestOutputJson(logs) + for (const result of testResults) { + if (result.testResults) { + for (const tr of result.testResults) { + const hasFailed = tr.assertionResults?.some( + (a) => a.status === 'failed' + ) + if (hasFailed) { + const shortPath = tr.name?.replace(/.*\/(test\/)/, '$1') + if (shortPath) { + if (!testFailBranches.has(shortPath)) { + testFailBranches.set(shortPath, new Set()) + } + testFailBranches.get(shortPath).add(branch) + } + } + } + } + } + } + } + + // A test is flaky if it fails on 2+ different branches + const flakyTestFiles = new Set() + for (const [testPath, branches] of testFailBranches) { + if (branches.size >= 2) { + flakyTestFiles.add(testPath) + } + } + + console.log( + ` Found ${flakyTestFiles.size} flaky tests (failing on 2+ different branches)` + ) + return flakyTestFiles +} + // ============================================================================ // Main Function // ============================================================================ @@ -972,7 +1203,8 @@ async function main() { runMetadata, emptyCategorizedJobs, {}, - reviewData + reviewData, + {} ) ) process.exit(0) @@ -1082,19 +1314,34 @@ async function main() { } } - // Step 8: Generate index.md + // Step 8: Check for known flaky tests across branches (skip with --skip-flaky-check) + let flakyTests = new Set() + if (!process.argv.includes('--skip-flaky-check')) { + flakyTests = await getFlakyTests(branchInfo.branchName, 5) + if (flakyTests.size > 0) { + await fs.writeFile( + path.join(OUTPUT_DIR, 'flaky-tests.json'), + JSON.stringify([...flakyTests].sort(), null, 2) + ) + } + } + + // Step 9: Generate index.md console.log('Generating index.md...') // Update categorizedJobs.failed with full processed metadata const finalCategorizedJobs = { ...categorizedJobs, failed: processedFailedJobs, } + const jobEnvMap = getJobEnvVarsFromWorkflow() const indexMd = generateIndexMd( branchInfo, runMetadata, finalCategorizedJobs, jobTestCounts, - reviewData + reviewData, + jobEnvMap, + flakyTests ) await fs.writeFile(path.join(OUTPUT_DIR, 'index.md'), indexMd)