next.js
10c5a6ac - Sequence CI for branch-based PR stacks (#99086)

Commit
3 days ago
Sequence CI for branch-based PR stacks (#99086) ### What? Replace the Graphite-specific CI optimizer with a **read-only TypeScript gate** for PR stacks formed by ordinary GitHub base/head branch links. This PR includes the work previously reviewed in #99116 and #99122. ### Why? Rebasing a large stack starts expensive `build_and_test` jobs on every PR at once. Let the first three PRs and the top PR run immediately, but defer middle PRs until nearby CI provides a useful signal. A deferred PR must **not** appear mergeable just because its jobs have not started. ### How? - **Find the stack from branch relationships**, not GitHub stack metadata. Middle PRs poll their three nearest predecessors every **five minutes**. Any predecessor with a successful, current-head/current-base `thank you, next` check releases full CI; three unsuccessful results **fail the gate and required aggregate**. While waiting, the gate stays in progress. After five hours without a decision, it releases full CI rather than stranding the PR. - **Revalidate before deciding.** Refresh PR and check data before release/failure so rebases, retargeting and reruns cannot make a stale check decisive. Transient GitHub 5xx/429 errors retry; permanent errors or ambiguous topology start full CI rather than claiming CI passed. - **Keep polling cheap and read-only.** Reuse PR head/base data from `pulls.list` (11 → **8 REST reads** per ordinary waiting poll), while still checking *all three* predecessors. The bundled action is checked out at the current test-merge `${{ github.sha }}` without persisted credentials; forks bypass the checkout/gate and run full CI. The six-hour gate stays on `ubuntu-latest` because `ubuntu-slim` has a 15-minute limit. <details> <summary>Implementation and review tradeoffs</summary> - The local action bundles a pinned Octokit. Isolated Jest tests use fake timers and a real check-run response fixture. CI runs the existing Next.js lint/examples/externals/browser checks before action-specific install, typecheck, build and Jest; a generated-diff guard catches stale bundles. - `uses: ./...` requires action files already in the job workspace; it does not fetch them like `owner/repo@ref`. The sparse checkout fetches only `action.yml` and `dist/index.js`. Caller and reusable workflow grant read-only `checks`, `contents` and `pull-requests` permissions; the gate inherits no secrets. - Cheap-check graph splitting was deliberately deferred. Job-level concurrency does not enforce predecessor *success*; stopping at the first pending predecessor misses an older success. GraphQL batching needs a verified check-association projection and measured cost before replacing the current REST lookup. - When the child commits became reachable from this branch, GitHub automatically marked #99116 and #99122 merged; no PR merge operation was used. [Action README](https://github.com/vercel/next.js/blob/ci/branch-stack-polling-gate/.github/actions/pr-stack-ci-gate/README.md) documents the contract. </details> ### Verification - **Focused checks:** TypeScript, Prettier, deterministic bundle/diff checks and **24/24 Jest** passed. The final comment-only change in `src/gate.ts` produced a byte-identical bundle. The [current root run](https://github.com/vercel/next.js/actions/runs/35967795126) passed its gate, lint and required `thank you, next` on **attempt 2** (same SHA); attempt 1 failed an intermittent redbox test. - **Live six-PR test on the compiled action:** the first three and top passed the gate immediately. Middle #99097 and #99098 **waited**. On initial attempts, both **failed closed** when their three predecessors were unsuccessful, with no expensive jobs started. On rerun, #99097 **released on #99086's success** despite #99096 failing and #99095 still pending; full CI ran but failed unrelated tests. #99098 waited for *its own* three predecessors, then failed closed again. Gate release never marked full CI successful. - **Known limitation:** product-test failures—not the gate—left four immediate/released PRs with failed full CI on various attempts. All five test drafts remain open for reuse. Workflow reruns in this experiment were initiated externally; this agent's GitHub integration received 403 (`actions=write` required) when attempting a rerun. <details> <summary>Current compiled-action six-PR results, with workflow and gate logs (2026-09-24)</summary> Each draft is one Jest assertion commit over its signed parent; the cumulative focused suite passes **24/24**. Times below are UTC. “Required” refers to `thank you, next`, not the gate check. | Position | PR / signed head / workflow | Gate outcome | Required check | Expensive work | | :-- | :-- | :-- | :-- | :-- | | 1 | [#99086](https://github.com/vercel/next.js/actions/runs/35967795126) `85444c9e` | immediate pass | failed attempt 1; **passed attempt 2** | ran twice | | 2 | [#99095](https://github.com/vercel/next.js/actions/runs/35969902709) `bc19787a` | immediate pass | failed attempts 1 and 2 | ran twice | | 3 | [#99096](https://github.com/vercel/next.js/actions/runs/35970016367) `98bfab58` | immediate pass | failed | ran | | 4 | [#99097](https://github.com/vercel/next.js/actions/runs/35970109573) `e260bf96` | [attempt 1](https://github.com/vercel/next.js/actions/runs/35970109573/job/107537584401): waited → failed 08:02; [attempt 2](https://github.com/vercel/next.js/actions/runs/35970109573/job/107609256427): **released 11:19** | failed both attempts (attempt 2: product tests) | none on attempt 1; full CI ran on attempt 2 | | 5 | [#99098](https://github.com/vercel/next.js/actions/runs/35970206069) `01222d4a` | [attempt 1](https://github.com/vercel/next.js/actions/runs/35970206069/job/107537887530): waited → failed 08:03; [attempt 2](https://github.com/vercel/next.js/actions/runs/35970206069/job/107609367420): waited → failed 11:50 | failed both attempts | **never started** | | 6 (top) | [#99099](https://github.com/vercel/next.js/actions/runs/35970312588) `c6f3cf4a` | immediate pass | failed attempts 1 and 2 | ran twice | **Decision evidence:** On attempt 1, #99097 saw #99096/#99095/#99086 all unsuccessful, and #99098 saw #99097/#99096/#99095 all unsuccessful; their required checks failed and downstream jobs were skipped *because their gates failed*. After #99086's successful rerun, #99097 attempt 2 logged `#99096=unsuccessful, #99095=waiting, #99086=success`; its gate passed and [build-next actually ran and passed](https://github.com/vercel/next.js/actions/runs/35970109573/job/107609336752). #99098 could not use #99086 (outside its nearest-three window); it failed at 11:50 when #99097/#99096/#99095 had all finished without success. Completed failed gates do not restart automatically when a predecessor reruns. </details> <details> <summary>Unrelated CI failures and recovered flaky tests</summary> - #99086's attempt 1 failed `lazy-dynamic-imports › does not parse a dynamic import target before activation`: redbox source was `null` after all in-job retries ([job](https://github.com/vercel/next.js/actions/runs/35967795126/job/107530613870)). **The same-head workflow passed on attempt 2.** - #99095, #99096, #99097 attempt 2 and #99099 failed `instant-insights-tab-overlay › should wrap the mobile overlay header only when it does not fit`: observed header top difference **24**, expected **< 4**, after in-job retries ([example job](https://github.com/vercel/next.js/actions/runs/35969902709/job/107537341995)). #99096 also failed `turbopack-loader-file-dependencies › should update when a build dependency changes` (`build-one` instead of `build-two`, [job](https://github.com/vercel/next.js/actions/runs/35970016367/job/107537932737)). #99097 attempt 2 additionally failed the `lazy-dynamic-imports` redbox assertion in an experimental cache-components shard ([job](https://github.com/vercel/next.js/actions/runs/35970109573/job/107609732882)). These failures are not gate-test failures. - **Recovered in-job flakes:** #99096 `instant-validation/head-and-reporting › invalid - runtime prefetch - dynamic viewport blocks navigation` (redbox did not open; passed retry 1/2); #99095 `use-cache-without-experimental-flag › should recover from the build error if useCache flag is set` (Playwright execution context destroyed on navigation; passed retry 1/2); #99097 attempt 2 `lazy-dynamic-imports` in a different Turbopack-dev shard (passed retry 2/2), `instant-validation/suspense-boundaries` (redbox did not open; passed retry 2/2), and `instant-validation/head-and-reporting.partial-prefetching` (redbox did not open; passed retry 1/2). </details> <details> <summary>Earlier implementation smoke tests and CJS-gate stack runs</summary> - The [folded-code root head `89f0ca34` full run](https://github.com/vercel/next.js/actions/runs/35965443069) passed gate, build, lint (including **24/24 Jest**) and required `thank you, next`. The earlier [compiled-action child head `25647f00` run](https://github.com/vercel/next.js/actions/runs/35922700289) also passed full CI, proving same-SHA sparse checkout before the lint-order follow-up was folded into root. - **CJS-gate fail-closed run (base `ec0abf41`, 2026-09-23):** [#99086](https://github.com/vercel/next.js/actions/runs/35875541530), [#99095](https://github.com/vercel/next.js/actions/runs/35875598833), [#99096](https://github.com/vercel/next.js/actions/runs/35875651890) and top [#99099](https://github.com/vercel/next.js/actions/runs/35875952426) ran full CI and failed unrelated tests. Middle [#99097](https://github.com/vercel/next.js/actions/runs/35875701404/job/107231099534) failed its gate at 15:20 and [#99098](https://github.com/vercel/next.js/actions/runs/35875873698/job/107231827956) at 15:21; required checks failed and expensive jobs never started. A Turbopack source-map snapshot failure was subsequently fixed in [#99106](https://github.com/vercel/next.js/pull/99106). - **CJS-gate release run (base `8ff7f2bd`, 2026-09-23):** [#99086](https://github.com/vercel/next.js/actions/runs/35887913444) and [#99095](https://github.com/vercel/next.js/actions/runs/35887957948) passed full CI. [#99097](https://github.com/vercel/next.js/actions/runs/35888074168/job/107273254039) and [#99098](https://github.com/vercel/next.js/actions/runs/35888146874/job/107273514756) waited, then released at the next poll on #99095's green check while a nearer predecessor was unresolved. Their later full CI failed; top [#99099](https://github.com/vercel/next.js/actions/runs/35888202575) started immediately (later canceled). These precede the TypeScript action and are not claimed as evidence for its current head. </details> <!-- NEXT_JS_LLM --> <!-- fleet d2ae64f1-7106-4ba4-b88a-7785c19904c6 --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Author
Parents
Loading