We used to not post a comment when a part of the bundler matrix got
cancelled. Conceptually, a cancelled job is just like a failed on in the
sense of not being able to produce stats.
Now we always aggregate stats and post stats from the bundlers that did
produce them.
The risk here is a race between a run that was cancelled because another
run superseded it and the run that superseded it. Though practically,
aggregating the stats isn't slower than running the full workflow. Not
impossible (especially given how slow the Webpack job is) but incredibly
rare.
This is the first change in a series that will let `next dev` cancel a
Server Components HMR refresh once a newer refresh supersedes it. Here
we only introduce the `experimental.serverComponentsHmrCancellation`
flag and plumb it to every place later changes will read it, without
adding any behavior yet, so on its own this change is an inert no-op.
The flag is declared in `ExperimentalConfig`, validated in the Zod
schema, and defaults to `false` in `defaultConfig`. We thread it into
the runtime config via the `NextConfigRuntime` pick list and
`getNextConfigRuntime`, expose it to user-bundled client code through
`define-env` as `process.env.__NEXT_SERVER_COMPONENTS_HMR_CANCELLATION`,
and pass it into the app render options in `base-server.ts` and the
`app-page` build template. The edge SSR template hardcodes it to
`false`, because edge rendering does not expose the Node response-close
signal the cancellation relies on.
So that the following changes are exercised as soon as their behavior
lands, `config.ts` honors a
`__NEXT_EXPERIMENTAL_SERVER_COMPONENTS_HMR_CANCELLATION` environment
variable that forces the flag on while still respecting an explicit user
config value, mirroring how `cachedNavigations` is rolled out. Both the
`test-cache-components-dev` and `test-cache-components-prod` CI shards
export that variable: the development shard so the feature is exercised
once its behavior lands, and the production shard as a guard that this
development-only feature stays inert in production even with the flag
enabled. The default stays `false`; a later change flips it once the
implementation is complete and drops the shard overrides.
Instead of hardcoding the list, we now automatically detect any
`.browser` sibling and create a compiler alias for that module.
Together with a linting rule, we'll ensure no future regressions are
handled automatically (detection of conditional require and missing
.browser sibling).
This isn't the smoothest DX yet. Ideally we'd generate the list at
Next.js compile time (i.e. before publishing) to avoid having to rerun a
script (I'll follow-up). Generating the list during build time (next dev
or next build) is just another tiny cost users would have to pay.
This is embarrassing, I wasn't paying enough attention when I put up
#95374. We have to both change the build dependency *and* switch the
machine type...
This should be a more robust version of
https://github.com/vercel/next.js/pull/95332 that re-introduces some
x86-64 runners, but also properly segments the artifact cache.
Even if we don't end up merging this, this does some cleanup of
`uploadNativeArtifact` that might be useful to pull out into a separate
PR.
On #95332, the `fetch-test-timings` job failed. That caused downstream
jobs to get a "skip" state, instead of a failure state, which then
caused `tests-pass` to succeed. This is because if a dependency job
fails, GitHub's default behavior is for the downstream job to skip, and
to not cascade the failure.
Depending on all of these helper jobs seems like the simplest way to
prevent this from happening in the future.
A 16 core x86-64 runner is $0.042/minute. A 16 core arm64 runner is
$0.026/minute.
https://docs.github.com/en/billing/reference/actions-runner-pricing
It also looks like they're faster! After factoring the performance
improvement, this appears to be a ~50% cost reduction for migrated jobs.
Datadog SQL:
```
SELECT
REGEXP_REPLACE(ci_job_name, '\s*\([0-9]+/[0-9]+\)', '') AS ci_job_name,
labels,
branch,
commit_date,
SUM(duration) / 1000 / 1000 / 1000 / 60 as minutes,
ci_pipeline_id
FROM dd.ci_pipelines(
columns => ARRAY ['@git.commit.committer.date', '@ci.pipeline.id', '@duration', '@git.branch', '@ci.node.labels', '@ci.job.name'],
event_type => 'job',
filter => '@git.repository.id_v2:github.com/vercel/next.js @ci.pipeline.name:build-and-test @ci.provider.instance:github-actions @ci.pipeline.id:(28266192824-1 OR 28266190122-2) -@ci.job.name:*changed*',
from_timestamp => now() - INTERVAL '30 days',
to_timestamp => now()
) AS (
commit_date VARCHAR,
ci_pipeline_id VARCHAR,
duration BIGINT,
branch VARCHAR,
labels VARCHAR,
ci_job_name VARCHAR
)
GROUP BY
REGEXP_REPLACE(ci_job_name, '\s*\([0-9]+/[0-9]+\)', ''),
labels,
commit_date,
branch,
ci_pipeline_id
ORDER BY
REGEXP_REPLACE(ci_job_name, '\s*\([0-9]+/[0-9]+\)', ''),
branch,
commit_date,
minutes;
```
https://app.datadoghq.com/ddsql/editor?ddsql_dataset_id=34de66b9-30f4-48dd-ac98-354dc7e67fdf
Full Data:
https://docs.google.com/spreadsheets/d/1SszR3RsPeJ13nVW5ggMakwy87fkQMqWMHbZDehHH8IA/edit?usp=sharing
Here's the abbreviated data for jobs longer than 5 minutes:
<img width="1187" height="656" alt="Screenshot 2026-06-26 at 4 38 00 PM"
src="https://github.com/user-attachments/assets/9341039e-c4ea-405c-8d24-cdbc5ed1ba24"
/>
It seems like for the jobs switched over to arm64, they run in about
~73-82% of the time as arm.
A `pnpm install` installs native bindings for Next.js by default. This
makes sense for local development where half of the team doesn't work on
native code and just needs the most recent bindings. However, CI almost
exclusively wants to test with bindings that are built from the current
commit. The rest doesn't need native bindings at all.
The only exception is `test_examples` which is likely not functional.
Though we still support it by allowing to opt-into automatically
installing native bindings in CI.
## test plan
- [x] fetch test timings still skips:
https://github.com/vercel/next.js/actions/runs/28087707205/job/83157705446?pr=95114#step:7:228
Add a `preview` release type so `@preview` npm releases can be cut
**ad-hoc from `canary`** via the `Trigger Release` workflow, retiring
the dedicated long-lived preview branch. The GitHub environment has been
updated accordingly.
A preview cut produces **two commits**, pushed in a single update:
1. **Bump-to-preview** — all packages set to the computed preview
version, tagged `v…-preview.N`. This tag's build publishes `@preview`
(relies on #95085).
2. **Revert-to-canary** — restores the `-canary.N` versions so `canary`
keeps advancing its own line and isn't stranded on a preview version.
```
B canary HEAD before cut (e.g. 16.3.0-canary.61)
│
P v16.3.0-preview.N ← tag here; tag build publishes @preview
│
R canary HEAD after cut (versions restored to 16.3.0-canary.61)
```
### Preview version numbering
`base = max(canary major.minor.patch, npm @preview major.minor.patch)`:
- base still on the published preview line → continue it (`n + 1`)
- canary advanced to a higher base (or no `@preview` published) → reset
to `.0`
So if `canary` jumps to `18.0.0-canary.x` since the last
`16.3.0-preview.x`, the next cut is `18.0.0-preview.0`, not a
continuation.
## Test plan
The dry mode of `start-release.js` was extended to intercept the GH
requests and log the relevant data.
```console
$ node ./scripts/start-release.js --release-type preview --dry-run
Dry run: keeping commits locally, skipping git push and GitHub release creation
Running pnpm release-preview...
[...]
> lerna version 16.3.0-preview.4 --force-publish -y --no-push --allow-branch '**'
[...]
Creating GitHub-signed release commit(s) for v16.3.0-preview.4 from local commits 0f5c1e865cc34edb257a16823ed6525d9cd58bfc..0062348c411776271fe7eac64e97b8bad2775b77
[dry-run] GitHub API POST /repos/vercel/next.js/git/blobs [...]
[dry-run] GitHub API POST /repos/vercel/next.js/git/commits {"message":"v16.3.0-preview.4","tree":"0000000000000000000000000000000000000016","parents":["0f5c1e865cc34edb257a16823ed6525d9cd58bfc"]}
[dry-run] GitHub API POST /repos/vercel/next.js/git/commits {"message":"Restore canary version 16.3.0-canary.61 after v16.3.0-preview.4 preview release","tree":"000000000000000000000000000000000000002d","parents":["0000000000000000000000000000000000000017"]}
[dry-run] GitHub API POST /repos/vercel/next.js/git/refs {"ref":"refs/tags/v16.3.0-preview.4","sha":"0000000000000000000000000000000000000017"}
[dry-run] GitHub API PATCH /repos/vercel/next.js/git/refs/heads/sebbie/preview-release-from-canary {"sha":"000000000000000000000000000000000000002e","force":false}
Dry run: skipping local branch sync; would set sebbie/preview-release-from-canary to 000000000000000000000000000000000000002e and tag v16.3.0-preview.4 at 0000000000000000000000000000000000000017
Created GitHub-signed release tag v16.3.0-preview.4 at 0000000000000000000000000000000000000017; branch sebbie/preview-release-from-canary now at 000000000000000000000000000000000000002e
Dry run: skipping GitHub release creation
Release process is finished
```
That way we can push multiple commits when doing a release. For
canaries, nothing will change.
In a follow-up, we'll start cutting `@preview` from the `canary` branch.
To support that case, we need to immediately revert the version in
`lerna.json`. We shouldn't push commits indidually to avoid having an
incomplete release if something else is pushed between `@preview`
version bumps and `@canary` version reverts.
## test plan
After merge, the next canary release should publish from the
`v…-canary.N` tag run (`deploy-target = production`), while the canary
branch push for the same commit shows `skipped` — exactly one production
run per release.
<!-- NEXT_JS_LLM_PR -->
Because it's not sure which one you'll want, playwright installs TWO
copies of Chromium by default. A full copy and a stripped down headless
version.
Most of the install time is spent in `apt-get`/`dpkg`, but this at least
buys us a few seconds back.
Before:

After:
<img width="1700" height="552" alt="Screenshot 2026-06-16 at 6 24 50 PM"
src="https://github.com/user-attachments/assets/d382cb8e-a875-4d67-92d6-32ff51c06488"
/>
Playwright also installs ffmpeg in case you want to perform a screen
recording (we don't), but it's much smaller (2.3 MiB), and there doesn't
appear to be a way to disable it.
https://github.com/vercel/next.js/pull/94622 tried to be more strict
with when a release is considered healthy. However, `@next/swc-wasm-web`
is not enrolled in Trusted Publishing and blocks the release train.
This PR restores the behavior for `@next/swc-wasm-*` packages prior to
https://github.com/vercel/next.js/pull/94622 i.e. we continue the
release train even if any `@next/swc-wasm-*` fails. Other NAPI bindings
will continue to block the release train if they fail to publish.
The bail vs no-bail behavior is controlled with a repository variable so
that we can mor easily test if `@next/swc-wasm-web` is enrolled in
Trusted Publishing.
In ci when testing our benchmarks use our test profile
`release-with-assertions`
This avoids an expensive `lto` step and ensures that our tests run with
debug asserts.
I noticed that our workflow for 'test cargo benches' was very slow and a
lot of that time was the build presumably due to lto overheads across so
many benchmark binaries.
Compare:
* cargo benches for this pr:
https://github.com/vercel/next.js/runs/79974366863?pr=94538
- build=4m42s
- test=1m46s
* cargo benches before this pr:
https://github.com/vercel/next.js/actions/runs/27097792020/job/79973082715#logs
- build=8m23s
- test=6m56s
Of course doing this revealed a few things
* a birthday paradox panic in one of the persistence tests
* some 'top level read' errors to address in the benchmarks and in the
turbopack-cli server used for some benchmarks.
And finally, that the persistence tests are just extremely slow in test
mode due to their initialization overhead (populating >1GB dbs), so that
harness is rewritten to remove some variants and also to reduce max
sizes in test mode
I noticed the `node-version-file` option for `setup-node` while
reviewing https://github.com/vercel/nextjs-react-compiler/pull/2
We should use it in places where we're already checking out the
repository anyways. It should make it easier to keep node versions in
sync.
If `binstall` fails to download a precompiled artifact it falls back to
`cargo install` and many tools (like `nextest`) require the `--locked`
flag. So pass the `--locked` flag.
## Summary
- upgrade `@rspack/core` to `2.0.0-rc.0`
- sync the in-tree next rspack Rust binding/toolchain with newer rspack
releases
- align Next server runtime selection so `NEXT_RSPACK` uses the
turbo-like app runtime path
- include missing compiled `@next/react-refresh-utils` output in
`ncc-compiled`
## What is done
- `@next/rspack` Rust binding builds locally
- `packages/next` builds locally
- rspack-related tests are running and have been reduced to deeper
runtime compatibility failures
## Rspack test investigation
Latest checked CI run: `build-and-test` run `27129243230` at head
`484e7bf1b30655a9f94d0855b9e00774b4b38cd0`.
Confirmed Rspack / webpack parity gaps:
-
`test/e2e/edge-runtime-configurable-guards/edge-runtime-configurable-guards.test.ts`:
Rspack does not expose the webpack parser hooks used by
`MiddlewarePlugin`. The current Rspack path falls back to SWC analysis
in `finishModules`, which can report diagnostics but cannot apply the
parser-time transforms that wrap `eval` / `new Function`, track
`InnerGraph` usage for used vs unused dynamic code, or apply
`unstable_allowDynamic` with webpack parity. Dev therefore misses the
expected dynamic-code runtime warnings and also reports false
`process.cwd` Edge warnings from Next internals; prod fails
allowed/unused dynamic-code cases.
-
`test/development/middleware-overrides-node.js-api/middleware-overrides-node.js-api.test.ts`:
same parser-hook limitation. Webpack can observe that `process.cwd` is
overwritten before use, while the Rspack SWC fallback statically flags
`process.cwd` as an unsupported Edge Node API.
- `test/production/css-features/css-compilation.test.ts`: the CSS is
minified and prefixed as expected, but the Rspack lightningcss path
currently does not expose/control source map generation the way this
test expects, so emitted CSS has no `sourceMappingURL` comment or `.map`
file to assert.
- `test/e2e/app-dir/server-source-maps/server-source-maps.test.ts`:
invalid source maps are not handled with the same graceful diagnostic as
webpack/Node source-map handling. Rspack logs the raw `webpack-internal`
frame for the damaged map case instead of surfacing the expected
`Invalid source map. Only conformant source maps...` message with the
cause.
-
`test/e2e/app-dir/parallel-routes-revalidation/parallel-routes-revalidation.test.ts`:
raw CI output observed a dev-mode timeout waiting for `networkidle`
after refresh/back/forward lazy fetching. This was not the final
structured failure in the latest run, but it remains a Rspack dev parity
follow-up around router cache lazy fetches and network settling.
- `test/e2e/middleware-src/middleware-src.test.ts`: after the test adds
root middleware files, Rspack dev still lets `src/middleware`
participate; the expected behavior is that only the root middleware
runs.
- `test/e2e/env-config/env-config.test.ts`: changing `.env` logs `Reload
env:` but the client keeps the previous `NEXT_PUBLIC_` value, pointing
at an Rspack dev env invalidation/HMR gap.
-
`test/e2e/app-dir/use-cache-without-experimental-flag/use-cache-without-experimental-flag.test.ts`:
after enabling the `useCache` flag, the dev server does not finish
restarting, pointing at build-error recovery/restart parity.
-
`test/development/app-dir/server-navigation-error/server-navigation-error.test.ts`:
middleware navigation-error cases hit `ERR_CONNECTION_REFUSED`,
consistent with the Rspack dev server exiting or restarting unexpectedly
during those error flows.
- `test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts`: the
custom-server dev process exits before the SDK handle is initialized,
then cleanup fails on `shutdown`; this needs follow-up in Rspack
dev/custom-server startup.
- `test/e2e/next-image-new/app-dir/app-dir.test.ts`: after toggling
image source, `onLoadingComplete` still reports the previous image
dimensions/source, pointing at an Rspack dev image/HMR or asset
invalidation parity issue.
- `test/e2e/node-cli-args/node-cli-args.test.ts`: on Node `20.19.x`,
`node --experimental-network-inspection` resolves instead of rejecting.
This looks like a Node-version behavior change rather than an Rspack
compiler issue.
Not attributed to this Rspack upgrade:
-
`test/e2e/app-dir/instant-navigation-testing-api/instant-navigation-testing-api.test.ts`
was marked by `scripts/pr-status.js` as a known flaky test across
branches.
-
`test/production/app-dir/build-output-prerender/build-output-prerender.test.ts`
ran in the webpack prod job and only differs by prerender error ordering
in the inline snapshot.
<!-- NEXT_JS_LLM_PR -->
---------
Co-authored-by: Benjamin Woodruff <benjamin.woodruff@vercel.com>
I determined that [`nvm` wasn't sufficient for our use-case since we
need to support
windows](https://github.com/vercel/next.js/pull/94175#issuecomment-4654622674),
so this doubles down on `fnm`.
This reduces risks of supply chain attacks with `fnm`.
This also cleans up some old stuff that was left over from self-hosted
runners and/or that was there for debugging, if it isn't useful anymore.
We're consolidating on `actions/cache` now that we got rid of
self-hosted runners. This doesn't remove all of the uses of turborepo's
cache (notably sccache), but this does eliminate the remote turborepo
cache for the native build cache and the docker image.
These build steps use docker so that we're building against an old
pinned glibc (2.31), and so that we can have access to both `aarch64`
and `x86_64` musl sysroots.
## Testing
**Local Build:** `node scripts/docker-native-build.js` still works (this
script just exists for being able to debug these builds locally)
**CI Build:**
- Run an automated-preview CI job, but cancel it before the native build
succeeds to test writing to the docker cache but not the native cache:
https://github.com/vercel/next.js/actions/runs/26986955366/job/79638633136?pr=94319
- Re-run the job with only the docker image cache, see that we get a
cache hit for the docker image:
https://github.com/vercel/next.js/actions/runs/26986955366/job/79639365126?pr=94319
- Re-run the job again and see that we hit the native build cache hit
and don't touch docker at all:
https://github.com/vercel/next.js/actions/runs/26986955366/job/79647324952?pr=94319
- Manually trigger the job to do a full preview build:
https://github.com/vercel/next.js/actions/runs/26989861549
We reverted the use of this docker image in
https://github.com/vercel/next.js/pull/94366
We're going to use GitHub actions custom runner images instead (which
don't use docker)
### What?
Reduce the `test-new-tests-dev` and `test-new-tests-start` workflow
matrices from ten groups to five.
### Why?
Restore these flake-detection jobs to five concurrent shards.
### How?
Change each matrix group list from `1/10` through `10/10` to `1/5`
through `5/5`.
### Verification
- `pnpm prettier --with-node-modules --ignore-path .prettierignore
--check .github/workflows/build_and_test.yml`
- `git diff --cached --check`
<!-- NEXT_JS_LLM_PR -->
The actual security implications of this are likely *very* minor, but in
the interest of reducing how many credentials we have, there's little
reason for us not to set this to `false`.
In cases where we create a new commit via GH actions, we already do that
via the GitHub API, so those don't need the on-disk git credentials to
push.
---
From: https://github.com/actions/checkout :
> The auth token is persisted in the local git config. This enables your
scripts to run authenticated git commands. The token is removed during
post-job cleanup. Set persist-credentials: false to opt-out.
```
# Whether to configure the token or SSH key with the local git config
# Default: true
persist-credentials: ''
```
Matches the environment name I asked IT to use for NPM trusted
publishing:
https://vercel.slack.com/archives/C01LN7C5QR5/p1780600987032909
Nothing else uses the `npm` environment, but the name is confusing. We
want to use separate environments for these publishing workflows as they
allow us to define things like reviewers, if we want to block the
release on a manual review.
We're going to go with custom images instead, though we don't have an
implementation for this yet.
This does save a meaningful amount of minutes, but it also significantly
increases complexity.
This is a clean revert, minus the fix that the PR included for listening
on localhost in `packages/next/src/build/analyze/index.ts`, which is
still good.
### What?
Defaults `experimental.useNodeStreams` to `true` while keeping the
experimental config flag and explicit `false` opt-out available. Removes
redundant dedicated node-stream CI jobs because the ordinary path
exercises Node streams after this change.
Depends on the standalone test compatibility update in #94347, which
intentionally remains outside the Node streams PR stack.
### Why?
Node streams can be exercised through the standard Node.js App Router
test path once they are the default, allowing a smaller rollout before
the larger follow-up that removes the flag and obsolete plumbing.
CI exposed one necessary compatibility fix for this intermediate state:
projects with a `next.config.*` received the new default after raw
experimental-feature processing, leaving the runtime stream selector on
the web implementation while compiled app code selected Node stream
helpers.
### How?
- Set `defaultConfig.experimental.useNodeStreams` to `true`.
- Synchronize `__NEXT_USE_NODE_STREAMS` from the fully resolved config,
including cached and standalone resolved configs, so defaults and
adapter modifications are reflected at runtime.
- Preserve the current public config/schema and explicit `experimental:
{ useNodeStreams: false }` opt-out; edge bundles continue to define the
stream flag as `false`.
- Remove the dedicated node-stream test jobs and now-unused manifest;
the affected streaming assertion compatibility changes are isolated in
#94347.
### Verification
Run before extracting the prerequisite assertion changes into #94347:
- `pnpm --filter=next types`
- `pnpm --filter=next build`
- `IS_WEBPACK_TEST=1 __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true pnpm
test-start-webpack
test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts`
- `IS_TURBOPACK_TEST=1 TURBOPACK_BUILD=1
__NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true pnpm test-start-turbo
test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts`
- `IS_WEBPACK_TEST=1 __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true pnpm
test-start-webpack
test/e2e/app-dir/use-server-inserted-html/use-server-inserted-html.test.ts`
- `pnpm test-dev-webpack
test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts
test/e2e/app-dir/use-server-inserted-html/use-server-inserted-html.test.ts`
- `pnpm test-dev-turbo
test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts
test/e2e/app-dir/use-server-inserted-html/use-server-inserted-html.test.ts`
- `pnpm test-start-webpack
test/production/app-dir/use-node-streams-env-precedence/use-node-streams-env-precedence.test.ts`
- `pnpm test-start-webpack test/e2e/app-dir/app/standalone.test.ts`
<!-- NEXT_JS_LLM_PR -->
### What?
Delay dynamic Node Fizz piping by one React render task after
`onShellReady()`, and keep the dedicated Node-stream CI jobs scoped to
`test/use-node-streams-tests-manifest.json`.
### Why?
The Node adapter started `pipeable.pipe(pt)` inside `onShellReady()`,
before the existing continuation delay could take effect. In App Router
renders backed by RSC, that caused short-lived Suspense boundaries to
flush fallback and reveal markup that the web-stream path avoids by
waiting before its first pull.
The previous test assertion relaxations hid this runtime mismatch. The
original assertions pass once piping starts after the settling window,
so they have been restored.
The workflow correction remains necessary because merging the
cache-components manifest into the ordinary Node-stream jobs filtered
out relevant e2e coverage.
### How?
- Resolve the Node Fizz shell promise from `onShellReady()` without
immediately piping.
- For dynamic renders, wait one React render task and then start piping
into the `PassThrough`.
- Preserve the existing `onAllReady()` behavior for static generation.
- Use the Node-stream manifest alone for ordinary Node-stream dev and
prod CI jobs.
### Verification
- `pnpm --filter=next build`
- `__NEXT_USE_NODE_STREAMS=true
__NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true NEXT_TEST_PREFER_OFFLINE=1
pnpm test-dev-turbo
test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts`
- `__NEXT_USE_NODE_STREAMS=true
__NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true NEXT_TEST_PREFER_OFFLINE=1
pnpm test-dev-turbo
test/e2e/app-dir/use-server-inserted-html/use-server-inserted-html.test.ts`
<!-- NEXT_JS_LLM_PR -->
Previously we pinned all of these to their current versions at the time.
That meant that some of these got pinned to really old versions. This
updates all of the "first-party" `actions/*` dependencies.
Prompted claude with:
```
For all of these uses of actions/*, make sure we're pulling in the latest stable version (check the gh cli). When upgrading across major versions, fetch the release notes from the GH releases using the gh CLI and make sure that there are no problems that the upgrade will cause.
```
Claude read all the release notes and seems to think these upgrades are
safe.

Turbopack is the default bundler, so we should prioritize testing that
in these variants of the CI runs (cache components, node streams,
windows).
There's also some limited evidence that indicates that it might be
faster for e2e test runs, though that's not conclusive (because of
different shard sizes):
https://vercel.slack.com/archives/C0B0LKK5QD7/p1779320615501259
If this action detects that you're likely using `npm` it (by default)
enables a cache for `npm`. We use `pnpm`, so it doesn't do anything (and
we manually set up a pnpm cache in some places), but the implicit
behavior here is a little dangerous: e.g.
https://adnanthekhan.com/posts/angular-compromise-through-dev-infra/#finding-the-pivot
So let's explicitly disable this everywhere. It wasn't doing anything
for us anyways.