ci: add Next.js deploy suite harness#1189
Conversation
commit: |
|
|
/bigbonk review |
There was a problem hiding this comment.
Review: ci: add Next.js deploy suite harness
Overall this is solid work — the scripts are well-structured, the CI workflow is cleanly separated into build/test jobs, and the debug artifact collection is thorough. A few things worth addressing before merge:
Workflow comment vs cron mismatch
.github/workflows/nextjs-deploy-suite.yml:5-6 — The comment says "Nightly at 06:00 UTC" but the cron is 0 2 * * * (02:00 UTC). Pick one.
Cache path includes . (entire workspace)
.github/workflows/nextjs-deploy-suite.yml:84-92 — The actions/cache/save path includes . (the entire vinext checkout). This means the cache key nextjs-deploy-build-${{ github.sha }}-${{ github.run_id }} stores the full workspace + next.js/ + Playwright browsers + the manifest. That's going to be a very large cache entry (Next.js checkout alone is sizable after pnpm build). Consider narrowing the cached paths to just what the test job actually needs:
packages/vinext/dist/(the built plugin)node_modules/(or just rely on setup-vp caching)next.js/(the prepared Next.js checkout)~/.cache/ms-playwrightnextjs-deploy-manifest.json
At minimum, caching . means the test job's actions/cache/restore will overwrite the checkout from the restore step, which works but is wasteful and fragile if the workspace has any ephemeral state.
cancel-in-progress: false with nightly cron
.github/workflows/nextjs-deploy-suite.yml:39 — With cancel-in-progress: false, if a nightly run is still going when the next day's cron fires (unlikely with 60min timeout, but possible with manual dispatches), both will run to completion. This is probably intentional for deploy suites (you want full results), but worth confirming that's the intent.
run_pnpm build in CI prepare step
.github/workflows/nextjs-deploy-suite.yml:70-74 and scripts/run-nextjs-deploy-suite.sh:40-44 — In the workflow, VINEXT_BUILD=0 is set because the workflow already built vinext with vp run vinext#build. But the run-nextjs-deploy-suite.sh script's build path (line 43) does run_pnpm build which would run the root build script via pnpm. For local development, this should probably be documented — a developer running ./scripts/run-nextjs-deploy-suite.sh locally will need pnpm available and the root build script to work correctly.
YAML catalog parser is fragile
scripts/e2e-deploy.sh:219-246 — The parseCatalog() function does line-by-line YAML parsing with regex. This works for the current pnpm-workspace.yaml format but will break if:
- Catalog entries use flow syntax (
catalog: { foo: ^1.0.0 }) - Indentation changes (it hardcodes 2-space indent check on line 231)
- Values are quoted with single quotes
- There are nested catalogs (
catalogName:sections)
This is a known trade-off (avoiding a YAML parser dependency in a throwaway temp dir), and the current workspace file uses the simple format, so it's fine for now. But a comment noting the assumption would help future maintainers.
Port allocation TOCTOU race
scripts/e2e-deploy.sh:68-84 — find_free_port() binds to port 0, reads the allocated port, then closes the server. Between close and vinext start --port $PORT, another process could grab that port. With -c 2 concurrency (two test apps deploying simultaneously), this race is real. The Node.js net.createServer approach is standard and usually fine, but with 16 shards × 2 concurrency = 32 parallel servers, collisions become more likely.
A mitigation would be to keep the server open until vinext start binds, or retry with a new port on EADDRINUSE. Not a blocker but worth a TODO comment.
Cleanup doesn't remove the PID file
scripts/e2e-cleanup.sh:22-28 — After killing the process, the PID file is left behind. If the Next.js harness reuses the same temp directory (unlikely but possible), a stale PID file could cause confusion. Add rm -f "${PID_FILE}" at the end.
ensure_python_command_for_native_builds PATH mutation
scripts/e2e-deploy.sh:116 — export PATH="$(pwd)/${shim_dir}:${PATH}" uses a subshell for $(pwd) which is fine, but the shim directory .vinext-native-build-bin is created in the test app's temp directory. If the cwd changes later in the script, the PATH entry still works (absolute path). This is correct, just noting it's intentional.
Hardcoded warning messages for specific test pages
scripts/e2e-deploy.sh:351-356 — These hardcoded warnings for large-page-data and blocking-fallback/[slug] are clearly there because specific Next.js tests assert on these warning strings in build output. This is pragmatic but brittle — if Next.js changes the warning text or threshold, these will silently diverge. A comment explaining why these exist (which Next.js test expects them) would help.
.next/trace empty file creation
scripts/e2e-deploy.sh:358-359 — Creating an empty .next/trace file is presumably because some Next.js test checks for its existence. Same as above — a brief comment would help.
IS_TURBOPACK_TEST=1 in the workflow
.github/workflows/nextjs-deploy-suite.yml:156 — This env var tells the Next.js test harness to treat this as a Turbopack test run. Since vinext uses Vite (not Turbopack), this is presumably set to match the deploy test manifest's expectations or skip webpack-specific assertions. A comment in the workflow explaining why would prevent someone from "fixing" it later.
Minor nits
scripts/e2e-deploy.sh:1and other scripts — all scripts should probably have a brief header comment explaining what they do (likenextjs-deploy-manifest.mjsdoes). The deploy script especially is 381 lines with no top-level doc comment.scripts/run-nextjs-deploy-suite.sh:7—[[ "${1}" != -* ]]uses bash-specific[[ ]]but the rest of the script uses POSIX[ ]. Consistent style would be nice but not blocking.
None of these are blockers — the biggest actionable items are the cache path scope and adding a few comments for the non-obvious compatibility shims (hardcoded warnings, .next/trace, IS_TURBOPACK_TEST). The overall structure is clean and follows the Next.js adapter testing conventions well.
|
Review posted on PR #1189. Here's a summary of the key findings: Actionable items:
Non-blocking observations:
Overall the PR is well-structured and follows the Next.js adapter testing conventions correctly. |
|
/bigbonk re-review |
There was a problem hiding this comment.
Re-review: ci: add Next.js deploy suite harness
All actionable items from the previous review have been addressed in e203abf:
| Previous feedback | Status |
|---|---|
| Cron comment mismatch (said 06:00, cron was 02:00) | Fixed — comment now says 02:00 UTC |
| Cache path too broad / undocumented | Documented — comment at lines 87-90 explains why . is intentional |
| Missing PID file cleanup in e2e-cleanup.sh | Fixed — rm -f "${PID_FILE}" added at line 30, before the kill |
IS_TURBOPACK_TEST=1 unexplained |
Fixed — comment at lines 158-159 explains manifest keying |
| Hardcoded warning messages undocumented | Fixed — comment block at lines 366-368 explains the test assertions |
.next/trace creation undocumented |
Fixed — comment at lines 376-377 |
| YAML catalog parser assumptions undocumented | Fixed — comment at lines 230-233 |
[[ ]] vs [ ] inconsistency |
Fixed — run-nextjs-deploy-suite.sh line 20 now uses [ ] consistently |
| Scripts missing header comments | Fixed — all scripts have header doc comments |
The new report job (lines 186-320) looks solid — recursive result file discovery, graceful handling of missing artifacts, summary table capped at 100 failures, and a JSON artifact for programmatic consumption. The core.setFailed() call correctly propagates failure to the workflow status.
One minor observation on the report job that's not blocking: the ${{ inputs.vinext-ref || github.ref }} expressions on lines 300-302 are interpolated by GitHub Actions into the JS string at workflow parse time, not at runtime. If any input value contains a single quote, it would break the JS string literal. This is unlikely for branch names and version refs, and github-script doesn't have a great escape hatch for this, so it's fine as-is.
LGTM — ready to merge.
|
Review posted. All 9 actionable items from the previous review have been addressed in the latest commit ( |
|
/bigbonk re-review |
9c72242 to
49ebd59
Compare
There was a problem hiding this comment.
Re-review: ci: add Next.js deploy suite harness
Reviewed at 9c72242. Previous review items are resolved, pull_request trigger removed. CI Check failure is a pre-existing oxlint false positive on client-reference-runtime-map.browser.spec.ts (rules-of-hooks on Playwright's use fixture), unrelated to this PR.
Actionable
1. vinext:jsx-in-js double-transforms every .js file
packages/vinext/src/index.ts — The vinext:jsx-in-js plugin runs transformWithOxc with lang: "jsx" on every .js/.mjs file outside node_modules, even files that contain zero JSX. OXC is fast enough that this is unlikely to be a latency problem in practice, but it means every .js module in a project gets an extra transform pass (parse → emit → sourcemap) that does nothing. A cheap early bail-out would avoid this:
// Skip files that don't contain any JSX syntax
if (!code.includes('<') || (!code.includes('/>') && !code.includes('</'))) return;The heuristic doesn't need to be perfect — false positives just mean a no-op transform, which is what happens today anyway. But for large codebases with hundreds of .js utility files, skipping the transform entirely is a measurable win. Not blocking, but worth doing.
2. CJS-to-ESM converter misses const { a, b } = require('mod') (destructured imports)
scripts/e2e-deploy.sh:~458-490 — The converter handles const X = require('mod') and const X = require('mod')(args) but doesn't handle destructured requires like const { withContentlayer } = require('next-contentlayer'). The regex \b(const|let|var)\s+(\w+)\s*= only matches a single identifier, not { ... } destructuring patterns.
This matters because Next.js config files commonly use destructured requires (plugins, config helpers). If a test app's next.config has one, it will be left as CJS require() in an ESM context and fail at runtime. The third regex (remaining require() → await import()) would partially catch it, but const { a } = (await import('mod')).default is not equivalent to const { a } = require('mod') — the destructuring would target the default export object, which may or may not have those properties.
For the deploy suite harness this is probably fine since the subset of test apps is controlled, but worth a TODO comment noting the limitation.
3. cleanup_on_error uses $PORT but it's not guaranteed to be set
scripts/e2e-deploy.sh:~184 — The cleanup_on_error trap references ${PORT} in the lsof fallback, but the PORT variable is set on line ~212. If the script fails before that line (e.g., during find_free_port), PORT is unset and lsof -ti "tcp:" would match nothing (harmless but messy). The trap already checks [ -f "${PORT_FILE}" ], which is sufficient — but it reads the port from the file contents, not from $PORT. This is fine because the file won't exist if PORT was never set. Just noting the inconsistency — consider reading PORT from PORT_FILE in the trap for correctness.
Non-blocking observations
nextjs-deploy-manifest.json cached but also in .
The cache path includes both . and nextjs-deploy-manifest.json. Since the manifest is written to $GITHUB_WORKSPACE/nextjs-deploy-manifest.json which is inside the workspace (.), the explicit path entry is redundant. Not harmful — actions/cache deduplicates — but could be cleaned up for clarity.
Report job JS string interpolation
Lines 300-302 of the workflow interpolate ${{ inputs.vinext-ref || github.ref }} etc. directly into JS string literals. As noted in the previous review, a branch name containing a single quote would break the JS syntax. This is extremely unlikely for the expected inputs (branch names, semver tags) and github-script doesn't offer a clean escape mechanism, so it's fine.
wait_for_http accepts any non-000 status
e2e-deploy.sh:~139 — The function treats any HTTP response (including 500, 502) as "server is ready." This is intentional per the deploy harness contract (the test runner handles response validation), but it means a crashing server that serves 500s will appear "ready." The 2-minute timeout is the backstop. Fine for the harness.
Previous review items all verified as fixed. The PR is in good shape. The CJS converter limitation (#2) is the most likely thing to cause a real failure in practice if a test app has destructured requires in next.config, but it's scoped to the test harness and easy to extend later.
LGTM — approve.
|
Review posted on PR #1189. Here's a summary of the findings: Actionable items (3):
Non-blocking observations:
The PR is approved — all previous review items are verified fixed, and the remaining findings are minor. |
Add the workflow, scripts, and manifest generator for running the Next.js Pages Router deploy test suite against vinext. Triggers: - Nightly cron (06:00 UTC) against main - Manual dispatch with configurable vinext-ref, next-ref, and concurrency Extracted from #922.
- Rename scripts to match Next.js convention: e2e-deploy.sh, e2e-logs.sh,
e2e-cleanup.sh
- Split workflow into build + test jobs (build once, shard 16 ways) per
the documented adapter testing pattern
- Generalize workflow from pages-only to configurable: add suite-filter
input (pages/app/all, defaults to pages)
- Add --filter flag to manifest generator (replaces the pages-hardcoded
version)
- Accept ADAPTER_DIR (Next.js docs convention) alongside VINEXT_DIR
- Rename workflow file to nextjs-deploy-suite.yml
- Use matrix.group strings ("1/16") instead of shard integers, matching
the docs example
… shims - Add a report job that aggregates .results.json across all shards into a GitHub Actions summary (pass/fail/skip counts, failed test details, passed suite list) and uploads a JSON report artifact - Upload test results from every shard (not just on failure) - Remove redundant next.js/ from cache paths (already under .) - Add comment explaining why IS_TURBOPACK_TEST=1 is needed - Clean up stale PID file in e2e-cleanup.sh - Add header comments to all scripts explaining their purpose and contract - Document the hardcoded warning messages and .next/trace shim - Document the YAML catalog parser assumptions - Fix [[ ]] -> [ ] inconsistency in run-nextjs-deploy-suite.sh
Bugs found and fixed: - IMMUTABLE_ASSET_TOKEN marker renamed to NEXT_SUPPORTS_IMMUTABLE_ASSETS to match what next-deploy.ts parseIdsFromCliOuput() actually parses. Without this, every test fails with 'Failed to get supportsImmutableAssets'. - Deploy script now injects "type": "module" into test app package.json and generates a vite.config.ts. Without type:module, Rolldown emits .mjs files but the RSC plugin's cross-environment imports expect .js. This mirrors what `vinext init` does (steps 3 and 5). - Added vp (Vite+) fallback to run_pnpm() for environments where pnpm and corepack are not available but vp is. - Cleanup script now uses process group kills (kill -TERM -PID) and a port-based lsof fallback to handle orphaned child processes from vp/pnpm exec wrappers. - Error cleanup trap now dumps the last 80 lines of the build log and last 40 lines of the server log to stderr, so failures are visible in CI output (previously only showed file sizes). - Test job uses run-install: false since the workspace is already built in the cache from the build job. Tested locally: deploy script succeeds, curl returns valid HTML, logs script outputs all three required markers (BUILD_ID, DEPLOYMENT_ID, NEXT_SUPPORTS_IMMUTABLE_ASSETS), cleanup kills the server.
…me list Replace the manual type:module / vite.config.ts / CJS rename logic in e2e-deploy.sh with a single `vinext init --skip-check --force` call. This runs after pnpm install (so deps are available) and before vinext build. Also add next.config.js to the CJS_CONFIG_FILES list in project.ts. Without this, vinext init adds type:module but doesn't rename CJS next.config.js files, causing 'module is not defined in ES module scope' errors. This was the root cause of the CI failures — Next.js test fixtures use module.exports in next.config.js. All 61 init tests pass.
Next.js doesn't support next.config.cjs, so we can't rename it. Instead, convert module.exports/require() to export default/import in-place after vinext init adds "type": "module". Also revert adding next.config.js to CJS_CONFIG_FILES — vinext init should not rename it since it's not a generic config file. Tolerate pnpm 10+ ERR_PNPM_IGNORED_BUILDS exit code 1 on install (the install completes, packages are in node_modules, but pnpm exits non-zero due to unapproved build scripts). Verify node_modules/vinext exists as a safety check. Tested against the real Next.js actions-streaming fixture: CJS next.config.js converted, app builds, server starts, HTTP 200.
v16.2.x parses IMMUTABLE_ASSET_TOKEN, canary renamed it to NEXT_SUPPORTS_IMMUTABLE_ASSETS. Emit both for cross-version compat.
Add vinext:jsx-in-js transform plugin to handle JSX in plain .js files. Next.js allows JSX in .js files (Babel/SWC handle it transparently), but Vite 8's built-in vite:oxc plugin excludes .js files by default (exclude: /\.js$/) AND infers lang: 'js' from the extension (which disables JSX parsing). Neither can be fixed via config alone. The plugin runs with enforce: 'pre' before vite:oxc, transforming .js files with OXC using lang: 'jsx' so JSX is compiled before vite:oxc sees the code. This unblocks ~60 test suites in the deploy suite. Also extend the deploy script's CJS-to-ESM config converter to handle next.config.ts files (not just .js), fixing ~6 more test failures. Tested: 1284 tests pass (jsx-in-js + shims + routing + app-router), zero regressions.
The naive regex converter missed two patterns:
- require('mod')(args) produced invalid ESM (import X from 'mod'(args))
- require() calls inside function bodies were not converted
New converter handles:
- module.exports = X → export default X
- const X = require('mod') → import X from 'mod'
- const X = require('mod')(args) → import _X from 'mod'; const X = _X(args)
- require('mod') in expressions → (await import('mod')).default
Tested against bundle-analyzer, path, and inline require patterns.
52ad8d1 to
a70748b
Compare
…le in cleanup
- Add pattern for const { a, b } = require('mod') → import { a, b } from 'mod'
- Read port from PORT_FILE in cleanup_on_error trap instead of using $PORT
variable which may be unset if the script fails before port allocation
- Add TODO noting remaining CJS edge cases (dynamic require, require.resolve)
|
/bigbonk review |
Co-authored-by: James Anderson <[email protected]>
There was a problem hiding this comment.
Re-review: ci: add Next.js deploy suite harness
Reviewed at e9fca14. All prior review items verified as addressed.
Changes since last review (9c72242 → e9fca14)
| Change | Status |
|---|---|
pull_request trigger removed from workflow |
Applied (via suggestion commits) |
tests/e2e/fixtures.ts lint suppress reverted to original location |
Applied — file is now unchanged from main, so the CI Check oxlint failure should be resolved |
| Destructured require handling in CJS converter | Already in at 40838c1 |
cleanup_on_error reads port from PORT_FILE |
Already in at 40838c1 |
Current state
The PR adds 8 files (workflow, 4 scripts, manifest generator, .gitignore entry, init.ts doc comment). All previous actionable items across three review rounds have been addressed. The fixtures.ts change has been fully reverted — the file is identical to main.
CI status: Check job is queued for e9fca14. The previous failure was the oxlint rules-of-hooks false positive on the moved suppress comment, which this commit fixes by reverting fixtures.ts entirely.
Remaining observations (non-blocking)
1. e2e-cleanup.sh does not clean up PORT_FILE
The cleanup script removes PID_FILE (line 30) but not PORT_FILE. The deploy script's cleanup_on_error also does not remove either file. Since the Next.js harness uses isolated temp directories that are discarded, this is harmless in practice — but for local development reruns in the same directory, stale files could cause confusion.
2. CJS converter regex ordering
The regex for const X = require('mod') (step 2) would match the LHS of const X = require('mod')(args) if step 1 did not run first. The current ordering is correct, but it is implicit — the regexes depend on execution order for correctness. A comment noting this dependency would prevent a future reordering from introducing a subtle bug.
3. Report job string interpolation
Lines 302-304 of the workflow embed ${{ inputs.vinext-ref || github.ref }} directly into a JS string literal. As noted in the previous review, a single quote in a branch name would break the JS. This remains extremely unlikely and there is no clean workaround in github-script, so it is fine.
None of the above are blockers. The PR is well-structured, all review feedback has been addressed across the iterations, and the CI failure path is clear.
LGTM — approve.
|
Review posted on PR #1189. Here's a summary: All prior review items are resolved. The latest commit ( 3 non-blocking observations:
PR approved — ready to merge once CI passes on the latest commit. |
Summary
Adds the scripts and GitHub Actions workflow for running the Next.js e2e deploy test suite against vinext, structured per the Next.js adapter testing docs.
Extracted from #922 (which also contains the Pages Router compatibility fixes that make the suites pass).
How it works
Next.js ships a test harness (
run-tests.js) that creates isolated temp apps, calls an adapter's deploy/logs/cleanup scripts, then runs Playwright tests against the running server. Our scripts wire that harness up to vinext: each test app gets vinext injected as a local dependency, built withvinext build, and served withvinext start.What's included
Workflow (
.github/workflows/nextjs-deploy-suite.yml):buildjob: builds vinext, prepares Next.js checkout, generates the deploy manifesttestjob: restores the build cache, shards tests 16 ways viarun-tests.jsworkflow_dispatchwithvinext-ref(branch to test),next-ref(Next.js version, default v16.2.6),suite-filter(app/pages/all, default app),test-concurrencymainScripts (named per Next.js docs convention):
scripts/e2e-deploy.sh— per-test deploy: injects vinext into the test app, builds withvinext build, starts withvinext start, prints URL to stdoutscripts/e2e-cleanup.sh— kills the server process, persists debug logsscripts/e2e-logs.sh— replays build/server logs (includes requiredBUILD_ID:/DEPLOYMENT_ID:/IMMUTABLE_ASSET_TOKEN:markers)scripts/run-nextjs-deploy-suite.sh— convenience wrapper for local runsManifest generator (
scripts/nextjs-deploy-manifest.mjs):--filter app|pages|allto select which suites to include from the Next.jsdeploy-tests-manifest.jsonOther:
.gitignoreupdated to ignorereports/pnpm in the scripts
The scripts use pnpm (not
vp) in two places, both intentionally:vphas no project context herevpis already used where it should be (building vinext itself).