fix(astro,sveltekit): skip generated writes that would not change the file - #3688

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/guard-generated-writes-frameworks
Aug 25, 2026
Merged

fix(astro,sveltekit): skip generated writes that would not change the file#3688
VaguelySerious merged 1 commit into
mainfrom
peter/guard-generated-writes-frameworks

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Aug 20, 2026

Copy link
Copy Markdown
Member

Description

Follow-up to #3454, which added the unchanged-write guard and applied it to @workflow/builders, @workflow/next and @workflow/nitro. The Astro and SvelteKit builders were not covered: they receive the bundle text from the base builder and do their own writeFile, so they never reach the guard. Their generated files live in Vite-watched source directories (src/pages/.well-known/workflow/v1 and src/routes/.well-known/workflow/v1), which is exactly the condition #3454 was about.

This routes those writes through writeFileIfChanged, and writes SvelteKit's static manifest copy from the same string rather than copyFile-ing it, matching what #3454 did for the Next public manifest.

Measured effect

Two consecutive pnpm build runs with no source change, comparing sha256 + mtime(ns) + inode of every generated file under the workflow directories, with WORKFLOW_PUBLIC_MANIFEST=1:

beforeafter
SvelteKit (src/routes/…, static/…)6 of 9 rewritten3
Astro (src/pages/…)5 of 8 rewritten3

Every one of those rewrites had an identical sha, i.e. they were pure no-op writes. Fixed here: both .gitignore files and the static/manifest.json copy on SvelteKit, and .gitignore plus manifest.json.js on Astro.

Relation to #3509

Rebased over #3509 (better SvelteKit routes-directory loading), which rewrote the same import block in packages/sveltekit/src/builder.ts. That was the only conflict and it was mechanical: keep its node:path import and drop the node:module/node:url imports it made dead, keep this PR's narrowed node:fs/promises import.

The two changes act at different layers and both still apply. #3509 caches the resolved routes directory so SvelteKit's secondary builds and worker threads do not reload the Svelte config and redo the build; this PR reduces what a build that does run actually writes. The measured numbers above were re-taken on top of #3509, with the guard-off control built from origin/main's builder, so the before/after pair is on the same base.

What this does NOT fix

Three files per framework still churn, for two reasons that need more than a call-site swap. Both are pre-existing and neither is made worse by this PR.

1. The read-transform-rewrite pattern defeats the guard — affects flow/+server.js and webhook/[token]/+server.js (SvelteKit), flow.js and webhook/[token].js (Astro).

Both builders call createCombinedBundle / createWebhookBundle, which write the route to disk through the base builder, then readFile it back, apply a regex transform, and write the transformed content. So each build writes the file twice with two different contents, and on the next build neither write can be skipped: the base write sees the previous build's transformed content on disk and differs from it, and the framework write then sees the untransformed content and differs from that. The signature is visible in the inode: the route gets a new inode (base write, temp+rename) and the webhook keeps its inode (framework write, plain overwrite).

The fix is to make the final content the only content written, e.g. by letting the framework pass its transform into the base builder so the route is generated in its final form. That is an API change to the base builder and wants its own PR.

2. One-shot esbuild.build() always rewrites its outfile — affects flow/__step_registrations.js on both frameworks.

ctx.rebuild() on an esbuild context skips writing when the output is byte-identical (verified on esbuild 0.28.1: mtime and inode both preserved). A one-shot esbuild.build() does not; it rewrites unconditionally. This is why Next dev, which uses contexts, was already protected for its esbuild outputs while these builders are not. The fix is either to use contexts in watch mode or to pass write: false and hand the output to writeFileIfChanged.

Other follow-ups from #3454

3. Nitro's dev handler should stop cache-busting off a single file's mtime.packages/nitro/src/index.ts derives version from statSync(handlerPath).mtimeMs for workflows.mjs and then uses it as the cache-buster for the steps.mjs import. A step-body-only edit leaves workflows.mjs identical, so the version freezes and stale step code is served. That dependency is the only reason skipsUnchangedGeneratedWrites exists.

Widening the key is cheap and keeps the per-request path to stats rather than file reads, since handlerImportPath and stepsImportPath are already in scope:

consts=statSync(handlerPath),t=statSync(stepsPath);constversion=`${s.size}:${s.mtimeMs}:${t.size}:${t.mtimeMs}`;

Including size costs nothing and narrows the case where two coalesced dev writes land on an effectively identical mtime. Note only the workflow/workflows.mjs variant needs the second stat; preloadSteps['workflow/webhook.mjs'] is empty. Both the v2 and v3 branches carry a copy. Once that lands, skipsUnchangedGeneratedWrites can be deleted from BaseBuilder and from the Nitro override.

4. There is no regression guard for the cross-framework case.write-if-changed.test.ts covers the helper and the opt-out, but nothing stops a new raw writeFile into a watched generated directory from silently restoring the redundant rebuild. The check that would catch it is the one used above: build twice, assert every generated file keeps its sha, mtime and inode. @workflow/astro has no test files today and @workflow/sveltekit has one unrelated test, so this needs a builder harness per framework rather than a one-line test.

How did you test your changes?

No automated coverage is added, for the reason in (4), so here are reproduction steps.

pnpm install && pnpm run build
cd workbench/sveltekit # and workbench/astroexport WORKFLOW_PUBLIC_MANIFEST=1
pnpm build
find src/routes/.well-known/workflow static/.well-known/workflow -type f \
| sort | xargs -I{} sh -c 'printf "%s %s %s\n" "$(shasum -a 256 {} | cut -d" " -f1)" "$(stat -f "%Fm:%i" {})" {}'> /tmp/s1
pnpm build
# ...repeat into /tmp/s2
diff /tmp/s1 /tmp/s2

Before this change the two .gitignore files and static/manifest.json appear in the diff with an identical sha and a moved mtime. After, they do not. The three files listed above still appear, for the reasons given.

Also run: pnpm run build (28/28 tasks), npx biome check on both changed files.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run to create a changelog for this PR
  • 🔒 DCO sign-off passes (run git commit --signoff on your commits)
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete

@vercel

vercelBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreview, v0Aug 25, 2026 8:55pm
example-nextjs-workflow-webpackReadyReadyPreview, v0Aug 25, 2026 8:55pm
example-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-astro-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-express-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-fastify-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-hono-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nestjs-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nitro-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nuxt-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-python-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-sveltekit-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-tanstack-start-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-vite-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-docsReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-swc-playgroundReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-tarballsReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-webReadyReadyPreview, v0Aug 25, 2026 8:55pm

@changeset-bot

changeset-botBot commented Aug 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 555b143

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/astroPatch
@workflow/sveltekitPatch
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 555b143 · Tue, 25 Aug 2026 21:17:13 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1215 (+465%) 🔻1315 🔴 (+33%) 🔻1332 🔴 (+29%) 🔻1375 🔴 (-4.8%)30
TTFSstream217 (+20%) 🔻1291 🔴 (+36%) 🔻1314 🔴 (+36%) 🔻1365 🔴 (+35%) 🔻30
TTFShook + stream1447 (+180%) 🔻1597 🔴 (+18%) 🔻1666 🔴 (+11%)1940 🔴 (-62%) 💚30
Fan-out TTFSPromise.all(100 steps)806 (+44%) 🔻1955 (+96%) 🔻1978 (+40%) 🔻2016 (+24%) 🔻10
Fan-out TTLSPromise.all(100 steps)2542 (+31%) 🔻5184 (+87%) 🔻6673 (+99%) 🔻9405 (+43%) 🔻10
STSO1020 steps (inline)99 (-7.5%)148 (+14%)165 (+8.6%)233 (+14%)1019
WO1020 steps146183 (+11%)146183 (+11%)146183 (+11%)146183 (+11%)1
CRTTfirst chunk (pooled)104 (+24%) 🔻145 (+9.0%)167 (+7.7%)271 (+13%)28

Streams

ScenarioCRTT 1stp75p90p99CDV maxiters
paced control (100/s, 60B)138 (+25%)156 (-3%)230 (-44%)419 (-34%)161 (+24%)10
size sweep (100/s, 160B-12KB)128 (+15%)164 (+20%)228 (-36%)629 (-20%)160 (+53%)10
replay gateway-gpt-5.4-nano-2000t (1x)113 (-23%)139 (+1%)187 (-28%)745 (-16%)368 (+11%)3
replay eve-gpt-5.6-sol-2000t (1x)195 (+7%)150 (+22%)192 (+19%)436 (+31%)434 (+36%)2
replay eve-gpt-5.6-sol-2000t (2x)145 (+14%)182 (+22%)241 (+25%)491 (+30%)286 (+10%)3
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 132084ms → this run 144847ms (Δ +12763ms, +10%)

 50-100 ms ┃ main 0 this 1 +1
100-150 ms ████████████████████┃███ main 905 this 781 -124
150-200 ms ███░░┃ main 103 this 217 +114
200-250 ms ┃ main 5 this 13 +8
250-300 ms ┃ main 2 this 2 +0
300-350 ms ┃ main 1 this 2 +1
350-400 ms ┃ main 1 this 2 +1
400-450 ms ┃ main 2 this 1 -1
📈 CRTT drill-down vs main (RTT distributions & profiles)
variant RTT 1ms→5s+ avg p50 p90 p99 n
control ······▃█▁···· 129.5 (+3%) 120 (+21%) 230 (-44%) 419 (-34%) 3000
sweep ······▂█▁▁··· 138.6 (+18%) 126 (+29%) 228 (-36%) 629 (-20%) 3000
gw 1x ·····▁▆█▁▁··· 119.6 (±0%) 105 (+9%) 187 (-28%) 745 (-16%) 5295
eve 1x ·····▁▅█▁▁··· 124.7 (+17%) 109 (+14%) 192 (+19%) 436 (+31%) 5186
eve 2x ·····▁▂█▂▁··· 150.3 (+20%) 134 (+16%) 241 (+25%) 491 (+30%) 7779

RTT over stream progress (avg per tenth of stream, bars scaled min→max):

control ▆█▄▃▂▁▄▂▄▃ 111–157ms
sweep ▃▃▃▂▂▁▄█▃▂ 121–184ms
gw 1x ▂▂▃▃▁█▂▂▁▂ 103–181ms
eve 1x ▄▁▃▆▃█▇▆▄▂ 106–146ms
eve 2x ▁▂▁▁▅▄▄█▆▃ 125–196ms

RTT by chunk size (avg per log size bin, ~160B → ~12KB serialized, bars scaled min→max):

sweep █▇▂▁▃▄▅ 136–141ms

Delivery jitter over stream progress (avg positive CDV per tenth of stream, bars scaled min→max):

control ▂▇▄▄▇▁█▄▅▄ 34–43ms
sweep ▁▄▁▄▆▄▄█▃▃ 47–68ms
gw 1x ▃▂▇▂▃█▃▂▁▄ 31–40ms
eve 1x ▄▁▂▆▆▆█▅▄▆ 20–26ms
eve 2x ▇▇▁▄▇█▅▁▅█ 19–27ms
📜 Previous results (1)

78301f5

Thu, 20 Aug 2026 01:50:16 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep458 (-58%) 💚1476 🔴 (+19%) 🔻1524 🔴 (+15%) 🔻1611 🔴 (-1.1%)30
TTFSstream1295 (+275%) 🔻1477 🔴 (+28%) 🔻1538 🔴 (+29%) 🔻1675 🔴 (+36%) 🔻30
TTFShook + stream1635 (+50%) 🔻2181 🔴 (+41%) 🔻2203 🔴 (+40%) 🔻2331 🔴 (-12%)30
Fan-out TTFSPromise.all(100 steps)593 (+10%)938 (-42%) 💚2355 (+42%) 🔻2542 (+40%) 🔻10
Fan-out TTLSPromise.all(100 steps)4866 (-2.6%)5507 (-11%)6831 (-2.6%)12617 (+65%) 🔻10
STSO1020 steps (inline)105 (-29%) 💚174 (-35%) 💚190 (-44%) 💚304 (-57%) 💚1019
WO1020 steps173964 (-34%) 💚173964 (-34%) 💚173964 (-34%) 💚173964 (-34%) 💚1
CRTTfirst chunk (pooled)124 (+20%) 🔻202 (+28%) 🔻300 (-8.0%)587 (+41%) 🔻28

Streams

ScenarioCRTT 1stp75p90p99CDV maxiters
paced control (100/s, 60B)163 (+14%)172 (-20%)257 (-33%)446 (-40%)119 (-61%)10
size sweep (100/s, 160B-12KB)168 (+21%)205 (-3%)256 (-26%)363 (-36%)139 (-16%)10
replay gateway-gpt-5.4-nano-2000t (1x)300 (+110%)160 (-45%)199 (-58%)532 (-23%)200 (-53%)3
replay eve-gpt-5.6-sol-2000t (1x)174 (+17%)163 (-54%)204 (-59%)582 (-34%)411 (-28%)2
replay eve-gpt-5.6-sol-2000t (2x)228 (+63%)251 (-50%)544 (-17%)1091 (+6%)542 (+19%)3
ℹ️ Metric definitions & methodology

Streams: first-chunk RTT (the stream-open path, before any buffering/backpressure), CRTT percentiles, and worst delivery stall (CDV max). Cells are medians across iterations; per-run values in the artifacts. No 🔴/🟢 marks until targets attach.

The collapsed STSO distribution section above buckets every step gap, split inline (same warm process — pure framework overhead) vs queue-hop (fresh process — dispatch, reinit, replay). = main, = this run, = fill.

The collapsed CRTT drill-down: per-variant RTT histograms (fixed log bins, · = empty) and mean RTT/positive-CDV profile lines over stream progress and chunk size. Histograms, avgs, and profiles merge exactly across runs; p50–p99 are percentile-of-percentiles. Per-index rows live in the artifacts.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body) · Fan-out TTFS: fan-out time to first step (in-deployment start() → first of the parallel step bodies to complete) · Fan-out TTLS: fan-out time to last step (in-deployment start() → last of the parallel step bodies to complete, i.e. when the Promise.all resolves) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · CRTT: chunk round-trip time (per-chunk write → read latency, one clock domain: deployment → stream backend → same deployment) · CDV: chunk delay variation / delivery jitter (inter-arrival gap minus inter-write gap per seq-adjacent pair; skew-free; the row is each run's MAX positive value, so one stall moves it)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · Promise.all(100 steps): 100 trivial no-op steps started together in a single Promise.all; Fan-out TTFS is the first of them to complete and Fan-out TTLS the last, both from the in-deployment clientStart, so their gap is the spread the runtime adds across the fan-out · paced control (100/s, 60B): the control: 300 tiny (~60B) deltas metronome-paced at 100/s — zero workload structure, so it reads the transport floor and flush cadence, and disambiguates transport-wide vs workload-specific when a replay row moves · size sweep (100/s, 160B-12KB): same pacing as the control with deltas padded in rotation across seven log-spaced sizes (~160B–12KB) — rotation decouples size from stream position, so it isolates whether chunk size causes latency · replay gateway-gpt-5.4-nano-2000t (1x): raw provider SSE cadence captured at the AI gateway boundary (gpt-5.4-nano, the most popular gateway model; per-token deltas p50 208B = the modal production chunk size), replayed exactly as measured — the typical customer's workload; its CDV is the typical customer's real delivery jitter · replay eve-gpt-5.6-sol-2000t (1x): a captured eve turn (gpt-5.6-sol, the most-used demanding eve model; ~2000 output tokens = production p50 turn length) replayed exactly as measured — eve's envelope protocol re-ships the cumulative message so sizes ramp 142B→13KB; the demanding outlier tenant's reality · replay eve-gpt-5.6-sol-2000t (2x): the same eve capture at 2x — the headroom/stress row; real fast-tier models emit the same chunk sizes at proportionally higher rate, so time compression is a faithful speed model · first chunk (pooled): every run's seq-0 RTT pooled across all stream scenarios — the first chunk precedes any workload differentiation, so pooling samples one shared stream-open path with exact percentiles

Replay cadences (semantic sha256) — eve-gpt-5.6-sol-2000teaf22f5946e7c61f3c65c7006d550df180cfabd4e706254a09f22aec0cfb420d · gateway-gpt-5.4-nano-2000t6f24ac518b6b83ff1d0e85a5fe78230db192716d66a7fc6b2fe022752001d041

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600

All timestamps are deployment-side; runs are triggered in-deployment, so the CI runner and api.vercel.com sit outside every measured window. TTFS = start() → first step body (includes dispatch + any cold start); Fan-out TTFS/TTLS = first/last step completion of one Promise.all from the same anchor (the gap is the runtime’s fan-out spread); STSO/WO between step bodies; CRTT inside the workflow (excludes the api.vercel.com read path).

Cold starts stay in the numbers (real bursty-workload latency, inflates P75+); Best is the warm floor.

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

▲ Vercel Production (8 failed)

python-node (8 failed):

  • promiseAllWorkflow | wrun_41M0XB8RB20GPSEEE0BP1F0AX5 | 🔍 observability
  • sleepingWorkflow | wrun_41M0XB9DN80GT0JYGEYSRDG0AZ | 🔍 observability
  • parallelSleepWorkflow | wrun_41M0XB9DSC0GGQFNQPSTD10TFN | 🔍 observability
  • nullByteWorkflow | wrun_41M0XB9MEH0GZ1PE9VA8VT338J | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41M0XBE3DY0GXJZZKSDADPJ273 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41M0XBE4D70GMTA2YASGGSMBW6 | 🔍 observability
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M0XBEDKS0GX7MW0EJVZNDKG1 | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M0XBEYPA0GT79Q7M4KQGXE2R | 🔍 observability

🌐 Cross-language Conformance (9 failed)

python (9 failed):

  • deploymentId: 'latest' is a no-op in non-Vercel worlds | wrun_01M0XBHEZA0V7PQ3KYK30VCQV6
  • promiseAllWorkflow | wrun_41M0XB8RB20GPSEEE0BP1F0AX5
  • sleepingWorkflow | wrun_41M0XB9DN80GT0JYGEYSRDG0AZ
  • parallelSleepWorkflow | wrun_41M0XB9DSC0GGQFNQPSTD10TFN
  • nullByteWorkflow | wrun_41M0XB9MEH0GZ1PE9VA8VT338J
  • cancelRun - cancelling a running workflow | wrun_41M0XBE3DY0GXJZZKSDADPJ273
  • cancelRun via CLI - cancelling a running workflow | wrun_41M0XBE4D70GMTA2YASGGSMBW6
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M0XBEDKS0GX7MW0EJVZNDKG1
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M0XBEYPA0GT79Q7M4KQGXE2R

⚠️ Flaky E2E Tests (passed on retry)

These tests failed at least once and passed on a retry. A recurring entry here is a real race worth investigating.

  • cancelRun via CLI - cancelling a running workflow (nextjs-turbopack)
  • cancelRun via CLI - cancelling a running workflow (sveltekit)
  • deploymentId: 'latest' is a no-op in non-Vercel worlds (tanstack-start)
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (nextjs-webpack)

🛠 Infra Events (absorbed by the harness)

Platform anomalies the e2e harness detected and worked around (e.g. a run the queue never picked up, replaced by a fresh run). Clustered timestamps indicate a backend blip; a steady drip indicates a platform issue worth escalating.

33 infra events
  • cold-start-warmup · suite warmup (python) · at 20:55:38Z · abandoned wrun_41M0XB6E7T0GJ9W8791BM6Z01B · (+7 more)
  • run-pickup-stall · sleepingWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TD0GMM929KHMH4A5PQ
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TD0GMM929KHMH4A5PR
  • run-pickup-stall · nullByteWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TG0GXS3FJPHWZ0AP3Z
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA34B0GKTASFX3VHN7ZF5
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:55:55Z · abandoned wrun_41M0XBA2T60GKVRBKEPG9JFNGQ
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 20:56:27Z · abandoned wrun_41M0XBB2NE0GYG9ZKWYWVYN0QG
  • run-pickup-stall · sleepingWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY390GR8JJH6G2563BMK
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY170GNNFJ2HYJF76VRJ
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY1P0GSFSBE1ENV48TWY
  • run-pickup-stall · nullByteWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY2C0GXP8214VHPKYDEN
  • cold-start-warmup · suite warmup (tanstack-start) · at 20:57:08Z · abandoned wrun_01M0XBC0795S33S7QA0QCC6MWM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 20:57:45Z · abandoned wrun_41M0XBC1V20GQEJYYHQRSWTMC5
  • cold-start-warmup · suite warmup (python) · at 20:58:25Z · abandoned wrun_01M0XBBGHK2AQJ0WK3BTSQXSBE · (+7 more)
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PANZ6ZFDYD87WHMYEG
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PANZ6ZFDYD87WHMYEH
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PJ61BWHDADBWKVQB4M
  • run-pickup-stall · nullByteWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PMHJQTM6D1Q5A1DGP8
  • run-pickup-stall · sleepingWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PFRRDDXM9ZQTKATH0V
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 20:59:26Z · abandoned wrun_41M0XBENXS0GNP64ENYV9QCDR6
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 20:59:26Z · abandoned wrun_41M0XBESQJ0GNJ9RHK6DQT9QJG
  • run-pickup-stall · sleepingWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A76KFYN7SAWZTZNJVC
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A4Q23JB7QFRPHW2M57
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A6RKK450KHQ6KKBNDM
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A861P4CVWVBHC9GRPV
  • run-pickup-stall · nullByteWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0ACMT5YMDRR1CRNSE1B
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 21:00:40Z · abandoned wrun_01M0XBJTXTEA82N9RWT7V8MWFM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 21:00:40Z · abandoned wrun_01M0XBJTXXTC9C59S58QCXKQDY
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 21:00:40Z · abandoned wrun_01M0XBJTY41EX832TVQ4QR069P
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 21:01:10Z · abandoned wrun_01M0XBKRA4M8A4FPV04NJY23SD
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 21:01:10Z · abandoned wrun_01M0XBKRA5GJYZXB04F49JH2DV
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 21:01:40Z · abandoned wrun_01M0XBMNH7M7QPBBRWZ6AX8HGC
  • run-pickup-stall · hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep (nextjs-webpack) · at 21:02:31Z · abandoned wrun_01M0XBP7PT25DV3EYT1BDRVKMK

E2E Test Summary

Summary
PassedFailedSkippedTotal
❌ ▲ Vercel Production357087424320
✅ 💻 Local Development392205584480
✅ 📦 Local Production392205584480
✅ 🐘 Local Postgres392205584480
✅ 🪟 Windows32000320
❌ 🌐 Cross-language Conformance09132141
✅ vercel-http-transport8170143960
✅ vercel-multi-region270027
✅ vercel-ws-transport553087640
Total1705317277819848
Details by Category

❌ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node132028
✅ astro-quickjs132028
✅ example-node132028
✅ example-quickjs132028
✅ express-node132028
✅ express-quickjs132028
✅ fastify-node132028
✅ fastify-quickjs132028
✅ hono-node132028
✅ hono-quickjs132028
✅ nest-node132028
✅ nest-quickjs132028
✅ nextjs-turbopack-node15703
✅ nextjs-turbopack-quickjs15703
✅ nextjs-webpack-node15703
✅ nextjs-webpack-quickjs15703
✅ nitro-node132028
✅ nitro-quickjs132028
✅ nuxt-node132028
✅ nuxt-quickjs132028
❌ python-node08152
✅ sveltekit-node15109
✅ sveltekit-quickjs15109
✅ tanstack-start-node132028
✅ tanstack-start-quickjs132028
✅ vite-node132028
✅ vite-quickjs132028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node16000
✅ nextjs-turbopack-quickjs16000

❌ 🌐 Cross-language Conformance

AppPassedFailedSkipped
❌ python09132

✅ vercel-http-transport

AppPassedFailedSkipped
✅ example132028
✅ express132028
✅ hono132028
✅ nextjs-turbopack15703
✅ nitro132028
✅ vite132028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

✅ vercel-ws-transport

AppPassedFailedSkipped
✅ example132028
✅ express132028
✅ nextjs-turbopack15703
✅ vite132028

📋 View full workflow run

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Sim World

Simulated world deterministic testing for races. Traces

🟠 world-sim scenario book — 1 fail of 41 total

fence=per-spec

scenariooutcomeeventsvirtreplayviolations
smoke-no-stepscompleted30msok0
smoke-one-stepcompleted60msok0
hook-at-step-startedcompleted120msok0
hook-at-step-completedcompleted120msok0
hook-at-hook-createdcompleted120msok0
deadline-hook-winscompleted71.0hok0
deadline-expirescompleted71.0hok0
long-sleepcompleted1130.0dok0
hook-never-arrivesstalled30msskipped0
step-retries-twicecompleted102.0sok0
parallel-stepscompleted90msok0
hook-on-execution-statecompleted120msok0
peek-hook-before-branchcompleted120msok0
peek-hook-after-branchcompleted120msok0
peek-hook-at-registrationcompleted120msok0
race-hook-before-probecompleted120msok0
race-hook-after-probecompleted120msok0
race-duplicate-deliverycompleted130msok0
attr-hook-before-stepcompleted110msok0
attr-hook-after-stepcompleted110msok0
attr-from-step-bodycompleted130msok0
fork-hook-after-timeoutcompleted141.0mok0
fork-hook-before-timeoutcompleted141.0mok0
count-hook-after-timeoutcompleted171.0mok0
count-hook-before-timeoutcompleted201.0mok0
stale-read-step-count-forkcompleted201.0mok0
stale-read-equal-step-countscompleted141.0mok0
step-vs-step-forkcompleted120msok0
step-vs-step-fork-fencedcompleted120msok0
fence-catches-benign-directioncompleted125msok0
in-flight-before-decisioncompleted171.0mok0
in-flight-before-decision-countedcompleted171.0mok0
in-flight-after-decisioncompleted192.0mok0
stale-read-step-count-fork-fencedcompleted201.0mok0
fork-hook-winscompleted131.0mok0
fork-timeout-winscompleted131.0mok0
unclaimed-payload-under-forkcompleted171.0mok0
claimed-payload-under-forkcompleted171.0mok0
writers-independent-step-bodiescompleted120msok0
writers-scripted-tempocompleted120msok0
cancel-mid-stepcancelled70msskipped0

Full trace: world-sim.txt

… file
Applies the unchanged-write guard from #3454 to the Astro and SvelteKit
builders, whose generated files live in Vite-watched source directories.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <peter.wielander@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor
FrameworkFlow routeStep reg.Framework output
hono200.9 KiB (±0)40.5 KiB (±0)1.76 MiB (±0)
nextjs-turbopack206.3 KiB (±0)439 B (±0)762.8 KiB (±0)
About these numbers

Sizes are gzip; parentheses show the change against main.
Flow route and Step reg. gate this job, on raw bytes rather than the gzip shown, at max(2%, 50.0 KiB). Framework output is informational.

555b143 · run

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this leaves the framework-specific behavior without automated regression coverage. A small integration test that builds Astro/SvelteKit twice and asserts watched generated outputs retain their mtime/inode would help prevent no-op writes from returning. Not blocking this change.

@VaguelySerious
VaguelySerious merged commit 0750fe8 into mainAug 25, 2026
182 of 184 checks passed
@VaguelySerious
VaguelySerious deleted the peter/guard-generated-writes-frameworks branch August 25, 2026 22:47
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 0750fe8 (AI decision).

The change depends on writeFileIfChanged from @workflow/builders, which was introduced by #3454 on main only — git grep writeFileIfChanged origin/stable finds nothing, so this cannot apply to stable without also backporting that main-only helper. It is also a dev-server efficiency improvement (avoiding redundant Vite invalidations/recompiles on no-op rebuilds) rather than a correctness, data-loss, or crash fix, so it does not meet the maintenance-branch bar on its own merits either.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

0750fe8958f50ee354cd24fe3f02a37c801bc4dc

@github-actionsgithub-actionsBot mentioned this pull request Aug 26, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(astro,sveltekit): skip generated writes that would not change the file - #3688

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/guard-generated-writes-frameworks
Aug 25, 2026
Merged

fix(astro,sveltekit): skip generated writes that would not change the file#3688
VaguelySerious merged 1 commit into
mainfrom
peter/guard-generated-writes-frameworks

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Aug 20, 2026

Copy link
Copy Markdown
Member

Description

Follow-up to #3454, which added the unchanged-write guard and applied it to @workflow/builders, @workflow/next and @workflow/nitro. The Astro and SvelteKit builders were not covered: they receive the bundle text from the base builder and do their own writeFile, so they never reach the guard. Their generated files live in Vite-watched source directories (src/pages/.well-known/workflow/v1 and src/routes/.well-known/workflow/v1), which is exactly the condition #3454 was about.

This routes those writes through writeFileIfChanged, and writes SvelteKit's static manifest copy from the same string rather than copyFile-ing it, matching what #3454 did for the Next public manifest.

Measured effect

Two consecutive pnpm build runs with no source change, comparing sha256 + mtime(ns) + inode of every generated file under the workflow directories, with WORKFLOW_PUBLIC_MANIFEST=1:

beforeafter
SvelteKit (src/routes/…, static/…)6 of 9 rewritten3
Astro (src/pages/…)5 of 8 rewritten3

Every one of those rewrites had an identical sha, i.e. they were pure no-op writes. Fixed here: both .gitignore files and the static/manifest.json copy on SvelteKit, and .gitignore plus manifest.json.js on Astro.

Relation to #3509

Rebased over #3509 (better SvelteKit routes-directory loading), which rewrote the same import block in packages/sveltekit/src/builder.ts. That was the only conflict and it was mechanical: keep its node:path import and drop the node:module/node:url imports it made dead, keep this PR's narrowed node:fs/promises import.

The two changes act at different layers and both still apply. #3509 caches the resolved routes directory so SvelteKit's secondary builds and worker threads do not reload the Svelte config and redo the build; this PR reduces what a build that does run actually writes. The measured numbers above were re-taken on top of #3509, with the guard-off control built from origin/main's builder, so the before/after pair is on the same base.

What this does NOT fix

Three files per framework still churn, for two reasons that need more than a call-site swap. Both are pre-existing and neither is made worse by this PR.

1. The read-transform-rewrite pattern defeats the guard — affects flow/+server.js and webhook/[token]/+server.js (SvelteKit), flow.js and webhook/[token].js (Astro).

Both builders call createCombinedBundle / createWebhookBundle, which write the route to disk through the base builder, then readFile it back, apply a regex transform, and write the transformed content. So each build writes the file twice with two different contents, and on the next build neither write can be skipped: the base write sees the previous build's transformed content on disk and differs from it, and the framework write then sees the untransformed content and differs from that. The signature is visible in the inode: the route gets a new inode (base write, temp+rename) and the webhook keeps its inode (framework write, plain overwrite).

The fix is to make the final content the only content written, e.g. by letting the framework pass its transform into the base builder so the route is generated in its final form. That is an API change to the base builder and wants its own PR.

2. One-shot esbuild.build() always rewrites its outfile — affects flow/__step_registrations.js on both frameworks.

ctx.rebuild() on an esbuild context skips writing when the output is byte-identical (verified on esbuild 0.28.1: mtime and inode both preserved). A one-shot esbuild.build() does not; it rewrites unconditionally. This is why Next dev, which uses contexts, was already protected for its esbuild outputs while these builders are not. The fix is either to use contexts in watch mode or to pass write: false and hand the output to writeFileIfChanged.

Other follow-ups from #3454

3. Nitro's dev handler should stop cache-busting off a single file's mtime.packages/nitro/src/index.ts derives version from statSync(handlerPath).mtimeMs for workflows.mjs and then uses it as the cache-buster for the steps.mjs import. A step-body-only edit leaves workflows.mjs identical, so the version freezes and stale step code is served. That dependency is the only reason skipsUnchangedGeneratedWrites exists.

Widening the key is cheap and keeps the per-request path to stats rather than file reads, since handlerImportPath and stepsImportPath are already in scope:

consts=statSync(handlerPath),t=statSync(stepsPath);constversion=`${s.size}:${s.mtimeMs}:${t.size}:${t.mtimeMs}`;

Including size costs nothing and narrows the case where two coalesced dev writes land on an effectively identical mtime. Note only the workflow/workflows.mjs variant needs the second stat; preloadSteps['workflow/webhook.mjs'] is empty. Both the v2 and v3 branches carry a copy. Once that lands, skipsUnchangedGeneratedWrites can be deleted from BaseBuilder and from the Nitro override.

4. There is no regression guard for the cross-framework case.write-if-changed.test.ts covers the helper and the opt-out, but nothing stops a new raw writeFile into a watched generated directory from silently restoring the redundant rebuild. The check that would catch it is the one used above: build twice, assert every generated file keeps its sha, mtime and inode. @workflow/astro has no test files today and @workflow/sveltekit has one unrelated test, so this needs a builder harness per framework rather than a one-line test.

How did you test your changes?

No automated coverage is added, for the reason in (4), so here are reproduction steps.

pnpm install && pnpm run build
cd workbench/sveltekit # and workbench/astroexport WORKFLOW_PUBLIC_MANIFEST=1
pnpm build
find src/routes/.well-known/workflow static/.well-known/workflow -type f \
| sort | xargs -I{} sh -c 'printf "%s %s %s\n" "$(shasum -a 256 {} | cut -d" " -f1)" "$(stat -f "%Fm:%i" {})" {}'> /tmp/s1
pnpm build
# ...repeat into /tmp/s2
diff /tmp/s1 /tmp/s2

Before this change the two .gitignore files and static/manifest.json appear in the diff with an identical sha and a moved mtime. After, they do not. The three files listed above still appear, for the reasons given.

Also run: pnpm run build (28/28 tasks), npx biome check on both changed files.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run to create a changelog for this PR
  • 🔒 DCO sign-off passes (run git commit --signoff on your commits)
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete

@vercel

vercelBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreview, v0Aug 25, 2026 8:55pm
example-nextjs-workflow-webpackReadyReadyPreview, v0Aug 25, 2026 8:55pm
example-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-astro-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-express-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-fastify-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-hono-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nestjs-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nitro-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nuxt-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-python-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-sveltekit-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-tanstack-start-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-vite-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-docsReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-swc-playgroundReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-tarballsReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-webReadyReadyPreview, v0Aug 25, 2026 8:55pm

@changeset-bot

changeset-botBot commented Aug 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 555b143

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/astroPatch
@workflow/sveltekitPatch
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 555b143 · Tue, 25 Aug 2026 21:17:13 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1215 (+465%) 🔻1315 🔴 (+33%) 🔻1332 🔴 (+29%) 🔻1375 🔴 (-4.8%)30
TTFSstream217 (+20%) 🔻1291 🔴 (+36%) 🔻1314 🔴 (+36%) 🔻1365 🔴 (+35%) 🔻30
TTFShook + stream1447 (+180%) 🔻1597 🔴 (+18%) 🔻1666 🔴 (+11%)1940 🔴 (-62%) 💚30
Fan-out TTFSPromise.all(100 steps)806 (+44%) 🔻1955 (+96%) 🔻1978 (+40%) 🔻2016 (+24%) 🔻10
Fan-out TTLSPromise.all(100 steps)2542 (+31%) 🔻5184 (+87%) 🔻6673 (+99%) 🔻9405 (+43%) 🔻10
STSO1020 steps (inline)99 (-7.5%)148 (+14%)165 (+8.6%)233 (+14%)1019
WO1020 steps146183 (+11%)146183 (+11%)146183 (+11%)146183 (+11%)1
CRTTfirst chunk (pooled)104 (+24%) 🔻145 (+9.0%)167 (+7.7%)271 (+13%)28

Streams

ScenarioCRTT 1stp75p90p99CDV maxiters
paced control (100/s, 60B)138 (+25%)156 (-3%)230 (-44%)419 (-34%)161 (+24%)10
size sweep (100/s, 160B-12KB)128 (+15%)164 (+20%)228 (-36%)629 (-20%)160 (+53%)10
replay gateway-gpt-5.4-nano-2000t (1x)113 (-23%)139 (+1%)187 (-28%)745 (-16%)368 (+11%)3
replay eve-gpt-5.6-sol-2000t (1x)195 (+7%)150 (+22%)192 (+19%)436 (+31%)434 (+36%)2
replay eve-gpt-5.6-sol-2000t (2x)145 (+14%)182 (+22%)241 (+25%)491 (+30%)286 (+10%)3
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 132084ms → this run 144847ms (Δ +12763ms, +10%)

 50-100 ms ┃ main 0 this 1 +1
100-150 ms ████████████████████┃███ main 905 this 781 -124
150-200 ms ███░░┃ main 103 this 217 +114
200-250 ms ┃ main 5 this 13 +8
250-300 ms ┃ main 2 this 2 +0
300-350 ms ┃ main 1 this 2 +1
350-400 ms ┃ main 1 this 2 +1
400-450 ms ┃ main 2 this 1 -1
📈 CRTT drill-down vs main (RTT distributions & profiles)
variant RTT 1ms→5s+ avg p50 p90 p99 n
control ······▃█▁···· 129.5 (+3%) 120 (+21%) 230 (-44%) 419 (-34%) 3000
sweep ······▂█▁▁··· 138.6 (+18%) 126 (+29%) 228 (-36%) 629 (-20%) 3000
gw 1x ·····▁▆█▁▁··· 119.6 (±0%) 105 (+9%) 187 (-28%) 745 (-16%) 5295
eve 1x ·····▁▅█▁▁··· 124.7 (+17%) 109 (+14%) 192 (+19%) 436 (+31%) 5186
eve 2x ·····▁▂█▂▁··· 150.3 (+20%) 134 (+16%) 241 (+25%) 491 (+30%) 7779

RTT over stream progress (avg per tenth of stream, bars scaled min→max):

control ▆█▄▃▂▁▄▂▄▃ 111–157ms
sweep ▃▃▃▂▂▁▄█▃▂ 121–184ms
gw 1x ▂▂▃▃▁█▂▂▁▂ 103–181ms
eve 1x ▄▁▃▆▃█▇▆▄▂ 106–146ms
eve 2x ▁▂▁▁▅▄▄█▆▃ 125–196ms

RTT by chunk size (avg per log size bin, ~160B → ~12KB serialized, bars scaled min→max):

sweep █▇▂▁▃▄▅ 136–141ms

Delivery jitter over stream progress (avg positive CDV per tenth of stream, bars scaled min→max):

control ▂▇▄▄▇▁█▄▅▄ 34–43ms
sweep ▁▄▁▄▆▄▄█▃▃ 47–68ms
gw 1x ▃▂▇▂▃█▃▂▁▄ 31–40ms
eve 1x ▄▁▂▆▆▆█▅▄▆ 20–26ms
eve 2x ▇▇▁▄▇█▅▁▅█ 19–27ms
📜 Previous results (1)

78301f5

Thu, 20 Aug 2026 01:50:16 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep458 (-58%) 💚1476 🔴 (+19%) 🔻1524 🔴 (+15%) 🔻1611 🔴 (-1.1%)30
TTFSstream1295 (+275%) 🔻1477 🔴 (+28%) 🔻1538 🔴 (+29%) 🔻1675 🔴 (+36%) 🔻30
TTFShook + stream1635 (+50%) 🔻2181 🔴 (+41%) 🔻2203 🔴 (+40%) 🔻2331 🔴 (-12%)30
Fan-out TTFSPromise.all(100 steps)593 (+10%)938 (-42%) 💚2355 (+42%) 🔻2542 (+40%) 🔻10
Fan-out TTLSPromise.all(100 steps)4866 (-2.6%)5507 (-11%)6831 (-2.6%)12617 (+65%) 🔻10
STSO1020 steps (inline)105 (-29%) 💚174 (-35%) 💚190 (-44%) 💚304 (-57%) 💚1019
WO1020 steps173964 (-34%) 💚173964 (-34%) 💚173964 (-34%) 💚173964 (-34%) 💚1
CRTTfirst chunk (pooled)124 (+20%) 🔻202 (+28%) 🔻300 (-8.0%)587 (+41%) 🔻28

Streams

ScenarioCRTT 1stp75p90p99CDV maxiters
paced control (100/s, 60B)163 (+14%)172 (-20%)257 (-33%)446 (-40%)119 (-61%)10
size sweep (100/s, 160B-12KB)168 (+21%)205 (-3%)256 (-26%)363 (-36%)139 (-16%)10
replay gateway-gpt-5.4-nano-2000t (1x)300 (+110%)160 (-45%)199 (-58%)532 (-23%)200 (-53%)3
replay eve-gpt-5.6-sol-2000t (1x)174 (+17%)163 (-54%)204 (-59%)582 (-34%)411 (-28%)2
replay eve-gpt-5.6-sol-2000t (2x)228 (+63%)251 (-50%)544 (-17%)1091 (+6%)542 (+19%)3
ℹ️ Metric definitions & methodology

Streams: first-chunk RTT (the stream-open path, before any buffering/backpressure), CRTT percentiles, and worst delivery stall (CDV max). Cells are medians across iterations; per-run values in the artifacts. No 🔴/🟢 marks until targets attach.

The collapsed STSO distribution section above buckets every step gap, split inline (same warm process — pure framework overhead) vs queue-hop (fresh process — dispatch, reinit, replay). = main, = this run, = fill.

The collapsed CRTT drill-down: per-variant RTT histograms (fixed log bins, · = empty) and mean RTT/positive-CDV profile lines over stream progress and chunk size. Histograms, avgs, and profiles merge exactly across runs; p50–p99 are percentile-of-percentiles. Per-index rows live in the artifacts.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body) · Fan-out TTFS: fan-out time to first step (in-deployment start() → first of the parallel step bodies to complete) · Fan-out TTLS: fan-out time to last step (in-deployment start() → last of the parallel step bodies to complete, i.e. when the Promise.all resolves) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · CRTT: chunk round-trip time (per-chunk write → read latency, one clock domain: deployment → stream backend → same deployment) · CDV: chunk delay variation / delivery jitter (inter-arrival gap minus inter-write gap per seq-adjacent pair; skew-free; the row is each run's MAX positive value, so one stall moves it)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · Promise.all(100 steps): 100 trivial no-op steps started together in a single Promise.all; Fan-out TTFS is the first of them to complete and Fan-out TTLS the last, both from the in-deployment clientStart, so their gap is the spread the runtime adds across the fan-out · paced control (100/s, 60B): the control: 300 tiny (~60B) deltas metronome-paced at 100/s — zero workload structure, so it reads the transport floor and flush cadence, and disambiguates transport-wide vs workload-specific when a replay row moves · size sweep (100/s, 160B-12KB): same pacing as the control with deltas padded in rotation across seven log-spaced sizes (~160B–12KB) — rotation decouples size from stream position, so it isolates whether chunk size causes latency · replay gateway-gpt-5.4-nano-2000t (1x): raw provider SSE cadence captured at the AI gateway boundary (gpt-5.4-nano, the most popular gateway model; per-token deltas p50 208B = the modal production chunk size), replayed exactly as measured — the typical customer's workload; its CDV is the typical customer's real delivery jitter · replay eve-gpt-5.6-sol-2000t (1x): a captured eve turn (gpt-5.6-sol, the most-used demanding eve model; ~2000 output tokens = production p50 turn length) replayed exactly as measured — eve's envelope protocol re-ships the cumulative message so sizes ramp 142B→13KB; the demanding outlier tenant's reality · replay eve-gpt-5.6-sol-2000t (2x): the same eve capture at 2x — the headroom/stress row; real fast-tier models emit the same chunk sizes at proportionally higher rate, so time compression is a faithful speed model · first chunk (pooled): every run's seq-0 RTT pooled across all stream scenarios — the first chunk precedes any workload differentiation, so pooling samples one shared stream-open path with exact percentiles

Replay cadences (semantic sha256) — eve-gpt-5.6-sol-2000teaf22f5946e7c61f3c65c7006d550df180cfabd4e706254a09f22aec0cfb420d · gateway-gpt-5.4-nano-2000t6f24ac518b6b83ff1d0e85a5fe78230db192716d66a7fc6b2fe022752001d041

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600

All timestamps are deployment-side; runs are triggered in-deployment, so the CI runner and api.vercel.com sit outside every measured window. TTFS = start() → first step body (includes dispatch + any cold start); Fan-out TTFS/TTLS = first/last step completion of one Promise.all from the same anchor (the gap is the runtime’s fan-out spread); STSO/WO between step bodies; CRTT inside the workflow (excludes the api.vercel.com read path).

Cold starts stay in the numbers (real bursty-workload latency, inflates P75+); Best is the warm floor.

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

▲ Vercel Production (8 failed)

python-node (8 failed):

  • promiseAllWorkflow | wrun_41M0XB8RB20GPSEEE0BP1F0AX5 | 🔍 observability
  • sleepingWorkflow | wrun_41M0XB9DN80GT0JYGEYSRDG0AZ | 🔍 observability
  • parallelSleepWorkflow | wrun_41M0XB9DSC0GGQFNQPSTD10TFN | 🔍 observability
  • nullByteWorkflow | wrun_41M0XB9MEH0GZ1PE9VA8VT338J | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41M0XBE3DY0GXJZZKSDADPJ273 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41M0XBE4D70GMTA2YASGGSMBW6 | 🔍 observability
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M0XBEDKS0GX7MW0EJVZNDKG1 | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M0XBEYPA0GT79Q7M4KQGXE2R | 🔍 observability

🌐 Cross-language Conformance (9 failed)

python (9 failed):

  • deploymentId: 'latest' is a no-op in non-Vercel worlds | wrun_01M0XBHEZA0V7PQ3KYK30VCQV6
  • promiseAllWorkflow | wrun_41M0XB8RB20GPSEEE0BP1F0AX5
  • sleepingWorkflow | wrun_41M0XB9DN80GT0JYGEYSRDG0AZ
  • parallelSleepWorkflow | wrun_41M0XB9DSC0GGQFNQPSTD10TFN
  • nullByteWorkflow | wrun_41M0XB9MEH0GZ1PE9VA8VT338J
  • cancelRun - cancelling a running workflow | wrun_41M0XBE3DY0GXJZZKSDADPJ273
  • cancelRun via CLI - cancelling a running workflow | wrun_41M0XBE4D70GMTA2YASGGSMBW6
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M0XBEDKS0GX7MW0EJVZNDKG1
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M0XBEYPA0GT79Q7M4KQGXE2R

⚠️ Flaky E2E Tests (passed on retry)

These tests failed at least once and passed on a retry. A recurring entry here is a real race worth investigating.

  • cancelRun via CLI - cancelling a running workflow (nextjs-turbopack)
  • cancelRun via CLI - cancelling a running workflow (sveltekit)
  • deploymentId: 'latest' is a no-op in non-Vercel worlds (tanstack-start)
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (nextjs-webpack)

🛠 Infra Events (absorbed by the harness)

Platform anomalies the e2e harness detected and worked around (e.g. a run the queue never picked up, replaced by a fresh run). Clustered timestamps indicate a backend blip; a steady drip indicates a platform issue worth escalating.

33 infra events
  • cold-start-warmup · suite warmup (python) · at 20:55:38Z · abandoned wrun_41M0XB6E7T0GJ9W8791BM6Z01B · (+7 more)
  • run-pickup-stall · sleepingWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TD0GMM929KHMH4A5PQ
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TD0GMM929KHMH4A5PR
  • run-pickup-stall · nullByteWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TG0GXS3FJPHWZ0AP3Z
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA34B0GKTASFX3VHN7ZF5
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:55:55Z · abandoned wrun_41M0XBA2T60GKVRBKEPG9JFNGQ
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 20:56:27Z · abandoned wrun_41M0XBB2NE0GYG9ZKWYWVYN0QG
  • run-pickup-stall · sleepingWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY390GR8JJH6G2563BMK
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY170GNNFJ2HYJF76VRJ
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY1P0GSFSBE1ENV48TWY
  • run-pickup-stall · nullByteWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY2C0GXP8214VHPKYDEN
  • cold-start-warmup · suite warmup (tanstack-start) · at 20:57:08Z · abandoned wrun_01M0XBC0795S33S7QA0QCC6MWM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 20:57:45Z · abandoned wrun_41M0XBC1V20GQEJYYHQRSWTMC5
  • cold-start-warmup · suite warmup (python) · at 20:58:25Z · abandoned wrun_01M0XBBGHK2AQJ0WK3BTSQXSBE · (+7 more)
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PANZ6ZFDYD87WHMYEG
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PANZ6ZFDYD87WHMYEH
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PJ61BWHDADBWKVQB4M
  • run-pickup-stall · nullByteWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PMHJQTM6D1Q5A1DGP8
  • run-pickup-stall · sleepingWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PFRRDDXM9ZQTKATH0V
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 20:59:26Z · abandoned wrun_41M0XBENXS0GNP64ENYV9QCDR6
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 20:59:26Z · abandoned wrun_41M0XBESQJ0GNJ9RHK6DQT9QJG
  • run-pickup-stall · sleepingWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A76KFYN7SAWZTZNJVC
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A4Q23JB7QFRPHW2M57
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A6RKK450KHQ6KKBNDM
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A861P4CVWVBHC9GRPV
  • run-pickup-stall · nullByteWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0ACMT5YMDRR1CRNSE1B
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 21:00:40Z · abandoned wrun_01M0XBJTXTEA82N9RWT7V8MWFM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 21:00:40Z · abandoned wrun_01M0XBJTXXTC9C59S58QCXKQDY
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 21:00:40Z · abandoned wrun_01M0XBJTY41EX832TVQ4QR069P
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 21:01:10Z · abandoned wrun_01M0XBKRA4M8A4FPV04NJY23SD
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 21:01:10Z · abandoned wrun_01M0XBKRA5GJYZXB04F49JH2DV
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 21:01:40Z · abandoned wrun_01M0XBMNH7M7QPBBRWZ6AX8HGC
  • run-pickup-stall · hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep (nextjs-webpack) · at 21:02:31Z · abandoned wrun_01M0XBP7PT25DV3EYT1BDRVKMK

E2E Test Summary

Summary
PassedFailedSkippedTotal
❌ ▲ Vercel Production357087424320
✅ 💻 Local Development392205584480
✅ 📦 Local Production392205584480
✅ 🐘 Local Postgres392205584480
✅ 🪟 Windows32000320
❌ 🌐 Cross-language Conformance09132141
✅ vercel-http-transport8170143960
✅ vercel-multi-region270027
✅ vercel-ws-transport553087640
Total1705317277819848
Details by Category

❌ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node132028
✅ astro-quickjs132028
✅ example-node132028
✅ example-quickjs132028
✅ express-node132028
✅ express-quickjs132028
✅ fastify-node132028
✅ fastify-quickjs132028
✅ hono-node132028
✅ hono-quickjs132028
✅ nest-node132028
✅ nest-quickjs132028
✅ nextjs-turbopack-node15703
✅ nextjs-turbopack-quickjs15703
✅ nextjs-webpack-node15703
✅ nextjs-webpack-quickjs15703
✅ nitro-node132028
✅ nitro-quickjs132028
✅ nuxt-node132028
✅ nuxt-quickjs132028
❌ python-node08152
✅ sveltekit-node15109
✅ sveltekit-quickjs15109
✅ tanstack-start-node132028
✅ tanstack-start-quickjs132028
✅ vite-node132028
✅ vite-quickjs132028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node16000
✅ nextjs-turbopack-quickjs16000

❌ 🌐 Cross-language Conformance

AppPassedFailedSkipped
❌ python09132

✅ vercel-http-transport

AppPassedFailedSkipped
✅ example132028
✅ express132028
✅ hono132028
✅ nextjs-turbopack15703
✅ nitro132028
✅ vite132028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

✅ vercel-ws-transport

AppPassedFailedSkipped
✅ example132028
✅ express132028
✅ nextjs-turbopack15703
✅ vite132028

📋 View full workflow run

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Sim World

Simulated world deterministic testing for races. Traces

🟠 world-sim scenario book — 1 fail of 41 total

fence=per-spec

scenariooutcomeeventsvirtreplayviolations
smoke-no-stepscompleted30msok0
smoke-one-stepcompleted60msok0
hook-at-step-startedcompleted120msok0
hook-at-step-completedcompleted120msok0
hook-at-hook-createdcompleted120msok0
deadline-hook-winscompleted71.0hok0
deadline-expirescompleted71.0hok0
long-sleepcompleted1130.0dok0
hook-never-arrivesstalled30msskipped0
step-retries-twicecompleted102.0sok0
parallel-stepscompleted90msok0
hook-on-execution-statecompleted120msok0
peek-hook-before-branchcompleted120msok0
peek-hook-after-branchcompleted120msok0
peek-hook-at-registrationcompleted120msok0
race-hook-before-probecompleted120msok0
race-hook-after-probecompleted120msok0
race-duplicate-deliverycompleted130msok0
attr-hook-before-stepcompleted110msok0
attr-hook-after-stepcompleted110msok0
attr-from-step-bodycompleted130msok0
fork-hook-after-timeoutcompleted141.0mok0
fork-hook-before-timeoutcompleted141.0mok0
count-hook-after-timeoutcompleted171.0mok0
count-hook-before-timeoutcompleted201.0mok0
stale-read-step-count-forkcompleted201.0mok0
stale-read-equal-step-countscompleted141.0mok0
step-vs-step-forkcompleted120msok0
step-vs-step-fork-fencedcompleted120msok0
fence-catches-benign-directioncompleted125msok0
in-flight-before-decisioncompleted171.0mok0
in-flight-before-decision-countedcompleted171.0mok0
in-flight-after-decisioncompleted192.0mok0
stale-read-step-count-fork-fencedcompleted201.0mok0
fork-hook-winscompleted131.0mok0
fork-timeout-winscompleted131.0mok0
unclaimed-payload-under-forkcompleted171.0mok0
claimed-payload-under-forkcompleted171.0mok0
writers-independent-step-bodiescompleted120msok0
writers-scripted-tempocompleted120msok0
cancel-mid-stepcancelled70msskipped0

Full trace: world-sim.txt

… file
Applies the unchanged-write guard from #3454 to the Astro and SvelteKit
builders, whose generated files live in Vite-watched source directories.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <peter.wielander@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor
FrameworkFlow routeStep reg.Framework output
hono200.9 KiB (±0)40.5 KiB (±0)1.76 MiB (±0)
nextjs-turbopack206.3 KiB (±0)439 B (±0)762.8 KiB (±0)
About these numbers

Sizes are gzip; parentheses show the change against main.
Flow route and Step reg. gate this job, on raw bytes rather than the gzip shown, at max(2%, 50.0 KiB). Framework output is informational.

555b143 · run

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this leaves the framework-specific behavior without automated regression coverage. A small integration test that builds Astro/SvelteKit twice and asserts watched generated outputs retain their mtime/inode would help prevent no-op writes from returning. Not blocking this change.

@VaguelySerious
VaguelySerious merged commit 0750fe8 into mainAug 25, 2026
182 of 184 checks passed
@VaguelySerious
VaguelySerious deleted the peter/guard-generated-writes-frameworks branch August 25, 2026 22:47
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 0750fe8 (AI decision).

The change depends on writeFileIfChanged from @workflow/builders, which was introduced by #3454 on main only — git grep writeFileIfChanged origin/stable finds nothing, so this cannot apply to stable without also backporting that main-only helper. It is also a dev-server efficiency improvement (avoiding redundant Vite invalidations/recompiles on no-op rebuilds) rather than a correctness, data-loss, or crash fix, so it does not meet the maintenance-branch bar on its own merits either.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

0750fe8958f50ee354cd24fe3f02a37c801bc4dc

@github-actionsgithub-actionsBot mentioned this pull request Aug 26, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(astro,sveltekit): skip generated writes that would not change the file - #3688

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/guard-generated-writes-frameworks
Aug 25, 2026
Merged

fix(astro,sveltekit): skip generated writes that would not change the file#3688
VaguelySerious merged 1 commit into
mainfrom
peter/guard-generated-writes-frameworks

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Aug 20, 2026

Copy link
Copy Markdown
Member

Description

Follow-up to #3454, which added the unchanged-write guard and applied it to @workflow/builders, @workflow/next and @workflow/nitro. The Astro and SvelteKit builders were not covered: they receive the bundle text from the base builder and do their own writeFile, so they never reach the guard. Their generated files live in Vite-watched source directories (src/pages/.well-known/workflow/v1 and src/routes/.well-known/workflow/v1), which is exactly the condition #3454 was about.

This routes those writes through writeFileIfChanged, and writes SvelteKit's static manifest copy from the same string rather than copyFile-ing it, matching what #3454 did for the Next public manifest.

Measured effect

Two consecutive pnpm build runs with no source change, comparing sha256 + mtime(ns) + inode of every generated file under the workflow directories, with WORKFLOW_PUBLIC_MANIFEST=1:

beforeafter
SvelteKit (src/routes/…, static/…)6 of 9 rewritten3
Astro (src/pages/…)5 of 8 rewritten3

Every one of those rewrites had an identical sha, i.e. they were pure no-op writes. Fixed here: both .gitignore files and the static/manifest.json copy on SvelteKit, and .gitignore plus manifest.json.js on Astro.

Relation to #3509

Rebased over #3509 (better SvelteKit routes-directory loading), which rewrote the same import block in packages/sveltekit/src/builder.ts. That was the only conflict and it was mechanical: keep its node:path import and drop the node:module/node:url imports it made dead, keep this PR's narrowed node:fs/promises import.

The two changes act at different layers and both still apply. #3509 caches the resolved routes directory so SvelteKit's secondary builds and worker threads do not reload the Svelte config and redo the build; this PR reduces what a build that does run actually writes. The measured numbers above were re-taken on top of #3509, with the guard-off control built from origin/main's builder, so the before/after pair is on the same base.

What this does NOT fix

Three files per framework still churn, for two reasons that need more than a call-site swap. Both are pre-existing and neither is made worse by this PR.

1. The read-transform-rewrite pattern defeats the guard — affects flow/+server.js and webhook/[token]/+server.js (SvelteKit), flow.js and webhook/[token].js (Astro).

Both builders call createCombinedBundle / createWebhookBundle, which write the route to disk through the base builder, then readFile it back, apply a regex transform, and write the transformed content. So each build writes the file twice with two different contents, and on the next build neither write can be skipped: the base write sees the previous build's transformed content on disk and differs from it, and the framework write then sees the untransformed content and differs from that. The signature is visible in the inode: the route gets a new inode (base write, temp+rename) and the webhook keeps its inode (framework write, plain overwrite).

The fix is to make the final content the only content written, e.g. by letting the framework pass its transform into the base builder so the route is generated in its final form. That is an API change to the base builder and wants its own PR.

2. One-shot esbuild.build() always rewrites its outfile — affects flow/__step_registrations.js on both frameworks.

ctx.rebuild() on an esbuild context skips writing when the output is byte-identical (verified on esbuild 0.28.1: mtime and inode both preserved). A one-shot esbuild.build() does not; it rewrites unconditionally. This is why Next dev, which uses contexts, was already protected for its esbuild outputs while these builders are not. The fix is either to use contexts in watch mode or to pass write: false and hand the output to writeFileIfChanged.

Other follow-ups from #3454

3. Nitro's dev handler should stop cache-busting off a single file's mtime.packages/nitro/src/index.ts derives version from statSync(handlerPath).mtimeMs for workflows.mjs and then uses it as the cache-buster for the steps.mjs import. A step-body-only edit leaves workflows.mjs identical, so the version freezes and stale step code is served. That dependency is the only reason skipsUnchangedGeneratedWrites exists.

Widening the key is cheap and keeps the per-request path to stats rather than file reads, since handlerImportPath and stepsImportPath are already in scope:

consts=statSync(handlerPath),t=statSync(stepsPath);constversion=`${s.size}:${s.mtimeMs}:${t.size}:${t.mtimeMs}`;

Including size costs nothing and narrows the case where two coalesced dev writes land on an effectively identical mtime. Note only the workflow/workflows.mjs variant needs the second stat; preloadSteps['workflow/webhook.mjs'] is empty. Both the v2 and v3 branches carry a copy. Once that lands, skipsUnchangedGeneratedWrites can be deleted from BaseBuilder and from the Nitro override.

4. There is no regression guard for the cross-framework case.write-if-changed.test.ts covers the helper and the opt-out, but nothing stops a new raw writeFile into a watched generated directory from silently restoring the redundant rebuild. The check that would catch it is the one used above: build twice, assert every generated file keeps its sha, mtime and inode. @workflow/astro has no test files today and @workflow/sveltekit has one unrelated test, so this needs a builder harness per framework rather than a one-line test.

How did you test your changes?

No automated coverage is added, for the reason in (4), so here are reproduction steps.

pnpm install && pnpm run build
cd workbench/sveltekit # and workbench/astroexport WORKFLOW_PUBLIC_MANIFEST=1
pnpm build
find src/routes/.well-known/workflow static/.well-known/workflow -type f \
| sort | xargs -I{} sh -c 'printf "%s %s %s\n" "$(shasum -a 256 {} | cut -d" " -f1)" "$(stat -f "%Fm:%i" {})" {}'> /tmp/s1
pnpm build
# ...repeat into /tmp/s2
diff /tmp/s1 /tmp/s2

Before this change the two .gitignore files and static/manifest.json appear in the diff with an identical sha and a moved mtime. After, they do not. The three files listed above still appear, for the reasons given.

Also run: pnpm run build (28/28 tasks), npx biome check on both changed files.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run to create a changelog for this PR
  • 🔒 DCO sign-off passes (run git commit --signoff on your commits)
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete

@vercel

vercelBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreview, v0Aug 25, 2026 8:55pm
example-nextjs-workflow-webpackReadyReadyPreview, v0Aug 25, 2026 8:55pm
example-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-astro-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-express-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-fastify-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-hono-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nestjs-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nitro-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nuxt-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-python-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-sveltekit-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-tanstack-start-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-vite-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-docsReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-swc-playgroundReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-tarballsReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-webReadyReadyPreview, v0Aug 25, 2026 8:55pm

@changeset-bot

changeset-botBot commented Aug 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 555b143

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/astroPatch
@workflow/sveltekitPatch
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 555b143 · Tue, 25 Aug 2026 21:17:13 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1215 (+465%) 🔻1315 🔴 (+33%) 🔻1332 🔴 (+29%) 🔻1375 🔴 (-4.8%)30
TTFSstream217 (+20%) 🔻1291 🔴 (+36%) 🔻1314 🔴 (+36%) 🔻1365 🔴 (+35%) 🔻30
TTFShook + stream1447 (+180%) 🔻1597 🔴 (+18%) 🔻1666 🔴 (+11%)1940 🔴 (-62%) 💚30
Fan-out TTFSPromise.all(100 steps)806 (+44%) 🔻1955 (+96%) 🔻1978 (+40%) 🔻2016 (+24%) 🔻10
Fan-out TTLSPromise.all(100 steps)2542 (+31%) 🔻5184 (+87%) 🔻6673 (+99%) 🔻9405 (+43%) 🔻10
STSO1020 steps (inline)99 (-7.5%)148 (+14%)165 (+8.6%)233 (+14%)1019
WO1020 steps146183 (+11%)146183 (+11%)146183 (+11%)146183 (+11%)1
CRTTfirst chunk (pooled)104 (+24%) 🔻145 (+9.0%)167 (+7.7%)271 (+13%)28

Streams

ScenarioCRTT 1stp75p90p99CDV maxiters
paced control (100/s, 60B)138 (+25%)156 (-3%)230 (-44%)419 (-34%)161 (+24%)10
size sweep (100/s, 160B-12KB)128 (+15%)164 (+20%)228 (-36%)629 (-20%)160 (+53%)10
replay gateway-gpt-5.4-nano-2000t (1x)113 (-23%)139 (+1%)187 (-28%)745 (-16%)368 (+11%)3
replay eve-gpt-5.6-sol-2000t (1x)195 (+7%)150 (+22%)192 (+19%)436 (+31%)434 (+36%)2
replay eve-gpt-5.6-sol-2000t (2x)145 (+14%)182 (+22%)241 (+25%)491 (+30%)286 (+10%)3
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 132084ms → this run 144847ms (Δ +12763ms, +10%)

 50-100 ms ┃ main 0 this 1 +1
100-150 ms ████████████████████┃███ main 905 this 781 -124
150-200 ms ███░░┃ main 103 this 217 +114
200-250 ms ┃ main 5 this 13 +8
250-300 ms ┃ main 2 this 2 +0
300-350 ms ┃ main 1 this 2 +1
350-400 ms ┃ main 1 this 2 +1
400-450 ms ┃ main 2 this 1 -1
📈 CRTT drill-down vs main (RTT distributions & profiles)
variant RTT 1ms→5s+ avg p50 p90 p99 n
control ······▃█▁···· 129.5 (+3%) 120 (+21%) 230 (-44%) 419 (-34%) 3000
sweep ······▂█▁▁··· 138.6 (+18%) 126 (+29%) 228 (-36%) 629 (-20%) 3000
gw 1x ·····▁▆█▁▁··· 119.6 (±0%) 105 (+9%) 187 (-28%) 745 (-16%) 5295
eve 1x ·····▁▅█▁▁··· 124.7 (+17%) 109 (+14%) 192 (+19%) 436 (+31%) 5186
eve 2x ·····▁▂█▂▁··· 150.3 (+20%) 134 (+16%) 241 (+25%) 491 (+30%) 7779

RTT over stream progress (avg per tenth of stream, bars scaled min→max):

control ▆█▄▃▂▁▄▂▄▃ 111–157ms
sweep ▃▃▃▂▂▁▄█▃▂ 121–184ms
gw 1x ▂▂▃▃▁█▂▂▁▂ 103–181ms
eve 1x ▄▁▃▆▃█▇▆▄▂ 106–146ms
eve 2x ▁▂▁▁▅▄▄█▆▃ 125–196ms

RTT by chunk size (avg per log size bin, ~160B → ~12KB serialized, bars scaled min→max):

sweep █▇▂▁▃▄▅ 136–141ms

Delivery jitter over stream progress (avg positive CDV per tenth of stream, bars scaled min→max):

control ▂▇▄▄▇▁█▄▅▄ 34–43ms
sweep ▁▄▁▄▆▄▄█▃▃ 47–68ms
gw 1x ▃▂▇▂▃█▃▂▁▄ 31–40ms
eve 1x ▄▁▂▆▆▆█▅▄▆ 20–26ms
eve 2x ▇▇▁▄▇█▅▁▅█ 19–27ms
📜 Previous results (1)

78301f5

Thu, 20 Aug 2026 01:50:16 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep458 (-58%) 💚1476 🔴 (+19%) 🔻1524 🔴 (+15%) 🔻1611 🔴 (-1.1%)30
TTFSstream1295 (+275%) 🔻1477 🔴 (+28%) 🔻1538 🔴 (+29%) 🔻1675 🔴 (+36%) 🔻30
TTFShook + stream1635 (+50%) 🔻2181 🔴 (+41%) 🔻2203 🔴 (+40%) 🔻2331 🔴 (-12%)30
Fan-out TTFSPromise.all(100 steps)593 (+10%)938 (-42%) 💚2355 (+42%) 🔻2542 (+40%) 🔻10
Fan-out TTLSPromise.all(100 steps)4866 (-2.6%)5507 (-11%)6831 (-2.6%)12617 (+65%) 🔻10
STSO1020 steps (inline)105 (-29%) 💚174 (-35%) 💚190 (-44%) 💚304 (-57%) 💚1019
WO1020 steps173964 (-34%) 💚173964 (-34%) 💚173964 (-34%) 💚173964 (-34%) 💚1
CRTTfirst chunk (pooled)124 (+20%) 🔻202 (+28%) 🔻300 (-8.0%)587 (+41%) 🔻28

Streams

ScenarioCRTT 1stp75p90p99CDV maxiters
paced control (100/s, 60B)163 (+14%)172 (-20%)257 (-33%)446 (-40%)119 (-61%)10
size sweep (100/s, 160B-12KB)168 (+21%)205 (-3%)256 (-26%)363 (-36%)139 (-16%)10
replay gateway-gpt-5.4-nano-2000t (1x)300 (+110%)160 (-45%)199 (-58%)532 (-23%)200 (-53%)3
replay eve-gpt-5.6-sol-2000t (1x)174 (+17%)163 (-54%)204 (-59%)582 (-34%)411 (-28%)2
replay eve-gpt-5.6-sol-2000t (2x)228 (+63%)251 (-50%)544 (-17%)1091 (+6%)542 (+19%)3
ℹ️ Metric definitions & methodology

Streams: first-chunk RTT (the stream-open path, before any buffering/backpressure), CRTT percentiles, and worst delivery stall (CDV max). Cells are medians across iterations; per-run values in the artifacts. No 🔴/🟢 marks until targets attach.

The collapsed STSO distribution section above buckets every step gap, split inline (same warm process — pure framework overhead) vs queue-hop (fresh process — dispatch, reinit, replay). = main, = this run, = fill.

The collapsed CRTT drill-down: per-variant RTT histograms (fixed log bins, · = empty) and mean RTT/positive-CDV profile lines over stream progress and chunk size. Histograms, avgs, and profiles merge exactly across runs; p50–p99 are percentile-of-percentiles. Per-index rows live in the artifacts.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body) · Fan-out TTFS: fan-out time to first step (in-deployment start() → first of the parallel step bodies to complete) · Fan-out TTLS: fan-out time to last step (in-deployment start() → last of the parallel step bodies to complete, i.e. when the Promise.all resolves) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · CRTT: chunk round-trip time (per-chunk write → read latency, one clock domain: deployment → stream backend → same deployment) · CDV: chunk delay variation / delivery jitter (inter-arrival gap minus inter-write gap per seq-adjacent pair; skew-free; the row is each run's MAX positive value, so one stall moves it)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · Promise.all(100 steps): 100 trivial no-op steps started together in a single Promise.all; Fan-out TTFS is the first of them to complete and Fan-out TTLS the last, both from the in-deployment clientStart, so their gap is the spread the runtime adds across the fan-out · paced control (100/s, 60B): the control: 300 tiny (~60B) deltas metronome-paced at 100/s — zero workload structure, so it reads the transport floor and flush cadence, and disambiguates transport-wide vs workload-specific when a replay row moves · size sweep (100/s, 160B-12KB): same pacing as the control with deltas padded in rotation across seven log-spaced sizes (~160B–12KB) — rotation decouples size from stream position, so it isolates whether chunk size causes latency · replay gateway-gpt-5.4-nano-2000t (1x): raw provider SSE cadence captured at the AI gateway boundary (gpt-5.4-nano, the most popular gateway model; per-token deltas p50 208B = the modal production chunk size), replayed exactly as measured — the typical customer's workload; its CDV is the typical customer's real delivery jitter · replay eve-gpt-5.6-sol-2000t (1x): a captured eve turn (gpt-5.6-sol, the most-used demanding eve model; ~2000 output tokens = production p50 turn length) replayed exactly as measured — eve's envelope protocol re-ships the cumulative message so sizes ramp 142B→13KB; the demanding outlier tenant's reality · replay eve-gpt-5.6-sol-2000t (2x): the same eve capture at 2x — the headroom/stress row; real fast-tier models emit the same chunk sizes at proportionally higher rate, so time compression is a faithful speed model · first chunk (pooled): every run's seq-0 RTT pooled across all stream scenarios — the first chunk precedes any workload differentiation, so pooling samples one shared stream-open path with exact percentiles

Replay cadences (semantic sha256) — eve-gpt-5.6-sol-2000teaf22f5946e7c61f3c65c7006d550df180cfabd4e706254a09f22aec0cfb420d · gateway-gpt-5.4-nano-2000t6f24ac518b6b83ff1d0e85a5fe78230db192716d66a7fc6b2fe022752001d041

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600

All timestamps are deployment-side; runs are triggered in-deployment, so the CI runner and api.vercel.com sit outside every measured window. TTFS = start() → first step body (includes dispatch + any cold start); Fan-out TTFS/TTLS = first/last step completion of one Promise.all from the same anchor (the gap is the runtime’s fan-out spread); STSO/WO between step bodies; CRTT inside the workflow (excludes the api.vercel.com read path).

Cold starts stay in the numbers (real bursty-workload latency, inflates P75+); Best is the warm floor.

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

▲ Vercel Production (8 failed)

python-node (8 failed):

  • promiseAllWorkflow | wrun_41M0XB8RB20GPSEEE0BP1F0AX5 | 🔍 observability
  • sleepingWorkflow | wrun_41M0XB9DN80GT0JYGEYSRDG0AZ | 🔍 observability
  • parallelSleepWorkflow | wrun_41M0XB9DSC0GGQFNQPSTD10TFN | 🔍 observability
  • nullByteWorkflow | wrun_41M0XB9MEH0GZ1PE9VA8VT338J | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41M0XBE3DY0GXJZZKSDADPJ273 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41M0XBE4D70GMTA2YASGGSMBW6 | 🔍 observability
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M0XBEDKS0GX7MW0EJVZNDKG1 | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M0XBEYPA0GT79Q7M4KQGXE2R | 🔍 observability

🌐 Cross-language Conformance (9 failed)

python (9 failed):

  • deploymentId: 'latest' is a no-op in non-Vercel worlds | wrun_01M0XBHEZA0V7PQ3KYK30VCQV6
  • promiseAllWorkflow | wrun_41M0XB8RB20GPSEEE0BP1F0AX5
  • sleepingWorkflow | wrun_41M0XB9DN80GT0JYGEYSRDG0AZ
  • parallelSleepWorkflow | wrun_41M0XB9DSC0GGQFNQPSTD10TFN
  • nullByteWorkflow | wrun_41M0XB9MEH0GZ1PE9VA8VT338J
  • cancelRun - cancelling a running workflow | wrun_41M0XBE3DY0GXJZZKSDADPJ273
  • cancelRun via CLI - cancelling a running workflow | wrun_41M0XBE4D70GMTA2YASGGSMBW6
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M0XBEDKS0GX7MW0EJVZNDKG1
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M0XBEYPA0GT79Q7M4KQGXE2R

⚠️ Flaky E2E Tests (passed on retry)

These tests failed at least once and passed on a retry. A recurring entry here is a real race worth investigating.

  • cancelRun via CLI - cancelling a running workflow (nextjs-turbopack)
  • cancelRun via CLI - cancelling a running workflow (sveltekit)
  • deploymentId: 'latest' is a no-op in non-Vercel worlds (tanstack-start)
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (nextjs-webpack)

🛠 Infra Events (absorbed by the harness)

Platform anomalies the e2e harness detected and worked around (e.g. a run the queue never picked up, replaced by a fresh run). Clustered timestamps indicate a backend blip; a steady drip indicates a platform issue worth escalating.

33 infra events
  • cold-start-warmup · suite warmup (python) · at 20:55:38Z · abandoned wrun_41M0XB6E7T0GJ9W8791BM6Z01B · (+7 more)
  • run-pickup-stall · sleepingWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TD0GMM929KHMH4A5PQ
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TD0GMM929KHMH4A5PR
  • run-pickup-stall · nullByteWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TG0GXS3FJPHWZ0AP3Z
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA34B0GKTASFX3VHN7ZF5
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:55:55Z · abandoned wrun_41M0XBA2T60GKVRBKEPG9JFNGQ
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 20:56:27Z · abandoned wrun_41M0XBB2NE0GYG9ZKWYWVYN0QG
  • run-pickup-stall · sleepingWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY390GR8JJH6G2563BMK
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY170GNNFJ2HYJF76VRJ
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY1P0GSFSBE1ENV48TWY
  • run-pickup-stall · nullByteWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY2C0GXP8214VHPKYDEN
  • cold-start-warmup · suite warmup (tanstack-start) · at 20:57:08Z · abandoned wrun_01M0XBC0795S33S7QA0QCC6MWM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 20:57:45Z · abandoned wrun_41M0XBC1V20GQEJYYHQRSWTMC5
  • cold-start-warmup · suite warmup (python) · at 20:58:25Z · abandoned wrun_01M0XBBGHK2AQJ0WK3BTSQXSBE · (+7 more)
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PANZ6ZFDYD87WHMYEG
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PANZ6ZFDYD87WHMYEH
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PJ61BWHDADBWKVQB4M
  • run-pickup-stall · nullByteWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PMHJQTM6D1Q5A1DGP8
  • run-pickup-stall · sleepingWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PFRRDDXM9ZQTKATH0V
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 20:59:26Z · abandoned wrun_41M0XBENXS0GNP64ENYV9QCDR6
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 20:59:26Z · abandoned wrun_41M0XBESQJ0GNJ9RHK6DQT9QJG
  • run-pickup-stall · sleepingWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A76KFYN7SAWZTZNJVC
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A4Q23JB7QFRPHW2M57
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A6RKK450KHQ6KKBNDM
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A861P4CVWVBHC9GRPV
  • run-pickup-stall · nullByteWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0ACMT5YMDRR1CRNSE1B
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 21:00:40Z · abandoned wrun_01M0XBJTXTEA82N9RWT7V8MWFM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 21:00:40Z · abandoned wrun_01M0XBJTXXTC9C59S58QCXKQDY
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 21:00:40Z · abandoned wrun_01M0XBJTY41EX832TVQ4QR069P
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 21:01:10Z · abandoned wrun_01M0XBKRA4M8A4FPV04NJY23SD
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 21:01:10Z · abandoned wrun_01M0XBKRA5GJYZXB04F49JH2DV
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 21:01:40Z · abandoned wrun_01M0XBMNH7M7QPBBRWZ6AX8HGC
  • run-pickup-stall · hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep (nextjs-webpack) · at 21:02:31Z · abandoned wrun_01M0XBP7PT25DV3EYT1BDRVKMK

E2E Test Summary

Summary
PassedFailedSkippedTotal
❌ ▲ Vercel Production357087424320
✅ 💻 Local Development392205584480
✅ 📦 Local Production392205584480
✅ 🐘 Local Postgres392205584480
✅ 🪟 Windows32000320
❌ 🌐 Cross-language Conformance09132141
✅ vercel-http-transport8170143960
✅ vercel-multi-region270027
✅ vercel-ws-transport553087640
Total1705317277819848
Details by Category

❌ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node132028
✅ astro-quickjs132028
✅ example-node132028
✅ example-quickjs132028
✅ express-node132028
✅ express-quickjs132028
✅ fastify-node132028
✅ fastify-quickjs132028
✅ hono-node132028
✅ hono-quickjs132028
✅ nest-node132028
✅ nest-quickjs132028
✅ nextjs-turbopack-node15703
✅ nextjs-turbopack-quickjs15703
✅ nextjs-webpack-node15703
✅ nextjs-webpack-quickjs15703
✅ nitro-node132028
✅ nitro-quickjs132028
✅ nuxt-node132028
✅ nuxt-quickjs132028
❌ python-node08152
✅ sveltekit-node15109
✅ sveltekit-quickjs15109
✅ tanstack-start-node132028
✅ tanstack-start-quickjs132028
✅ vite-node132028
✅ vite-quickjs132028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node16000
✅ nextjs-turbopack-quickjs16000

❌ 🌐 Cross-language Conformance

AppPassedFailedSkipped
❌ python09132

✅ vercel-http-transport

AppPassedFailedSkipped
✅ example132028
✅ express132028
✅ hono132028
✅ nextjs-turbopack15703
✅ nitro132028
✅ vite132028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

✅ vercel-ws-transport

AppPassedFailedSkipped
✅ example132028
✅ express132028
✅ nextjs-turbopack15703
✅ vite132028

📋 View full workflow run

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Sim World

Simulated world deterministic testing for races. Traces

🟠 world-sim scenario book — 1 fail of 41 total

fence=per-spec

scenariooutcomeeventsvirtreplayviolations
smoke-no-stepscompleted30msok0
smoke-one-stepcompleted60msok0
hook-at-step-startedcompleted120msok0
hook-at-step-completedcompleted120msok0
hook-at-hook-createdcompleted120msok0
deadline-hook-winscompleted71.0hok0
deadline-expirescompleted71.0hok0
long-sleepcompleted1130.0dok0
hook-never-arrivesstalled30msskipped0
step-retries-twicecompleted102.0sok0
parallel-stepscompleted90msok0
hook-on-execution-statecompleted120msok0
peek-hook-before-branchcompleted120msok0
peek-hook-after-branchcompleted120msok0
peek-hook-at-registrationcompleted120msok0
race-hook-before-probecompleted120msok0
race-hook-after-probecompleted120msok0
race-duplicate-deliverycompleted130msok0
attr-hook-before-stepcompleted110msok0
attr-hook-after-stepcompleted110msok0
attr-from-step-bodycompleted130msok0
fork-hook-after-timeoutcompleted141.0mok0
fork-hook-before-timeoutcompleted141.0mok0
count-hook-after-timeoutcompleted171.0mok0
count-hook-before-timeoutcompleted201.0mok0
stale-read-step-count-forkcompleted201.0mok0
stale-read-equal-step-countscompleted141.0mok0
step-vs-step-forkcompleted120msok0
step-vs-step-fork-fencedcompleted120msok0
fence-catches-benign-directioncompleted125msok0
in-flight-before-decisioncompleted171.0mok0
in-flight-before-decision-countedcompleted171.0mok0
in-flight-after-decisioncompleted192.0mok0
stale-read-step-count-fork-fencedcompleted201.0mok0
fork-hook-winscompleted131.0mok0
fork-timeout-winscompleted131.0mok0
unclaimed-payload-under-forkcompleted171.0mok0
claimed-payload-under-forkcompleted171.0mok0
writers-independent-step-bodiescompleted120msok0
writers-scripted-tempocompleted120msok0
cancel-mid-stepcancelled70msskipped0

Full trace: world-sim.txt

… file
Applies the unchanged-write guard from #3454 to the Astro and SvelteKit
builders, whose generated files live in Vite-watched source directories.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <peter.wielander@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor
FrameworkFlow routeStep reg.Framework output
hono200.9 KiB (±0)40.5 KiB (±0)1.76 MiB (±0)
nextjs-turbopack206.3 KiB (±0)439 B (±0)762.8 KiB (±0)
About these numbers

Sizes are gzip; parentheses show the change against main.
Flow route and Step reg. gate this job, on raw bytes rather than the gzip shown, at max(2%, 50.0 KiB). Framework output is informational.

555b143 · run

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this leaves the framework-specific behavior without automated regression coverage. A small integration test that builds Astro/SvelteKit twice and asserts watched generated outputs retain their mtime/inode would help prevent no-op writes from returning. Not blocking this change.

@VaguelySerious
VaguelySerious merged commit 0750fe8 into mainAug 25, 2026
182 of 184 checks passed
@VaguelySerious
VaguelySerious deleted the peter/guard-generated-writes-frameworks branch August 25, 2026 22:47
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 0750fe8 (AI decision).

The change depends on writeFileIfChanged from @workflow/builders, which was introduced by #3454 on main only — git grep writeFileIfChanged origin/stable finds nothing, so this cannot apply to stable without also backporting that main-only helper. It is also a dev-server efficiency improvement (avoiding redundant Vite invalidations/recompiles on no-op rebuilds) rather than a correctness, data-loss, or crash fix, so it does not meet the maintenance-branch bar on its own merits either.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

0750fe8958f50ee354cd24fe3f02a37c801bc4dc

@github-actionsgithub-actionsBot mentioned this pull request Aug 26, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(astro,sveltekit): skip generated writes that would not change the file - #3688

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/guard-generated-writes-frameworks
Aug 25, 2026
Merged

fix(astro,sveltekit): skip generated writes that would not change the file#3688
VaguelySerious merged 1 commit into
mainfrom
peter/guard-generated-writes-frameworks

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Aug 20, 2026

Copy link
Copy Markdown
Member

Description

Follow-up to #3454, which added the unchanged-write guard and applied it to @workflow/builders, @workflow/next and @workflow/nitro. The Astro and SvelteKit builders were not covered: they receive the bundle text from the base builder and do their own writeFile, so they never reach the guard. Their generated files live in Vite-watched source directories (src/pages/.well-known/workflow/v1 and src/routes/.well-known/workflow/v1), which is exactly the condition #3454 was about.

This routes those writes through writeFileIfChanged, and writes SvelteKit's static manifest copy from the same string rather than copyFile-ing it, matching what #3454 did for the Next public manifest.

Measured effect

Two consecutive pnpm build runs with no source change, comparing sha256 + mtime(ns) + inode of every generated file under the workflow directories, with WORKFLOW_PUBLIC_MANIFEST=1:

beforeafter
SvelteKit (src/routes/…, static/…)6 of 9 rewritten3
Astro (src/pages/…)5 of 8 rewritten3

Every one of those rewrites had an identical sha, i.e. they were pure no-op writes. Fixed here: both .gitignore files and the static/manifest.json copy on SvelteKit, and .gitignore plus manifest.json.js on Astro.

Relation to #3509

Rebased over #3509 (better SvelteKit routes-directory loading), which rewrote the same import block in packages/sveltekit/src/builder.ts. That was the only conflict and it was mechanical: keep its node:path import and drop the node:module/node:url imports it made dead, keep this PR's narrowed node:fs/promises import.

The two changes act at different layers and both still apply. #3509 caches the resolved routes directory so SvelteKit's secondary builds and worker threads do not reload the Svelte config and redo the build; this PR reduces what a build that does run actually writes. The measured numbers above were re-taken on top of #3509, with the guard-off control built from origin/main's builder, so the before/after pair is on the same base.

What this does NOT fix

Three files per framework still churn, for two reasons that need more than a call-site swap. Both are pre-existing and neither is made worse by this PR.

1. The read-transform-rewrite pattern defeats the guard — affects flow/+server.js and webhook/[token]/+server.js (SvelteKit), flow.js and webhook/[token].js (Astro).

Both builders call createCombinedBundle / createWebhookBundle, which write the route to disk through the base builder, then readFile it back, apply a regex transform, and write the transformed content. So each build writes the file twice with two different contents, and on the next build neither write can be skipped: the base write sees the previous build's transformed content on disk and differs from it, and the framework write then sees the untransformed content and differs from that. The signature is visible in the inode: the route gets a new inode (base write, temp+rename) and the webhook keeps its inode (framework write, plain overwrite).

The fix is to make the final content the only content written, e.g. by letting the framework pass its transform into the base builder so the route is generated in its final form. That is an API change to the base builder and wants its own PR.

2. One-shot esbuild.build() always rewrites its outfile — affects flow/__step_registrations.js on both frameworks.

ctx.rebuild() on an esbuild context skips writing when the output is byte-identical (verified on esbuild 0.28.1: mtime and inode both preserved). A one-shot esbuild.build() does not; it rewrites unconditionally. This is why Next dev, which uses contexts, was already protected for its esbuild outputs while these builders are not. The fix is either to use contexts in watch mode or to pass write: false and hand the output to writeFileIfChanged.

Other follow-ups from #3454

3. Nitro's dev handler should stop cache-busting off a single file's mtime.packages/nitro/src/index.ts derives version from statSync(handlerPath).mtimeMs for workflows.mjs and then uses it as the cache-buster for the steps.mjs import. A step-body-only edit leaves workflows.mjs identical, so the version freezes and stale step code is served. That dependency is the only reason skipsUnchangedGeneratedWrites exists.

Widening the key is cheap and keeps the per-request path to stats rather than file reads, since handlerImportPath and stepsImportPath are already in scope:

consts=statSync(handlerPath),t=statSync(stepsPath);constversion=`${s.size}:${s.mtimeMs}:${t.size}:${t.mtimeMs}`;

Including size costs nothing and narrows the case where two coalesced dev writes land on an effectively identical mtime. Note only the workflow/workflows.mjs variant needs the second stat; preloadSteps['workflow/webhook.mjs'] is empty. Both the v2 and v3 branches carry a copy. Once that lands, skipsUnchangedGeneratedWrites can be deleted from BaseBuilder and from the Nitro override.

4. There is no regression guard for the cross-framework case.write-if-changed.test.ts covers the helper and the opt-out, but nothing stops a new raw writeFile into a watched generated directory from silently restoring the redundant rebuild. The check that would catch it is the one used above: build twice, assert every generated file keeps its sha, mtime and inode. @workflow/astro has no test files today and @workflow/sveltekit has one unrelated test, so this needs a builder harness per framework rather than a one-line test.

How did you test your changes?

No automated coverage is added, for the reason in (4), so here are reproduction steps.

pnpm install && pnpm run build
cd workbench/sveltekit # and workbench/astroexport WORKFLOW_PUBLIC_MANIFEST=1
pnpm build
find src/routes/.well-known/workflow static/.well-known/workflow -type f \
| sort | xargs -I{} sh -c 'printf "%s %s %s\n" "$(shasum -a 256 {} | cut -d" " -f1)" "$(stat -f "%Fm:%i" {})" {}'> /tmp/s1
pnpm build
# ...repeat into /tmp/s2
diff /tmp/s1 /tmp/s2

Before this change the two .gitignore files and static/manifest.json appear in the diff with an identical sha and a moved mtime. After, they do not. The three files listed above still appear, for the reasons given.

Also run: pnpm run build (28/28 tasks), npx biome check on both changed files.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run to create a changelog for this PR
  • 🔒 DCO sign-off passes (run git commit --signoff on your commits)
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete

@vercel

vercelBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreview, v0Aug 25, 2026 8:55pm
example-nextjs-workflow-webpackReadyReadyPreview, v0Aug 25, 2026 8:55pm
example-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-astro-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-express-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-fastify-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-hono-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nestjs-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nitro-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nuxt-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-python-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-sveltekit-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-tanstack-start-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-vite-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-docsReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-swc-playgroundReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-tarballsReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-webReadyReadyPreview, v0Aug 25, 2026 8:55pm

@changeset-bot

changeset-botBot commented Aug 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 555b143

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/astroPatch
@workflow/sveltekitPatch
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 555b143 · Tue, 25 Aug 2026 21:17:13 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1215 (+465%) 🔻1315 🔴 (+33%) 🔻1332 🔴 (+29%) 🔻1375 🔴 (-4.8%)30
TTFSstream217 (+20%) 🔻1291 🔴 (+36%) 🔻1314 🔴 (+36%) 🔻1365 🔴 (+35%) 🔻30
TTFShook + stream1447 (+180%) 🔻1597 🔴 (+18%) 🔻1666 🔴 (+11%)1940 🔴 (-62%) 💚30
Fan-out TTFSPromise.all(100 steps)806 (+44%) 🔻1955 (+96%) 🔻1978 (+40%) 🔻2016 (+24%) 🔻10
Fan-out TTLSPromise.all(100 steps)2542 (+31%) 🔻5184 (+87%) 🔻6673 (+99%) 🔻9405 (+43%) 🔻10
STSO1020 steps (inline)99 (-7.5%)148 (+14%)165 (+8.6%)233 (+14%)1019
WO1020 steps146183 (+11%)146183 (+11%)146183 (+11%)146183 (+11%)1
CRTTfirst chunk (pooled)104 (+24%) 🔻145 (+9.0%)167 (+7.7%)271 (+13%)28

Streams

ScenarioCRTT 1stp75p90p99CDV maxiters
paced control (100/s, 60B)138 (+25%)156 (-3%)230 (-44%)419 (-34%)161 (+24%)10
size sweep (100/s, 160B-12KB)128 (+15%)164 (+20%)228 (-36%)629 (-20%)160 (+53%)10
replay gateway-gpt-5.4-nano-2000t (1x)113 (-23%)139 (+1%)187 (-28%)745 (-16%)368 (+11%)3
replay eve-gpt-5.6-sol-2000t (1x)195 (+7%)150 (+22%)192 (+19%)436 (+31%)434 (+36%)2
replay eve-gpt-5.6-sol-2000t (2x)145 (+14%)182 (+22%)241 (+25%)491 (+30%)286 (+10%)3
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 132084ms → this run 144847ms (Δ +12763ms, +10%)

 50-100 ms ┃ main 0 this 1 +1
100-150 ms ████████████████████┃███ main 905 this 781 -124
150-200 ms ███░░┃ main 103 this 217 +114
200-250 ms ┃ main 5 this 13 +8
250-300 ms ┃ main 2 this 2 +0
300-350 ms ┃ main 1 this 2 +1
350-400 ms ┃ main 1 this 2 +1
400-450 ms ┃ main 2 this 1 -1
📈 CRTT drill-down vs main (RTT distributions & profiles)
variant RTT 1ms→5s+ avg p50 p90 p99 n
control ······▃█▁···· 129.5 (+3%) 120 (+21%) 230 (-44%) 419 (-34%) 3000
sweep ······▂█▁▁··· 138.6 (+18%) 126 (+29%) 228 (-36%) 629 (-20%) 3000
gw 1x ·····▁▆█▁▁··· 119.6 (±0%) 105 (+9%) 187 (-28%) 745 (-16%) 5295
eve 1x ·····▁▅█▁▁··· 124.7 (+17%) 109 (+14%) 192 (+19%) 436 (+31%) 5186
eve 2x ·····▁▂█▂▁··· 150.3 (+20%) 134 (+16%) 241 (+25%) 491 (+30%) 7779

RTT over stream progress (avg per tenth of stream, bars scaled min→max):

control ▆█▄▃▂▁▄▂▄▃ 111–157ms
sweep ▃▃▃▂▂▁▄█▃▂ 121–184ms
gw 1x ▂▂▃▃▁█▂▂▁▂ 103–181ms
eve 1x ▄▁▃▆▃█▇▆▄▂ 106–146ms
eve 2x ▁▂▁▁▅▄▄█▆▃ 125–196ms

RTT by chunk size (avg per log size bin, ~160B → ~12KB serialized, bars scaled min→max):

sweep █▇▂▁▃▄▅ 136–141ms

Delivery jitter over stream progress (avg positive CDV per tenth of stream, bars scaled min→max):

control ▂▇▄▄▇▁█▄▅▄ 34–43ms
sweep ▁▄▁▄▆▄▄█▃▃ 47–68ms
gw 1x ▃▂▇▂▃█▃▂▁▄ 31–40ms
eve 1x ▄▁▂▆▆▆█▅▄▆ 20–26ms
eve 2x ▇▇▁▄▇█▅▁▅█ 19–27ms
📜 Previous results (1)

78301f5

Thu, 20 Aug 2026 01:50:16 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep458 (-58%) 💚1476 🔴 (+19%) 🔻1524 🔴 (+15%) 🔻1611 🔴 (-1.1%)30
TTFSstream1295 (+275%) 🔻1477 🔴 (+28%) 🔻1538 🔴 (+29%) 🔻1675 🔴 (+36%) 🔻30
TTFShook + stream1635 (+50%) 🔻2181 🔴 (+41%) 🔻2203 🔴 (+40%) 🔻2331 🔴 (-12%)30
Fan-out TTFSPromise.all(100 steps)593 (+10%)938 (-42%) 💚2355 (+42%) 🔻2542 (+40%) 🔻10
Fan-out TTLSPromise.all(100 steps)4866 (-2.6%)5507 (-11%)6831 (-2.6%)12617 (+65%) 🔻10
STSO1020 steps (inline)105 (-29%) 💚174 (-35%) 💚190 (-44%) 💚304 (-57%) 💚1019
WO1020 steps173964 (-34%) 💚173964 (-34%) 💚173964 (-34%) 💚173964 (-34%) 💚1
CRTTfirst chunk (pooled)124 (+20%) 🔻202 (+28%) 🔻300 (-8.0%)587 (+41%) 🔻28

Streams

ScenarioCRTT 1stp75p90p99CDV maxiters
paced control (100/s, 60B)163 (+14%)172 (-20%)257 (-33%)446 (-40%)119 (-61%)10
size sweep (100/s, 160B-12KB)168 (+21%)205 (-3%)256 (-26%)363 (-36%)139 (-16%)10
replay gateway-gpt-5.4-nano-2000t (1x)300 (+110%)160 (-45%)199 (-58%)532 (-23%)200 (-53%)3
replay eve-gpt-5.6-sol-2000t (1x)174 (+17%)163 (-54%)204 (-59%)582 (-34%)411 (-28%)2
replay eve-gpt-5.6-sol-2000t (2x)228 (+63%)251 (-50%)544 (-17%)1091 (+6%)542 (+19%)3
ℹ️ Metric definitions & methodology

Streams: first-chunk RTT (the stream-open path, before any buffering/backpressure), CRTT percentiles, and worst delivery stall (CDV max). Cells are medians across iterations; per-run values in the artifacts. No 🔴/🟢 marks until targets attach.

The collapsed STSO distribution section above buckets every step gap, split inline (same warm process — pure framework overhead) vs queue-hop (fresh process — dispatch, reinit, replay). = main, = this run, = fill.

The collapsed CRTT drill-down: per-variant RTT histograms (fixed log bins, · = empty) and mean RTT/positive-CDV profile lines over stream progress and chunk size. Histograms, avgs, and profiles merge exactly across runs; p50–p99 are percentile-of-percentiles. Per-index rows live in the artifacts.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body) · Fan-out TTFS: fan-out time to first step (in-deployment start() → first of the parallel step bodies to complete) · Fan-out TTLS: fan-out time to last step (in-deployment start() → last of the parallel step bodies to complete, i.e. when the Promise.all resolves) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · CRTT: chunk round-trip time (per-chunk write → read latency, one clock domain: deployment → stream backend → same deployment) · CDV: chunk delay variation / delivery jitter (inter-arrival gap minus inter-write gap per seq-adjacent pair; skew-free; the row is each run's MAX positive value, so one stall moves it)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · Promise.all(100 steps): 100 trivial no-op steps started together in a single Promise.all; Fan-out TTFS is the first of them to complete and Fan-out TTLS the last, both from the in-deployment clientStart, so their gap is the spread the runtime adds across the fan-out · paced control (100/s, 60B): the control: 300 tiny (~60B) deltas metronome-paced at 100/s — zero workload structure, so it reads the transport floor and flush cadence, and disambiguates transport-wide vs workload-specific when a replay row moves · size sweep (100/s, 160B-12KB): same pacing as the control with deltas padded in rotation across seven log-spaced sizes (~160B–12KB) — rotation decouples size from stream position, so it isolates whether chunk size causes latency · replay gateway-gpt-5.4-nano-2000t (1x): raw provider SSE cadence captured at the AI gateway boundary (gpt-5.4-nano, the most popular gateway model; per-token deltas p50 208B = the modal production chunk size), replayed exactly as measured — the typical customer's workload; its CDV is the typical customer's real delivery jitter · replay eve-gpt-5.6-sol-2000t (1x): a captured eve turn (gpt-5.6-sol, the most-used demanding eve model; ~2000 output tokens = production p50 turn length) replayed exactly as measured — eve's envelope protocol re-ships the cumulative message so sizes ramp 142B→13KB; the demanding outlier tenant's reality · replay eve-gpt-5.6-sol-2000t (2x): the same eve capture at 2x — the headroom/stress row; real fast-tier models emit the same chunk sizes at proportionally higher rate, so time compression is a faithful speed model · first chunk (pooled): every run's seq-0 RTT pooled across all stream scenarios — the first chunk precedes any workload differentiation, so pooling samples one shared stream-open path with exact percentiles

Replay cadences (semantic sha256) — eve-gpt-5.6-sol-2000teaf22f5946e7c61f3c65c7006d550df180cfabd4e706254a09f22aec0cfb420d · gateway-gpt-5.4-nano-2000t6f24ac518b6b83ff1d0e85a5fe78230db192716d66a7fc6b2fe022752001d041

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600

All timestamps are deployment-side; runs are triggered in-deployment, so the CI runner and api.vercel.com sit outside every measured window. TTFS = start() → first step body (includes dispatch + any cold start); Fan-out TTFS/TTLS = first/last step completion of one Promise.all from the same anchor (the gap is the runtime’s fan-out spread); STSO/WO between step bodies; CRTT inside the workflow (excludes the api.vercel.com read path).

Cold starts stay in the numbers (real bursty-workload latency, inflates P75+); Best is the warm floor.

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

▲ Vercel Production (8 failed)

python-node (8 failed):

  • promiseAllWorkflow | wrun_41M0XB8RB20GPSEEE0BP1F0AX5 | 🔍 observability
  • sleepingWorkflow | wrun_41M0XB9DN80GT0JYGEYSRDG0AZ | 🔍 observability
  • parallelSleepWorkflow | wrun_41M0XB9DSC0GGQFNQPSTD10TFN | 🔍 observability
  • nullByteWorkflow | wrun_41M0XB9MEH0GZ1PE9VA8VT338J | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41M0XBE3DY0GXJZZKSDADPJ273 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41M0XBE4D70GMTA2YASGGSMBW6 | 🔍 observability
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M0XBEDKS0GX7MW0EJVZNDKG1 | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M0XBEYPA0GT79Q7M4KQGXE2R | 🔍 observability

🌐 Cross-language Conformance (9 failed)

python (9 failed):

  • deploymentId: 'latest' is a no-op in non-Vercel worlds | wrun_01M0XBHEZA0V7PQ3KYK30VCQV6
  • promiseAllWorkflow | wrun_41M0XB8RB20GPSEEE0BP1F0AX5
  • sleepingWorkflow | wrun_41M0XB9DN80GT0JYGEYSRDG0AZ
  • parallelSleepWorkflow | wrun_41M0XB9DSC0GGQFNQPSTD10TFN
  • nullByteWorkflow | wrun_41M0XB9MEH0GZ1PE9VA8VT338J
  • cancelRun - cancelling a running workflow | wrun_41M0XBE3DY0GXJZZKSDADPJ273
  • cancelRun via CLI - cancelling a running workflow | wrun_41M0XBE4D70GMTA2YASGGSMBW6
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M0XBEDKS0GX7MW0EJVZNDKG1
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M0XBEYPA0GT79Q7M4KQGXE2R

⚠️ Flaky E2E Tests (passed on retry)

These tests failed at least once and passed on a retry. A recurring entry here is a real race worth investigating.

  • cancelRun via CLI - cancelling a running workflow (nextjs-turbopack)
  • cancelRun via CLI - cancelling a running workflow (sveltekit)
  • deploymentId: 'latest' is a no-op in non-Vercel worlds (tanstack-start)
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (nextjs-webpack)

🛠 Infra Events (absorbed by the harness)

Platform anomalies the e2e harness detected and worked around (e.g. a run the queue never picked up, replaced by a fresh run). Clustered timestamps indicate a backend blip; a steady drip indicates a platform issue worth escalating.

33 infra events
  • cold-start-warmup · suite warmup (python) · at 20:55:38Z · abandoned wrun_41M0XB6E7T0GJ9W8791BM6Z01B · (+7 more)
  • run-pickup-stall · sleepingWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TD0GMM929KHMH4A5PQ
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TD0GMM929KHMH4A5PR
  • run-pickup-stall · nullByteWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TG0GXS3FJPHWZ0AP3Z
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA34B0GKTASFX3VHN7ZF5
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:55:55Z · abandoned wrun_41M0XBA2T60GKVRBKEPG9JFNGQ
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 20:56:27Z · abandoned wrun_41M0XBB2NE0GYG9ZKWYWVYN0QG
  • run-pickup-stall · sleepingWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY390GR8JJH6G2563BMK
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY170GNNFJ2HYJF76VRJ
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY1P0GSFSBE1ENV48TWY
  • run-pickup-stall · nullByteWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY2C0GXP8214VHPKYDEN
  • cold-start-warmup · suite warmup (tanstack-start) · at 20:57:08Z · abandoned wrun_01M0XBC0795S33S7QA0QCC6MWM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 20:57:45Z · abandoned wrun_41M0XBC1V20GQEJYYHQRSWTMC5
  • cold-start-warmup · suite warmup (python) · at 20:58:25Z · abandoned wrun_01M0XBBGHK2AQJ0WK3BTSQXSBE · (+7 more)
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PANZ6ZFDYD87WHMYEG
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PANZ6ZFDYD87WHMYEH
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PJ61BWHDADBWKVQB4M
  • run-pickup-stall · nullByteWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PMHJQTM6D1Q5A1DGP8
  • run-pickup-stall · sleepingWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PFRRDDXM9ZQTKATH0V
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 20:59:26Z · abandoned wrun_41M0XBENXS0GNP64ENYV9QCDR6
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 20:59:26Z · abandoned wrun_41M0XBESQJ0GNJ9RHK6DQT9QJG
  • run-pickup-stall · sleepingWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A76KFYN7SAWZTZNJVC
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A4Q23JB7QFRPHW2M57
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A6RKK450KHQ6KKBNDM
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A861P4CVWVBHC9GRPV
  • run-pickup-stall · nullByteWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0ACMT5YMDRR1CRNSE1B
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 21:00:40Z · abandoned wrun_01M0XBJTXTEA82N9RWT7V8MWFM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 21:00:40Z · abandoned wrun_01M0XBJTXXTC9C59S58QCXKQDY
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 21:00:40Z · abandoned wrun_01M0XBJTY41EX832TVQ4QR069P
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 21:01:10Z · abandoned wrun_01M0XBKRA4M8A4FPV04NJY23SD
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 21:01:10Z · abandoned wrun_01M0XBKRA5GJYZXB04F49JH2DV
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 21:01:40Z · abandoned wrun_01M0XBMNH7M7QPBBRWZ6AX8HGC
  • run-pickup-stall · hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep (nextjs-webpack) · at 21:02:31Z · abandoned wrun_01M0XBP7PT25DV3EYT1BDRVKMK

E2E Test Summary

Summary
PassedFailedSkippedTotal
❌ ▲ Vercel Production357087424320
✅ 💻 Local Development392205584480
✅ 📦 Local Production392205584480
✅ 🐘 Local Postgres392205584480
✅ 🪟 Windows32000320
❌ 🌐 Cross-language Conformance09132141
✅ vercel-http-transport8170143960
✅ vercel-multi-region270027
✅ vercel-ws-transport553087640
Total1705317277819848
Details by Category

❌ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node132028
✅ astro-quickjs132028
✅ example-node132028
✅ example-quickjs132028
✅ express-node132028
✅ express-quickjs132028
✅ fastify-node132028
✅ fastify-quickjs132028
✅ hono-node132028
✅ hono-quickjs132028
✅ nest-node132028
✅ nest-quickjs132028
✅ nextjs-turbopack-node15703
✅ nextjs-turbopack-quickjs15703
✅ nextjs-webpack-node15703
✅ nextjs-webpack-quickjs15703
✅ nitro-node132028
✅ nitro-quickjs132028
✅ nuxt-node132028
✅ nuxt-quickjs132028
❌ python-node08152
✅ sveltekit-node15109
✅ sveltekit-quickjs15109
✅ tanstack-start-node132028
✅ tanstack-start-quickjs132028
✅ vite-node132028
✅ vite-quickjs132028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node16000
✅ nextjs-turbopack-quickjs16000

❌ 🌐 Cross-language Conformance

AppPassedFailedSkipped
❌ python09132

✅ vercel-http-transport

AppPassedFailedSkipped
✅ example132028
✅ express132028
✅ hono132028
✅ nextjs-turbopack15703
✅ nitro132028
✅ vite132028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

✅ vercel-ws-transport

AppPassedFailedSkipped
✅ example132028
✅ express132028
✅ nextjs-turbopack15703
✅ vite132028

📋 View full workflow run

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Sim World

Simulated world deterministic testing for races. Traces

🟠 world-sim scenario book — 1 fail of 41 total

fence=per-spec

scenariooutcomeeventsvirtreplayviolations
smoke-no-stepscompleted30msok0
smoke-one-stepcompleted60msok0
hook-at-step-startedcompleted120msok0
hook-at-step-completedcompleted120msok0
hook-at-hook-createdcompleted120msok0
deadline-hook-winscompleted71.0hok0
deadline-expirescompleted71.0hok0
long-sleepcompleted1130.0dok0
hook-never-arrivesstalled30msskipped0
step-retries-twicecompleted102.0sok0
parallel-stepscompleted90msok0
hook-on-execution-statecompleted120msok0
peek-hook-before-branchcompleted120msok0
peek-hook-after-branchcompleted120msok0
peek-hook-at-registrationcompleted120msok0
race-hook-before-probecompleted120msok0
race-hook-after-probecompleted120msok0
race-duplicate-deliverycompleted130msok0
attr-hook-before-stepcompleted110msok0
attr-hook-after-stepcompleted110msok0
attr-from-step-bodycompleted130msok0
fork-hook-after-timeoutcompleted141.0mok0
fork-hook-before-timeoutcompleted141.0mok0
count-hook-after-timeoutcompleted171.0mok0
count-hook-before-timeoutcompleted201.0mok0
stale-read-step-count-forkcompleted201.0mok0
stale-read-equal-step-countscompleted141.0mok0
step-vs-step-forkcompleted120msok0
step-vs-step-fork-fencedcompleted120msok0
fence-catches-benign-directioncompleted125msok0
in-flight-before-decisioncompleted171.0mok0
in-flight-before-decision-countedcompleted171.0mok0
in-flight-after-decisioncompleted192.0mok0
stale-read-step-count-fork-fencedcompleted201.0mok0
fork-hook-winscompleted131.0mok0
fork-timeout-winscompleted131.0mok0
unclaimed-payload-under-forkcompleted171.0mok0
claimed-payload-under-forkcompleted171.0mok0
writers-independent-step-bodiescompleted120msok0
writers-scripted-tempocompleted120msok0
cancel-mid-stepcancelled70msskipped0

Full trace: world-sim.txt

… file
Applies the unchanged-write guard from #3454 to the Astro and SvelteKit
builders, whose generated files live in Vite-watched source directories.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <peter.wielander@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor
FrameworkFlow routeStep reg.Framework output
hono200.9 KiB (±0)40.5 KiB (±0)1.76 MiB (±0)
nextjs-turbopack206.3 KiB (±0)439 B (±0)762.8 KiB (±0)
About these numbers

Sizes are gzip; parentheses show the change against main.
Flow route and Step reg. gate this job, on raw bytes rather than the gzip shown, at max(2%, 50.0 KiB). Framework output is informational.

555b143 · run

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this leaves the framework-specific behavior without automated regression coverage. A small integration test that builds Astro/SvelteKit twice and asserts watched generated outputs retain their mtime/inode would help prevent no-op writes from returning. Not blocking this change.

@VaguelySerious
VaguelySerious merged commit 0750fe8 into mainAug 25, 2026
182 of 184 checks passed
@VaguelySerious
VaguelySerious deleted the peter/guard-generated-writes-frameworks branch August 25, 2026 22:47
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 0750fe8 (AI decision).

The change depends on writeFileIfChanged from @workflow/builders, which was introduced by #3454 on main only — git grep writeFileIfChanged origin/stable finds nothing, so this cannot apply to stable without also backporting that main-only helper. It is also a dev-server efficiency improvement (avoiding redundant Vite invalidations/recompiles on no-op rebuilds) rather than a correctness, data-loss, or crash fix, so it does not meet the maintenance-branch bar on its own merits either.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

0750fe8958f50ee354cd24fe3f02a37c801bc4dc

@github-actionsgithub-actionsBot mentioned this pull request Aug 26, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(astro,sveltekit): skip generated writes that would not change the file - #3688

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/guard-generated-writes-frameworks
Aug 25, 2026
Merged

fix(astro,sveltekit): skip generated writes that would not change the file#3688
VaguelySerious merged 1 commit into
mainfrom
peter/guard-generated-writes-frameworks

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Aug 20, 2026

Copy link
Copy Markdown
Member

Description

Follow-up to #3454, which added the unchanged-write guard and applied it to @workflow/builders, @workflow/next and @workflow/nitro. The Astro and SvelteKit builders were not covered: they receive the bundle text from the base builder and do their own writeFile, so they never reach the guard. Their generated files live in Vite-watched source directories (src/pages/.well-known/workflow/v1 and src/routes/.well-known/workflow/v1), which is exactly the condition #3454 was about.

This routes those writes through writeFileIfChanged, and writes SvelteKit's static manifest copy from the same string rather than copyFile-ing it, matching what #3454 did for the Next public manifest.

Measured effect

Two consecutive pnpm build runs with no source change, comparing sha256 + mtime(ns) + inode of every generated file under the workflow directories, with WORKFLOW_PUBLIC_MANIFEST=1:

beforeafter
SvelteKit (src/routes/…, static/…)6 of 9 rewritten3
Astro (src/pages/…)5 of 8 rewritten3

Every one of those rewrites had an identical sha, i.e. they were pure no-op writes. Fixed here: both .gitignore files and the static/manifest.json copy on SvelteKit, and .gitignore plus manifest.json.js on Astro.

Relation to #3509

Rebased over #3509 (better SvelteKit routes-directory loading), which rewrote the same import block in packages/sveltekit/src/builder.ts. That was the only conflict and it was mechanical: keep its node:path import and drop the node:module/node:url imports it made dead, keep this PR's narrowed node:fs/promises import.

The two changes act at different layers and both still apply. #3509 caches the resolved routes directory so SvelteKit's secondary builds and worker threads do not reload the Svelte config and redo the build; this PR reduces what a build that does run actually writes. The measured numbers above were re-taken on top of #3509, with the guard-off control built from origin/main's builder, so the before/after pair is on the same base.

What this does NOT fix

Three files per framework still churn, for two reasons that need more than a call-site swap. Both are pre-existing and neither is made worse by this PR.

1. The read-transform-rewrite pattern defeats the guard — affects flow/+server.js and webhook/[token]/+server.js (SvelteKit), flow.js and webhook/[token].js (Astro).

Both builders call createCombinedBundle / createWebhookBundle, which write the route to disk through the base builder, then readFile it back, apply a regex transform, and write the transformed content. So each build writes the file twice with two different contents, and on the next build neither write can be skipped: the base write sees the previous build's transformed content on disk and differs from it, and the framework write then sees the untransformed content and differs from that. The signature is visible in the inode: the route gets a new inode (base write, temp+rename) and the webhook keeps its inode (framework write, plain overwrite).

The fix is to make the final content the only content written, e.g. by letting the framework pass its transform into the base builder so the route is generated in its final form. That is an API change to the base builder and wants its own PR.

2. One-shot esbuild.build() always rewrites its outfile — affects flow/__step_registrations.js on both frameworks.

ctx.rebuild() on an esbuild context skips writing when the output is byte-identical (verified on esbuild 0.28.1: mtime and inode both preserved). A one-shot esbuild.build() does not; it rewrites unconditionally. This is why Next dev, which uses contexts, was already protected for its esbuild outputs while these builders are not. The fix is either to use contexts in watch mode or to pass write: false and hand the output to writeFileIfChanged.

Other follow-ups from #3454

3. Nitro's dev handler should stop cache-busting off a single file's mtime.packages/nitro/src/index.ts derives version from statSync(handlerPath).mtimeMs for workflows.mjs and then uses it as the cache-buster for the steps.mjs import. A step-body-only edit leaves workflows.mjs identical, so the version freezes and stale step code is served. That dependency is the only reason skipsUnchangedGeneratedWrites exists.

Widening the key is cheap and keeps the per-request path to stats rather than file reads, since handlerImportPath and stepsImportPath are already in scope:

consts=statSync(handlerPath),t=statSync(stepsPath);constversion=`${s.size}:${s.mtimeMs}:${t.size}:${t.mtimeMs}`;

Including size costs nothing and narrows the case where two coalesced dev writes land on an effectively identical mtime. Note only the workflow/workflows.mjs variant needs the second stat; preloadSteps['workflow/webhook.mjs'] is empty. Both the v2 and v3 branches carry a copy. Once that lands, skipsUnchangedGeneratedWrites can be deleted from BaseBuilder and from the Nitro override.

4. There is no regression guard for the cross-framework case.write-if-changed.test.ts covers the helper and the opt-out, but nothing stops a new raw writeFile into a watched generated directory from silently restoring the redundant rebuild. The check that would catch it is the one used above: build twice, assert every generated file keeps its sha, mtime and inode. @workflow/astro has no test files today and @workflow/sveltekit has one unrelated test, so this needs a builder harness per framework rather than a one-line test.

How did you test your changes?

No automated coverage is added, for the reason in (4), so here are reproduction steps.

pnpm install && pnpm run build
cd workbench/sveltekit # and workbench/astroexport WORKFLOW_PUBLIC_MANIFEST=1
pnpm build
find src/routes/.well-known/workflow static/.well-known/workflow -type f \
| sort | xargs -I{} sh -c 'printf "%s %s %s\n" "$(shasum -a 256 {} | cut -d" " -f1)" "$(stat -f "%Fm:%i" {})" {}'> /tmp/s1
pnpm build
# ...repeat into /tmp/s2
diff /tmp/s1 /tmp/s2

Before this change the two .gitignore files and static/manifest.json appear in the diff with an identical sha and a moved mtime. After, they do not. The three files listed above still appear, for the reasons given.

Also run: pnpm run build (28/28 tasks), npx biome check on both changed files.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run to create a changelog for this PR
  • 🔒 DCO sign-off passes (run git commit --signoff on your commits)
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete

@vercel

vercelBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreview, v0Aug 25, 2026 8:55pm
example-nextjs-workflow-webpackReadyReadyPreview, v0Aug 25, 2026 8:55pm
example-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-astro-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-express-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-fastify-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-hono-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nestjs-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nitro-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nuxt-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-python-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-sveltekit-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-tanstack-start-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-vite-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-docsReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-swc-playgroundReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-tarballsReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-webReadyReadyPreview, v0Aug 25, 2026 8:55pm

@changeset-bot

changeset-botBot commented Aug 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 555b143

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/astroPatch
@workflow/sveltekitPatch
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 555b143 · Tue, 25 Aug 2026 21:17:13 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1215 (+465%) 🔻1315 🔴 (+33%) 🔻1332 🔴 (+29%) 🔻1375 🔴 (-4.8%)30
TTFSstream217 (+20%) 🔻1291 🔴 (+36%) 🔻1314 🔴 (+36%) 🔻1365 🔴 (+35%) 🔻30
TTFShook + stream1447 (+180%) 🔻1597 🔴 (+18%) 🔻1666 🔴 (+11%)1940 🔴 (-62%) 💚30
Fan-out TTFSPromise.all(100 steps)806 (+44%) 🔻1955 (+96%) 🔻1978 (+40%) 🔻2016 (+24%) 🔻10
Fan-out TTLSPromise.all(100 steps)2542 (+31%) 🔻5184 (+87%) 🔻6673 (+99%) 🔻9405 (+43%) 🔻10
STSO1020 steps (inline)99 (-7.5%)148 (+14%)165 (+8.6%)233 (+14%)1019
WO1020 steps146183 (+11%)146183 (+11%)146183 (+11%)146183 (+11%)1
CRTTfirst chunk (pooled)104 (+24%) 🔻145 (+9.0%)167 (+7.7%)271 (+13%)28

Streams

ScenarioCRTT 1stp75p90p99CDV maxiters
paced control (100/s, 60B)138 (+25%)156 (-3%)230 (-44%)419 (-34%)161 (+24%)10
size sweep (100/s, 160B-12KB)128 (+15%)164 (+20%)228 (-36%)629 (-20%)160 (+53%)10
replay gateway-gpt-5.4-nano-2000t (1x)113 (-23%)139 (+1%)187 (-28%)745 (-16%)368 (+11%)3
replay eve-gpt-5.6-sol-2000t (1x)195 (+7%)150 (+22%)192 (+19%)436 (+31%)434 (+36%)2
replay eve-gpt-5.6-sol-2000t (2x)145 (+14%)182 (+22%)241 (+25%)491 (+30%)286 (+10%)3
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 132084ms → this run 144847ms (Δ +12763ms, +10%)

 50-100 ms ┃ main 0 this 1 +1
100-150 ms ████████████████████┃███ main 905 this 781 -124
150-200 ms ███░░┃ main 103 this 217 +114
200-250 ms ┃ main 5 this 13 +8
250-300 ms ┃ main 2 this 2 +0
300-350 ms ┃ main 1 this 2 +1
350-400 ms ┃ main 1 this 2 +1
400-450 ms ┃ main 2 this 1 -1
📈 CRTT drill-down vs main (RTT distributions & profiles)
variant RTT 1ms→5s+ avg p50 p90 p99 n
control ······▃█▁···· 129.5 (+3%) 120 (+21%) 230 (-44%) 419 (-34%) 3000
sweep ······▂█▁▁··· 138.6 (+18%) 126 (+29%) 228 (-36%) 629 (-20%) 3000
gw 1x ·····▁▆█▁▁··· 119.6 (±0%) 105 (+9%) 187 (-28%) 745 (-16%) 5295
eve 1x ·····▁▅█▁▁··· 124.7 (+17%) 109 (+14%) 192 (+19%) 436 (+31%) 5186
eve 2x ·····▁▂█▂▁··· 150.3 (+20%) 134 (+16%) 241 (+25%) 491 (+30%) 7779

RTT over stream progress (avg per tenth of stream, bars scaled min→max):

control ▆█▄▃▂▁▄▂▄▃ 111–157ms
sweep ▃▃▃▂▂▁▄█▃▂ 121–184ms
gw 1x ▂▂▃▃▁█▂▂▁▂ 103–181ms
eve 1x ▄▁▃▆▃█▇▆▄▂ 106–146ms
eve 2x ▁▂▁▁▅▄▄█▆▃ 125–196ms

RTT by chunk size (avg per log size bin, ~160B → ~12KB serialized, bars scaled min→max):

sweep █▇▂▁▃▄▅ 136–141ms

Delivery jitter over stream progress (avg positive CDV per tenth of stream, bars scaled min→max):

control ▂▇▄▄▇▁█▄▅▄ 34–43ms
sweep ▁▄▁▄▆▄▄█▃▃ 47–68ms
gw 1x ▃▂▇▂▃█▃▂▁▄ 31–40ms
eve 1x ▄▁▂▆▆▆█▅▄▆ 20–26ms
eve 2x ▇▇▁▄▇█▅▁▅█ 19–27ms
📜 Previous results (1)

78301f5

Thu, 20 Aug 2026 01:50:16 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep458 (-58%) 💚1476 🔴 (+19%) 🔻1524 🔴 (+15%) 🔻1611 🔴 (-1.1%)30
TTFSstream1295 (+275%) 🔻1477 🔴 (+28%) 🔻1538 🔴 (+29%) 🔻1675 🔴 (+36%) 🔻30
TTFShook + stream1635 (+50%) 🔻2181 🔴 (+41%) 🔻2203 🔴 (+40%) 🔻2331 🔴 (-12%)30
Fan-out TTFSPromise.all(100 steps)593 (+10%)938 (-42%) 💚2355 (+42%) 🔻2542 (+40%) 🔻10
Fan-out TTLSPromise.all(100 steps)4866 (-2.6%)5507 (-11%)6831 (-2.6%)12617 (+65%) 🔻10
STSO1020 steps (inline)105 (-29%) 💚174 (-35%) 💚190 (-44%) 💚304 (-57%) 💚1019
WO1020 steps173964 (-34%) 💚173964 (-34%) 💚173964 (-34%) 💚173964 (-34%) 💚1
CRTTfirst chunk (pooled)124 (+20%) 🔻202 (+28%) 🔻300 (-8.0%)587 (+41%) 🔻28

Streams

ScenarioCRTT 1stp75p90p99CDV maxiters
paced control (100/s, 60B)163 (+14%)172 (-20%)257 (-33%)446 (-40%)119 (-61%)10
size sweep (100/s, 160B-12KB)168 (+21%)205 (-3%)256 (-26%)363 (-36%)139 (-16%)10
replay gateway-gpt-5.4-nano-2000t (1x)300 (+110%)160 (-45%)199 (-58%)532 (-23%)200 (-53%)3
replay eve-gpt-5.6-sol-2000t (1x)174 (+17%)163 (-54%)204 (-59%)582 (-34%)411 (-28%)2
replay eve-gpt-5.6-sol-2000t (2x)228 (+63%)251 (-50%)544 (-17%)1091 (+6%)542 (+19%)3
ℹ️ Metric definitions & methodology

Streams: first-chunk RTT (the stream-open path, before any buffering/backpressure), CRTT percentiles, and worst delivery stall (CDV max). Cells are medians across iterations; per-run values in the artifacts. No 🔴/🟢 marks until targets attach.

The collapsed STSO distribution section above buckets every step gap, split inline (same warm process — pure framework overhead) vs queue-hop (fresh process — dispatch, reinit, replay). = main, = this run, = fill.

The collapsed CRTT drill-down: per-variant RTT histograms (fixed log bins, · = empty) and mean RTT/positive-CDV profile lines over stream progress and chunk size. Histograms, avgs, and profiles merge exactly across runs; p50–p99 are percentile-of-percentiles. Per-index rows live in the artifacts.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body) · Fan-out TTFS: fan-out time to first step (in-deployment start() → first of the parallel step bodies to complete) · Fan-out TTLS: fan-out time to last step (in-deployment start() → last of the parallel step bodies to complete, i.e. when the Promise.all resolves) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · CRTT: chunk round-trip time (per-chunk write → read latency, one clock domain: deployment → stream backend → same deployment) · CDV: chunk delay variation / delivery jitter (inter-arrival gap minus inter-write gap per seq-adjacent pair; skew-free; the row is each run's MAX positive value, so one stall moves it)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · Promise.all(100 steps): 100 trivial no-op steps started together in a single Promise.all; Fan-out TTFS is the first of them to complete and Fan-out TTLS the last, both from the in-deployment clientStart, so their gap is the spread the runtime adds across the fan-out · paced control (100/s, 60B): the control: 300 tiny (~60B) deltas metronome-paced at 100/s — zero workload structure, so it reads the transport floor and flush cadence, and disambiguates transport-wide vs workload-specific when a replay row moves · size sweep (100/s, 160B-12KB): same pacing as the control with deltas padded in rotation across seven log-spaced sizes (~160B–12KB) — rotation decouples size from stream position, so it isolates whether chunk size causes latency · replay gateway-gpt-5.4-nano-2000t (1x): raw provider SSE cadence captured at the AI gateway boundary (gpt-5.4-nano, the most popular gateway model; per-token deltas p50 208B = the modal production chunk size), replayed exactly as measured — the typical customer's workload; its CDV is the typical customer's real delivery jitter · replay eve-gpt-5.6-sol-2000t (1x): a captured eve turn (gpt-5.6-sol, the most-used demanding eve model; ~2000 output tokens = production p50 turn length) replayed exactly as measured — eve's envelope protocol re-ships the cumulative message so sizes ramp 142B→13KB; the demanding outlier tenant's reality · replay eve-gpt-5.6-sol-2000t (2x): the same eve capture at 2x — the headroom/stress row; real fast-tier models emit the same chunk sizes at proportionally higher rate, so time compression is a faithful speed model · first chunk (pooled): every run's seq-0 RTT pooled across all stream scenarios — the first chunk precedes any workload differentiation, so pooling samples one shared stream-open path with exact percentiles

Replay cadences (semantic sha256) — eve-gpt-5.6-sol-2000teaf22f5946e7c61f3c65c7006d550df180cfabd4e706254a09f22aec0cfb420d · gateway-gpt-5.4-nano-2000t6f24ac518b6b83ff1d0e85a5fe78230db192716d66a7fc6b2fe022752001d041

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600

All timestamps are deployment-side; runs are triggered in-deployment, so the CI runner and api.vercel.com sit outside every measured window. TTFS = start() → first step body (includes dispatch + any cold start); Fan-out TTFS/TTLS = first/last step completion of one Promise.all from the same anchor (the gap is the runtime’s fan-out spread); STSO/WO between step bodies; CRTT inside the workflow (excludes the api.vercel.com read path).

Cold starts stay in the numbers (real bursty-workload latency, inflates P75+); Best is the warm floor.

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

▲ Vercel Production (8 failed)

python-node (8 failed):

  • promiseAllWorkflow | wrun_41M0XB8RB20GPSEEE0BP1F0AX5 | 🔍 observability
  • sleepingWorkflow | wrun_41M0XB9DN80GT0JYGEYSRDG0AZ | 🔍 observability
  • parallelSleepWorkflow | wrun_41M0XB9DSC0GGQFNQPSTD10TFN | 🔍 observability
  • nullByteWorkflow | wrun_41M0XB9MEH0GZ1PE9VA8VT338J | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41M0XBE3DY0GXJZZKSDADPJ273 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41M0XBE4D70GMTA2YASGGSMBW6 | 🔍 observability
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M0XBEDKS0GX7MW0EJVZNDKG1 | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M0XBEYPA0GT79Q7M4KQGXE2R | 🔍 observability

🌐 Cross-language Conformance (9 failed)

python (9 failed):

  • deploymentId: 'latest' is a no-op in non-Vercel worlds | wrun_01M0XBHEZA0V7PQ3KYK30VCQV6
  • promiseAllWorkflow | wrun_41M0XB8RB20GPSEEE0BP1F0AX5
  • sleepingWorkflow | wrun_41M0XB9DN80GT0JYGEYSRDG0AZ
  • parallelSleepWorkflow | wrun_41M0XB9DSC0GGQFNQPSTD10TFN
  • nullByteWorkflow | wrun_41M0XB9MEH0GZ1PE9VA8VT338J
  • cancelRun - cancelling a running workflow | wrun_41M0XBE3DY0GXJZZKSDADPJ273
  • cancelRun via CLI - cancelling a running workflow | wrun_41M0XBE4D70GMTA2YASGGSMBW6
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M0XBEDKS0GX7MW0EJVZNDKG1
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M0XBEYPA0GT79Q7M4KQGXE2R

⚠️ Flaky E2E Tests (passed on retry)

These tests failed at least once and passed on a retry. A recurring entry here is a real race worth investigating.

  • cancelRun via CLI - cancelling a running workflow (nextjs-turbopack)
  • cancelRun via CLI - cancelling a running workflow (sveltekit)
  • deploymentId: 'latest' is a no-op in non-Vercel worlds (tanstack-start)
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (nextjs-webpack)

🛠 Infra Events (absorbed by the harness)

Platform anomalies the e2e harness detected and worked around (e.g. a run the queue never picked up, replaced by a fresh run). Clustered timestamps indicate a backend blip; a steady drip indicates a platform issue worth escalating.

33 infra events
  • cold-start-warmup · suite warmup (python) · at 20:55:38Z · abandoned wrun_41M0XB6E7T0GJ9W8791BM6Z01B · (+7 more)
  • run-pickup-stall · sleepingWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TD0GMM929KHMH4A5PQ
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TD0GMM929KHMH4A5PR
  • run-pickup-stall · nullByteWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TG0GXS3FJPHWZ0AP3Z
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA34B0GKTASFX3VHN7ZF5
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:55:55Z · abandoned wrun_41M0XBA2T60GKVRBKEPG9JFNGQ
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 20:56:27Z · abandoned wrun_41M0XBB2NE0GYG9ZKWYWVYN0QG
  • run-pickup-stall · sleepingWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY390GR8JJH6G2563BMK
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY170GNNFJ2HYJF76VRJ
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY1P0GSFSBE1ENV48TWY
  • run-pickup-stall · nullByteWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY2C0GXP8214VHPKYDEN
  • cold-start-warmup · suite warmup (tanstack-start) · at 20:57:08Z · abandoned wrun_01M0XBC0795S33S7QA0QCC6MWM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 20:57:45Z · abandoned wrun_41M0XBC1V20GQEJYYHQRSWTMC5
  • cold-start-warmup · suite warmup (python) · at 20:58:25Z · abandoned wrun_01M0XBBGHK2AQJ0WK3BTSQXSBE · (+7 more)
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PANZ6ZFDYD87WHMYEG
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PANZ6ZFDYD87WHMYEH
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PJ61BWHDADBWKVQB4M
  • run-pickup-stall · nullByteWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PMHJQTM6D1Q5A1DGP8
  • run-pickup-stall · sleepingWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PFRRDDXM9ZQTKATH0V
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 20:59:26Z · abandoned wrun_41M0XBENXS0GNP64ENYV9QCDR6
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 20:59:26Z · abandoned wrun_41M0XBESQJ0GNJ9RHK6DQT9QJG
  • run-pickup-stall · sleepingWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A76KFYN7SAWZTZNJVC
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A4Q23JB7QFRPHW2M57
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A6RKK450KHQ6KKBNDM
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A861P4CVWVBHC9GRPV
  • run-pickup-stall · nullByteWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0ACMT5YMDRR1CRNSE1B
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 21:00:40Z · abandoned wrun_01M0XBJTXTEA82N9RWT7V8MWFM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 21:00:40Z · abandoned wrun_01M0XBJTXXTC9C59S58QCXKQDY
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 21:00:40Z · abandoned wrun_01M0XBJTY41EX832TVQ4QR069P
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 21:01:10Z · abandoned wrun_01M0XBKRA4M8A4FPV04NJY23SD
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 21:01:10Z · abandoned wrun_01M0XBKRA5GJYZXB04F49JH2DV
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 21:01:40Z · abandoned wrun_01M0XBMNH7M7QPBBRWZ6AX8HGC
  • run-pickup-stall · hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep (nextjs-webpack) · at 21:02:31Z · abandoned wrun_01M0XBP7PT25DV3EYT1BDRVKMK

E2E Test Summary

Summary
PassedFailedSkippedTotal
❌ ▲ Vercel Production357087424320
✅ 💻 Local Development392205584480
✅ 📦 Local Production392205584480
✅ 🐘 Local Postgres392205584480
✅ 🪟 Windows32000320
❌ 🌐 Cross-language Conformance09132141
✅ vercel-http-transport8170143960
✅ vercel-multi-region270027
✅ vercel-ws-transport553087640
Total1705317277819848
Details by Category

❌ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node132028
✅ astro-quickjs132028
✅ example-node132028
✅ example-quickjs132028
✅ express-node132028
✅ express-quickjs132028
✅ fastify-node132028
✅ fastify-quickjs132028
✅ hono-node132028
✅ hono-quickjs132028
✅ nest-node132028
✅ nest-quickjs132028
✅ nextjs-turbopack-node15703
✅ nextjs-turbopack-quickjs15703
✅ nextjs-webpack-node15703
✅ nextjs-webpack-quickjs15703
✅ nitro-node132028
✅ nitro-quickjs132028
✅ nuxt-node132028
✅ nuxt-quickjs132028
❌ python-node08152
✅ sveltekit-node15109
✅ sveltekit-quickjs15109
✅ tanstack-start-node132028
✅ tanstack-start-quickjs132028
✅ vite-node132028
✅ vite-quickjs132028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node16000
✅ nextjs-turbopack-quickjs16000

❌ 🌐 Cross-language Conformance

AppPassedFailedSkipped
❌ python09132

✅ vercel-http-transport

AppPassedFailedSkipped
✅ example132028
✅ express132028
✅ hono132028
✅ nextjs-turbopack15703
✅ nitro132028
✅ vite132028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

✅ vercel-ws-transport

AppPassedFailedSkipped
✅ example132028
✅ express132028
✅ nextjs-turbopack15703
✅ vite132028

📋 View full workflow run

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Sim World

Simulated world deterministic testing for races. Traces

🟠 world-sim scenario book — 1 fail of 41 total

fence=per-spec

scenariooutcomeeventsvirtreplayviolations
smoke-no-stepscompleted30msok0
smoke-one-stepcompleted60msok0
hook-at-step-startedcompleted120msok0
hook-at-step-completedcompleted120msok0
hook-at-hook-createdcompleted120msok0
deadline-hook-winscompleted71.0hok0
deadline-expirescompleted71.0hok0
long-sleepcompleted1130.0dok0
hook-never-arrivesstalled30msskipped0
step-retries-twicecompleted102.0sok0
parallel-stepscompleted90msok0
hook-on-execution-statecompleted120msok0
peek-hook-before-branchcompleted120msok0
peek-hook-after-branchcompleted120msok0
peek-hook-at-registrationcompleted120msok0
race-hook-before-probecompleted120msok0
race-hook-after-probecompleted120msok0
race-duplicate-deliverycompleted130msok0
attr-hook-before-stepcompleted110msok0
attr-hook-after-stepcompleted110msok0
attr-from-step-bodycompleted130msok0
fork-hook-after-timeoutcompleted141.0mok0
fork-hook-before-timeoutcompleted141.0mok0
count-hook-after-timeoutcompleted171.0mok0
count-hook-before-timeoutcompleted201.0mok0
stale-read-step-count-forkcompleted201.0mok0
stale-read-equal-step-countscompleted141.0mok0
step-vs-step-forkcompleted120msok0
step-vs-step-fork-fencedcompleted120msok0
fence-catches-benign-directioncompleted125msok0
in-flight-before-decisioncompleted171.0mok0
in-flight-before-decision-countedcompleted171.0mok0
in-flight-after-decisioncompleted192.0mok0
stale-read-step-count-fork-fencedcompleted201.0mok0
fork-hook-winscompleted131.0mok0
fork-timeout-winscompleted131.0mok0
unclaimed-payload-under-forkcompleted171.0mok0
claimed-payload-under-forkcompleted171.0mok0
writers-independent-step-bodiescompleted120msok0
writers-scripted-tempocompleted120msok0
cancel-mid-stepcancelled70msskipped0

Full trace: world-sim.txt

… file
Applies the unchanged-write guard from #3454 to the Astro and SvelteKit
builders, whose generated files live in Vite-watched source directories.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <peter.wielander@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor
FrameworkFlow routeStep reg.Framework output
hono200.9 KiB (±0)40.5 KiB (±0)1.76 MiB (±0)
nextjs-turbopack206.3 KiB (±0)439 B (±0)762.8 KiB (±0)
About these numbers

Sizes are gzip; parentheses show the change against main.
Flow route and Step reg. gate this job, on raw bytes rather than the gzip shown, at max(2%, 50.0 KiB). Framework output is informational.

555b143 · run

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this leaves the framework-specific behavior without automated regression coverage. A small integration test that builds Astro/SvelteKit twice and asserts watched generated outputs retain their mtime/inode would help prevent no-op writes from returning. Not blocking this change.

@VaguelySerious
VaguelySerious merged commit 0750fe8 into mainAug 25, 2026
182 of 184 checks passed
@VaguelySerious
VaguelySerious deleted the peter/guard-generated-writes-frameworks branch August 25, 2026 22:47
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 0750fe8 (AI decision).

The change depends on writeFileIfChanged from @workflow/builders, which was introduced by #3454 on main only — git grep writeFileIfChanged origin/stable finds nothing, so this cannot apply to stable without also backporting that main-only helper. It is also a dev-server efficiency improvement (avoiding redundant Vite invalidations/recompiles on no-op rebuilds) rather than a correctness, data-loss, or crash fix, so it does not meet the maintenance-branch bar on its own merits either.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

0750fe8958f50ee354cd24fe3f02a37c801bc4dc

@github-actionsgithub-actionsBot mentioned this pull request Aug 26, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(astro,sveltekit): skip generated writes that would not change the file - #3688

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/guard-generated-writes-frameworks
Aug 25, 2026
Merged

fix(astro,sveltekit): skip generated writes that would not change the file#3688
VaguelySerious merged 1 commit into
mainfrom
peter/guard-generated-writes-frameworks

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Aug 20, 2026

Copy link
Copy Markdown
Member

Description

Follow-up to #3454, which added the unchanged-write guard and applied it to @workflow/builders, @workflow/next and @workflow/nitro. The Astro and SvelteKit builders were not covered: they receive the bundle text from the base builder and do their own writeFile, so they never reach the guard. Their generated files live in Vite-watched source directories (src/pages/.well-known/workflow/v1 and src/routes/.well-known/workflow/v1), which is exactly the condition #3454 was about.

This routes those writes through writeFileIfChanged, and writes SvelteKit's static manifest copy from the same string rather than copyFile-ing it, matching what #3454 did for the Next public manifest.

Measured effect

Two consecutive pnpm build runs with no source change, comparing sha256 + mtime(ns) + inode of every generated file under the workflow directories, with WORKFLOW_PUBLIC_MANIFEST=1:

beforeafter
SvelteKit (src/routes/…, static/…)6 of 9 rewritten3
Astro (src/pages/…)5 of 8 rewritten3

Every one of those rewrites had an identical sha, i.e. they were pure no-op writes. Fixed here: both .gitignore files and the static/manifest.json copy on SvelteKit, and .gitignore plus manifest.json.js on Astro.

Relation to #3509

Rebased over #3509 (better SvelteKit routes-directory loading), which rewrote the same import block in packages/sveltekit/src/builder.ts. That was the only conflict and it was mechanical: keep its node:path import and drop the node:module/node:url imports it made dead, keep this PR's narrowed node:fs/promises import.

The two changes act at different layers and both still apply. #3509 caches the resolved routes directory so SvelteKit's secondary builds and worker threads do not reload the Svelte config and redo the build; this PR reduces what a build that does run actually writes. The measured numbers above were re-taken on top of #3509, with the guard-off control built from origin/main's builder, so the before/after pair is on the same base.

What this does NOT fix

Three files per framework still churn, for two reasons that need more than a call-site swap. Both are pre-existing and neither is made worse by this PR.

1. The read-transform-rewrite pattern defeats the guard — affects flow/+server.js and webhook/[token]/+server.js (SvelteKit), flow.js and webhook/[token].js (Astro).

Both builders call createCombinedBundle / createWebhookBundle, which write the route to disk through the base builder, then readFile it back, apply a regex transform, and write the transformed content. So each build writes the file twice with two different contents, and on the next build neither write can be skipped: the base write sees the previous build's transformed content on disk and differs from it, and the framework write then sees the untransformed content and differs from that. The signature is visible in the inode: the route gets a new inode (base write, temp+rename) and the webhook keeps its inode (framework write, plain overwrite).

The fix is to make the final content the only content written, e.g. by letting the framework pass its transform into the base builder so the route is generated in its final form. That is an API change to the base builder and wants its own PR.

2. One-shot esbuild.build() always rewrites its outfile — affects flow/__step_registrations.js on both frameworks.

ctx.rebuild() on an esbuild context skips writing when the output is byte-identical (verified on esbuild 0.28.1: mtime and inode both preserved). A one-shot esbuild.build() does not; it rewrites unconditionally. This is why Next dev, which uses contexts, was already protected for its esbuild outputs while these builders are not. The fix is either to use contexts in watch mode or to pass write: false and hand the output to writeFileIfChanged.

Other follow-ups from #3454

3. Nitro's dev handler should stop cache-busting off a single file's mtime.packages/nitro/src/index.ts derives version from statSync(handlerPath).mtimeMs for workflows.mjs and then uses it as the cache-buster for the steps.mjs import. A step-body-only edit leaves workflows.mjs identical, so the version freezes and stale step code is served. That dependency is the only reason skipsUnchangedGeneratedWrites exists.

Widening the key is cheap and keeps the per-request path to stats rather than file reads, since handlerImportPath and stepsImportPath are already in scope:

consts=statSync(handlerPath),t=statSync(stepsPath);constversion=`${s.size}:${s.mtimeMs}:${t.size}:${t.mtimeMs}`;

Including size costs nothing and narrows the case where two coalesced dev writes land on an effectively identical mtime. Note only the workflow/workflows.mjs variant needs the second stat; preloadSteps['workflow/webhook.mjs'] is empty. Both the v2 and v3 branches carry a copy. Once that lands, skipsUnchangedGeneratedWrites can be deleted from BaseBuilder and from the Nitro override.

4. There is no regression guard for the cross-framework case.write-if-changed.test.ts covers the helper and the opt-out, but nothing stops a new raw writeFile into a watched generated directory from silently restoring the redundant rebuild. The check that would catch it is the one used above: build twice, assert every generated file keeps its sha, mtime and inode. @workflow/astro has no test files today and @workflow/sveltekit has one unrelated test, so this needs a builder harness per framework rather than a one-line test.

How did you test your changes?

No automated coverage is added, for the reason in (4), so here are reproduction steps.

pnpm install && pnpm run build
cd workbench/sveltekit # and workbench/astroexport WORKFLOW_PUBLIC_MANIFEST=1
pnpm build
find src/routes/.well-known/workflow static/.well-known/workflow -type f \
| sort | xargs -I{} sh -c 'printf "%s %s %s\n" "$(shasum -a 256 {} | cut -d" " -f1)" "$(stat -f "%Fm:%i" {})" {}'> /tmp/s1
pnpm build
# ...repeat into /tmp/s2
diff /tmp/s1 /tmp/s2

Before this change the two .gitignore files and static/manifest.json appear in the diff with an identical sha and a moved mtime. After, they do not. The three files listed above still appear, for the reasons given.

Also run: pnpm run build (28/28 tasks), npx biome check on both changed files.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run to create a changelog for this PR
  • 🔒 DCO sign-off passes (run git commit --signoff on your commits)
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete

@vercel

vercelBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreview, v0Aug 25, 2026 8:55pm
example-nextjs-workflow-webpackReadyReadyPreview, v0Aug 25, 2026 8:55pm
example-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-astro-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-express-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-fastify-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-hono-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nestjs-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nitro-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nuxt-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-python-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-sveltekit-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-tanstack-start-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-vite-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-docsReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-swc-playgroundReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-tarballsReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-webReadyReadyPreview, v0Aug 25, 2026 8:55pm

@changeset-bot

changeset-botBot commented Aug 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 555b143

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/astroPatch
@workflow/sveltekitPatch
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 555b143 · Tue, 25 Aug 2026 21:17:13 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1215 (+465%) 🔻1315 🔴 (+33%) 🔻1332 🔴 (+29%) 🔻1375 🔴 (-4.8%)30
TTFSstream217 (+20%) 🔻1291 🔴 (+36%) 🔻1314 🔴 (+36%) 🔻1365 🔴 (+35%) 🔻30
TTFShook + stream1447 (+180%) 🔻1597 🔴 (+18%) 🔻1666 🔴 (+11%)1940 🔴 (-62%) 💚30
Fan-out TTFSPromise.all(100 steps)806 (+44%) 🔻1955 (+96%) 🔻1978 (+40%) 🔻2016 (+24%) 🔻10
Fan-out TTLSPromise.all(100 steps)2542 (+31%) 🔻5184 (+87%) 🔻6673 (+99%) 🔻9405 (+43%) 🔻10
STSO1020 steps (inline)99 (-7.5%)148 (+14%)165 (+8.6%)233 (+14%)1019
WO1020 steps146183 (+11%)146183 (+11%)146183 (+11%)146183 (+11%)1
CRTTfirst chunk (pooled)104 (+24%) 🔻145 (+9.0%)167 (+7.7%)271 (+13%)28

Streams

ScenarioCRTT 1stp75p90p99CDV maxiters
paced control (100/s, 60B)138 (+25%)156 (-3%)230 (-44%)419 (-34%)161 (+24%)10
size sweep (100/s, 160B-12KB)128 (+15%)164 (+20%)228 (-36%)629 (-20%)160 (+53%)10
replay gateway-gpt-5.4-nano-2000t (1x)113 (-23%)139 (+1%)187 (-28%)745 (-16%)368 (+11%)3
replay eve-gpt-5.6-sol-2000t (1x)195 (+7%)150 (+22%)192 (+19%)436 (+31%)434 (+36%)2
replay eve-gpt-5.6-sol-2000t (2x)145 (+14%)182 (+22%)241 (+25%)491 (+30%)286 (+10%)3
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 132084ms → this run 144847ms (Δ +12763ms, +10%)

 50-100 ms ┃ main 0 this 1 +1
100-150 ms ████████████████████┃███ main 905 this 781 -124
150-200 ms ███░░┃ main 103 this 217 +114
200-250 ms ┃ main 5 this 13 +8
250-300 ms ┃ main 2 this 2 +0
300-350 ms ┃ main 1 this 2 +1
350-400 ms ┃ main 1 this 2 +1
400-450 ms ┃ main 2 this 1 -1
📈 CRTT drill-down vs main (RTT distributions & profiles)
variant RTT 1ms→5s+ avg p50 p90 p99 n
control ······▃█▁···· 129.5 (+3%) 120 (+21%) 230 (-44%) 419 (-34%) 3000
sweep ······▂█▁▁··· 138.6 (+18%) 126 (+29%) 228 (-36%) 629 (-20%) 3000
gw 1x ·····▁▆█▁▁··· 119.6 (±0%) 105 (+9%) 187 (-28%) 745 (-16%) 5295
eve 1x ·····▁▅█▁▁··· 124.7 (+17%) 109 (+14%) 192 (+19%) 436 (+31%) 5186
eve 2x ·····▁▂█▂▁··· 150.3 (+20%) 134 (+16%) 241 (+25%) 491 (+30%) 7779

RTT over stream progress (avg per tenth of stream, bars scaled min→max):

control ▆█▄▃▂▁▄▂▄▃ 111–157ms
sweep ▃▃▃▂▂▁▄█▃▂ 121–184ms
gw 1x ▂▂▃▃▁█▂▂▁▂ 103–181ms
eve 1x ▄▁▃▆▃█▇▆▄▂ 106–146ms
eve 2x ▁▂▁▁▅▄▄█▆▃ 125–196ms

RTT by chunk size (avg per log size bin, ~160B → ~12KB serialized, bars scaled min→max):

sweep █▇▂▁▃▄▅ 136–141ms

Delivery jitter over stream progress (avg positive CDV per tenth of stream, bars scaled min→max):

control ▂▇▄▄▇▁█▄▅▄ 34–43ms
sweep ▁▄▁▄▆▄▄█▃▃ 47–68ms
gw 1x ▃▂▇▂▃█▃▂▁▄ 31–40ms
eve 1x ▄▁▂▆▆▆█▅▄▆ 20–26ms
eve 2x ▇▇▁▄▇█▅▁▅█ 19–27ms
📜 Previous results (1)

78301f5

Thu, 20 Aug 2026 01:50:16 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep458 (-58%) 💚1476 🔴 (+19%) 🔻1524 🔴 (+15%) 🔻1611 🔴 (-1.1%)30
TTFSstream1295 (+275%) 🔻1477 🔴 (+28%) 🔻1538 🔴 (+29%) 🔻1675 🔴 (+36%) 🔻30
TTFShook + stream1635 (+50%) 🔻2181 🔴 (+41%) 🔻2203 🔴 (+40%) 🔻2331 🔴 (-12%)30
Fan-out TTFSPromise.all(100 steps)593 (+10%)938 (-42%) 💚2355 (+42%) 🔻2542 (+40%) 🔻10
Fan-out TTLSPromise.all(100 steps)4866 (-2.6%)5507 (-11%)6831 (-2.6%)12617 (+65%) 🔻10
STSO1020 steps (inline)105 (-29%) 💚174 (-35%) 💚190 (-44%) 💚304 (-57%) 💚1019
WO1020 steps173964 (-34%) 💚173964 (-34%) 💚173964 (-34%) 💚173964 (-34%) 💚1
CRTTfirst chunk (pooled)124 (+20%) 🔻202 (+28%) 🔻300 (-8.0%)587 (+41%) 🔻28

Streams

ScenarioCRTT 1stp75p90p99CDV maxiters
paced control (100/s, 60B)163 (+14%)172 (-20%)257 (-33%)446 (-40%)119 (-61%)10
size sweep (100/s, 160B-12KB)168 (+21%)205 (-3%)256 (-26%)363 (-36%)139 (-16%)10
replay gateway-gpt-5.4-nano-2000t (1x)300 (+110%)160 (-45%)199 (-58%)532 (-23%)200 (-53%)3
replay eve-gpt-5.6-sol-2000t (1x)174 (+17%)163 (-54%)204 (-59%)582 (-34%)411 (-28%)2
replay eve-gpt-5.6-sol-2000t (2x)228 (+63%)251 (-50%)544 (-17%)1091 (+6%)542 (+19%)3
ℹ️ Metric definitions & methodology

Streams: first-chunk RTT (the stream-open path, before any buffering/backpressure), CRTT percentiles, and worst delivery stall (CDV max). Cells are medians across iterations; per-run values in the artifacts. No 🔴/🟢 marks until targets attach.

The collapsed STSO distribution section above buckets every step gap, split inline (same warm process — pure framework overhead) vs queue-hop (fresh process — dispatch, reinit, replay). = main, = this run, = fill.

The collapsed CRTT drill-down: per-variant RTT histograms (fixed log bins, · = empty) and mean RTT/positive-CDV profile lines over stream progress and chunk size. Histograms, avgs, and profiles merge exactly across runs; p50–p99 are percentile-of-percentiles. Per-index rows live in the artifacts.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body) · Fan-out TTFS: fan-out time to first step (in-deployment start() → first of the parallel step bodies to complete) · Fan-out TTLS: fan-out time to last step (in-deployment start() → last of the parallel step bodies to complete, i.e. when the Promise.all resolves) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · CRTT: chunk round-trip time (per-chunk write → read latency, one clock domain: deployment → stream backend → same deployment) · CDV: chunk delay variation / delivery jitter (inter-arrival gap minus inter-write gap per seq-adjacent pair; skew-free; the row is each run's MAX positive value, so one stall moves it)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · Promise.all(100 steps): 100 trivial no-op steps started together in a single Promise.all; Fan-out TTFS is the first of them to complete and Fan-out TTLS the last, both from the in-deployment clientStart, so their gap is the spread the runtime adds across the fan-out · paced control (100/s, 60B): the control: 300 tiny (~60B) deltas metronome-paced at 100/s — zero workload structure, so it reads the transport floor and flush cadence, and disambiguates transport-wide vs workload-specific when a replay row moves · size sweep (100/s, 160B-12KB): same pacing as the control with deltas padded in rotation across seven log-spaced sizes (~160B–12KB) — rotation decouples size from stream position, so it isolates whether chunk size causes latency · replay gateway-gpt-5.4-nano-2000t (1x): raw provider SSE cadence captured at the AI gateway boundary (gpt-5.4-nano, the most popular gateway model; per-token deltas p50 208B = the modal production chunk size), replayed exactly as measured — the typical customer's workload; its CDV is the typical customer's real delivery jitter · replay eve-gpt-5.6-sol-2000t (1x): a captured eve turn (gpt-5.6-sol, the most-used demanding eve model; ~2000 output tokens = production p50 turn length) replayed exactly as measured — eve's envelope protocol re-ships the cumulative message so sizes ramp 142B→13KB; the demanding outlier tenant's reality · replay eve-gpt-5.6-sol-2000t (2x): the same eve capture at 2x — the headroom/stress row; real fast-tier models emit the same chunk sizes at proportionally higher rate, so time compression is a faithful speed model · first chunk (pooled): every run's seq-0 RTT pooled across all stream scenarios — the first chunk precedes any workload differentiation, so pooling samples one shared stream-open path with exact percentiles

Replay cadences (semantic sha256) — eve-gpt-5.6-sol-2000teaf22f5946e7c61f3c65c7006d550df180cfabd4e706254a09f22aec0cfb420d · gateway-gpt-5.4-nano-2000t6f24ac518b6b83ff1d0e85a5fe78230db192716d66a7fc6b2fe022752001d041

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600

All timestamps are deployment-side; runs are triggered in-deployment, so the CI runner and api.vercel.com sit outside every measured window. TTFS = start() → first step body (includes dispatch + any cold start); Fan-out TTFS/TTLS = first/last step completion of one Promise.all from the same anchor (the gap is the runtime’s fan-out spread); STSO/WO between step bodies; CRTT inside the workflow (excludes the api.vercel.com read path).

Cold starts stay in the numbers (real bursty-workload latency, inflates P75+); Best is the warm floor.

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

▲ Vercel Production (8 failed)

python-node (8 failed):

  • promiseAllWorkflow | wrun_41M0XB8RB20GPSEEE0BP1F0AX5 | 🔍 observability
  • sleepingWorkflow | wrun_41M0XB9DN80GT0JYGEYSRDG0AZ | 🔍 observability
  • parallelSleepWorkflow | wrun_41M0XB9DSC0GGQFNQPSTD10TFN | 🔍 observability
  • nullByteWorkflow | wrun_41M0XB9MEH0GZ1PE9VA8VT338J | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41M0XBE3DY0GXJZZKSDADPJ273 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41M0XBE4D70GMTA2YASGGSMBW6 | 🔍 observability
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M0XBEDKS0GX7MW0EJVZNDKG1 | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M0XBEYPA0GT79Q7M4KQGXE2R | 🔍 observability

🌐 Cross-language Conformance (9 failed)

python (9 failed):

  • deploymentId: 'latest' is a no-op in non-Vercel worlds | wrun_01M0XBHEZA0V7PQ3KYK30VCQV6
  • promiseAllWorkflow | wrun_41M0XB8RB20GPSEEE0BP1F0AX5
  • sleepingWorkflow | wrun_41M0XB9DN80GT0JYGEYSRDG0AZ
  • parallelSleepWorkflow | wrun_41M0XB9DSC0GGQFNQPSTD10TFN
  • nullByteWorkflow | wrun_41M0XB9MEH0GZ1PE9VA8VT338J
  • cancelRun - cancelling a running workflow | wrun_41M0XBE3DY0GXJZZKSDADPJ273
  • cancelRun via CLI - cancelling a running workflow | wrun_41M0XBE4D70GMTA2YASGGSMBW6
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M0XBEDKS0GX7MW0EJVZNDKG1
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M0XBEYPA0GT79Q7M4KQGXE2R

⚠️ Flaky E2E Tests (passed on retry)

These tests failed at least once and passed on a retry. A recurring entry here is a real race worth investigating.

  • cancelRun via CLI - cancelling a running workflow (nextjs-turbopack)
  • cancelRun via CLI - cancelling a running workflow (sveltekit)
  • deploymentId: 'latest' is a no-op in non-Vercel worlds (tanstack-start)
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (nextjs-webpack)

🛠 Infra Events (absorbed by the harness)

Platform anomalies the e2e harness detected and worked around (e.g. a run the queue never picked up, replaced by a fresh run). Clustered timestamps indicate a backend blip; a steady drip indicates a platform issue worth escalating.

33 infra events
  • cold-start-warmup · suite warmup (python) · at 20:55:38Z · abandoned wrun_41M0XB6E7T0GJ9W8791BM6Z01B · (+7 more)
  • run-pickup-stall · sleepingWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TD0GMM929KHMH4A5PQ
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TD0GMM929KHMH4A5PR
  • run-pickup-stall · nullByteWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TG0GXS3FJPHWZ0AP3Z
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA34B0GKTASFX3VHN7ZF5
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:55:55Z · abandoned wrun_41M0XBA2T60GKVRBKEPG9JFNGQ
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 20:56:27Z · abandoned wrun_41M0XBB2NE0GYG9ZKWYWVYN0QG
  • run-pickup-stall · sleepingWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY390GR8JJH6G2563BMK
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY170GNNFJ2HYJF76VRJ
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY1P0GSFSBE1ENV48TWY
  • run-pickup-stall · nullByteWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY2C0GXP8214VHPKYDEN
  • cold-start-warmup · suite warmup (tanstack-start) · at 20:57:08Z · abandoned wrun_01M0XBC0795S33S7QA0QCC6MWM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 20:57:45Z · abandoned wrun_41M0XBC1V20GQEJYYHQRSWTMC5
  • cold-start-warmup · suite warmup (python) · at 20:58:25Z · abandoned wrun_01M0XBBGHK2AQJ0WK3BTSQXSBE · (+7 more)
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PANZ6ZFDYD87WHMYEG
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PANZ6ZFDYD87WHMYEH
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PJ61BWHDADBWKVQB4M
  • run-pickup-stall · nullByteWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PMHJQTM6D1Q5A1DGP8
  • run-pickup-stall · sleepingWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PFRRDDXM9ZQTKATH0V
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 20:59:26Z · abandoned wrun_41M0XBENXS0GNP64ENYV9QCDR6
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 20:59:26Z · abandoned wrun_41M0XBESQJ0GNJ9RHK6DQT9QJG
  • run-pickup-stall · sleepingWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A76KFYN7SAWZTZNJVC
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A4Q23JB7QFRPHW2M57
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A6RKK450KHQ6KKBNDM
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A861P4CVWVBHC9GRPV
  • run-pickup-stall · nullByteWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0ACMT5YMDRR1CRNSE1B
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 21:00:40Z · abandoned wrun_01M0XBJTXTEA82N9RWT7V8MWFM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 21:00:40Z · abandoned wrun_01M0XBJTXXTC9C59S58QCXKQDY
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 21:00:40Z · abandoned wrun_01M0XBJTY41EX832TVQ4QR069P
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 21:01:10Z · abandoned wrun_01M0XBKRA4M8A4FPV04NJY23SD
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 21:01:10Z · abandoned wrun_01M0XBKRA5GJYZXB04F49JH2DV
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 21:01:40Z · abandoned wrun_01M0XBMNH7M7QPBBRWZ6AX8HGC
  • run-pickup-stall · hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep (nextjs-webpack) · at 21:02:31Z · abandoned wrun_01M0XBP7PT25DV3EYT1BDRVKMK

E2E Test Summary

Summary
PassedFailedSkippedTotal
❌ ▲ Vercel Production357087424320
✅ 💻 Local Development392205584480
✅ 📦 Local Production392205584480
✅ 🐘 Local Postgres392205584480
✅ 🪟 Windows32000320
❌ 🌐 Cross-language Conformance09132141
✅ vercel-http-transport8170143960
✅ vercel-multi-region270027
✅ vercel-ws-transport553087640
Total1705317277819848
Details by Category

❌ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node132028
✅ astro-quickjs132028
✅ example-node132028
✅ example-quickjs132028
✅ express-node132028
✅ express-quickjs132028
✅ fastify-node132028
✅ fastify-quickjs132028
✅ hono-node132028
✅ hono-quickjs132028
✅ nest-node132028
✅ nest-quickjs132028
✅ nextjs-turbopack-node15703
✅ nextjs-turbopack-quickjs15703
✅ nextjs-webpack-node15703
✅ nextjs-webpack-quickjs15703
✅ nitro-node132028
✅ nitro-quickjs132028
✅ nuxt-node132028
✅ nuxt-quickjs132028
❌ python-node08152
✅ sveltekit-node15109
✅ sveltekit-quickjs15109
✅ tanstack-start-node132028
✅ tanstack-start-quickjs132028
✅ vite-node132028
✅ vite-quickjs132028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node16000
✅ nextjs-turbopack-quickjs16000

❌ 🌐 Cross-language Conformance

AppPassedFailedSkipped
❌ python09132

✅ vercel-http-transport

AppPassedFailedSkipped
✅ example132028
✅ express132028
✅ hono132028
✅ nextjs-turbopack15703
✅ nitro132028
✅ vite132028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

✅ vercel-ws-transport

AppPassedFailedSkipped
✅ example132028
✅ express132028
✅ nextjs-turbopack15703
✅ vite132028

📋 View full workflow run

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Sim World

Simulated world deterministic testing for races. Traces

🟠 world-sim scenario book — 1 fail of 41 total

fence=per-spec

scenariooutcomeeventsvirtreplayviolations
smoke-no-stepscompleted30msok0
smoke-one-stepcompleted60msok0
hook-at-step-startedcompleted120msok0
hook-at-step-completedcompleted120msok0
hook-at-hook-createdcompleted120msok0
deadline-hook-winscompleted71.0hok0
deadline-expirescompleted71.0hok0
long-sleepcompleted1130.0dok0
hook-never-arrivesstalled30msskipped0
step-retries-twicecompleted102.0sok0
parallel-stepscompleted90msok0
hook-on-execution-statecompleted120msok0
peek-hook-before-branchcompleted120msok0
peek-hook-after-branchcompleted120msok0
peek-hook-at-registrationcompleted120msok0
race-hook-before-probecompleted120msok0
race-hook-after-probecompleted120msok0
race-duplicate-deliverycompleted130msok0
attr-hook-before-stepcompleted110msok0
attr-hook-after-stepcompleted110msok0
attr-from-step-bodycompleted130msok0
fork-hook-after-timeoutcompleted141.0mok0
fork-hook-before-timeoutcompleted141.0mok0
count-hook-after-timeoutcompleted171.0mok0
count-hook-before-timeoutcompleted201.0mok0
stale-read-step-count-forkcompleted201.0mok0
stale-read-equal-step-countscompleted141.0mok0
step-vs-step-forkcompleted120msok0
step-vs-step-fork-fencedcompleted120msok0
fence-catches-benign-directioncompleted125msok0
in-flight-before-decisioncompleted171.0mok0
in-flight-before-decision-countedcompleted171.0mok0
in-flight-after-decisioncompleted192.0mok0
stale-read-step-count-fork-fencedcompleted201.0mok0
fork-hook-winscompleted131.0mok0
fork-timeout-winscompleted131.0mok0
unclaimed-payload-under-forkcompleted171.0mok0
claimed-payload-under-forkcompleted171.0mok0
writers-independent-step-bodiescompleted120msok0
writers-scripted-tempocompleted120msok0
cancel-mid-stepcancelled70msskipped0

Full trace: world-sim.txt

… file
Applies the unchanged-write guard from #3454 to the Astro and SvelteKit
builders, whose generated files live in Vite-watched source directories.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <peter.wielander@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor
FrameworkFlow routeStep reg.Framework output
hono200.9 KiB (±0)40.5 KiB (±0)1.76 MiB (±0)
nextjs-turbopack206.3 KiB (±0)439 B (±0)762.8 KiB (±0)
About these numbers

Sizes are gzip; parentheses show the change against main.
Flow route and Step reg. gate this job, on raw bytes rather than the gzip shown, at max(2%, 50.0 KiB). Framework output is informational.

555b143 · run

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this leaves the framework-specific behavior without automated regression coverage. A small integration test that builds Astro/SvelteKit twice and asserts watched generated outputs retain their mtime/inode would help prevent no-op writes from returning. Not blocking this change.

@VaguelySerious
VaguelySerious merged commit 0750fe8 into mainAug 25, 2026
182 of 184 checks passed
@VaguelySerious
VaguelySerious deleted the peter/guard-generated-writes-frameworks branch August 25, 2026 22:47
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 0750fe8 (AI decision).

The change depends on writeFileIfChanged from @workflow/builders, which was introduced by #3454 on main only — git grep writeFileIfChanged origin/stable finds nothing, so this cannot apply to stable without also backporting that main-only helper. It is also a dev-server efficiency improvement (avoiding redundant Vite invalidations/recompiles on no-op rebuilds) rather than a correctness, data-loss, or crash fix, so it does not meet the maintenance-branch bar on its own merits either.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

0750fe8958f50ee354cd24fe3f02a37c801bc4dc

@github-actionsgithub-actionsBot mentioned this pull request Aug 26, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(astro,sveltekit): skip generated writes that would not change the file - #3688

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/guard-generated-writes-frameworks
Aug 25, 2026
Merged

fix(astro,sveltekit): skip generated writes that would not change the file#3688
VaguelySerious merged 1 commit into
mainfrom
peter/guard-generated-writes-frameworks

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Aug 20, 2026

Copy link
Copy Markdown
Member

Description

Follow-up to #3454, which added the unchanged-write guard and applied it to @workflow/builders, @workflow/next and @workflow/nitro. The Astro and SvelteKit builders were not covered: they receive the bundle text from the base builder and do their own writeFile, so they never reach the guard. Their generated files live in Vite-watched source directories (src/pages/.well-known/workflow/v1 and src/routes/.well-known/workflow/v1), which is exactly the condition #3454 was about.

This routes those writes through writeFileIfChanged, and writes SvelteKit's static manifest copy from the same string rather than copyFile-ing it, matching what #3454 did for the Next public manifest.

Measured effect

Two consecutive pnpm build runs with no source change, comparing sha256 + mtime(ns) + inode of every generated file under the workflow directories, with WORKFLOW_PUBLIC_MANIFEST=1:

beforeafter
SvelteKit (src/routes/…, static/…)6 of 9 rewritten3
Astro (src/pages/…)5 of 8 rewritten3

Every one of those rewrites had an identical sha, i.e. they were pure no-op writes. Fixed here: both .gitignore files and the static/manifest.json copy on SvelteKit, and .gitignore plus manifest.json.js on Astro.

Relation to #3509

Rebased over #3509 (better SvelteKit routes-directory loading), which rewrote the same import block in packages/sveltekit/src/builder.ts. That was the only conflict and it was mechanical: keep its node:path import and drop the node:module/node:url imports it made dead, keep this PR's narrowed node:fs/promises import.

The two changes act at different layers and both still apply. #3509 caches the resolved routes directory so SvelteKit's secondary builds and worker threads do not reload the Svelte config and redo the build; this PR reduces what a build that does run actually writes. The measured numbers above were re-taken on top of #3509, with the guard-off control built from origin/main's builder, so the before/after pair is on the same base.

What this does NOT fix

Three files per framework still churn, for two reasons that need more than a call-site swap. Both are pre-existing and neither is made worse by this PR.

1. The read-transform-rewrite pattern defeats the guard — affects flow/+server.js and webhook/[token]/+server.js (SvelteKit), flow.js and webhook/[token].js (Astro).

Both builders call createCombinedBundle / createWebhookBundle, which write the route to disk through the base builder, then readFile it back, apply a regex transform, and write the transformed content. So each build writes the file twice with two different contents, and on the next build neither write can be skipped: the base write sees the previous build's transformed content on disk and differs from it, and the framework write then sees the untransformed content and differs from that. The signature is visible in the inode: the route gets a new inode (base write, temp+rename) and the webhook keeps its inode (framework write, plain overwrite).

The fix is to make the final content the only content written, e.g. by letting the framework pass its transform into the base builder so the route is generated in its final form. That is an API change to the base builder and wants its own PR.

2. One-shot esbuild.build() always rewrites its outfile — affects flow/__step_registrations.js on both frameworks.

ctx.rebuild() on an esbuild context skips writing when the output is byte-identical (verified on esbuild 0.28.1: mtime and inode both preserved). A one-shot esbuild.build() does not; it rewrites unconditionally. This is why Next dev, which uses contexts, was already protected for its esbuild outputs while these builders are not. The fix is either to use contexts in watch mode or to pass write: false and hand the output to writeFileIfChanged.

Other follow-ups from #3454

3. Nitro's dev handler should stop cache-busting off a single file's mtime.packages/nitro/src/index.ts derives version from statSync(handlerPath).mtimeMs for workflows.mjs and then uses it as the cache-buster for the steps.mjs import. A step-body-only edit leaves workflows.mjs identical, so the version freezes and stale step code is served. That dependency is the only reason skipsUnchangedGeneratedWrites exists.

Widening the key is cheap and keeps the per-request path to stats rather than file reads, since handlerImportPath and stepsImportPath are already in scope:

consts=statSync(handlerPath),t=statSync(stepsPath);constversion=`${s.size}:${s.mtimeMs}:${t.size}:${t.mtimeMs}`;

Including size costs nothing and narrows the case where two coalesced dev writes land on an effectively identical mtime. Note only the workflow/workflows.mjs variant needs the second stat; preloadSteps['workflow/webhook.mjs'] is empty. Both the v2 and v3 branches carry a copy. Once that lands, skipsUnchangedGeneratedWrites can be deleted from BaseBuilder and from the Nitro override.

4. There is no regression guard for the cross-framework case.write-if-changed.test.ts covers the helper and the opt-out, but nothing stops a new raw writeFile into a watched generated directory from silently restoring the redundant rebuild. The check that would catch it is the one used above: build twice, assert every generated file keeps its sha, mtime and inode. @workflow/astro has no test files today and @workflow/sveltekit has one unrelated test, so this needs a builder harness per framework rather than a one-line test.

How did you test your changes?

No automated coverage is added, for the reason in (4), so here are reproduction steps.

pnpm install && pnpm run build
cd workbench/sveltekit # and workbench/astroexport WORKFLOW_PUBLIC_MANIFEST=1
pnpm build
find src/routes/.well-known/workflow static/.well-known/workflow -type f \
| sort | xargs -I{} sh -c 'printf "%s %s %s\n" "$(shasum -a 256 {} | cut -d" " -f1)" "$(stat -f "%Fm:%i" {})" {}'> /tmp/s1
pnpm build
# ...repeat into /tmp/s2
diff /tmp/s1 /tmp/s2

Before this change the two .gitignore files and static/manifest.json appear in the diff with an identical sha and a moved mtime. After, they do not. The three files listed above still appear, for the reasons given.

Also run: pnpm run build (28/28 tasks), npx biome check on both changed files.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run to create a changelog for this PR
  • 🔒 DCO sign-off passes (run git commit --signoff on your commits)
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete

@vercel

vercelBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreview, v0Aug 25, 2026 8:55pm
example-nextjs-workflow-webpackReadyReadyPreview, v0Aug 25, 2026 8:55pm
example-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-astro-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-express-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-fastify-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-hono-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nestjs-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nitro-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nuxt-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-python-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-sveltekit-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-tanstack-start-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-vite-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-docsReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-swc-playgroundReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-tarballsReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-webReadyReadyPreview, v0Aug 25, 2026 8:55pm

@changeset-bot

changeset-botBot commented Aug 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 555b143

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/astroPatch
@workflow/sveltekitPatch
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 555b143 · Tue, 25 Aug 2026 21:17:13 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1215 (+465%) 🔻1315 🔴 (+33%) 🔻1332 🔴 (+29%) 🔻1375 🔴 (-4.8%)30
TTFSstream217 (+20%) 🔻1291 🔴 (+36%) 🔻1314 🔴 (+36%) 🔻1365 🔴 (+35%) 🔻30
TTFShook + stream1447 (+180%) 🔻1597 🔴 (+18%) 🔻1666 🔴 (+11%)1940 🔴 (-62%) 💚30
Fan-out TTFSPromise.all(100 steps)806 (+44%) 🔻1955 (+96%) 🔻1978 (+40%) 🔻2016 (+24%) 🔻10
Fan-out TTLSPromise.all(100 steps)2542 (+31%) 🔻5184 (+87%) 🔻6673 (+99%) 🔻9405 (+43%) 🔻10
STSO1020 steps (inline)99 (-7.5%)148 (+14%)165 (+8.6%)233 (+14%)1019
WO1020 steps146183 (+11%)146183 (+11%)146183 (+11%)146183 (+11%)1
CRTTfirst chunk (pooled)104 (+24%) 🔻145 (+9.0%)167 (+7.7%)271 (+13%)28

Streams

ScenarioCRTT 1stp75p90p99CDV maxiters
paced control (100/s, 60B)138 (+25%)156 (-3%)230 (-44%)419 (-34%)161 (+24%)10
size sweep (100/s, 160B-12KB)128 (+15%)164 (+20%)228 (-36%)629 (-20%)160 (+53%)10
replay gateway-gpt-5.4-nano-2000t (1x)113 (-23%)139 (+1%)187 (-28%)745 (-16%)368 (+11%)3
replay eve-gpt-5.6-sol-2000t (1x)195 (+7%)150 (+22%)192 (+19%)436 (+31%)434 (+36%)2
replay eve-gpt-5.6-sol-2000t (2x)145 (+14%)182 (+22%)241 (+25%)491 (+30%)286 (+10%)3
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 132084ms → this run 144847ms (Δ +12763ms, +10%)

 50-100 ms ┃ main 0 this 1 +1
100-150 ms ████████████████████┃███ main 905 this 781 -124
150-200 ms ███░░┃ main 103 this 217 +114
200-250 ms ┃ main 5 this 13 +8
250-300 ms ┃ main 2 this 2 +0
300-350 ms ┃ main 1 this 2 +1
350-400 ms ┃ main 1 this 2 +1
400-450 ms ┃ main 2 this 1 -1
📈 CRTT drill-down vs main (RTT distributions & profiles)
variant RTT 1ms→5s+ avg p50 p90 p99 n
control ······▃█▁···· 129.5 (+3%) 120 (+21%) 230 (-44%) 419 (-34%) 3000
sweep ······▂█▁▁··· 138.6 (+18%) 126 (+29%) 228 (-36%) 629 (-20%) 3000
gw 1x ·····▁▆█▁▁··· 119.6 (±0%) 105 (+9%) 187 (-28%) 745 (-16%) 5295
eve 1x ·····▁▅█▁▁··· 124.7 (+17%) 109 (+14%) 192 (+19%) 436 (+31%) 5186
eve 2x ·····▁▂█▂▁··· 150.3 (+20%) 134 (+16%) 241 (+25%) 491 (+30%) 7779

RTT over stream progress (avg per tenth of stream, bars scaled min→max):

control ▆█▄▃▂▁▄▂▄▃ 111–157ms
sweep ▃▃▃▂▂▁▄█▃▂ 121–184ms
gw 1x ▂▂▃▃▁█▂▂▁▂ 103–181ms
eve 1x ▄▁▃▆▃█▇▆▄▂ 106–146ms
eve 2x ▁▂▁▁▅▄▄█▆▃ 125–196ms

RTT by chunk size (avg per log size bin, ~160B → ~12KB serialized, bars scaled min→max):

sweep █▇▂▁▃▄▅ 136–141ms

Delivery jitter over stream progress (avg positive CDV per tenth of stream, bars scaled min→max):

control ▂▇▄▄▇▁█▄▅▄ 34–43ms
sweep ▁▄▁▄▆▄▄█▃▃ 47–68ms
gw 1x ▃▂▇▂▃█▃▂▁▄ 31–40ms
eve 1x ▄▁▂▆▆▆█▅▄▆ 20–26ms
eve 2x ▇▇▁▄▇█▅▁▅█ 19–27ms
📜 Previous results (1)

78301f5

Thu, 20 Aug 2026 01:50:16 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep458 (-58%) 💚1476 🔴 (+19%) 🔻1524 🔴 (+15%) 🔻1611 🔴 (-1.1%)30
TTFSstream1295 (+275%) 🔻1477 🔴 (+28%) 🔻1538 🔴 (+29%) 🔻1675 🔴 (+36%) 🔻30
TTFShook + stream1635 (+50%) 🔻2181 🔴 (+41%) 🔻2203 🔴 (+40%) 🔻2331 🔴 (-12%)30
Fan-out TTFSPromise.all(100 steps)593 (+10%)938 (-42%) 💚2355 (+42%) 🔻2542 (+40%) 🔻10
Fan-out TTLSPromise.all(100 steps)4866 (-2.6%)5507 (-11%)6831 (-2.6%)12617 (+65%) 🔻10
STSO1020 steps (inline)105 (-29%) 💚174 (-35%) 💚190 (-44%) 💚304 (-57%) 💚1019
WO1020 steps173964 (-34%) 💚173964 (-34%) 💚173964 (-34%) 💚173964 (-34%) 💚1
CRTTfirst chunk (pooled)124 (+20%) 🔻202 (+28%) 🔻300 (-8.0%)587 (+41%) 🔻28

Streams

ScenarioCRTT 1stp75p90p99CDV maxiters
paced control (100/s, 60B)163 (+14%)172 (-20%)257 (-33%)446 (-40%)119 (-61%)10
size sweep (100/s, 160B-12KB)168 (+21%)205 (-3%)256 (-26%)363 (-36%)139 (-16%)10
replay gateway-gpt-5.4-nano-2000t (1x)300 (+110%)160 (-45%)199 (-58%)532 (-23%)200 (-53%)3
replay eve-gpt-5.6-sol-2000t (1x)174 (+17%)163 (-54%)204 (-59%)582 (-34%)411 (-28%)2
replay eve-gpt-5.6-sol-2000t (2x)228 (+63%)251 (-50%)544 (-17%)1091 (+6%)542 (+19%)3
ℹ️ Metric definitions & methodology

Streams: first-chunk RTT (the stream-open path, before any buffering/backpressure), CRTT percentiles, and worst delivery stall (CDV max). Cells are medians across iterations; per-run values in the artifacts. No 🔴/🟢 marks until targets attach.

The collapsed STSO distribution section above buckets every step gap, split inline (same warm process — pure framework overhead) vs queue-hop (fresh process — dispatch, reinit, replay). = main, = this run, = fill.

The collapsed CRTT drill-down: per-variant RTT histograms (fixed log bins, · = empty) and mean RTT/positive-CDV profile lines over stream progress and chunk size. Histograms, avgs, and profiles merge exactly across runs; p50–p99 are percentile-of-percentiles. Per-index rows live in the artifacts.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body) · Fan-out TTFS: fan-out time to first step (in-deployment start() → first of the parallel step bodies to complete) · Fan-out TTLS: fan-out time to last step (in-deployment start() → last of the parallel step bodies to complete, i.e. when the Promise.all resolves) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · CRTT: chunk round-trip time (per-chunk write → read latency, one clock domain: deployment → stream backend → same deployment) · CDV: chunk delay variation / delivery jitter (inter-arrival gap minus inter-write gap per seq-adjacent pair; skew-free; the row is each run's MAX positive value, so one stall moves it)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · Promise.all(100 steps): 100 trivial no-op steps started together in a single Promise.all; Fan-out TTFS is the first of them to complete and Fan-out TTLS the last, both from the in-deployment clientStart, so their gap is the spread the runtime adds across the fan-out · paced control (100/s, 60B): the control: 300 tiny (~60B) deltas metronome-paced at 100/s — zero workload structure, so it reads the transport floor and flush cadence, and disambiguates transport-wide vs workload-specific when a replay row moves · size sweep (100/s, 160B-12KB): same pacing as the control with deltas padded in rotation across seven log-spaced sizes (~160B–12KB) — rotation decouples size from stream position, so it isolates whether chunk size causes latency · replay gateway-gpt-5.4-nano-2000t (1x): raw provider SSE cadence captured at the AI gateway boundary (gpt-5.4-nano, the most popular gateway model; per-token deltas p50 208B = the modal production chunk size), replayed exactly as measured — the typical customer's workload; its CDV is the typical customer's real delivery jitter · replay eve-gpt-5.6-sol-2000t (1x): a captured eve turn (gpt-5.6-sol, the most-used demanding eve model; ~2000 output tokens = production p50 turn length) replayed exactly as measured — eve's envelope protocol re-ships the cumulative message so sizes ramp 142B→13KB; the demanding outlier tenant's reality · replay eve-gpt-5.6-sol-2000t (2x): the same eve capture at 2x — the headroom/stress row; real fast-tier models emit the same chunk sizes at proportionally higher rate, so time compression is a faithful speed model · first chunk (pooled): every run's seq-0 RTT pooled across all stream scenarios — the first chunk precedes any workload differentiation, so pooling samples one shared stream-open path with exact percentiles

Replay cadences (semantic sha256) — eve-gpt-5.6-sol-2000teaf22f5946e7c61f3c65c7006d550df180cfabd4e706254a09f22aec0cfb420d · gateway-gpt-5.4-nano-2000t6f24ac518b6b83ff1d0e85a5fe78230db192716d66a7fc6b2fe022752001d041

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600

All timestamps are deployment-side; runs are triggered in-deployment, so the CI runner and api.vercel.com sit outside every measured window. TTFS = start() → first step body (includes dispatch + any cold start); Fan-out TTFS/TTLS = first/last step completion of one Promise.all from the same anchor (the gap is the runtime’s fan-out spread); STSO/WO between step bodies; CRTT inside the workflow (excludes the api.vercel.com read path).

Cold starts stay in the numbers (real bursty-workload latency, inflates P75+); Best is the warm floor.

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

▲ Vercel Production (8 failed)

python-node (8 failed):

  • promiseAllWorkflow | wrun_41M0XB8RB20GPSEEE0BP1F0AX5 | 🔍 observability
  • sleepingWorkflow | wrun_41M0XB9DN80GT0JYGEYSRDG0AZ | 🔍 observability
  • parallelSleepWorkflow | wrun_41M0XB9DSC0GGQFNQPSTD10TFN | 🔍 observability
  • nullByteWorkflow | wrun_41M0XB9MEH0GZ1PE9VA8VT338J | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41M0XBE3DY0GXJZZKSDADPJ273 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41M0XBE4D70GMTA2YASGGSMBW6 | 🔍 observability
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M0XBEDKS0GX7MW0EJVZNDKG1 | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M0XBEYPA0GT79Q7M4KQGXE2R | 🔍 observability

🌐 Cross-language Conformance (9 failed)

python (9 failed):

  • deploymentId: 'latest' is a no-op in non-Vercel worlds | wrun_01M0XBHEZA0V7PQ3KYK30VCQV6
  • promiseAllWorkflow | wrun_41M0XB8RB20GPSEEE0BP1F0AX5
  • sleepingWorkflow | wrun_41M0XB9DN80GT0JYGEYSRDG0AZ
  • parallelSleepWorkflow | wrun_41M0XB9DSC0GGQFNQPSTD10TFN
  • nullByteWorkflow | wrun_41M0XB9MEH0GZ1PE9VA8VT338J
  • cancelRun - cancelling a running workflow | wrun_41M0XBE3DY0GXJZZKSDADPJ273
  • cancelRun via CLI - cancelling a running workflow | wrun_41M0XBE4D70GMTA2YASGGSMBW6
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M0XBEDKS0GX7MW0EJVZNDKG1
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M0XBEYPA0GT79Q7M4KQGXE2R

⚠️ Flaky E2E Tests (passed on retry)

These tests failed at least once and passed on a retry. A recurring entry here is a real race worth investigating.

  • cancelRun via CLI - cancelling a running workflow (nextjs-turbopack)
  • cancelRun via CLI - cancelling a running workflow (sveltekit)
  • deploymentId: 'latest' is a no-op in non-Vercel worlds (tanstack-start)
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (nextjs-webpack)

🛠 Infra Events (absorbed by the harness)

Platform anomalies the e2e harness detected and worked around (e.g. a run the queue never picked up, replaced by a fresh run). Clustered timestamps indicate a backend blip; a steady drip indicates a platform issue worth escalating.

33 infra events
  • cold-start-warmup · suite warmup (python) · at 20:55:38Z · abandoned wrun_41M0XB6E7T0GJ9W8791BM6Z01B · (+7 more)
  • run-pickup-stall · sleepingWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TD0GMM929KHMH4A5PQ
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TD0GMM929KHMH4A5PR
  • run-pickup-stall · nullByteWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TG0GXS3FJPHWZ0AP3Z
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA34B0GKTASFX3VHN7ZF5
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:55:55Z · abandoned wrun_41M0XBA2T60GKVRBKEPG9JFNGQ
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 20:56:27Z · abandoned wrun_41M0XBB2NE0GYG9ZKWYWVYN0QG
  • run-pickup-stall · sleepingWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY390GR8JJH6G2563BMK
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY170GNNFJ2HYJF76VRJ
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY1P0GSFSBE1ENV48TWY
  • run-pickup-stall · nullByteWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY2C0GXP8214VHPKYDEN
  • cold-start-warmup · suite warmup (tanstack-start) · at 20:57:08Z · abandoned wrun_01M0XBC0795S33S7QA0QCC6MWM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 20:57:45Z · abandoned wrun_41M0XBC1V20GQEJYYHQRSWTMC5
  • cold-start-warmup · suite warmup (python) · at 20:58:25Z · abandoned wrun_01M0XBBGHK2AQJ0WK3BTSQXSBE · (+7 more)
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PANZ6ZFDYD87WHMYEG
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PANZ6ZFDYD87WHMYEH
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PJ61BWHDADBWKVQB4M
  • run-pickup-stall · nullByteWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PMHJQTM6D1Q5A1DGP8
  • run-pickup-stall · sleepingWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PFRRDDXM9ZQTKATH0V
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 20:59:26Z · abandoned wrun_41M0XBENXS0GNP64ENYV9QCDR6
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 20:59:26Z · abandoned wrun_41M0XBESQJ0GNJ9RHK6DQT9QJG
  • run-pickup-stall · sleepingWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A76KFYN7SAWZTZNJVC
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A4Q23JB7QFRPHW2M57
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A6RKK450KHQ6KKBNDM
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A861P4CVWVBHC9GRPV
  • run-pickup-stall · nullByteWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0ACMT5YMDRR1CRNSE1B
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 21:00:40Z · abandoned wrun_01M0XBJTXTEA82N9RWT7V8MWFM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 21:00:40Z · abandoned wrun_01M0XBJTXXTC9C59S58QCXKQDY
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 21:00:40Z · abandoned wrun_01M0XBJTY41EX832TVQ4QR069P
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 21:01:10Z · abandoned wrun_01M0XBKRA4M8A4FPV04NJY23SD
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 21:01:10Z · abandoned wrun_01M0XBKRA5GJYZXB04F49JH2DV
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 21:01:40Z · abandoned wrun_01M0XBMNH7M7QPBBRWZ6AX8HGC
  • run-pickup-stall · hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep (nextjs-webpack) · at 21:02:31Z · abandoned wrun_01M0XBP7PT25DV3EYT1BDRVKMK

E2E Test Summary

Summary
PassedFailedSkippedTotal
❌ ▲ Vercel Production357087424320
✅ 💻 Local Development392205584480
✅ 📦 Local Production392205584480
✅ 🐘 Local Postgres392205584480
✅ 🪟 Windows32000320
❌ 🌐 Cross-language Conformance09132141
✅ vercel-http-transport8170143960
✅ vercel-multi-region270027
✅ vercel-ws-transport553087640
Total1705317277819848
Details by Category

❌ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node132028
✅ astro-quickjs132028
✅ example-node132028
✅ example-quickjs132028
✅ express-node132028
✅ express-quickjs132028
✅ fastify-node132028
✅ fastify-quickjs132028
✅ hono-node132028
✅ hono-quickjs132028
✅ nest-node132028
✅ nest-quickjs132028
✅ nextjs-turbopack-node15703
✅ nextjs-turbopack-quickjs15703
✅ nextjs-webpack-node15703
✅ nextjs-webpack-quickjs15703
✅ nitro-node132028
✅ nitro-quickjs132028
✅ nuxt-node132028
✅ nuxt-quickjs132028
❌ python-node08152
✅ sveltekit-node15109
✅ sveltekit-quickjs15109
✅ tanstack-start-node132028
✅ tanstack-start-quickjs132028
✅ vite-node132028
✅ vite-quickjs132028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node16000
✅ nextjs-turbopack-quickjs16000

❌ 🌐 Cross-language Conformance

AppPassedFailedSkipped
❌ python09132

✅ vercel-http-transport

AppPassedFailedSkipped
✅ example132028
✅ express132028
✅ hono132028
✅ nextjs-turbopack15703
✅ nitro132028
✅ vite132028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

✅ vercel-ws-transport

AppPassedFailedSkipped
✅ example132028
✅ express132028
✅ nextjs-turbopack15703
✅ vite132028

📋 View full workflow run

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Sim World

Simulated world deterministic testing for races. Traces

🟠 world-sim scenario book — 1 fail of 41 total

fence=per-spec

scenariooutcomeeventsvirtreplayviolations
smoke-no-stepscompleted30msok0
smoke-one-stepcompleted60msok0
hook-at-step-startedcompleted120msok0
hook-at-step-completedcompleted120msok0
hook-at-hook-createdcompleted120msok0
deadline-hook-winscompleted71.0hok0
deadline-expirescompleted71.0hok0
long-sleepcompleted1130.0dok0
hook-never-arrivesstalled30msskipped0
step-retries-twicecompleted102.0sok0
parallel-stepscompleted90msok0
hook-on-execution-statecompleted120msok0
peek-hook-before-branchcompleted120msok0
peek-hook-after-branchcompleted120msok0
peek-hook-at-registrationcompleted120msok0
race-hook-before-probecompleted120msok0
race-hook-after-probecompleted120msok0
race-duplicate-deliverycompleted130msok0
attr-hook-before-stepcompleted110msok0
attr-hook-after-stepcompleted110msok0
attr-from-step-bodycompleted130msok0
fork-hook-after-timeoutcompleted141.0mok0
fork-hook-before-timeoutcompleted141.0mok0
count-hook-after-timeoutcompleted171.0mok0
count-hook-before-timeoutcompleted201.0mok0
stale-read-step-count-forkcompleted201.0mok0
stale-read-equal-step-countscompleted141.0mok0
step-vs-step-forkcompleted120msok0
step-vs-step-fork-fencedcompleted120msok0
fence-catches-benign-directioncompleted125msok0
in-flight-before-decisioncompleted171.0mok0
in-flight-before-decision-countedcompleted171.0mok0
in-flight-after-decisioncompleted192.0mok0
stale-read-step-count-fork-fencedcompleted201.0mok0
fork-hook-winscompleted131.0mok0
fork-timeout-winscompleted131.0mok0
unclaimed-payload-under-forkcompleted171.0mok0
claimed-payload-under-forkcompleted171.0mok0
writers-independent-step-bodiescompleted120msok0
writers-scripted-tempocompleted120msok0
cancel-mid-stepcancelled70msskipped0

Full trace: world-sim.txt

… file
Applies the unchanged-write guard from #3454 to the Astro and SvelteKit
builders, whose generated files live in Vite-watched source directories.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <peter.wielander@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor
FrameworkFlow routeStep reg.Framework output
hono200.9 KiB (±0)40.5 KiB (±0)1.76 MiB (±0)
nextjs-turbopack206.3 KiB (±0)439 B (±0)762.8 KiB (±0)
About these numbers

Sizes are gzip; parentheses show the change against main.
Flow route and Step reg. gate this job, on raw bytes rather than the gzip shown, at max(2%, 50.0 KiB). Framework output is informational.

555b143 · run

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this leaves the framework-specific behavior without automated regression coverage. A small integration test that builds Astro/SvelteKit twice and asserts watched generated outputs retain their mtime/inode would help prevent no-op writes from returning. Not blocking this change.

@VaguelySerious
VaguelySerious merged commit 0750fe8 into mainAug 25, 2026
182 of 184 checks passed
@VaguelySerious
VaguelySerious deleted the peter/guard-generated-writes-frameworks branch August 25, 2026 22:47
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 0750fe8 (AI decision).

The change depends on writeFileIfChanged from @workflow/builders, which was introduced by #3454 on main only — git grep writeFileIfChanged origin/stable finds nothing, so this cannot apply to stable without also backporting that main-only helper. It is also a dev-server efficiency improvement (avoiding redundant Vite invalidations/recompiles on no-op rebuilds) rather than a correctness, data-loss, or crash fix, so it does not meet the maintenance-branch bar on its own merits either.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

0750fe8958f50ee354cd24fe3f02a37c801bc4dc

@github-actionsgithub-actionsBot mentioned this pull request Aug 26, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(astro,sveltekit): skip generated writes that would not change the file - #3688

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/guard-generated-writes-frameworks
Aug 25, 2026
Merged

fix(astro,sveltekit): skip generated writes that would not change the file#3688
VaguelySerious merged 1 commit into
mainfrom
peter/guard-generated-writes-frameworks

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Aug 20, 2026

Copy link
Copy Markdown
Member

Description

Follow-up to #3454, which added the unchanged-write guard and applied it to @workflow/builders, @workflow/next and @workflow/nitro. The Astro and SvelteKit builders were not covered: they receive the bundle text from the base builder and do their own writeFile, so they never reach the guard. Their generated files live in Vite-watched source directories (src/pages/.well-known/workflow/v1 and src/routes/.well-known/workflow/v1), which is exactly the condition #3454 was about.

This routes those writes through writeFileIfChanged, and writes SvelteKit's static manifest copy from the same string rather than copyFile-ing it, matching what #3454 did for the Next public manifest.

Measured effect

Two consecutive pnpm build runs with no source change, comparing sha256 + mtime(ns) + inode of every generated file under the workflow directories, with WORKFLOW_PUBLIC_MANIFEST=1:

beforeafter
SvelteKit (src/routes/…, static/…)6 of 9 rewritten3
Astro (src/pages/…)5 of 8 rewritten3

Every one of those rewrites had an identical sha, i.e. they were pure no-op writes. Fixed here: both .gitignore files and the static/manifest.json copy on SvelteKit, and .gitignore plus manifest.json.js on Astro.

Relation to #3509

Rebased over #3509 (better SvelteKit routes-directory loading), which rewrote the same import block in packages/sveltekit/src/builder.ts. That was the only conflict and it was mechanical: keep its node:path import and drop the node:module/node:url imports it made dead, keep this PR's narrowed node:fs/promises import.

The two changes act at different layers and both still apply. #3509 caches the resolved routes directory so SvelteKit's secondary builds and worker threads do not reload the Svelte config and redo the build; this PR reduces what a build that does run actually writes. The measured numbers above were re-taken on top of #3509, with the guard-off control built from origin/main's builder, so the before/after pair is on the same base.

What this does NOT fix

Three files per framework still churn, for two reasons that need more than a call-site swap. Both are pre-existing and neither is made worse by this PR.

1. The read-transform-rewrite pattern defeats the guard — affects flow/+server.js and webhook/[token]/+server.js (SvelteKit), flow.js and webhook/[token].js (Astro).

Both builders call createCombinedBundle / createWebhookBundle, which write the route to disk through the base builder, then readFile it back, apply a regex transform, and write the transformed content. So each build writes the file twice with two different contents, and on the next build neither write can be skipped: the base write sees the previous build's transformed content on disk and differs from it, and the framework write then sees the untransformed content and differs from that. The signature is visible in the inode: the route gets a new inode (base write, temp+rename) and the webhook keeps its inode (framework write, plain overwrite).

The fix is to make the final content the only content written, e.g. by letting the framework pass its transform into the base builder so the route is generated in its final form. That is an API change to the base builder and wants its own PR.

2. One-shot esbuild.build() always rewrites its outfile — affects flow/__step_registrations.js on both frameworks.

ctx.rebuild() on an esbuild context skips writing when the output is byte-identical (verified on esbuild 0.28.1: mtime and inode both preserved). A one-shot esbuild.build() does not; it rewrites unconditionally. This is why Next dev, which uses contexts, was already protected for its esbuild outputs while these builders are not. The fix is either to use contexts in watch mode or to pass write: false and hand the output to writeFileIfChanged.

Other follow-ups from #3454

3. Nitro's dev handler should stop cache-busting off a single file's mtime.packages/nitro/src/index.ts derives version from statSync(handlerPath).mtimeMs for workflows.mjs and then uses it as the cache-buster for the steps.mjs import. A step-body-only edit leaves workflows.mjs identical, so the version freezes and stale step code is served. That dependency is the only reason skipsUnchangedGeneratedWrites exists.

Widening the key is cheap and keeps the per-request path to stats rather than file reads, since handlerImportPath and stepsImportPath are already in scope:

consts=statSync(handlerPath),t=statSync(stepsPath);constversion=`${s.size}:${s.mtimeMs}:${t.size}:${t.mtimeMs}`;

Including size costs nothing and narrows the case where two coalesced dev writes land on an effectively identical mtime. Note only the workflow/workflows.mjs variant needs the second stat; preloadSteps['workflow/webhook.mjs'] is empty. Both the v2 and v3 branches carry a copy. Once that lands, skipsUnchangedGeneratedWrites can be deleted from BaseBuilder and from the Nitro override.

4. There is no regression guard for the cross-framework case.write-if-changed.test.ts covers the helper and the opt-out, but nothing stops a new raw writeFile into a watched generated directory from silently restoring the redundant rebuild. The check that would catch it is the one used above: build twice, assert every generated file keeps its sha, mtime and inode. @workflow/astro has no test files today and @workflow/sveltekit has one unrelated test, so this needs a builder harness per framework rather than a one-line test.

How did you test your changes?

No automated coverage is added, for the reason in (4), so here are reproduction steps.

pnpm install && pnpm run build
cd workbench/sveltekit # and workbench/astroexport WORKFLOW_PUBLIC_MANIFEST=1
pnpm build
find src/routes/.well-known/workflow static/.well-known/workflow -type f \
| sort | xargs -I{} sh -c 'printf "%s %s %s\n" "$(shasum -a 256 {} | cut -d" " -f1)" "$(stat -f "%Fm:%i" {})" {}'> /tmp/s1
pnpm build
# ...repeat into /tmp/s2
diff /tmp/s1 /tmp/s2

Before this change the two .gitignore files and static/manifest.json appear in the diff with an identical sha and a moved mtime. After, they do not. The three files listed above still appear, for the reasons given.

Also run: pnpm run build (28/28 tasks), npx biome check on both changed files.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run to create a changelog for this PR
  • 🔒 DCO sign-off passes (run git commit --signoff on your commits)
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete

@vercel

vercelBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreview, v0Aug 25, 2026 8:55pm
example-nextjs-workflow-webpackReadyReadyPreview, v0Aug 25, 2026 8:55pm
example-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-astro-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-express-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-fastify-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-hono-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nestjs-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nitro-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-nuxt-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-python-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-sveltekit-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-tanstack-start-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workbench-vite-workflowReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-docsReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-swc-playgroundReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-tarballsReadyReadyPreview, v0Aug 25, 2026 8:55pm
workflow-webReadyReadyPreview, v0Aug 25, 2026 8:55pm

@changeset-bot

changeset-botBot commented Aug 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 555b143

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/astroPatch
@workflow/sveltekitPatch
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 555b143 · Tue, 25 Aug 2026 21:17:13 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1215 (+465%) 🔻1315 🔴 (+33%) 🔻1332 🔴 (+29%) 🔻1375 🔴 (-4.8%)30
TTFSstream217 (+20%) 🔻1291 🔴 (+36%) 🔻1314 🔴 (+36%) 🔻1365 🔴 (+35%) 🔻30
TTFShook + stream1447 (+180%) 🔻1597 🔴 (+18%) 🔻1666 🔴 (+11%)1940 🔴 (-62%) 💚30
Fan-out TTFSPromise.all(100 steps)806 (+44%) 🔻1955 (+96%) 🔻1978 (+40%) 🔻2016 (+24%) 🔻10
Fan-out TTLSPromise.all(100 steps)2542 (+31%) 🔻5184 (+87%) 🔻6673 (+99%) 🔻9405 (+43%) 🔻10
STSO1020 steps (inline)99 (-7.5%)148 (+14%)165 (+8.6%)233 (+14%)1019
WO1020 steps146183 (+11%)146183 (+11%)146183 (+11%)146183 (+11%)1
CRTTfirst chunk (pooled)104 (+24%) 🔻145 (+9.0%)167 (+7.7%)271 (+13%)28

Streams

ScenarioCRTT 1stp75p90p99CDV maxiters
paced control (100/s, 60B)138 (+25%)156 (-3%)230 (-44%)419 (-34%)161 (+24%)10
size sweep (100/s, 160B-12KB)128 (+15%)164 (+20%)228 (-36%)629 (-20%)160 (+53%)10
replay gateway-gpt-5.4-nano-2000t (1x)113 (-23%)139 (+1%)187 (-28%)745 (-16%)368 (+11%)3
replay eve-gpt-5.6-sol-2000t (1x)195 (+7%)150 (+22%)192 (+19%)436 (+31%)434 (+36%)2
replay eve-gpt-5.6-sol-2000t (2x)145 (+14%)182 (+22%)241 (+25%)491 (+30%)286 (+10%)3
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 132084ms → this run 144847ms (Δ +12763ms, +10%)

 50-100 ms ┃ main 0 this 1 +1
100-150 ms ████████████████████┃███ main 905 this 781 -124
150-200 ms ███░░┃ main 103 this 217 +114
200-250 ms ┃ main 5 this 13 +8
250-300 ms ┃ main 2 this 2 +0
300-350 ms ┃ main 1 this 2 +1
350-400 ms ┃ main 1 this 2 +1
400-450 ms ┃ main 2 this 1 -1
📈 CRTT drill-down vs main (RTT distributions & profiles)
variant RTT 1ms→5s+ avg p50 p90 p99 n
control ······▃█▁···· 129.5 (+3%) 120 (+21%) 230 (-44%) 419 (-34%) 3000
sweep ······▂█▁▁··· 138.6 (+18%) 126 (+29%) 228 (-36%) 629 (-20%) 3000
gw 1x ·····▁▆█▁▁··· 119.6 (±0%) 105 (+9%) 187 (-28%) 745 (-16%) 5295
eve 1x ·····▁▅█▁▁··· 124.7 (+17%) 109 (+14%) 192 (+19%) 436 (+31%) 5186
eve 2x ·····▁▂█▂▁··· 150.3 (+20%) 134 (+16%) 241 (+25%) 491 (+30%) 7779

RTT over stream progress (avg per tenth of stream, bars scaled min→max):

control ▆█▄▃▂▁▄▂▄▃ 111–157ms
sweep ▃▃▃▂▂▁▄█▃▂ 121–184ms
gw 1x ▂▂▃▃▁█▂▂▁▂ 103–181ms
eve 1x ▄▁▃▆▃█▇▆▄▂ 106–146ms
eve 2x ▁▂▁▁▅▄▄█▆▃ 125–196ms

RTT by chunk size (avg per log size bin, ~160B → ~12KB serialized, bars scaled min→max):

sweep █▇▂▁▃▄▅ 136–141ms

Delivery jitter over stream progress (avg positive CDV per tenth of stream, bars scaled min→max):

control ▂▇▄▄▇▁█▄▅▄ 34–43ms
sweep ▁▄▁▄▆▄▄█▃▃ 47–68ms
gw 1x ▃▂▇▂▃█▃▂▁▄ 31–40ms
eve 1x ▄▁▂▆▆▆█▅▄▆ 20–26ms
eve 2x ▇▇▁▄▇█▅▁▅█ 19–27ms
📜 Previous results (1)

78301f5

Thu, 20 Aug 2026 01:50:16 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep458 (-58%) 💚1476 🔴 (+19%) 🔻1524 🔴 (+15%) 🔻1611 🔴 (-1.1%)30
TTFSstream1295 (+275%) 🔻1477 🔴 (+28%) 🔻1538 🔴 (+29%) 🔻1675 🔴 (+36%) 🔻30
TTFShook + stream1635 (+50%) 🔻2181 🔴 (+41%) 🔻2203 🔴 (+40%) 🔻2331 🔴 (-12%)30
Fan-out TTFSPromise.all(100 steps)593 (+10%)938 (-42%) 💚2355 (+42%) 🔻2542 (+40%) 🔻10
Fan-out TTLSPromise.all(100 steps)4866 (-2.6%)5507 (-11%)6831 (-2.6%)12617 (+65%) 🔻10
STSO1020 steps (inline)105 (-29%) 💚174 (-35%) 💚190 (-44%) 💚304 (-57%) 💚1019
WO1020 steps173964 (-34%) 💚173964 (-34%) 💚173964 (-34%) 💚173964 (-34%) 💚1
CRTTfirst chunk (pooled)124 (+20%) 🔻202 (+28%) 🔻300 (-8.0%)587 (+41%) 🔻28

Streams

ScenarioCRTT 1stp75p90p99CDV maxiters
paced control (100/s, 60B)163 (+14%)172 (-20%)257 (-33%)446 (-40%)119 (-61%)10
size sweep (100/s, 160B-12KB)168 (+21%)205 (-3%)256 (-26%)363 (-36%)139 (-16%)10
replay gateway-gpt-5.4-nano-2000t (1x)300 (+110%)160 (-45%)199 (-58%)532 (-23%)200 (-53%)3
replay eve-gpt-5.6-sol-2000t (1x)174 (+17%)163 (-54%)204 (-59%)582 (-34%)411 (-28%)2
replay eve-gpt-5.6-sol-2000t (2x)228 (+63%)251 (-50%)544 (-17%)1091 (+6%)542 (+19%)3
ℹ️ Metric definitions & methodology

Streams: first-chunk RTT (the stream-open path, before any buffering/backpressure), CRTT percentiles, and worst delivery stall (CDV max). Cells are medians across iterations; per-run values in the artifacts. No 🔴/🟢 marks until targets attach.

The collapsed STSO distribution section above buckets every step gap, split inline (same warm process — pure framework overhead) vs queue-hop (fresh process — dispatch, reinit, replay). = main, = this run, = fill.

The collapsed CRTT drill-down: per-variant RTT histograms (fixed log bins, · = empty) and mean RTT/positive-CDV profile lines over stream progress and chunk size. Histograms, avgs, and profiles merge exactly across runs; p50–p99 are percentile-of-percentiles. Per-index rows live in the artifacts.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body) · Fan-out TTFS: fan-out time to first step (in-deployment start() → first of the parallel step bodies to complete) · Fan-out TTLS: fan-out time to last step (in-deployment start() → last of the parallel step bodies to complete, i.e. when the Promise.all resolves) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · CRTT: chunk round-trip time (per-chunk write → read latency, one clock domain: deployment → stream backend → same deployment) · CDV: chunk delay variation / delivery jitter (inter-arrival gap minus inter-write gap per seq-adjacent pair; skew-free; the row is each run's MAX positive value, so one stall moves it)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · Promise.all(100 steps): 100 trivial no-op steps started together in a single Promise.all; Fan-out TTFS is the first of them to complete and Fan-out TTLS the last, both from the in-deployment clientStart, so their gap is the spread the runtime adds across the fan-out · paced control (100/s, 60B): the control: 300 tiny (~60B) deltas metronome-paced at 100/s — zero workload structure, so it reads the transport floor and flush cadence, and disambiguates transport-wide vs workload-specific when a replay row moves · size sweep (100/s, 160B-12KB): same pacing as the control with deltas padded in rotation across seven log-spaced sizes (~160B–12KB) — rotation decouples size from stream position, so it isolates whether chunk size causes latency · replay gateway-gpt-5.4-nano-2000t (1x): raw provider SSE cadence captured at the AI gateway boundary (gpt-5.4-nano, the most popular gateway model; per-token deltas p50 208B = the modal production chunk size), replayed exactly as measured — the typical customer's workload; its CDV is the typical customer's real delivery jitter · replay eve-gpt-5.6-sol-2000t (1x): a captured eve turn (gpt-5.6-sol, the most-used demanding eve model; ~2000 output tokens = production p50 turn length) replayed exactly as measured — eve's envelope protocol re-ships the cumulative message so sizes ramp 142B→13KB; the demanding outlier tenant's reality · replay eve-gpt-5.6-sol-2000t (2x): the same eve capture at 2x — the headroom/stress row; real fast-tier models emit the same chunk sizes at proportionally higher rate, so time compression is a faithful speed model · first chunk (pooled): every run's seq-0 RTT pooled across all stream scenarios — the first chunk precedes any workload differentiation, so pooling samples one shared stream-open path with exact percentiles

Replay cadences (semantic sha256) — eve-gpt-5.6-sol-2000teaf22f5946e7c61f3c65c7006d550df180cfabd4e706254a09f22aec0cfb420d · gateway-gpt-5.4-nano-2000t6f24ac518b6b83ff1d0e85a5fe78230db192716d66a7fc6b2fe022752001d041

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600

All timestamps are deployment-side; runs are triggered in-deployment, so the CI runner and api.vercel.com sit outside every measured window. TTFS = start() → first step body (includes dispatch + any cold start); Fan-out TTFS/TTLS = first/last step completion of one Promise.all from the same anchor (the gap is the runtime’s fan-out spread); STSO/WO between step bodies; CRTT inside the workflow (excludes the api.vercel.com read path).

Cold starts stay in the numbers (real bursty-workload latency, inflates P75+); Best is the warm floor.

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

▲ Vercel Production (8 failed)

python-node (8 failed):

  • promiseAllWorkflow | wrun_41M0XB8RB20GPSEEE0BP1F0AX5 | 🔍 observability
  • sleepingWorkflow | wrun_41M0XB9DN80GT0JYGEYSRDG0AZ | 🔍 observability
  • parallelSleepWorkflow | wrun_41M0XB9DSC0GGQFNQPSTD10TFN | 🔍 observability
  • nullByteWorkflow | wrun_41M0XB9MEH0GZ1PE9VA8VT338J | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41M0XBE3DY0GXJZZKSDADPJ273 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41M0XBE4D70GMTA2YASGGSMBW6 | 🔍 observability
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M0XBEDKS0GX7MW0EJVZNDKG1 | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M0XBEYPA0GT79Q7M4KQGXE2R | 🔍 observability

🌐 Cross-language Conformance (9 failed)

python (9 failed):

  • deploymentId: 'latest' is a no-op in non-Vercel worlds | wrun_01M0XBHEZA0V7PQ3KYK30VCQV6
  • promiseAllWorkflow | wrun_41M0XB8RB20GPSEEE0BP1F0AX5
  • sleepingWorkflow | wrun_41M0XB9DN80GT0JYGEYSRDG0AZ
  • parallelSleepWorkflow | wrun_41M0XB9DSC0GGQFNQPSTD10TFN
  • nullByteWorkflow | wrun_41M0XB9MEH0GZ1PE9VA8VT338J
  • cancelRun - cancelling a running workflow | wrun_41M0XBE3DY0GXJZZKSDADPJ273
  • cancelRun via CLI - cancelling a running workflow | wrun_41M0XBE4D70GMTA2YASGGSMBW6
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M0XBEDKS0GX7MW0EJVZNDKG1
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M0XBEYPA0GT79Q7M4KQGXE2R

⚠️ Flaky E2E Tests (passed on retry)

These tests failed at least once and passed on a retry. A recurring entry here is a real race worth investigating.

  • cancelRun via CLI - cancelling a running workflow (nextjs-turbopack)
  • cancelRun via CLI - cancelling a running workflow (sveltekit)
  • deploymentId: 'latest' is a no-op in non-Vercel worlds (tanstack-start)
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (nextjs-webpack)

🛠 Infra Events (absorbed by the harness)

Platform anomalies the e2e harness detected and worked around (e.g. a run the queue never picked up, replaced by a fresh run). Clustered timestamps indicate a backend blip; a steady drip indicates a platform issue worth escalating.

33 infra events
  • cold-start-warmup · suite warmup (python) · at 20:55:38Z · abandoned wrun_41M0XB6E7T0GJ9W8791BM6Z01B · (+7 more)
  • run-pickup-stall · sleepingWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TD0GMM929KHMH4A5PQ
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TD0GMM929KHMH4A5PR
  • run-pickup-stall · nullByteWorkflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA2TG0GXS3FJPHWZ0AP3Z
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 20:55:54Z · abandoned wrun_41M0XBA34B0GKTASFX3VHN7ZF5
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:55:55Z · abandoned wrun_41M0XBA2T60GKVRBKEPG9JFNGQ
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 20:56:27Z · abandoned wrun_41M0XBB2NE0GYG9ZKWYWVYN0QG
  • run-pickup-stall · sleepingWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY390GR8JJH6G2563BMK
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY170GNNFJ2HYJF76VRJ
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY1P0GSFSBE1ENV48TWY
  • run-pickup-stall · nullByteWorkflow (python) · at 20:56:55Z · abandoned wrun_41M0XBBY2C0GXP8214VHPKYDEN
  • cold-start-warmup · suite warmup (tanstack-start) · at 20:57:08Z · abandoned wrun_01M0XBC0795S33S7QA0QCC6MWM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 20:57:45Z · abandoned wrun_41M0XBC1V20GQEJYYHQRSWTMC5
  • cold-start-warmup · suite warmup (python) · at 20:58:25Z · abandoned wrun_01M0XBBGHK2AQJ0WK3BTSQXSBE · (+7 more)
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PANZ6ZFDYD87WHMYEG
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PANZ6ZFDYD87WHMYEH
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PJ61BWHDADBWKVQB4M
  • run-pickup-stall · nullByteWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PMHJQTM6D1Q5A1DGP8
  • run-pickup-stall · sleepingWorkflow (python) · at 20:58:40Z · abandoned wrun_01M0XBF5PFRRDDXM9ZQTKATH0V
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 20:59:26Z · abandoned wrun_41M0XBENXS0GNP64ENYV9QCDR6
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 20:59:26Z · abandoned wrun_41M0XBESQJ0GNJ9RHK6DQT9QJG
  • run-pickup-stall · sleepingWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A76KFYN7SAWZTZNJVC
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A4Q23JB7QFRPHW2M57
  • run-pickup-stall · promiseAllWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A6RKK450KHQ6KKBNDM
  • run-pickup-stall · parallelSleepWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0A861P4CVWVBHC9GRPV
  • run-pickup-stall · nullByteWorkflow (python) · at 20:59:40Z · abandoned wrun_01M0XBH0ACMT5YMDRR1CRNSE1B
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 21:00:40Z · abandoned wrun_01M0XBJTXTEA82N9RWT7V8MWFM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 21:00:40Z · abandoned wrun_01M0XBJTXXTC9C59S58QCXKQDY
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 21:00:40Z · abandoned wrun_01M0XBJTY41EX832TVQ4QR069P
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 21:01:10Z · abandoned wrun_01M0XBKRA4M8A4FPV04NJY23SD
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 21:01:10Z · abandoned wrun_01M0XBKRA5GJYZXB04F49JH2DV
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 21:01:40Z · abandoned wrun_01M0XBMNH7M7QPBBRWZ6AX8HGC
  • run-pickup-stall · hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep (nextjs-webpack) · at 21:02:31Z · abandoned wrun_01M0XBP7PT25DV3EYT1BDRVKMK

E2E Test Summary

Summary
PassedFailedSkippedTotal
❌ ▲ Vercel Production357087424320
✅ 💻 Local Development392205584480
✅ 📦 Local Production392205584480
✅ 🐘 Local Postgres392205584480
✅ 🪟 Windows32000320
❌ 🌐 Cross-language Conformance09132141
✅ vercel-http-transport8170143960
✅ vercel-multi-region270027
✅ vercel-ws-transport553087640
Total1705317277819848
Details by Category

❌ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node132028
✅ astro-quickjs132028
✅ example-node132028
✅ example-quickjs132028
✅ express-node132028
✅ express-quickjs132028
✅ fastify-node132028
✅ fastify-quickjs132028
✅ hono-node132028
✅ hono-quickjs132028
✅ nest-node132028
✅ nest-quickjs132028
✅ nextjs-turbopack-node15703
✅ nextjs-turbopack-quickjs15703
✅ nextjs-webpack-node15703
✅ nextjs-webpack-quickjs15703
✅ nitro-node132028
✅ nitro-quickjs132028
✅ nuxt-node132028
✅ nuxt-quickjs132028
❌ python-node08152
✅ sveltekit-node15109
✅ sveltekit-quickjs15109
✅ tanstack-start-node132028
✅ tanstack-start-quickjs132028
✅ vite-node132028
✅ vite-quickjs132028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node134026
✅ astro-stable-quickjs134026
✅ express-stable-node134026
✅ express-stable-quickjs134026
✅ fastify-stable-node134026
✅ fastify-stable-quickjs134026
✅ hono-stable-node134026
✅ hono-stable-quickjs134026
✅ nest-stable-node134026
✅ nest-stable-quickjs134026
✅ nextjs-turbopack-canary-node141019
✅ nextjs-turbopack-canary-quickjs141019
✅ nextjs-turbopack-stable-node16000
✅ nextjs-turbopack-stable-quickjs16000
✅ nextjs-webpack-canary-node141019
✅ nextjs-webpack-canary-quickjs141019
✅ nextjs-webpack-stable-node16000
✅ nextjs-webpack-stable-quickjs16000
✅ nitro-stable-node134026
✅ nitro-stable-quickjs134026
✅ nuxt-stable-node134026
✅ nuxt-stable-quickjs134026
✅ sveltekit-stable-node15307
✅ sveltekit-stable-quickjs15307
✅ tanstack-start-node134026
✅ tanstack-start-quickjs134026
✅ vite-stable-node134026
✅ vite-stable-quickjs134026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node16000
✅ nextjs-turbopack-quickjs16000

❌ 🌐 Cross-language Conformance

AppPassedFailedSkipped
❌ python09132

✅ vercel-http-transport

AppPassedFailedSkipped
✅ example132028
✅ express132028
✅ hono132028
✅ nextjs-turbopack15703
✅ nitro132028
✅ vite132028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

✅ vercel-ws-transport

AppPassedFailedSkipped
✅ example132028
✅ express132028
✅ nextjs-turbopack15703
✅ vite132028

📋 View full workflow run

@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Sim World

Simulated world deterministic testing for races. Traces

🟠 world-sim scenario book — 1 fail of 41 total

fence=per-spec

scenariooutcomeeventsvirtreplayviolations
smoke-no-stepscompleted30msok0
smoke-one-stepcompleted60msok0
hook-at-step-startedcompleted120msok0
hook-at-step-completedcompleted120msok0
hook-at-hook-createdcompleted120msok0
deadline-hook-winscompleted71.0hok0
deadline-expirescompleted71.0hok0
long-sleepcompleted1130.0dok0
hook-never-arrivesstalled30msskipped0
step-retries-twicecompleted102.0sok0
parallel-stepscompleted90msok0
hook-on-execution-statecompleted120msok0
peek-hook-before-branchcompleted120msok0
peek-hook-after-branchcompleted120msok0
peek-hook-at-registrationcompleted120msok0
race-hook-before-probecompleted120msok0
race-hook-after-probecompleted120msok0
race-duplicate-deliverycompleted130msok0
attr-hook-before-stepcompleted110msok0
attr-hook-after-stepcompleted110msok0
attr-from-step-bodycompleted130msok0
fork-hook-after-timeoutcompleted141.0mok0
fork-hook-before-timeoutcompleted141.0mok0
count-hook-after-timeoutcompleted171.0mok0
count-hook-before-timeoutcompleted201.0mok0
stale-read-step-count-forkcompleted201.0mok0
stale-read-equal-step-countscompleted141.0mok0
step-vs-step-forkcompleted120msok0
step-vs-step-fork-fencedcompleted120msok0
fence-catches-benign-directioncompleted125msok0
in-flight-before-decisioncompleted171.0mok0
in-flight-before-decision-countedcompleted171.0mok0
in-flight-after-decisioncompleted192.0mok0
stale-read-step-count-fork-fencedcompleted201.0mok0
fork-hook-winscompleted131.0mok0
fork-timeout-winscompleted131.0mok0
unclaimed-payload-under-forkcompleted171.0mok0
claimed-payload-under-forkcompleted171.0mok0
writers-independent-step-bodiescompleted120msok0
writers-scripted-tempocompleted120msok0
cancel-mid-stepcancelled70msskipped0

Full trace: world-sim.txt

… file
Applies the unchanged-write guard from #3454 to the Astro and SvelteKit
builders, whose generated files live in Vite-watched source directories.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <peter.wielander@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor
FrameworkFlow routeStep reg.Framework output
hono200.9 KiB (±0)40.5 KiB (±0)1.76 MiB (±0)
nextjs-turbopack206.3 KiB (±0)439 B (±0)762.8 KiB (±0)
About these numbers

Sizes are gzip; parentheses show the change against main.
Flow route and Step reg. gate this job, on raw bytes rather than the gzip shown, at max(2%, 50.0 KiB). Framework output is informational.

555b143 · run

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this leaves the framework-specific behavior without automated regression coverage. A small integration test that builds Astro/SvelteKit twice and asserts watched generated outputs retain their mtime/inode would help prevent no-op writes from returning. Not blocking this change.

@VaguelySerious
VaguelySerious merged commit 0750fe8 into mainAug 25, 2026
182 of 184 checks passed
@VaguelySerious
VaguelySerious deleted the peter/guard-generated-writes-frameworks branch August 25, 2026 22:47
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 0750fe8 (AI decision).

The change depends on writeFileIfChanged from @workflow/builders, which was introduced by #3454 on main only — git grep writeFileIfChanged origin/stable finds nothing, so this cannot apply to stable without also backporting that main-only helper. It is also a dev-server efficiency improvement (avoiding redundant Vite invalidations/recompiles on no-op rebuilds) rather than a correctness, data-loss, or crash fix, so it does not meet the maintenance-branch bar on its own merits either.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

0750fe8958f50ee354cd24fe3f02a37c801bc4dc

@github-actionsgithub-actionsBot mentioned this pull request Aug 26, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3