ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources - #1882

Merged
TooTallNate merged 24 commits into
mainfrom
ci/oidc-trusted-sources
May 2, 2026
Merged

ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources#1882
TooTallNate merged 24 commits into
mainfrom
ci/oidc-trusted-sources

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 30, 2026

Copy link
Copy Markdown
Member

Summary

Switch the e2e, benchmark, and docs-smoke CI jobs from the static VERCEL_AUTOMATION_BYPASS_SECRET deployment-protection bypass token over to Vercel's OIDC Trusted Sources flow. Same conceptual swap is applied in the runtime world-vercel SDK: outbound requests to a protected workflow-server preview now carry an x-vercel-trusted-oidc-idp-token header instead of x-vercel-protection-bypass.

What changes

CI workflows

.github/workflows/tests.yml, benchmarks.yml, docs-checks.yml:

  • Add permissions: id-token: write (and explicit contents: read, deployments: read) to the affected jobs so the runner can mint OIDC tokens.
  • Tests/benchmarks/smoke checks pick up the ACTIONS_ID_TOKEN_REQUEST_URL / ACTIONS_ID_TOKEN_REQUEST_TOKEN env vars that GitHub injects on id-token: write–enabled jobs and mint OIDC tokens on demand (see helper below). The VERCEL_AUTOMATION_BYPASS_SECRET env var and the (now no-op) VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS env var are removed.

Shared on-demand minting helper — scripts/trusted-sources-headers.mjs

GitHub Actions OIDC tokens have a hard 5-minute lifetime. Pre-minting once at the start of the job and forwarding the result down doesn't work for long e2e suites — tests that run late in the run hit an expired token and 401. The new getTrustedSourcesHeaders() is async, mints fresh tokens from the runner endpoint on demand, caches them in-process, and re-mints 60 s before the JWT's exp. Falls back to process.env.VERCEL_OIDC_TOKEN for non-CI contexts (Vercel runtime, local dev).

Used by packages/core/e2e/{e2e.test.ts,bench.bench.ts,utils.ts} and docs/scripts/check-docs-smoke.mjs — all callers now await the helper.

@workflow/world-vercel runtime

packages/world-vercel/src/utils.ts:getHttpConfig:

  • Proxied path (usingProxy=true, hits api.vercel.com/v1/workflow): bearer auth from config.token only. The api-workflow proxy authenticates the caller with a regular Vercel auth token, so the trusted-sources header is meaningless on this hop. We now throw a clear error if config.token is missing instead of letting an opaque 401 bubble up.
  • Direct path (usingProxy=false, hits the workflow-server URL directly from a Vercel deployment): bearer auth prefers config.token and falls back to getVercelOidcToken(). The trusted-sources bypass header always uses getVercelOidcToken() (per-request token Vercel injects on every invocation). This avoids the trap of reading process.env.VERCEL_OIDC_TOKEN directly, which carries the deployment's bake-time token and goes stale after project-config changes.

packages/world-vercel/src/{encryption,resolve-latest-deployment}.ts: drop the (now-unused) getProtectionBypassHeader calls — both go to the public api.vercel.com, which never needs the bypass.

Vercel-side configuration (out of band)

Each of the 11 workbench app projects has its trust configuration updated:

  • trustedSources.projects — entries for all 11 workbench projects with a customAllow: [{ from: any-env, to: any-env }] rule, so any workbench's Vercel-issued OIDC token can reach any other (and itself) on any environment. This is what enables the local-dev recipe in AGENTS.md: vercel env pull from any workbench app yields a VERCEL_OIDC_TOKEN that bypasses Deployment Protection on every workbench preview/prod URL.
  • trustedSources.oidcProviders["https://token.actions.githubusercontent.com"] — accepts GitHub Actions OIDC tokens from vercel/workflow CI for the direct-fetch tests against the deployed workbench apps.

Local-development recipe (AGENTS.md)

Updated the local-against-preview e2e recipe to document vercel env pull as the source of VERCEL_OIDC_TOKEN. Replaces the old VERCEL_AUTOMATION_BYPASS_SECRET instruction.

Files changed

FileChange
.github/workflows/tests.ymlpermissions: id-token: write, dropped pre-mint step + VERCEL_AUTOMATION_BYPASS_SECRET / VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS
.github/workflows/benchmarks.ymlsame
.github/workflows/docs-checks.ymlsame
scripts/trusted-sources-headers.mjs (new)async on-demand minting helper
packages/core/e2e/e2e.test.tsawait getTrustedSourcesHeaders() at every callsite
packages/core/e2e/bench.bench.tssame
packages/core/e2e/utils.tssame
docs/scripts/check-docs-smoke.mjssame
packages/world-vercel/src/utils.tsbearer/trusted-sources split between proxied and direct paths; throw on missing config.token for proxied path; removed dead getProtectionBypassHeader
packages/world-vercel/src/utils.test.tstests for the new direct/proxied behaviour
packages/world-vercel/src/encryption.tsdrop bypass header (api.vercel.com is public)
packages/world-vercel/src/resolve-latest-deployment.tsdrop bypass header (api.vercel.com is public)
AGENTS.mdlocal-dev recipe uses vercel env pull for VERCEL_OIDC_TOKEN
.changeset/world-vercel-trusted-sources.md (new)minor bump for @workflow/world-vercel

Untouched (separate concerns)

  • WORKFLOW_VERCEL_AUTH_TOKEN (secrets.VERCEL_LABS_TOKEN) — Vercel API auth, still needed for the proxied path's config.token.
  • VERCEL_WORKFLOW_SERVER_URL env var — still used to point CI at the workflow-server preview on PR runs.

Cleanup follow-up

Once a CI run on main confirms everything works, the VERCEL_AUTOMATION_BYPASS_SECRET and VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS repo secrets can be deleted from GitHub.

The e2e, benchmark, and docs-smoke CI jobs previously used the static
`VERCEL_AUTOMATION_BYPASS_SECRET` deployment-protection bypass token
to reach protected Vercel deployments. Switch them over to the new OIDC
Trusted Sources flow: the GitHub Actions runner mints a short-lived
OIDC token via `core.getIDToken()` and forwards it on requests in the
`x-vercel-trusted-oidc-idp-token` header.
Each workbench project (and `workflow-docs`) has been configured with a
matching trusted-source rule:
aud=https://github.com/vercel, repository=vercel/workflow
The shared header helper now lives at `scripts/trusted-sources-headers.mjs`
and is imported by both the e2e/bench tests and the docs smoke script,
removing the previous duplication.
CopilotAI review requested due to automatic review settings April 30, 2026 17:50
@changeset-bot

changeset-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c125b54

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@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

@vercel

vercelBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.043s (-2.5%)1.006s (~)0.962s101.00x
💻 LocalNitro0.049s (+13.9% 🔺)1.011s (~)0.961s101.14x
🐘 PostgresNitro0.059s (-38.3% 🟢)1.010s (-3.1%)0.952s101.36x
🐘 PostgresExpress0.060s (+3.3%)1.011s (~)0.951s101.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.314s (+33.2% 🔺)2.234s (+4.6%)1.920s101.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.125s (~)2.005s (~)0.880s101.00x
💻 LocalNitro1.127s (~)2.006s (~)0.879s101.00x
🐘 PostgresExpress1.143s (~)2.010s (~)0.867s101.02x
🐘 PostgresNitro1.149s (+0.8%)2.010s (~)0.862s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.920s (+2.4%)3.677s (-3.4%)1.757s101.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.880s (~)11.019s (~)0.139s31.00x
💻 LocalNitro10.928s (~)11.023s (~)0.095s31.00x
💻 LocalExpress10.947s (~)11.022s (~)0.075s31.01x
🐘 PostgresExpress10.955s (~)11.353s (+3.0%)0.398s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.546s (+3.3%)19.315s (-3.5%)1.770s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.542s (~)15.022s (~)0.480s41.00x
🐘 PostgresNitro14.553s (~)15.025s (~)0.472s41.00x
💻 LocalExpress14.970s (~)15.280s (+1.7%)0.310s41.03x
💻 LocalNitro14.986s (-0.5%)15.279s (-4.7%)0.293s41.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express31.530s (-37.3% 🟢)33.430s (-36.4% 🟢)1.899s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.022s (~)14.447s (-1.0%)0.425s71.00x
🐘 PostgresNitro14.038s (+0.5%)14.737s (+3.0%)0.699s71.00x
💻 LocalExpress16.492s (-0.7%)17.031s (~)0.539s61.18x
💻 LocalNitro16.588s (-1.2%)17.029s (~)0.441s61.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express55.578s (-54.2% 🟢)58.406s (-52.8% 🟢)2.828s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.249s (-0.9%)2.010s (~)0.760s151.00x
🐘 PostgresNitro1.283s (+0.6%)2.011s (~)0.728s151.03x
💻 LocalExpress1.508s (+1.3%)2.006s (~)0.498s151.21x
💻 LocalNitro1.533s (-6.1% 🟢)2.006s (-3.3%)0.473s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.403s (-16.0% 🟢)3.950s (-14.6% 🟢)1.547s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.345s (-0.7%)3.009s (~)0.664s101.00x
🐘 PostgresNitro2.359s (~)3.011s (~)0.651s101.01x
💻 LocalExpress2.817s (-4.6%)3.108s (-10.0% 🟢)0.292s101.20x
💻 LocalNitro2.910s (-7.4% 🟢)3.207s (-17.5% 🟢)0.297s101.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.388s (-34.0% 🟢)4.195s (-17.9% 🟢)1.807s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.462s (-0.7%)4.010s (~)0.548s81.00x
🐘 PostgresNitro3.494s (~)4.014s (~)0.520s81.01x
💻 LocalExpress7.361s (-11.7% 🟢)8.020s (-11.1% 🟢)0.659s42.13x
💻 LocalNitro7.874s (-5.7% 🟢)8.269s (-8.3% 🟢)0.395s42.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.002s (-5.6% 🟢)6.045s (-1.3%)2.042s51.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.261s (~)2.009s (~)0.748s151.00x
🐘 PostgresExpress1.266s (+0.7%)2.008s (~)0.743s151.00x
💻 LocalExpress1.522s (-19.6% 🟢)2.006s (-15.1% 🟢)0.483s151.21x
💻 LocalNitro1.553s (-16.8% 🟢)2.006s (-14.3% 🟢)0.453s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.335s (-9.6% 🟢)3.671s (-15.6% 🟢)1.336s91.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.338s (~)3.010s (~)0.672s101.00x
🐘 PostgresExpress2.348s (~)3.011s (~)0.663s101.00x
💻 LocalExpress2.906s (-7.2% 🟢)3.008s (-20.1% 🟢)0.102s101.24x
💻 LocalNitro2.983s (-2.7%)3.565s (-8.3% 🟢)0.582s91.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.317s (-27.4% 🟢)3.822s (-20.3% 🟢)1.505s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.469s (-0.9%)4.010s (~)0.540s81.00x
🐘 PostgresNitro3.478s (~)4.011s (~)0.533s81.00x
💻 LocalExpress8.107s (-7.9% 🟢)9.025s (-2.7%)0.917s42.34x
💻 LocalNitro8.436s (-7.7% 🟢)9.024s (-10.0% 🟢)0.588s42.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.081s (-52.0% 🟢)4.799s (-41.3% 🟢)1.718s71.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.844s (+0.6%)1.023s (~)0.179s591.00x
🐘 PostgresNitro0.884s (+7.8% 🔺)1.041s (+3.5%)0.157s581.05x
💻 LocalNitro0.987s (+0.6%)1.181s (+8.0% 🔺)0.194s511.17x
💻 LocalExpress1.022s (+3.8%)1.941s (+80.4% 🔺)0.920s311.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.175s (-51.8% 🟢)10.768s (-49.5% 🟢)1.593s61.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.972s (~)2.402s (+6.4% 🔺)0.430s381.00x
🐘 PostgresNitro1.987s (+3.1%)2.403s (+14.4% 🔺)0.416s381.01x
💻 LocalNitro3.023s (~)3.649s (-2.9%)0.625s251.53x
💻 LocalExpress3.038s (+0.7%)3.759s (+4.8%)0.721s241.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express28.216s (-18.3% 🟢)30.015s (-18.5% 🟢)1.800s31.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.999s (~)4.456s (+2.0%)0.457s271.00x
🐘 PostgresNitro4.047s (-1.4%)4.627s (~)0.580s261.01x
💻 LocalExpress8.938s (-3.0%)9.325s (-6.9% 🟢)0.387s132.23x
💻 LocalNitro9.121s (-1.9%)9.710s (-3.1%)0.589s132.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express74.288s (-42.9% 🟢)76.387s (-42.2% 🟢)2.098s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.278s (-1.6%)1.007s (~)0.729s601.00x
🐘 PostgresNitro0.292s (+2.9%)1.007s (~)0.716s601.05x
💻 LocalExpress0.594s (+5.9% 🔺)1.005s (~)0.411s602.14x
💻 LocalNitro0.782s (+29.3% 🔺)1.205s (+17.9% 🔺)0.422s502.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.563s (-20.0% 🟢)3.059s (-15.9% 🟢)1.496s201.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.494s (-3.1%)1.007s (~)0.513s901.00x
🐘 PostgresNitro0.509s (+2.6%)1.008s (~)0.499s901.03x
💻 LocalExpress2.408s (-4.2%)3.009s (~)0.600s304.87x
💻 LocalNitro2.460s (-3.1%)3.009s (~)0.548s304.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.780s (-8.8% 🟢)4.406s (-8.4% 🟢)1.626s211.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.790s (-3.5%)1.007s (-1.0%)0.218s1201.00x
🐘 PostgresNitro0.798s (+1.0%)1.009s (~)0.210s1191.01x
💻 LocalExpress10.360s (-7.4% 🟢)11.028s (-7.6% 🟢)0.668s1113.12x
💻 LocalNitro10.737s (-4.1%)11.121s (-4.7%)0.384s1113.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express7.045s (-5.1% 🟢)8.589s (-7.1% 🟢)1.544s151.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.203s (-4.9%)1.004s (~)0.011s (-9.6% 🟢)1.017s (~)0.814s101.00x
🐘 PostgresNitro0.208s (+1.5%)0.999s (~)0.002s (~)1.011s (~)0.802s101.02x
💻 LocalExpress0.209s (+5.2% 🔺)1.004s (~)0.011s (-13.2% 🟢)1.016s (~)0.807s101.03x
🐘 PostgresExpress0.210s (+2.3%)0.998s (~)0.001s (-43.8% 🟢)1.010s (~)0.800s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.484s (-40.8% 🟢)3.012s (-26.4% 🟢)1.734s (+80.5% 🔺)5.137s (-8.1% 🟢)3.653s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.620s (-0.7%)1.005s (~)0.004s (-2.0%)1.023s (~)0.403s591.00x
🐘 PostgresExpress0.635s (+0.8%)1.005s (~)0.004s (+7.1% 🔺)1.023s (~)0.388s591.02x
💻 LocalExpress0.764s (+0.9%)1.013s (-1.6%)0.009s (-6.0% 🟢)1.023s (-1.6%)0.260s591.23x
💻 LocalNitro0.833s (-0.7%)1.011s (~)0.010s (+5.1% 🔺)1.116s (~)0.283s541.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.166s (-35.9% 🟢)5.514s (-31.2% 🟢)0.218s (-46.7% 🟢)6.081s (-31.2% 🟢)1.914s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.918s (-4.5%)1.149s (-10.1% 🟢)0.000s (-11.5% 🟢)1.168s (-10.6% 🟢)0.250s521.00x
🐘 PostgresNitro0.971s (~)1.264s (+1.3%)0.000s (-50.0% 🟢)1.290s (+2.5%)0.319s481.06x
💻 LocalNitro1.203s (-1.6%)2.020s (~)0.000s (+133.3% 🔺)2.021s (~)0.818s301.31x
💻 LocalExpress1.226s (~)2.021s (~)0.000s (-50.0% 🟢)2.023s (~)0.797s301.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.185s (-14.8% 🟢)4.482s (-12.2% 🟢)0.000s (-100.0% 🟢)4.856s (-12.2% 🟢)1.671s131.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.795s (+1.3%)2.177s (~)0.000s (NaN%)2.189s (~)0.394s281.00x
🐘 PostgresNitro1.832s (+2.3%)2.219s (+3.6%)0.000s (+3.7%)2.233s (+2.7%)0.400s271.02x
💻 LocalExpress3.392s (-2.2%)3.973s (-1.5%)0.001s (+25.0% 🔺)3.977s (-1.5%)0.584s161.89x
💻 LocalNitro3.499s (+3.3%)4.032s (~)0.000s (-12.5% 🟢)4.035s (~)0.536s151.95x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.518s (-1.5%)5.994s (~)0.000s (NaN%)6.413s (-0.7%)1.896s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress16/21
🐘 PostgresExpress15/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres15/21
Nitro🐘 Postgres18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production10000671067
✅ 💻 Local Development10780861164
✅ 📦 Local Production10780861164
✅ 🐘 Local Postgres10780861164
✅ 🪟 Windows970097
✅ 📋 Other273018291
Total460403434947

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9007
✅ example9007
✅ express9007
✅ fastify9007
✅ hono9007
✅ nextjs-turbopack9502
✅ nextjs-webpack9502
✅ nitro9007
✅ nuxt9007
✅ sveltekit9007
✅ vite9007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9700
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9106
✅ e2e-local-postgres-nest-stable9106
✅ e2e-local-prod-nest-stable9106

📋 View full workflow run

CopilotAI 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.

Pull request overview

Switches CI jobs that hit protected Vercel deployments from a long-lived bypass secret to Vercel Deployment Protection “Trusted Sources” using GitHub Actions OIDC, and centralizes the request header logic in a shared helper.

Changes:

  • Add OIDC token minting in relevant CI jobs and pass it via VERCEL_TRUSTED_OIDC_TOKEN.
  • Introduce scripts/trusted-sources-headers.mjs and update e2e/bench/docs smoke callers to use it.
  • Update local dev docs (AGENTS.md) to reflect the new bypass mechanism.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
scripts/trusted-sources-headers.mjsNew shared helper to attach the Vercel Trusted Sources OIDC header when present.
packages/core/e2e/utils.tsRemoves legacy bypass-secret helper; uses the shared Trusted Sources headers helper.
packages/core/e2e/e2e.test.tsUpdates HTTP calls to use Trusted Sources headers.
packages/core/e2e/bench.bench.tsUpdates manifest fetch to use Trusted Sources headers.
docs/scripts/check-docs-smoke.mjsUpdates docs smoke fetches to use Trusted Sources headers.
AGENTS.mdDocuments the new env var and local-development limitation.
.github/workflows/tests.ymlAdds OIDC token minting + env wiring for e2e Vercel prod tests.
.github/workflows/docs-checks.ymlAdds OIDC token minting + env wiring for docs preview smoke checks.
.github/workflows/benchmarks.ymlAdds OIDC token minting + env wiring for Vercel benchmarks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread.github/workflows/tests.yml
Comment thread.github/workflows/docs-checks.yml
Comment thread.github/workflows/benchmarks.yml
- Rename the env var from VERCEL_TRUSTED_OIDC_TOKEN to VERCEL_OIDC_TOKEN
to match Vercel's convention (also read by @vercel/oidc's
getVercelOidcToken()).
- In @workflow/world-vercel, replace the legacy
VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS / x-vercel-protection-bypass
flow with VERCEL_OIDC_TOKEN / x-vercel-trusted-oidc-idp-token. The
trusted-source header is attached on every outbound workflow-server
request (both proxied through api.vercel.com and direct).
- Drop the bypass header from the encryption-key and
resolve-latest-deployment fetches: those go to api.vercel.com which
is public.
- Drop VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS plumbing from tests.yml.
- Update the pending world-vercel changeset to describe the final
trusted-sources flow.
Adds a debug step that does two HEAD requests against the docs preview
deployment (with and without the OIDC trusted-sources header) and prints
the response status line plus `x-vercel-id` for each. The proxy-side
trusted-sources changes for GitHub Actions OIDC tokens are rolling out
gradually (~12+ hours), so the edge-node identifier in `x-vercel-id`
helps explain why a request might succeed or fail during the rollout
window.
Also includes `x-vercel-id` in the `waitForServer` timeout error so
post-mortem analysis of failing runs has the same edge-node info.
…ix reaches the serving edge node
The probe served its purpose: confirmed the bypass is functional once
the request lands on a region that has the proxy-side trusted-sources
fix rolled out. The waitForServer error message still surfaces
x-vercel-id for any future rollout-window debugging.
Adds a one-shot diagnostic that prints the non-sensitive claims of the
OIDC token (`iss`, `aud`, `owner_id`, `project_id`, `environment`,
`sub`, `scope`, `exp`) on the first request that uses bearer auth.
This is invaluable for debugging Vercel deployment-protection
trusted-source rule mismatches: a 401 from the edge tells you nothing
about why the rule didn't match, and the token's claims are the only
thing that determines that. The signature is never logged.
Gated to once per process — Vercel-issued tokens are process-stable for
the lambda's lifetime so further log lines would just be redundant
spam.
The Authorization bearer correctly preferred config.token (a static
Vercel auth token from CLI / Actions runner) and fell back to
getVercelOidcToken() inside a Vercel function. But the trusted-sources
bypass header (x-vercel-trusted-oidc-idp-token) was being read directly
from process.env.VERCEL_OIDC_TOKEN inside getHeaders(). That env var is
the bake-time token, frozen at deployment-creation time — on a project
that has been redeployed after a settings change, it carries stale
claims (e.g. an iss from when the project was briefly in 'global' mode)
that no longer match the workflow-server's trusted-sources rule.
Move trusted-sources header attachment from getHeaders() (sync) to
getHttpConfig() (async) and source it from getVercelOidcToken(). That
function reads getContext().headers['x-vercel-oidc-token'] first — a
freshly minted per-request token that always reflects current project
settings — and only falls back to the env var when that header is
missing.
Bearer auth source remains config.token-first.
Also expand the diagnostic to log claims from BOTH the per-request OIDC
token AND the bake-time env var so the divergence is visible in logs
when debugging future trusted-source mismatches.
Removes the now-misleading getProtectionBypassHeader() helper (its
'read env var directly' semantics were exactly the bug).
The two outbound flows have different auth requirements:
1. Proxied (usingProxy=true) — calls api.vercel.com/v1/workflow.
Public endpoint, authenticated with a static Vercel auth token via
config.token. The api-workflow proxy mints its own OIDC token
before forwarding to workflow-server, so the trusted-sources
bypass header on the SDK→proxy hop is meaningless. CLI, GitHub
Actions, and other API-client callers take this path.
2. Direct (usingProxy=false) — runs inside a Vercel deployment
talking straight to workflow-server. workflow-server validates a
Vercel OIDC bearer; Vercel's edge validates the trusted-sources
header. Both must come from getVercelOidcToken() (the per-request
fresh token), not process.env.VERCEL_OIDC_TOKEN (the bake-time
token that can be stale after a project config change).
Previously getHttpConfig attached x-vercel-trusted-oidc-idp-token on
both paths whenever getVercelOidcToken() resolved. That accidentally
forwarded the GitHub Actions OIDC token (when wired into
VERCEL_OIDC_TOKEN by the test runner) onto every SDK→proxy request,
which is harmless but wrong-by-design — the proxy is public, doesn't
look at that header on its inbound side, and the GHA token isn't its
intended audience.
Bearer auth source rules:
- Proxied: only config.token. (No fallback to OIDC; that auth
pathway doesn't go through the proxy's auth checks.)
- Direct: config.token (for tests / local dev), falling back to
getVercelOidcToken() (for Vercel-runtime calls).
The api-workflow proxy authenticates the caller with a regular Vercel
auth token (not OIDC), so reaching the proxied path with no
config.token is always wrong: the proxy will reject the request and
the SDK caller would see an opaque 401 with no actionable hint.
Throw at config-resolution time with a clear message that points to
the WORKFLOW_VERCEL_AUTH_TOKEN env var the SDK reads from. Adds tests
covering both the no-token-throws case and the with-token-attaches-
bearer-and-skips-trusted-sources case.
When the trusted-sources bypass returns 401, the error message now
surfaces the response's x-vercel-id header so we can identify which
edge node served the failure. Helps distinguish proxy-rollout
incompleteness from actual config errors during incremental
rollouts of edge-side changes.
Comment threadpackages/core/e2e/e2e.test.ts Outdated
Comment threadpackages/world-vercel/src/jwt-claims.ts Outdated
Comment threadpackages/world-vercel/src/utils.test.ts Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
GitHub Actions OIDC tokens have a hard 5-minute lifetime that cannot be
extended (no API to ask for a longer TTL — exp is always iat + ~300s).
Pre-minting once at the start of the job and shipping the result down
to the test runner via env var means tests that run late in the suite
hit an expired token and 401 on /api/trigger-pages (and any other
trusted-sources protected endpoint).
Move minting into scripts/trusted-sources-headers.mjs:
- getTrustedSourcesHeaders() is now async.
- It calls the runner's ACTIONS_ID_TOKEN_REQUEST_URL endpoint directly
(the env vars GHA exposes when permissions: id-token: write is on)
and re-mints 60s before the cached token's exp.
- Falls back to process.env.VERCEL_OIDC_TOKEN for non-GHA contexts
(Vercel runtime, local dev).
Workflow files drop the now-redundant 'Mint OIDC token' step and the
VERCEL_OIDC_TOKEN env-var passthrough on the test step. The runner env
vars propagate to subsequent steps automatically.
Updates all 17 callers in e2e.test.ts / bench.bench.ts / utils.ts /
docs/scripts/check-docs-smoke.mjs to await the now-async call.
Comment thread.changeset/world-vercel-protection-bypass.md
Comment thread.changeset/world-vercel-trusted-sources.md Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
- Drop `statuses: read` from the three workflow permission blocks (the
wait-for-vercel-project action works without it on a public repo).
- Revert the `x-vercel-id` debug logging in `startWorkflowViaHttp`.
- Delete `packages/world-vercel/src/jwt-claims.ts` (debug-only helper).
- Drop the JWT claims diagnostic logging from `getHttpConfig`.
- Tighten the auth-flow comment in `getHttpConfig` and remove the
historical 'no longer attaches' note from `getHeaders`/its test.
- Restore `.changeset/world-vercel-protection-bypass.md` (already
shipped in a beta release per .changeset/pre.json).
- Trim the `.changeset/world-vercel-trusted-sources.md` description to
one short paragraph.
Configured trustedSources.projects on all 11 workbench app projects so
each one accepts a Vercel-issued OIDC token from any of the others. A
developer running e2e locally can now do `vercel env pull` from any
workbench app's directory and use the resulting VERCEL_OIDC_TOKEN to
bypass Deployment Protection on any of the workbench preview/prod
deployments — no need to disable protection on the project just to run
the suite locally.
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.

3 participants

@TooTallNate@pranaygp
, '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

ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources - #1882

Merged
TooTallNate merged 24 commits into
mainfrom
ci/oidc-trusted-sources
May 2, 2026
Merged

ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources#1882
TooTallNate merged 24 commits into
mainfrom
ci/oidc-trusted-sources

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 30, 2026

Copy link
Copy Markdown
Member

Summary

Switch the e2e, benchmark, and docs-smoke CI jobs from the static VERCEL_AUTOMATION_BYPASS_SECRET deployment-protection bypass token over to Vercel's OIDC Trusted Sources flow. Same conceptual swap is applied in the runtime world-vercel SDK: outbound requests to a protected workflow-server preview now carry an x-vercel-trusted-oidc-idp-token header instead of x-vercel-protection-bypass.

What changes

CI workflows

.github/workflows/tests.yml, benchmarks.yml, docs-checks.yml:

  • Add permissions: id-token: write (and explicit contents: read, deployments: read) to the affected jobs so the runner can mint OIDC tokens.
  • Tests/benchmarks/smoke checks pick up the ACTIONS_ID_TOKEN_REQUEST_URL / ACTIONS_ID_TOKEN_REQUEST_TOKEN env vars that GitHub injects on id-token: write–enabled jobs and mint OIDC tokens on demand (see helper below). The VERCEL_AUTOMATION_BYPASS_SECRET env var and the (now no-op) VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS env var are removed.

Shared on-demand minting helper — scripts/trusted-sources-headers.mjs

GitHub Actions OIDC tokens have a hard 5-minute lifetime. Pre-minting once at the start of the job and forwarding the result down doesn't work for long e2e suites — tests that run late in the run hit an expired token and 401. The new getTrustedSourcesHeaders() is async, mints fresh tokens from the runner endpoint on demand, caches them in-process, and re-mints 60 s before the JWT's exp. Falls back to process.env.VERCEL_OIDC_TOKEN for non-CI contexts (Vercel runtime, local dev).

Used by packages/core/e2e/{e2e.test.ts,bench.bench.ts,utils.ts} and docs/scripts/check-docs-smoke.mjs — all callers now await the helper.

@workflow/world-vercel runtime

packages/world-vercel/src/utils.ts:getHttpConfig:

  • Proxied path (usingProxy=true, hits api.vercel.com/v1/workflow): bearer auth from config.token only. The api-workflow proxy authenticates the caller with a regular Vercel auth token, so the trusted-sources header is meaningless on this hop. We now throw a clear error if config.token is missing instead of letting an opaque 401 bubble up.
  • Direct path (usingProxy=false, hits the workflow-server URL directly from a Vercel deployment): bearer auth prefers config.token and falls back to getVercelOidcToken(). The trusted-sources bypass header always uses getVercelOidcToken() (per-request token Vercel injects on every invocation). This avoids the trap of reading process.env.VERCEL_OIDC_TOKEN directly, which carries the deployment's bake-time token and goes stale after project-config changes.

packages/world-vercel/src/{encryption,resolve-latest-deployment}.ts: drop the (now-unused) getProtectionBypassHeader calls — both go to the public api.vercel.com, which never needs the bypass.

Vercel-side configuration (out of band)

Each of the 11 workbench app projects has its trust configuration updated:

  • trustedSources.projects — entries for all 11 workbench projects with a customAllow: [{ from: any-env, to: any-env }] rule, so any workbench's Vercel-issued OIDC token can reach any other (and itself) on any environment. This is what enables the local-dev recipe in AGENTS.md: vercel env pull from any workbench app yields a VERCEL_OIDC_TOKEN that bypasses Deployment Protection on every workbench preview/prod URL.
  • trustedSources.oidcProviders["https://token.actions.githubusercontent.com"] — accepts GitHub Actions OIDC tokens from vercel/workflow CI for the direct-fetch tests against the deployed workbench apps.

Local-development recipe (AGENTS.md)

Updated the local-against-preview e2e recipe to document vercel env pull as the source of VERCEL_OIDC_TOKEN. Replaces the old VERCEL_AUTOMATION_BYPASS_SECRET instruction.

Files changed

FileChange
.github/workflows/tests.ymlpermissions: id-token: write, dropped pre-mint step + VERCEL_AUTOMATION_BYPASS_SECRET / VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS
.github/workflows/benchmarks.ymlsame
.github/workflows/docs-checks.ymlsame
scripts/trusted-sources-headers.mjs (new)async on-demand minting helper
packages/core/e2e/e2e.test.tsawait getTrustedSourcesHeaders() at every callsite
packages/core/e2e/bench.bench.tssame
packages/core/e2e/utils.tssame
docs/scripts/check-docs-smoke.mjssame
packages/world-vercel/src/utils.tsbearer/trusted-sources split between proxied and direct paths; throw on missing config.token for proxied path; removed dead getProtectionBypassHeader
packages/world-vercel/src/utils.test.tstests for the new direct/proxied behaviour
packages/world-vercel/src/encryption.tsdrop bypass header (api.vercel.com is public)
packages/world-vercel/src/resolve-latest-deployment.tsdrop bypass header (api.vercel.com is public)
AGENTS.mdlocal-dev recipe uses vercel env pull for VERCEL_OIDC_TOKEN
.changeset/world-vercel-trusted-sources.md (new)minor bump for @workflow/world-vercel

Untouched (separate concerns)

  • WORKFLOW_VERCEL_AUTH_TOKEN (secrets.VERCEL_LABS_TOKEN) — Vercel API auth, still needed for the proxied path's config.token.
  • VERCEL_WORKFLOW_SERVER_URL env var — still used to point CI at the workflow-server preview on PR runs.

Cleanup follow-up

Once a CI run on main confirms everything works, the VERCEL_AUTOMATION_BYPASS_SECRET and VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS repo secrets can be deleted from GitHub.

The e2e, benchmark, and docs-smoke CI jobs previously used the static
`VERCEL_AUTOMATION_BYPASS_SECRET` deployment-protection bypass token
to reach protected Vercel deployments. Switch them over to the new OIDC
Trusted Sources flow: the GitHub Actions runner mints a short-lived
OIDC token via `core.getIDToken()` and forwards it on requests in the
`x-vercel-trusted-oidc-idp-token` header.
Each workbench project (and `workflow-docs`) has been configured with a
matching trusted-source rule:
aud=https://github.com/vercel, repository=vercel/workflow
The shared header helper now lives at `scripts/trusted-sources-headers.mjs`
and is imported by both the e2e/bench tests and the docs smoke script,
removing the previous duplication.
CopilotAI review requested due to automatic review settings April 30, 2026 17:50
@changeset-bot

changeset-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c125b54

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@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

@vercel

vercelBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.043s (-2.5%)1.006s (~)0.962s101.00x
💻 LocalNitro0.049s (+13.9% 🔺)1.011s (~)0.961s101.14x
🐘 PostgresNitro0.059s (-38.3% 🟢)1.010s (-3.1%)0.952s101.36x
🐘 PostgresExpress0.060s (+3.3%)1.011s (~)0.951s101.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.314s (+33.2% 🔺)2.234s (+4.6%)1.920s101.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.125s (~)2.005s (~)0.880s101.00x
💻 LocalNitro1.127s (~)2.006s (~)0.879s101.00x
🐘 PostgresExpress1.143s (~)2.010s (~)0.867s101.02x
🐘 PostgresNitro1.149s (+0.8%)2.010s (~)0.862s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.920s (+2.4%)3.677s (-3.4%)1.757s101.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.880s (~)11.019s (~)0.139s31.00x
💻 LocalNitro10.928s (~)11.023s (~)0.095s31.00x
💻 LocalExpress10.947s (~)11.022s (~)0.075s31.01x
🐘 PostgresExpress10.955s (~)11.353s (+3.0%)0.398s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.546s (+3.3%)19.315s (-3.5%)1.770s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.542s (~)15.022s (~)0.480s41.00x
🐘 PostgresNitro14.553s (~)15.025s (~)0.472s41.00x
💻 LocalExpress14.970s (~)15.280s (+1.7%)0.310s41.03x
💻 LocalNitro14.986s (-0.5%)15.279s (-4.7%)0.293s41.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express31.530s (-37.3% 🟢)33.430s (-36.4% 🟢)1.899s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.022s (~)14.447s (-1.0%)0.425s71.00x
🐘 PostgresNitro14.038s (+0.5%)14.737s (+3.0%)0.699s71.00x
💻 LocalExpress16.492s (-0.7%)17.031s (~)0.539s61.18x
💻 LocalNitro16.588s (-1.2%)17.029s (~)0.441s61.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express55.578s (-54.2% 🟢)58.406s (-52.8% 🟢)2.828s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.249s (-0.9%)2.010s (~)0.760s151.00x
🐘 PostgresNitro1.283s (+0.6%)2.011s (~)0.728s151.03x
💻 LocalExpress1.508s (+1.3%)2.006s (~)0.498s151.21x
💻 LocalNitro1.533s (-6.1% 🟢)2.006s (-3.3%)0.473s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.403s (-16.0% 🟢)3.950s (-14.6% 🟢)1.547s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.345s (-0.7%)3.009s (~)0.664s101.00x
🐘 PostgresNitro2.359s (~)3.011s (~)0.651s101.01x
💻 LocalExpress2.817s (-4.6%)3.108s (-10.0% 🟢)0.292s101.20x
💻 LocalNitro2.910s (-7.4% 🟢)3.207s (-17.5% 🟢)0.297s101.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.388s (-34.0% 🟢)4.195s (-17.9% 🟢)1.807s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.462s (-0.7%)4.010s (~)0.548s81.00x
🐘 PostgresNitro3.494s (~)4.014s (~)0.520s81.01x
💻 LocalExpress7.361s (-11.7% 🟢)8.020s (-11.1% 🟢)0.659s42.13x
💻 LocalNitro7.874s (-5.7% 🟢)8.269s (-8.3% 🟢)0.395s42.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.002s (-5.6% 🟢)6.045s (-1.3%)2.042s51.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.261s (~)2.009s (~)0.748s151.00x
🐘 PostgresExpress1.266s (+0.7%)2.008s (~)0.743s151.00x
💻 LocalExpress1.522s (-19.6% 🟢)2.006s (-15.1% 🟢)0.483s151.21x
💻 LocalNitro1.553s (-16.8% 🟢)2.006s (-14.3% 🟢)0.453s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.335s (-9.6% 🟢)3.671s (-15.6% 🟢)1.336s91.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.338s (~)3.010s (~)0.672s101.00x
🐘 PostgresExpress2.348s (~)3.011s (~)0.663s101.00x
💻 LocalExpress2.906s (-7.2% 🟢)3.008s (-20.1% 🟢)0.102s101.24x
💻 LocalNitro2.983s (-2.7%)3.565s (-8.3% 🟢)0.582s91.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.317s (-27.4% 🟢)3.822s (-20.3% 🟢)1.505s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.469s (-0.9%)4.010s (~)0.540s81.00x
🐘 PostgresNitro3.478s (~)4.011s (~)0.533s81.00x
💻 LocalExpress8.107s (-7.9% 🟢)9.025s (-2.7%)0.917s42.34x
💻 LocalNitro8.436s (-7.7% 🟢)9.024s (-10.0% 🟢)0.588s42.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.081s (-52.0% 🟢)4.799s (-41.3% 🟢)1.718s71.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.844s (+0.6%)1.023s (~)0.179s591.00x
🐘 PostgresNitro0.884s (+7.8% 🔺)1.041s (+3.5%)0.157s581.05x
💻 LocalNitro0.987s (+0.6%)1.181s (+8.0% 🔺)0.194s511.17x
💻 LocalExpress1.022s (+3.8%)1.941s (+80.4% 🔺)0.920s311.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.175s (-51.8% 🟢)10.768s (-49.5% 🟢)1.593s61.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.972s (~)2.402s (+6.4% 🔺)0.430s381.00x
🐘 PostgresNitro1.987s (+3.1%)2.403s (+14.4% 🔺)0.416s381.01x
💻 LocalNitro3.023s (~)3.649s (-2.9%)0.625s251.53x
💻 LocalExpress3.038s (+0.7%)3.759s (+4.8%)0.721s241.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express28.216s (-18.3% 🟢)30.015s (-18.5% 🟢)1.800s31.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.999s (~)4.456s (+2.0%)0.457s271.00x
🐘 PostgresNitro4.047s (-1.4%)4.627s (~)0.580s261.01x
💻 LocalExpress8.938s (-3.0%)9.325s (-6.9% 🟢)0.387s132.23x
💻 LocalNitro9.121s (-1.9%)9.710s (-3.1%)0.589s132.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express74.288s (-42.9% 🟢)76.387s (-42.2% 🟢)2.098s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.278s (-1.6%)1.007s (~)0.729s601.00x
🐘 PostgresNitro0.292s (+2.9%)1.007s (~)0.716s601.05x
💻 LocalExpress0.594s (+5.9% 🔺)1.005s (~)0.411s602.14x
💻 LocalNitro0.782s (+29.3% 🔺)1.205s (+17.9% 🔺)0.422s502.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.563s (-20.0% 🟢)3.059s (-15.9% 🟢)1.496s201.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.494s (-3.1%)1.007s (~)0.513s901.00x
🐘 PostgresNitro0.509s (+2.6%)1.008s (~)0.499s901.03x
💻 LocalExpress2.408s (-4.2%)3.009s (~)0.600s304.87x
💻 LocalNitro2.460s (-3.1%)3.009s (~)0.548s304.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.780s (-8.8% 🟢)4.406s (-8.4% 🟢)1.626s211.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.790s (-3.5%)1.007s (-1.0%)0.218s1201.00x
🐘 PostgresNitro0.798s (+1.0%)1.009s (~)0.210s1191.01x
💻 LocalExpress10.360s (-7.4% 🟢)11.028s (-7.6% 🟢)0.668s1113.12x
💻 LocalNitro10.737s (-4.1%)11.121s (-4.7%)0.384s1113.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express7.045s (-5.1% 🟢)8.589s (-7.1% 🟢)1.544s151.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.203s (-4.9%)1.004s (~)0.011s (-9.6% 🟢)1.017s (~)0.814s101.00x
🐘 PostgresNitro0.208s (+1.5%)0.999s (~)0.002s (~)1.011s (~)0.802s101.02x
💻 LocalExpress0.209s (+5.2% 🔺)1.004s (~)0.011s (-13.2% 🟢)1.016s (~)0.807s101.03x
🐘 PostgresExpress0.210s (+2.3%)0.998s (~)0.001s (-43.8% 🟢)1.010s (~)0.800s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.484s (-40.8% 🟢)3.012s (-26.4% 🟢)1.734s (+80.5% 🔺)5.137s (-8.1% 🟢)3.653s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.620s (-0.7%)1.005s (~)0.004s (-2.0%)1.023s (~)0.403s591.00x
🐘 PostgresExpress0.635s (+0.8%)1.005s (~)0.004s (+7.1% 🔺)1.023s (~)0.388s591.02x
💻 LocalExpress0.764s (+0.9%)1.013s (-1.6%)0.009s (-6.0% 🟢)1.023s (-1.6%)0.260s591.23x
💻 LocalNitro0.833s (-0.7%)1.011s (~)0.010s (+5.1% 🔺)1.116s (~)0.283s541.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.166s (-35.9% 🟢)5.514s (-31.2% 🟢)0.218s (-46.7% 🟢)6.081s (-31.2% 🟢)1.914s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.918s (-4.5%)1.149s (-10.1% 🟢)0.000s (-11.5% 🟢)1.168s (-10.6% 🟢)0.250s521.00x
🐘 PostgresNitro0.971s (~)1.264s (+1.3%)0.000s (-50.0% 🟢)1.290s (+2.5%)0.319s481.06x
💻 LocalNitro1.203s (-1.6%)2.020s (~)0.000s (+133.3% 🔺)2.021s (~)0.818s301.31x
💻 LocalExpress1.226s (~)2.021s (~)0.000s (-50.0% 🟢)2.023s (~)0.797s301.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.185s (-14.8% 🟢)4.482s (-12.2% 🟢)0.000s (-100.0% 🟢)4.856s (-12.2% 🟢)1.671s131.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.795s (+1.3%)2.177s (~)0.000s (NaN%)2.189s (~)0.394s281.00x
🐘 PostgresNitro1.832s (+2.3%)2.219s (+3.6%)0.000s (+3.7%)2.233s (+2.7%)0.400s271.02x
💻 LocalExpress3.392s (-2.2%)3.973s (-1.5%)0.001s (+25.0% 🔺)3.977s (-1.5%)0.584s161.89x
💻 LocalNitro3.499s (+3.3%)4.032s (~)0.000s (-12.5% 🟢)4.035s (~)0.536s151.95x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.518s (-1.5%)5.994s (~)0.000s (NaN%)6.413s (-0.7%)1.896s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress16/21
🐘 PostgresExpress15/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres15/21
Nitro🐘 Postgres18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production10000671067
✅ 💻 Local Development10780861164
✅ 📦 Local Production10780861164
✅ 🐘 Local Postgres10780861164
✅ 🪟 Windows970097
✅ 📋 Other273018291
Total460403434947

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9007
✅ example9007
✅ express9007
✅ fastify9007
✅ hono9007
✅ nextjs-turbopack9502
✅ nextjs-webpack9502
✅ nitro9007
✅ nuxt9007
✅ sveltekit9007
✅ vite9007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9700
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9106
✅ e2e-local-postgres-nest-stable9106
✅ e2e-local-prod-nest-stable9106

📋 View full workflow run

CopilotAI 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.

Pull request overview

Switches CI jobs that hit protected Vercel deployments from a long-lived bypass secret to Vercel Deployment Protection “Trusted Sources” using GitHub Actions OIDC, and centralizes the request header logic in a shared helper.

Changes:

  • Add OIDC token minting in relevant CI jobs and pass it via VERCEL_TRUSTED_OIDC_TOKEN.
  • Introduce scripts/trusted-sources-headers.mjs and update e2e/bench/docs smoke callers to use it.
  • Update local dev docs (AGENTS.md) to reflect the new bypass mechanism.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
scripts/trusted-sources-headers.mjsNew shared helper to attach the Vercel Trusted Sources OIDC header when present.
packages/core/e2e/utils.tsRemoves legacy bypass-secret helper; uses the shared Trusted Sources headers helper.
packages/core/e2e/e2e.test.tsUpdates HTTP calls to use Trusted Sources headers.
packages/core/e2e/bench.bench.tsUpdates manifest fetch to use Trusted Sources headers.
docs/scripts/check-docs-smoke.mjsUpdates docs smoke fetches to use Trusted Sources headers.
AGENTS.mdDocuments the new env var and local-development limitation.
.github/workflows/tests.ymlAdds OIDC token minting + env wiring for e2e Vercel prod tests.
.github/workflows/docs-checks.ymlAdds OIDC token minting + env wiring for docs preview smoke checks.
.github/workflows/benchmarks.ymlAdds OIDC token minting + env wiring for Vercel benchmarks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread.github/workflows/tests.yml
Comment thread.github/workflows/docs-checks.yml
Comment thread.github/workflows/benchmarks.yml
- Rename the env var from VERCEL_TRUSTED_OIDC_TOKEN to VERCEL_OIDC_TOKEN
to match Vercel's convention (also read by @vercel/oidc's
getVercelOidcToken()).
- In @workflow/world-vercel, replace the legacy
VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS / x-vercel-protection-bypass
flow with VERCEL_OIDC_TOKEN / x-vercel-trusted-oidc-idp-token. The
trusted-source header is attached on every outbound workflow-server
request (both proxied through api.vercel.com and direct).
- Drop the bypass header from the encryption-key and
resolve-latest-deployment fetches: those go to api.vercel.com which
is public.
- Drop VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS plumbing from tests.yml.
- Update the pending world-vercel changeset to describe the final
trusted-sources flow.
Adds a debug step that does two HEAD requests against the docs preview
deployment (with and without the OIDC trusted-sources header) and prints
the response status line plus `x-vercel-id` for each. The proxy-side
trusted-sources changes for GitHub Actions OIDC tokens are rolling out
gradually (~12+ hours), so the edge-node identifier in `x-vercel-id`
helps explain why a request might succeed or fail during the rollout
window.
Also includes `x-vercel-id` in the `waitForServer` timeout error so
post-mortem analysis of failing runs has the same edge-node info.
…ix reaches the serving edge node
The probe served its purpose: confirmed the bypass is functional once
the request lands on a region that has the proxy-side trusted-sources
fix rolled out. The waitForServer error message still surfaces
x-vercel-id for any future rollout-window debugging.
Adds a one-shot diagnostic that prints the non-sensitive claims of the
OIDC token (`iss`, `aud`, `owner_id`, `project_id`, `environment`,
`sub`, `scope`, `exp`) on the first request that uses bearer auth.
This is invaluable for debugging Vercel deployment-protection
trusted-source rule mismatches: a 401 from the edge tells you nothing
about why the rule didn't match, and the token's claims are the only
thing that determines that. The signature is never logged.
Gated to once per process — Vercel-issued tokens are process-stable for
the lambda's lifetime so further log lines would just be redundant
spam.
The Authorization bearer correctly preferred config.token (a static
Vercel auth token from CLI / Actions runner) and fell back to
getVercelOidcToken() inside a Vercel function. But the trusted-sources
bypass header (x-vercel-trusted-oidc-idp-token) was being read directly
from process.env.VERCEL_OIDC_TOKEN inside getHeaders(). That env var is
the bake-time token, frozen at deployment-creation time — on a project
that has been redeployed after a settings change, it carries stale
claims (e.g. an iss from when the project was briefly in 'global' mode)
that no longer match the workflow-server's trusted-sources rule.
Move trusted-sources header attachment from getHeaders() (sync) to
getHttpConfig() (async) and source it from getVercelOidcToken(). That
function reads getContext().headers['x-vercel-oidc-token'] first — a
freshly minted per-request token that always reflects current project
settings — and only falls back to the env var when that header is
missing.
Bearer auth source remains config.token-first.
Also expand the diagnostic to log claims from BOTH the per-request OIDC
token AND the bake-time env var so the divergence is visible in logs
when debugging future trusted-source mismatches.
Removes the now-misleading getProtectionBypassHeader() helper (its
'read env var directly' semantics were exactly the bug).
The two outbound flows have different auth requirements:
1. Proxied (usingProxy=true) — calls api.vercel.com/v1/workflow.
Public endpoint, authenticated with a static Vercel auth token via
config.token. The api-workflow proxy mints its own OIDC token
before forwarding to workflow-server, so the trusted-sources
bypass header on the SDK→proxy hop is meaningless. CLI, GitHub
Actions, and other API-client callers take this path.
2. Direct (usingProxy=false) — runs inside a Vercel deployment
talking straight to workflow-server. workflow-server validates a
Vercel OIDC bearer; Vercel's edge validates the trusted-sources
header. Both must come from getVercelOidcToken() (the per-request
fresh token), not process.env.VERCEL_OIDC_TOKEN (the bake-time
token that can be stale after a project config change).
Previously getHttpConfig attached x-vercel-trusted-oidc-idp-token on
both paths whenever getVercelOidcToken() resolved. That accidentally
forwarded the GitHub Actions OIDC token (when wired into
VERCEL_OIDC_TOKEN by the test runner) onto every SDK→proxy request,
which is harmless but wrong-by-design — the proxy is public, doesn't
look at that header on its inbound side, and the GHA token isn't its
intended audience.
Bearer auth source rules:
- Proxied: only config.token. (No fallback to OIDC; that auth
pathway doesn't go through the proxy's auth checks.)
- Direct: config.token (for tests / local dev), falling back to
getVercelOidcToken() (for Vercel-runtime calls).
The api-workflow proxy authenticates the caller with a regular Vercel
auth token (not OIDC), so reaching the proxied path with no
config.token is always wrong: the proxy will reject the request and
the SDK caller would see an opaque 401 with no actionable hint.
Throw at config-resolution time with a clear message that points to
the WORKFLOW_VERCEL_AUTH_TOKEN env var the SDK reads from. Adds tests
covering both the no-token-throws case and the with-token-attaches-
bearer-and-skips-trusted-sources case.
When the trusted-sources bypass returns 401, the error message now
surfaces the response's x-vercel-id header so we can identify which
edge node served the failure. Helps distinguish proxy-rollout
incompleteness from actual config errors during incremental
rollouts of edge-side changes.
Comment threadpackages/core/e2e/e2e.test.ts Outdated
Comment threadpackages/world-vercel/src/jwt-claims.ts Outdated
Comment threadpackages/world-vercel/src/utils.test.ts Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
GitHub Actions OIDC tokens have a hard 5-minute lifetime that cannot be
extended (no API to ask for a longer TTL — exp is always iat + ~300s).
Pre-minting once at the start of the job and shipping the result down
to the test runner via env var means tests that run late in the suite
hit an expired token and 401 on /api/trigger-pages (and any other
trusted-sources protected endpoint).
Move minting into scripts/trusted-sources-headers.mjs:
- getTrustedSourcesHeaders() is now async.
- It calls the runner's ACTIONS_ID_TOKEN_REQUEST_URL endpoint directly
(the env vars GHA exposes when permissions: id-token: write is on)
and re-mints 60s before the cached token's exp.
- Falls back to process.env.VERCEL_OIDC_TOKEN for non-GHA contexts
(Vercel runtime, local dev).
Workflow files drop the now-redundant 'Mint OIDC token' step and the
VERCEL_OIDC_TOKEN env-var passthrough on the test step. The runner env
vars propagate to subsequent steps automatically.
Updates all 17 callers in e2e.test.ts / bench.bench.ts / utils.ts /
docs/scripts/check-docs-smoke.mjs to await the now-async call.
Comment thread.changeset/world-vercel-protection-bypass.md
Comment thread.changeset/world-vercel-trusted-sources.md Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
- Drop `statuses: read` from the three workflow permission blocks (the
wait-for-vercel-project action works without it on a public repo).
- Revert the `x-vercel-id` debug logging in `startWorkflowViaHttp`.
- Delete `packages/world-vercel/src/jwt-claims.ts` (debug-only helper).
- Drop the JWT claims diagnostic logging from `getHttpConfig`.
- Tighten the auth-flow comment in `getHttpConfig` and remove the
historical 'no longer attaches' note from `getHeaders`/its test.
- Restore `.changeset/world-vercel-protection-bypass.md` (already
shipped in a beta release per .changeset/pre.json).
- Trim the `.changeset/world-vercel-trusted-sources.md` description to
one short paragraph.
Configured trustedSources.projects on all 11 workbench app projects so
each one accepts a Vercel-issued OIDC token from any of the others. A
developer running e2e locally can now do `vercel env pull` from any
workbench app's directory and use the resulting VERCEL_OIDC_TOKEN to
bypass Deployment Protection on any of the workbench preview/prod
deployments — no need to disable protection on the project just to run
the suite locally.
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.

3 participants

@TooTallNate@pranaygp
, '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

ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources - #1882

Merged
TooTallNate merged 24 commits into
mainfrom
ci/oidc-trusted-sources
May 2, 2026
Merged

ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources#1882
TooTallNate merged 24 commits into
mainfrom
ci/oidc-trusted-sources

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 30, 2026

Copy link
Copy Markdown
Member

Summary

Switch the e2e, benchmark, and docs-smoke CI jobs from the static VERCEL_AUTOMATION_BYPASS_SECRET deployment-protection bypass token over to Vercel's OIDC Trusted Sources flow. Same conceptual swap is applied in the runtime world-vercel SDK: outbound requests to a protected workflow-server preview now carry an x-vercel-trusted-oidc-idp-token header instead of x-vercel-protection-bypass.

What changes

CI workflows

.github/workflows/tests.yml, benchmarks.yml, docs-checks.yml:

  • Add permissions: id-token: write (and explicit contents: read, deployments: read) to the affected jobs so the runner can mint OIDC tokens.
  • Tests/benchmarks/smoke checks pick up the ACTIONS_ID_TOKEN_REQUEST_URL / ACTIONS_ID_TOKEN_REQUEST_TOKEN env vars that GitHub injects on id-token: write–enabled jobs and mint OIDC tokens on demand (see helper below). The VERCEL_AUTOMATION_BYPASS_SECRET env var and the (now no-op) VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS env var are removed.

Shared on-demand minting helper — scripts/trusted-sources-headers.mjs

GitHub Actions OIDC tokens have a hard 5-minute lifetime. Pre-minting once at the start of the job and forwarding the result down doesn't work for long e2e suites — tests that run late in the run hit an expired token and 401. The new getTrustedSourcesHeaders() is async, mints fresh tokens from the runner endpoint on demand, caches them in-process, and re-mints 60 s before the JWT's exp. Falls back to process.env.VERCEL_OIDC_TOKEN for non-CI contexts (Vercel runtime, local dev).

Used by packages/core/e2e/{e2e.test.ts,bench.bench.ts,utils.ts} and docs/scripts/check-docs-smoke.mjs — all callers now await the helper.

@workflow/world-vercel runtime

packages/world-vercel/src/utils.ts:getHttpConfig:

  • Proxied path (usingProxy=true, hits api.vercel.com/v1/workflow): bearer auth from config.token only. The api-workflow proxy authenticates the caller with a regular Vercel auth token, so the trusted-sources header is meaningless on this hop. We now throw a clear error if config.token is missing instead of letting an opaque 401 bubble up.
  • Direct path (usingProxy=false, hits the workflow-server URL directly from a Vercel deployment): bearer auth prefers config.token and falls back to getVercelOidcToken(). The trusted-sources bypass header always uses getVercelOidcToken() (per-request token Vercel injects on every invocation). This avoids the trap of reading process.env.VERCEL_OIDC_TOKEN directly, which carries the deployment's bake-time token and goes stale after project-config changes.

packages/world-vercel/src/{encryption,resolve-latest-deployment}.ts: drop the (now-unused) getProtectionBypassHeader calls — both go to the public api.vercel.com, which never needs the bypass.

Vercel-side configuration (out of band)

Each of the 11 workbench app projects has its trust configuration updated:

  • trustedSources.projects — entries for all 11 workbench projects with a customAllow: [{ from: any-env, to: any-env }] rule, so any workbench's Vercel-issued OIDC token can reach any other (and itself) on any environment. This is what enables the local-dev recipe in AGENTS.md: vercel env pull from any workbench app yields a VERCEL_OIDC_TOKEN that bypasses Deployment Protection on every workbench preview/prod URL.
  • trustedSources.oidcProviders["https://token.actions.githubusercontent.com"] — accepts GitHub Actions OIDC tokens from vercel/workflow CI for the direct-fetch tests against the deployed workbench apps.

Local-development recipe (AGENTS.md)

Updated the local-against-preview e2e recipe to document vercel env pull as the source of VERCEL_OIDC_TOKEN. Replaces the old VERCEL_AUTOMATION_BYPASS_SECRET instruction.

Files changed

FileChange
.github/workflows/tests.ymlpermissions: id-token: write, dropped pre-mint step + VERCEL_AUTOMATION_BYPASS_SECRET / VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS
.github/workflows/benchmarks.ymlsame
.github/workflows/docs-checks.ymlsame
scripts/trusted-sources-headers.mjs (new)async on-demand minting helper
packages/core/e2e/e2e.test.tsawait getTrustedSourcesHeaders() at every callsite
packages/core/e2e/bench.bench.tssame
packages/core/e2e/utils.tssame
docs/scripts/check-docs-smoke.mjssame
packages/world-vercel/src/utils.tsbearer/trusted-sources split between proxied and direct paths; throw on missing config.token for proxied path; removed dead getProtectionBypassHeader
packages/world-vercel/src/utils.test.tstests for the new direct/proxied behaviour
packages/world-vercel/src/encryption.tsdrop bypass header (api.vercel.com is public)
packages/world-vercel/src/resolve-latest-deployment.tsdrop bypass header (api.vercel.com is public)
AGENTS.mdlocal-dev recipe uses vercel env pull for VERCEL_OIDC_TOKEN
.changeset/world-vercel-trusted-sources.md (new)minor bump for @workflow/world-vercel

Untouched (separate concerns)

  • WORKFLOW_VERCEL_AUTH_TOKEN (secrets.VERCEL_LABS_TOKEN) — Vercel API auth, still needed for the proxied path's config.token.
  • VERCEL_WORKFLOW_SERVER_URL env var — still used to point CI at the workflow-server preview on PR runs.

Cleanup follow-up

Once a CI run on main confirms everything works, the VERCEL_AUTOMATION_BYPASS_SECRET and VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS repo secrets can be deleted from GitHub.

The e2e, benchmark, and docs-smoke CI jobs previously used the static
`VERCEL_AUTOMATION_BYPASS_SECRET` deployment-protection bypass token
to reach protected Vercel deployments. Switch them over to the new OIDC
Trusted Sources flow: the GitHub Actions runner mints a short-lived
OIDC token via `core.getIDToken()` and forwards it on requests in the
`x-vercel-trusted-oidc-idp-token` header.
Each workbench project (and `workflow-docs`) has been configured with a
matching trusted-source rule:
aud=https://github.com/vercel, repository=vercel/workflow
The shared header helper now lives at `scripts/trusted-sources-headers.mjs`
and is imported by both the e2e/bench tests and the docs smoke script,
removing the previous duplication.
CopilotAI review requested due to automatic review settings April 30, 2026 17:50
@changeset-bot

changeset-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c125b54

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@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

@vercel

vercelBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.043s (-2.5%)1.006s (~)0.962s101.00x
💻 LocalNitro0.049s (+13.9% 🔺)1.011s (~)0.961s101.14x
🐘 PostgresNitro0.059s (-38.3% 🟢)1.010s (-3.1%)0.952s101.36x
🐘 PostgresExpress0.060s (+3.3%)1.011s (~)0.951s101.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.314s (+33.2% 🔺)2.234s (+4.6%)1.920s101.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.125s (~)2.005s (~)0.880s101.00x
💻 LocalNitro1.127s (~)2.006s (~)0.879s101.00x
🐘 PostgresExpress1.143s (~)2.010s (~)0.867s101.02x
🐘 PostgresNitro1.149s (+0.8%)2.010s (~)0.862s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.920s (+2.4%)3.677s (-3.4%)1.757s101.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.880s (~)11.019s (~)0.139s31.00x
💻 LocalNitro10.928s (~)11.023s (~)0.095s31.00x
💻 LocalExpress10.947s (~)11.022s (~)0.075s31.01x
🐘 PostgresExpress10.955s (~)11.353s (+3.0%)0.398s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.546s (+3.3%)19.315s (-3.5%)1.770s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.542s (~)15.022s (~)0.480s41.00x
🐘 PostgresNitro14.553s (~)15.025s (~)0.472s41.00x
💻 LocalExpress14.970s (~)15.280s (+1.7%)0.310s41.03x
💻 LocalNitro14.986s (-0.5%)15.279s (-4.7%)0.293s41.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express31.530s (-37.3% 🟢)33.430s (-36.4% 🟢)1.899s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.022s (~)14.447s (-1.0%)0.425s71.00x
🐘 PostgresNitro14.038s (+0.5%)14.737s (+3.0%)0.699s71.00x
💻 LocalExpress16.492s (-0.7%)17.031s (~)0.539s61.18x
💻 LocalNitro16.588s (-1.2%)17.029s (~)0.441s61.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express55.578s (-54.2% 🟢)58.406s (-52.8% 🟢)2.828s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.249s (-0.9%)2.010s (~)0.760s151.00x
🐘 PostgresNitro1.283s (+0.6%)2.011s (~)0.728s151.03x
💻 LocalExpress1.508s (+1.3%)2.006s (~)0.498s151.21x
💻 LocalNitro1.533s (-6.1% 🟢)2.006s (-3.3%)0.473s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.403s (-16.0% 🟢)3.950s (-14.6% 🟢)1.547s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.345s (-0.7%)3.009s (~)0.664s101.00x
🐘 PostgresNitro2.359s (~)3.011s (~)0.651s101.01x
💻 LocalExpress2.817s (-4.6%)3.108s (-10.0% 🟢)0.292s101.20x
💻 LocalNitro2.910s (-7.4% 🟢)3.207s (-17.5% 🟢)0.297s101.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.388s (-34.0% 🟢)4.195s (-17.9% 🟢)1.807s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.462s (-0.7%)4.010s (~)0.548s81.00x
🐘 PostgresNitro3.494s (~)4.014s (~)0.520s81.01x
💻 LocalExpress7.361s (-11.7% 🟢)8.020s (-11.1% 🟢)0.659s42.13x
💻 LocalNitro7.874s (-5.7% 🟢)8.269s (-8.3% 🟢)0.395s42.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.002s (-5.6% 🟢)6.045s (-1.3%)2.042s51.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.261s (~)2.009s (~)0.748s151.00x
🐘 PostgresExpress1.266s (+0.7%)2.008s (~)0.743s151.00x
💻 LocalExpress1.522s (-19.6% 🟢)2.006s (-15.1% 🟢)0.483s151.21x
💻 LocalNitro1.553s (-16.8% 🟢)2.006s (-14.3% 🟢)0.453s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.335s (-9.6% 🟢)3.671s (-15.6% 🟢)1.336s91.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.338s (~)3.010s (~)0.672s101.00x
🐘 PostgresExpress2.348s (~)3.011s (~)0.663s101.00x
💻 LocalExpress2.906s (-7.2% 🟢)3.008s (-20.1% 🟢)0.102s101.24x
💻 LocalNitro2.983s (-2.7%)3.565s (-8.3% 🟢)0.582s91.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.317s (-27.4% 🟢)3.822s (-20.3% 🟢)1.505s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.469s (-0.9%)4.010s (~)0.540s81.00x
🐘 PostgresNitro3.478s (~)4.011s (~)0.533s81.00x
💻 LocalExpress8.107s (-7.9% 🟢)9.025s (-2.7%)0.917s42.34x
💻 LocalNitro8.436s (-7.7% 🟢)9.024s (-10.0% 🟢)0.588s42.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.081s (-52.0% 🟢)4.799s (-41.3% 🟢)1.718s71.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.844s (+0.6%)1.023s (~)0.179s591.00x
🐘 PostgresNitro0.884s (+7.8% 🔺)1.041s (+3.5%)0.157s581.05x
💻 LocalNitro0.987s (+0.6%)1.181s (+8.0% 🔺)0.194s511.17x
💻 LocalExpress1.022s (+3.8%)1.941s (+80.4% 🔺)0.920s311.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.175s (-51.8% 🟢)10.768s (-49.5% 🟢)1.593s61.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.972s (~)2.402s (+6.4% 🔺)0.430s381.00x
🐘 PostgresNitro1.987s (+3.1%)2.403s (+14.4% 🔺)0.416s381.01x
💻 LocalNitro3.023s (~)3.649s (-2.9%)0.625s251.53x
💻 LocalExpress3.038s (+0.7%)3.759s (+4.8%)0.721s241.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express28.216s (-18.3% 🟢)30.015s (-18.5% 🟢)1.800s31.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.999s (~)4.456s (+2.0%)0.457s271.00x
🐘 PostgresNitro4.047s (-1.4%)4.627s (~)0.580s261.01x
💻 LocalExpress8.938s (-3.0%)9.325s (-6.9% 🟢)0.387s132.23x
💻 LocalNitro9.121s (-1.9%)9.710s (-3.1%)0.589s132.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express74.288s (-42.9% 🟢)76.387s (-42.2% 🟢)2.098s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.278s (-1.6%)1.007s (~)0.729s601.00x
🐘 PostgresNitro0.292s (+2.9%)1.007s (~)0.716s601.05x
💻 LocalExpress0.594s (+5.9% 🔺)1.005s (~)0.411s602.14x
💻 LocalNitro0.782s (+29.3% 🔺)1.205s (+17.9% 🔺)0.422s502.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.563s (-20.0% 🟢)3.059s (-15.9% 🟢)1.496s201.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.494s (-3.1%)1.007s (~)0.513s901.00x
🐘 PostgresNitro0.509s (+2.6%)1.008s (~)0.499s901.03x
💻 LocalExpress2.408s (-4.2%)3.009s (~)0.600s304.87x
💻 LocalNitro2.460s (-3.1%)3.009s (~)0.548s304.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.780s (-8.8% 🟢)4.406s (-8.4% 🟢)1.626s211.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.790s (-3.5%)1.007s (-1.0%)0.218s1201.00x
🐘 PostgresNitro0.798s (+1.0%)1.009s (~)0.210s1191.01x
💻 LocalExpress10.360s (-7.4% 🟢)11.028s (-7.6% 🟢)0.668s1113.12x
💻 LocalNitro10.737s (-4.1%)11.121s (-4.7%)0.384s1113.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express7.045s (-5.1% 🟢)8.589s (-7.1% 🟢)1.544s151.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.203s (-4.9%)1.004s (~)0.011s (-9.6% 🟢)1.017s (~)0.814s101.00x
🐘 PostgresNitro0.208s (+1.5%)0.999s (~)0.002s (~)1.011s (~)0.802s101.02x
💻 LocalExpress0.209s (+5.2% 🔺)1.004s (~)0.011s (-13.2% 🟢)1.016s (~)0.807s101.03x
🐘 PostgresExpress0.210s (+2.3%)0.998s (~)0.001s (-43.8% 🟢)1.010s (~)0.800s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.484s (-40.8% 🟢)3.012s (-26.4% 🟢)1.734s (+80.5% 🔺)5.137s (-8.1% 🟢)3.653s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.620s (-0.7%)1.005s (~)0.004s (-2.0%)1.023s (~)0.403s591.00x
🐘 PostgresExpress0.635s (+0.8%)1.005s (~)0.004s (+7.1% 🔺)1.023s (~)0.388s591.02x
💻 LocalExpress0.764s (+0.9%)1.013s (-1.6%)0.009s (-6.0% 🟢)1.023s (-1.6%)0.260s591.23x
💻 LocalNitro0.833s (-0.7%)1.011s (~)0.010s (+5.1% 🔺)1.116s (~)0.283s541.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.166s (-35.9% 🟢)5.514s (-31.2% 🟢)0.218s (-46.7% 🟢)6.081s (-31.2% 🟢)1.914s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.918s (-4.5%)1.149s (-10.1% 🟢)0.000s (-11.5% 🟢)1.168s (-10.6% 🟢)0.250s521.00x
🐘 PostgresNitro0.971s (~)1.264s (+1.3%)0.000s (-50.0% 🟢)1.290s (+2.5%)0.319s481.06x
💻 LocalNitro1.203s (-1.6%)2.020s (~)0.000s (+133.3% 🔺)2.021s (~)0.818s301.31x
💻 LocalExpress1.226s (~)2.021s (~)0.000s (-50.0% 🟢)2.023s (~)0.797s301.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.185s (-14.8% 🟢)4.482s (-12.2% 🟢)0.000s (-100.0% 🟢)4.856s (-12.2% 🟢)1.671s131.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.795s (+1.3%)2.177s (~)0.000s (NaN%)2.189s (~)0.394s281.00x
🐘 PostgresNitro1.832s (+2.3%)2.219s (+3.6%)0.000s (+3.7%)2.233s (+2.7%)0.400s271.02x
💻 LocalExpress3.392s (-2.2%)3.973s (-1.5%)0.001s (+25.0% 🔺)3.977s (-1.5%)0.584s161.89x
💻 LocalNitro3.499s (+3.3%)4.032s (~)0.000s (-12.5% 🟢)4.035s (~)0.536s151.95x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.518s (-1.5%)5.994s (~)0.000s (NaN%)6.413s (-0.7%)1.896s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress16/21
🐘 PostgresExpress15/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres15/21
Nitro🐘 Postgres18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production10000671067
✅ 💻 Local Development10780861164
✅ 📦 Local Production10780861164
✅ 🐘 Local Postgres10780861164
✅ 🪟 Windows970097
✅ 📋 Other273018291
Total460403434947

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9007
✅ example9007
✅ express9007
✅ fastify9007
✅ hono9007
✅ nextjs-turbopack9502
✅ nextjs-webpack9502
✅ nitro9007
✅ nuxt9007
✅ sveltekit9007
✅ vite9007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9700
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9106
✅ e2e-local-postgres-nest-stable9106
✅ e2e-local-prod-nest-stable9106

📋 View full workflow run

CopilotAI 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.

Pull request overview

Switches CI jobs that hit protected Vercel deployments from a long-lived bypass secret to Vercel Deployment Protection “Trusted Sources” using GitHub Actions OIDC, and centralizes the request header logic in a shared helper.

Changes:

  • Add OIDC token minting in relevant CI jobs and pass it via VERCEL_TRUSTED_OIDC_TOKEN.
  • Introduce scripts/trusted-sources-headers.mjs and update e2e/bench/docs smoke callers to use it.
  • Update local dev docs (AGENTS.md) to reflect the new bypass mechanism.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
scripts/trusted-sources-headers.mjsNew shared helper to attach the Vercel Trusted Sources OIDC header when present.
packages/core/e2e/utils.tsRemoves legacy bypass-secret helper; uses the shared Trusted Sources headers helper.
packages/core/e2e/e2e.test.tsUpdates HTTP calls to use Trusted Sources headers.
packages/core/e2e/bench.bench.tsUpdates manifest fetch to use Trusted Sources headers.
docs/scripts/check-docs-smoke.mjsUpdates docs smoke fetches to use Trusted Sources headers.
AGENTS.mdDocuments the new env var and local-development limitation.
.github/workflows/tests.ymlAdds OIDC token minting + env wiring for e2e Vercel prod tests.
.github/workflows/docs-checks.ymlAdds OIDC token minting + env wiring for docs preview smoke checks.
.github/workflows/benchmarks.ymlAdds OIDC token minting + env wiring for Vercel benchmarks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread.github/workflows/tests.yml
Comment thread.github/workflows/docs-checks.yml
Comment thread.github/workflows/benchmarks.yml
- Rename the env var from VERCEL_TRUSTED_OIDC_TOKEN to VERCEL_OIDC_TOKEN
to match Vercel's convention (also read by @vercel/oidc's
getVercelOidcToken()).
- In @workflow/world-vercel, replace the legacy
VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS / x-vercel-protection-bypass
flow with VERCEL_OIDC_TOKEN / x-vercel-trusted-oidc-idp-token. The
trusted-source header is attached on every outbound workflow-server
request (both proxied through api.vercel.com and direct).
- Drop the bypass header from the encryption-key and
resolve-latest-deployment fetches: those go to api.vercel.com which
is public.
- Drop VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS plumbing from tests.yml.
- Update the pending world-vercel changeset to describe the final
trusted-sources flow.
Adds a debug step that does two HEAD requests against the docs preview
deployment (with and without the OIDC trusted-sources header) and prints
the response status line plus `x-vercel-id` for each. The proxy-side
trusted-sources changes for GitHub Actions OIDC tokens are rolling out
gradually (~12+ hours), so the edge-node identifier in `x-vercel-id`
helps explain why a request might succeed or fail during the rollout
window.
Also includes `x-vercel-id` in the `waitForServer` timeout error so
post-mortem analysis of failing runs has the same edge-node info.
…ix reaches the serving edge node
The probe served its purpose: confirmed the bypass is functional once
the request lands on a region that has the proxy-side trusted-sources
fix rolled out. The waitForServer error message still surfaces
x-vercel-id for any future rollout-window debugging.
Adds a one-shot diagnostic that prints the non-sensitive claims of the
OIDC token (`iss`, `aud`, `owner_id`, `project_id`, `environment`,
`sub`, `scope`, `exp`) on the first request that uses bearer auth.
This is invaluable for debugging Vercel deployment-protection
trusted-source rule mismatches: a 401 from the edge tells you nothing
about why the rule didn't match, and the token's claims are the only
thing that determines that. The signature is never logged.
Gated to once per process — Vercel-issued tokens are process-stable for
the lambda's lifetime so further log lines would just be redundant
spam.
The Authorization bearer correctly preferred config.token (a static
Vercel auth token from CLI / Actions runner) and fell back to
getVercelOidcToken() inside a Vercel function. But the trusted-sources
bypass header (x-vercel-trusted-oidc-idp-token) was being read directly
from process.env.VERCEL_OIDC_TOKEN inside getHeaders(). That env var is
the bake-time token, frozen at deployment-creation time — on a project
that has been redeployed after a settings change, it carries stale
claims (e.g. an iss from when the project was briefly in 'global' mode)
that no longer match the workflow-server's trusted-sources rule.
Move trusted-sources header attachment from getHeaders() (sync) to
getHttpConfig() (async) and source it from getVercelOidcToken(). That
function reads getContext().headers['x-vercel-oidc-token'] first — a
freshly minted per-request token that always reflects current project
settings — and only falls back to the env var when that header is
missing.
Bearer auth source remains config.token-first.
Also expand the diagnostic to log claims from BOTH the per-request OIDC
token AND the bake-time env var so the divergence is visible in logs
when debugging future trusted-source mismatches.
Removes the now-misleading getProtectionBypassHeader() helper (its
'read env var directly' semantics were exactly the bug).
The two outbound flows have different auth requirements:
1. Proxied (usingProxy=true) — calls api.vercel.com/v1/workflow.
Public endpoint, authenticated with a static Vercel auth token via
config.token. The api-workflow proxy mints its own OIDC token
before forwarding to workflow-server, so the trusted-sources
bypass header on the SDK→proxy hop is meaningless. CLI, GitHub
Actions, and other API-client callers take this path.
2. Direct (usingProxy=false) — runs inside a Vercel deployment
talking straight to workflow-server. workflow-server validates a
Vercel OIDC bearer; Vercel's edge validates the trusted-sources
header. Both must come from getVercelOidcToken() (the per-request
fresh token), not process.env.VERCEL_OIDC_TOKEN (the bake-time
token that can be stale after a project config change).
Previously getHttpConfig attached x-vercel-trusted-oidc-idp-token on
both paths whenever getVercelOidcToken() resolved. That accidentally
forwarded the GitHub Actions OIDC token (when wired into
VERCEL_OIDC_TOKEN by the test runner) onto every SDK→proxy request,
which is harmless but wrong-by-design — the proxy is public, doesn't
look at that header on its inbound side, and the GHA token isn't its
intended audience.
Bearer auth source rules:
- Proxied: only config.token. (No fallback to OIDC; that auth
pathway doesn't go through the proxy's auth checks.)
- Direct: config.token (for tests / local dev), falling back to
getVercelOidcToken() (for Vercel-runtime calls).
The api-workflow proxy authenticates the caller with a regular Vercel
auth token (not OIDC), so reaching the proxied path with no
config.token is always wrong: the proxy will reject the request and
the SDK caller would see an opaque 401 with no actionable hint.
Throw at config-resolution time with a clear message that points to
the WORKFLOW_VERCEL_AUTH_TOKEN env var the SDK reads from. Adds tests
covering both the no-token-throws case and the with-token-attaches-
bearer-and-skips-trusted-sources case.
When the trusted-sources bypass returns 401, the error message now
surfaces the response's x-vercel-id header so we can identify which
edge node served the failure. Helps distinguish proxy-rollout
incompleteness from actual config errors during incremental
rollouts of edge-side changes.
Comment threadpackages/core/e2e/e2e.test.ts Outdated
Comment threadpackages/world-vercel/src/jwt-claims.ts Outdated
Comment threadpackages/world-vercel/src/utils.test.ts Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
GitHub Actions OIDC tokens have a hard 5-minute lifetime that cannot be
extended (no API to ask for a longer TTL — exp is always iat + ~300s).
Pre-minting once at the start of the job and shipping the result down
to the test runner via env var means tests that run late in the suite
hit an expired token and 401 on /api/trigger-pages (and any other
trusted-sources protected endpoint).
Move minting into scripts/trusted-sources-headers.mjs:
- getTrustedSourcesHeaders() is now async.
- It calls the runner's ACTIONS_ID_TOKEN_REQUEST_URL endpoint directly
(the env vars GHA exposes when permissions: id-token: write is on)
and re-mints 60s before the cached token's exp.
- Falls back to process.env.VERCEL_OIDC_TOKEN for non-GHA contexts
(Vercel runtime, local dev).
Workflow files drop the now-redundant 'Mint OIDC token' step and the
VERCEL_OIDC_TOKEN env-var passthrough on the test step. The runner env
vars propagate to subsequent steps automatically.
Updates all 17 callers in e2e.test.ts / bench.bench.ts / utils.ts /
docs/scripts/check-docs-smoke.mjs to await the now-async call.
Comment thread.changeset/world-vercel-protection-bypass.md
Comment thread.changeset/world-vercel-trusted-sources.md Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
- Drop `statuses: read` from the three workflow permission blocks (the
wait-for-vercel-project action works without it on a public repo).
- Revert the `x-vercel-id` debug logging in `startWorkflowViaHttp`.
- Delete `packages/world-vercel/src/jwt-claims.ts` (debug-only helper).
- Drop the JWT claims diagnostic logging from `getHttpConfig`.
- Tighten the auth-flow comment in `getHttpConfig` and remove the
historical 'no longer attaches' note from `getHeaders`/its test.
- Restore `.changeset/world-vercel-protection-bypass.md` (already
shipped in a beta release per .changeset/pre.json).
- Trim the `.changeset/world-vercel-trusted-sources.md` description to
one short paragraph.
Configured trustedSources.projects on all 11 workbench app projects so
each one accepts a Vercel-issued OIDC token from any of the others. A
developer running e2e locally can now do `vercel env pull` from any
workbench app's directory and use the resulting VERCEL_OIDC_TOKEN to
bypass Deployment Protection on any of the workbench preview/prod
deployments — no need to disable protection on the project just to run
the suite locally.
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.

3 participants

@TooTallNate@pranaygp
, '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

ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources - #1882

Merged
TooTallNate merged 24 commits into
mainfrom
ci/oidc-trusted-sources
May 2, 2026
Merged

ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources#1882
TooTallNate merged 24 commits into
mainfrom
ci/oidc-trusted-sources

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 30, 2026

Copy link
Copy Markdown
Member

Summary

Switch the e2e, benchmark, and docs-smoke CI jobs from the static VERCEL_AUTOMATION_BYPASS_SECRET deployment-protection bypass token over to Vercel's OIDC Trusted Sources flow. Same conceptual swap is applied in the runtime world-vercel SDK: outbound requests to a protected workflow-server preview now carry an x-vercel-trusted-oidc-idp-token header instead of x-vercel-protection-bypass.

What changes

CI workflows

.github/workflows/tests.yml, benchmarks.yml, docs-checks.yml:

  • Add permissions: id-token: write (and explicit contents: read, deployments: read) to the affected jobs so the runner can mint OIDC tokens.
  • Tests/benchmarks/smoke checks pick up the ACTIONS_ID_TOKEN_REQUEST_URL / ACTIONS_ID_TOKEN_REQUEST_TOKEN env vars that GitHub injects on id-token: write–enabled jobs and mint OIDC tokens on demand (see helper below). The VERCEL_AUTOMATION_BYPASS_SECRET env var and the (now no-op) VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS env var are removed.

Shared on-demand minting helper — scripts/trusted-sources-headers.mjs

GitHub Actions OIDC tokens have a hard 5-minute lifetime. Pre-minting once at the start of the job and forwarding the result down doesn't work for long e2e suites — tests that run late in the run hit an expired token and 401. The new getTrustedSourcesHeaders() is async, mints fresh tokens from the runner endpoint on demand, caches them in-process, and re-mints 60 s before the JWT's exp. Falls back to process.env.VERCEL_OIDC_TOKEN for non-CI contexts (Vercel runtime, local dev).

Used by packages/core/e2e/{e2e.test.ts,bench.bench.ts,utils.ts} and docs/scripts/check-docs-smoke.mjs — all callers now await the helper.

@workflow/world-vercel runtime

packages/world-vercel/src/utils.ts:getHttpConfig:

  • Proxied path (usingProxy=true, hits api.vercel.com/v1/workflow): bearer auth from config.token only. The api-workflow proxy authenticates the caller with a regular Vercel auth token, so the trusted-sources header is meaningless on this hop. We now throw a clear error if config.token is missing instead of letting an opaque 401 bubble up.
  • Direct path (usingProxy=false, hits the workflow-server URL directly from a Vercel deployment): bearer auth prefers config.token and falls back to getVercelOidcToken(). The trusted-sources bypass header always uses getVercelOidcToken() (per-request token Vercel injects on every invocation). This avoids the trap of reading process.env.VERCEL_OIDC_TOKEN directly, which carries the deployment's bake-time token and goes stale after project-config changes.

packages/world-vercel/src/{encryption,resolve-latest-deployment}.ts: drop the (now-unused) getProtectionBypassHeader calls — both go to the public api.vercel.com, which never needs the bypass.

Vercel-side configuration (out of band)

Each of the 11 workbench app projects has its trust configuration updated:

  • trustedSources.projects — entries for all 11 workbench projects with a customAllow: [{ from: any-env, to: any-env }] rule, so any workbench's Vercel-issued OIDC token can reach any other (and itself) on any environment. This is what enables the local-dev recipe in AGENTS.md: vercel env pull from any workbench app yields a VERCEL_OIDC_TOKEN that bypasses Deployment Protection on every workbench preview/prod URL.
  • trustedSources.oidcProviders["https://token.actions.githubusercontent.com"] — accepts GitHub Actions OIDC tokens from vercel/workflow CI for the direct-fetch tests against the deployed workbench apps.

Local-development recipe (AGENTS.md)

Updated the local-against-preview e2e recipe to document vercel env pull as the source of VERCEL_OIDC_TOKEN. Replaces the old VERCEL_AUTOMATION_BYPASS_SECRET instruction.

Files changed

FileChange
.github/workflows/tests.ymlpermissions: id-token: write, dropped pre-mint step + VERCEL_AUTOMATION_BYPASS_SECRET / VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS
.github/workflows/benchmarks.ymlsame
.github/workflows/docs-checks.ymlsame
scripts/trusted-sources-headers.mjs (new)async on-demand minting helper
packages/core/e2e/e2e.test.tsawait getTrustedSourcesHeaders() at every callsite
packages/core/e2e/bench.bench.tssame
packages/core/e2e/utils.tssame
docs/scripts/check-docs-smoke.mjssame
packages/world-vercel/src/utils.tsbearer/trusted-sources split between proxied and direct paths; throw on missing config.token for proxied path; removed dead getProtectionBypassHeader
packages/world-vercel/src/utils.test.tstests for the new direct/proxied behaviour
packages/world-vercel/src/encryption.tsdrop bypass header (api.vercel.com is public)
packages/world-vercel/src/resolve-latest-deployment.tsdrop bypass header (api.vercel.com is public)
AGENTS.mdlocal-dev recipe uses vercel env pull for VERCEL_OIDC_TOKEN
.changeset/world-vercel-trusted-sources.md (new)minor bump for @workflow/world-vercel

Untouched (separate concerns)

  • WORKFLOW_VERCEL_AUTH_TOKEN (secrets.VERCEL_LABS_TOKEN) — Vercel API auth, still needed for the proxied path's config.token.
  • VERCEL_WORKFLOW_SERVER_URL env var — still used to point CI at the workflow-server preview on PR runs.

Cleanup follow-up

Once a CI run on main confirms everything works, the VERCEL_AUTOMATION_BYPASS_SECRET and VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS repo secrets can be deleted from GitHub.

The e2e, benchmark, and docs-smoke CI jobs previously used the static
`VERCEL_AUTOMATION_BYPASS_SECRET` deployment-protection bypass token
to reach protected Vercel deployments. Switch them over to the new OIDC
Trusted Sources flow: the GitHub Actions runner mints a short-lived
OIDC token via `core.getIDToken()` and forwards it on requests in the
`x-vercel-trusted-oidc-idp-token` header.
Each workbench project (and `workflow-docs`) has been configured with a
matching trusted-source rule:
aud=https://github.com/vercel, repository=vercel/workflow
The shared header helper now lives at `scripts/trusted-sources-headers.mjs`
and is imported by both the e2e/bench tests and the docs smoke script,
removing the previous duplication.
CopilotAI review requested due to automatic review settings April 30, 2026 17:50
@changeset-bot

changeset-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c125b54

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@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

@vercel

vercelBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.043s (-2.5%)1.006s (~)0.962s101.00x
💻 LocalNitro0.049s (+13.9% 🔺)1.011s (~)0.961s101.14x
🐘 PostgresNitro0.059s (-38.3% 🟢)1.010s (-3.1%)0.952s101.36x
🐘 PostgresExpress0.060s (+3.3%)1.011s (~)0.951s101.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.314s (+33.2% 🔺)2.234s (+4.6%)1.920s101.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.125s (~)2.005s (~)0.880s101.00x
💻 LocalNitro1.127s (~)2.006s (~)0.879s101.00x
🐘 PostgresExpress1.143s (~)2.010s (~)0.867s101.02x
🐘 PostgresNitro1.149s (+0.8%)2.010s (~)0.862s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.920s (+2.4%)3.677s (-3.4%)1.757s101.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.880s (~)11.019s (~)0.139s31.00x
💻 LocalNitro10.928s (~)11.023s (~)0.095s31.00x
💻 LocalExpress10.947s (~)11.022s (~)0.075s31.01x
🐘 PostgresExpress10.955s (~)11.353s (+3.0%)0.398s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.546s (+3.3%)19.315s (-3.5%)1.770s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.542s (~)15.022s (~)0.480s41.00x
🐘 PostgresNitro14.553s (~)15.025s (~)0.472s41.00x
💻 LocalExpress14.970s (~)15.280s (+1.7%)0.310s41.03x
💻 LocalNitro14.986s (-0.5%)15.279s (-4.7%)0.293s41.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express31.530s (-37.3% 🟢)33.430s (-36.4% 🟢)1.899s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.022s (~)14.447s (-1.0%)0.425s71.00x
🐘 PostgresNitro14.038s (+0.5%)14.737s (+3.0%)0.699s71.00x
💻 LocalExpress16.492s (-0.7%)17.031s (~)0.539s61.18x
💻 LocalNitro16.588s (-1.2%)17.029s (~)0.441s61.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express55.578s (-54.2% 🟢)58.406s (-52.8% 🟢)2.828s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.249s (-0.9%)2.010s (~)0.760s151.00x
🐘 PostgresNitro1.283s (+0.6%)2.011s (~)0.728s151.03x
💻 LocalExpress1.508s (+1.3%)2.006s (~)0.498s151.21x
💻 LocalNitro1.533s (-6.1% 🟢)2.006s (-3.3%)0.473s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.403s (-16.0% 🟢)3.950s (-14.6% 🟢)1.547s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.345s (-0.7%)3.009s (~)0.664s101.00x
🐘 PostgresNitro2.359s (~)3.011s (~)0.651s101.01x
💻 LocalExpress2.817s (-4.6%)3.108s (-10.0% 🟢)0.292s101.20x
💻 LocalNitro2.910s (-7.4% 🟢)3.207s (-17.5% 🟢)0.297s101.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.388s (-34.0% 🟢)4.195s (-17.9% 🟢)1.807s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.462s (-0.7%)4.010s (~)0.548s81.00x
🐘 PostgresNitro3.494s (~)4.014s (~)0.520s81.01x
💻 LocalExpress7.361s (-11.7% 🟢)8.020s (-11.1% 🟢)0.659s42.13x
💻 LocalNitro7.874s (-5.7% 🟢)8.269s (-8.3% 🟢)0.395s42.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.002s (-5.6% 🟢)6.045s (-1.3%)2.042s51.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.261s (~)2.009s (~)0.748s151.00x
🐘 PostgresExpress1.266s (+0.7%)2.008s (~)0.743s151.00x
💻 LocalExpress1.522s (-19.6% 🟢)2.006s (-15.1% 🟢)0.483s151.21x
💻 LocalNitro1.553s (-16.8% 🟢)2.006s (-14.3% 🟢)0.453s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.335s (-9.6% 🟢)3.671s (-15.6% 🟢)1.336s91.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.338s (~)3.010s (~)0.672s101.00x
🐘 PostgresExpress2.348s (~)3.011s (~)0.663s101.00x
💻 LocalExpress2.906s (-7.2% 🟢)3.008s (-20.1% 🟢)0.102s101.24x
💻 LocalNitro2.983s (-2.7%)3.565s (-8.3% 🟢)0.582s91.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.317s (-27.4% 🟢)3.822s (-20.3% 🟢)1.505s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.469s (-0.9%)4.010s (~)0.540s81.00x
🐘 PostgresNitro3.478s (~)4.011s (~)0.533s81.00x
💻 LocalExpress8.107s (-7.9% 🟢)9.025s (-2.7%)0.917s42.34x
💻 LocalNitro8.436s (-7.7% 🟢)9.024s (-10.0% 🟢)0.588s42.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.081s (-52.0% 🟢)4.799s (-41.3% 🟢)1.718s71.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.844s (+0.6%)1.023s (~)0.179s591.00x
🐘 PostgresNitro0.884s (+7.8% 🔺)1.041s (+3.5%)0.157s581.05x
💻 LocalNitro0.987s (+0.6%)1.181s (+8.0% 🔺)0.194s511.17x
💻 LocalExpress1.022s (+3.8%)1.941s (+80.4% 🔺)0.920s311.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.175s (-51.8% 🟢)10.768s (-49.5% 🟢)1.593s61.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.972s (~)2.402s (+6.4% 🔺)0.430s381.00x
🐘 PostgresNitro1.987s (+3.1%)2.403s (+14.4% 🔺)0.416s381.01x
💻 LocalNitro3.023s (~)3.649s (-2.9%)0.625s251.53x
💻 LocalExpress3.038s (+0.7%)3.759s (+4.8%)0.721s241.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express28.216s (-18.3% 🟢)30.015s (-18.5% 🟢)1.800s31.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.999s (~)4.456s (+2.0%)0.457s271.00x
🐘 PostgresNitro4.047s (-1.4%)4.627s (~)0.580s261.01x
💻 LocalExpress8.938s (-3.0%)9.325s (-6.9% 🟢)0.387s132.23x
💻 LocalNitro9.121s (-1.9%)9.710s (-3.1%)0.589s132.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express74.288s (-42.9% 🟢)76.387s (-42.2% 🟢)2.098s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.278s (-1.6%)1.007s (~)0.729s601.00x
🐘 PostgresNitro0.292s (+2.9%)1.007s (~)0.716s601.05x
💻 LocalExpress0.594s (+5.9% 🔺)1.005s (~)0.411s602.14x
💻 LocalNitro0.782s (+29.3% 🔺)1.205s (+17.9% 🔺)0.422s502.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.563s (-20.0% 🟢)3.059s (-15.9% 🟢)1.496s201.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.494s (-3.1%)1.007s (~)0.513s901.00x
🐘 PostgresNitro0.509s (+2.6%)1.008s (~)0.499s901.03x
💻 LocalExpress2.408s (-4.2%)3.009s (~)0.600s304.87x
💻 LocalNitro2.460s (-3.1%)3.009s (~)0.548s304.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.780s (-8.8% 🟢)4.406s (-8.4% 🟢)1.626s211.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.790s (-3.5%)1.007s (-1.0%)0.218s1201.00x
🐘 PostgresNitro0.798s (+1.0%)1.009s (~)0.210s1191.01x
💻 LocalExpress10.360s (-7.4% 🟢)11.028s (-7.6% 🟢)0.668s1113.12x
💻 LocalNitro10.737s (-4.1%)11.121s (-4.7%)0.384s1113.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express7.045s (-5.1% 🟢)8.589s (-7.1% 🟢)1.544s151.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.203s (-4.9%)1.004s (~)0.011s (-9.6% 🟢)1.017s (~)0.814s101.00x
🐘 PostgresNitro0.208s (+1.5%)0.999s (~)0.002s (~)1.011s (~)0.802s101.02x
💻 LocalExpress0.209s (+5.2% 🔺)1.004s (~)0.011s (-13.2% 🟢)1.016s (~)0.807s101.03x
🐘 PostgresExpress0.210s (+2.3%)0.998s (~)0.001s (-43.8% 🟢)1.010s (~)0.800s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.484s (-40.8% 🟢)3.012s (-26.4% 🟢)1.734s (+80.5% 🔺)5.137s (-8.1% 🟢)3.653s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.620s (-0.7%)1.005s (~)0.004s (-2.0%)1.023s (~)0.403s591.00x
🐘 PostgresExpress0.635s (+0.8%)1.005s (~)0.004s (+7.1% 🔺)1.023s (~)0.388s591.02x
💻 LocalExpress0.764s (+0.9%)1.013s (-1.6%)0.009s (-6.0% 🟢)1.023s (-1.6%)0.260s591.23x
💻 LocalNitro0.833s (-0.7%)1.011s (~)0.010s (+5.1% 🔺)1.116s (~)0.283s541.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.166s (-35.9% 🟢)5.514s (-31.2% 🟢)0.218s (-46.7% 🟢)6.081s (-31.2% 🟢)1.914s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.918s (-4.5%)1.149s (-10.1% 🟢)0.000s (-11.5% 🟢)1.168s (-10.6% 🟢)0.250s521.00x
🐘 PostgresNitro0.971s (~)1.264s (+1.3%)0.000s (-50.0% 🟢)1.290s (+2.5%)0.319s481.06x
💻 LocalNitro1.203s (-1.6%)2.020s (~)0.000s (+133.3% 🔺)2.021s (~)0.818s301.31x
💻 LocalExpress1.226s (~)2.021s (~)0.000s (-50.0% 🟢)2.023s (~)0.797s301.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.185s (-14.8% 🟢)4.482s (-12.2% 🟢)0.000s (-100.0% 🟢)4.856s (-12.2% 🟢)1.671s131.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.795s (+1.3%)2.177s (~)0.000s (NaN%)2.189s (~)0.394s281.00x
🐘 PostgresNitro1.832s (+2.3%)2.219s (+3.6%)0.000s (+3.7%)2.233s (+2.7%)0.400s271.02x
💻 LocalExpress3.392s (-2.2%)3.973s (-1.5%)0.001s (+25.0% 🔺)3.977s (-1.5%)0.584s161.89x
💻 LocalNitro3.499s (+3.3%)4.032s (~)0.000s (-12.5% 🟢)4.035s (~)0.536s151.95x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.518s (-1.5%)5.994s (~)0.000s (NaN%)6.413s (-0.7%)1.896s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress16/21
🐘 PostgresExpress15/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres15/21
Nitro🐘 Postgres18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production10000671067
✅ 💻 Local Development10780861164
✅ 📦 Local Production10780861164
✅ 🐘 Local Postgres10780861164
✅ 🪟 Windows970097
✅ 📋 Other273018291
Total460403434947

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9007
✅ example9007
✅ express9007
✅ fastify9007
✅ hono9007
✅ nextjs-turbopack9502
✅ nextjs-webpack9502
✅ nitro9007
✅ nuxt9007
✅ sveltekit9007
✅ vite9007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9700
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9106
✅ e2e-local-postgres-nest-stable9106
✅ e2e-local-prod-nest-stable9106

📋 View full workflow run

CopilotAI 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.

Pull request overview

Switches CI jobs that hit protected Vercel deployments from a long-lived bypass secret to Vercel Deployment Protection “Trusted Sources” using GitHub Actions OIDC, and centralizes the request header logic in a shared helper.

Changes:

  • Add OIDC token minting in relevant CI jobs and pass it via VERCEL_TRUSTED_OIDC_TOKEN.
  • Introduce scripts/trusted-sources-headers.mjs and update e2e/bench/docs smoke callers to use it.
  • Update local dev docs (AGENTS.md) to reflect the new bypass mechanism.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
scripts/trusted-sources-headers.mjsNew shared helper to attach the Vercel Trusted Sources OIDC header when present.
packages/core/e2e/utils.tsRemoves legacy bypass-secret helper; uses the shared Trusted Sources headers helper.
packages/core/e2e/e2e.test.tsUpdates HTTP calls to use Trusted Sources headers.
packages/core/e2e/bench.bench.tsUpdates manifest fetch to use Trusted Sources headers.
docs/scripts/check-docs-smoke.mjsUpdates docs smoke fetches to use Trusted Sources headers.
AGENTS.mdDocuments the new env var and local-development limitation.
.github/workflows/tests.ymlAdds OIDC token minting + env wiring for e2e Vercel prod tests.
.github/workflows/docs-checks.ymlAdds OIDC token minting + env wiring for docs preview smoke checks.
.github/workflows/benchmarks.ymlAdds OIDC token minting + env wiring for Vercel benchmarks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread.github/workflows/tests.yml
Comment thread.github/workflows/docs-checks.yml
Comment thread.github/workflows/benchmarks.yml
- Rename the env var from VERCEL_TRUSTED_OIDC_TOKEN to VERCEL_OIDC_TOKEN
to match Vercel's convention (also read by @vercel/oidc's
getVercelOidcToken()).
- In @workflow/world-vercel, replace the legacy
VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS / x-vercel-protection-bypass
flow with VERCEL_OIDC_TOKEN / x-vercel-trusted-oidc-idp-token. The
trusted-source header is attached on every outbound workflow-server
request (both proxied through api.vercel.com and direct).
- Drop the bypass header from the encryption-key and
resolve-latest-deployment fetches: those go to api.vercel.com which
is public.
- Drop VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS plumbing from tests.yml.
- Update the pending world-vercel changeset to describe the final
trusted-sources flow.
Adds a debug step that does two HEAD requests against the docs preview
deployment (with and without the OIDC trusted-sources header) and prints
the response status line plus `x-vercel-id` for each. The proxy-side
trusted-sources changes for GitHub Actions OIDC tokens are rolling out
gradually (~12+ hours), so the edge-node identifier in `x-vercel-id`
helps explain why a request might succeed or fail during the rollout
window.
Also includes `x-vercel-id` in the `waitForServer` timeout error so
post-mortem analysis of failing runs has the same edge-node info.
…ix reaches the serving edge node
The probe served its purpose: confirmed the bypass is functional once
the request lands on a region that has the proxy-side trusted-sources
fix rolled out. The waitForServer error message still surfaces
x-vercel-id for any future rollout-window debugging.
Adds a one-shot diagnostic that prints the non-sensitive claims of the
OIDC token (`iss`, `aud`, `owner_id`, `project_id`, `environment`,
`sub`, `scope`, `exp`) on the first request that uses bearer auth.
This is invaluable for debugging Vercel deployment-protection
trusted-source rule mismatches: a 401 from the edge tells you nothing
about why the rule didn't match, and the token's claims are the only
thing that determines that. The signature is never logged.
Gated to once per process — Vercel-issued tokens are process-stable for
the lambda's lifetime so further log lines would just be redundant
spam.
The Authorization bearer correctly preferred config.token (a static
Vercel auth token from CLI / Actions runner) and fell back to
getVercelOidcToken() inside a Vercel function. But the trusted-sources
bypass header (x-vercel-trusted-oidc-idp-token) was being read directly
from process.env.VERCEL_OIDC_TOKEN inside getHeaders(). That env var is
the bake-time token, frozen at deployment-creation time — on a project
that has been redeployed after a settings change, it carries stale
claims (e.g. an iss from when the project was briefly in 'global' mode)
that no longer match the workflow-server's trusted-sources rule.
Move trusted-sources header attachment from getHeaders() (sync) to
getHttpConfig() (async) and source it from getVercelOidcToken(). That
function reads getContext().headers['x-vercel-oidc-token'] first — a
freshly minted per-request token that always reflects current project
settings — and only falls back to the env var when that header is
missing.
Bearer auth source remains config.token-first.
Also expand the diagnostic to log claims from BOTH the per-request OIDC
token AND the bake-time env var so the divergence is visible in logs
when debugging future trusted-source mismatches.
Removes the now-misleading getProtectionBypassHeader() helper (its
'read env var directly' semantics were exactly the bug).
The two outbound flows have different auth requirements:
1. Proxied (usingProxy=true) — calls api.vercel.com/v1/workflow.
Public endpoint, authenticated with a static Vercel auth token via
config.token. The api-workflow proxy mints its own OIDC token
before forwarding to workflow-server, so the trusted-sources
bypass header on the SDK→proxy hop is meaningless. CLI, GitHub
Actions, and other API-client callers take this path.
2. Direct (usingProxy=false) — runs inside a Vercel deployment
talking straight to workflow-server. workflow-server validates a
Vercel OIDC bearer; Vercel's edge validates the trusted-sources
header. Both must come from getVercelOidcToken() (the per-request
fresh token), not process.env.VERCEL_OIDC_TOKEN (the bake-time
token that can be stale after a project config change).
Previously getHttpConfig attached x-vercel-trusted-oidc-idp-token on
both paths whenever getVercelOidcToken() resolved. That accidentally
forwarded the GitHub Actions OIDC token (when wired into
VERCEL_OIDC_TOKEN by the test runner) onto every SDK→proxy request,
which is harmless but wrong-by-design — the proxy is public, doesn't
look at that header on its inbound side, and the GHA token isn't its
intended audience.
Bearer auth source rules:
- Proxied: only config.token. (No fallback to OIDC; that auth
pathway doesn't go through the proxy's auth checks.)
- Direct: config.token (for tests / local dev), falling back to
getVercelOidcToken() (for Vercel-runtime calls).
The api-workflow proxy authenticates the caller with a regular Vercel
auth token (not OIDC), so reaching the proxied path with no
config.token is always wrong: the proxy will reject the request and
the SDK caller would see an opaque 401 with no actionable hint.
Throw at config-resolution time with a clear message that points to
the WORKFLOW_VERCEL_AUTH_TOKEN env var the SDK reads from. Adds tests
covering both the no-token-throws case and the with-token-attaches-
bearer-and-skips-trusted-sources case.
When the trusted-sources bypass returns 401, the error message now
surfaces the response's x-vercel-id header so we can identify which
edge node served the failure. Helps distinguish proxy-rollout
incompleteness from actual config errors during incremental
rollouts of edge-side changes.
Comment threadpackages/core/e2e/e2e.test.ts Outdated
Comment threadpackages/world-vercel/src/jwt-claims.ts Outdated
Comment threadpackages/world-vercel/src/utils.test.ts Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
GitHub Actions OIDC tokens have a hard 5-minute lifetime that cannot be
extended (no API to ask for a longer TTL — exp is always iat + ~300s).
Pre-minting once at the start of the job and shipping the result down
to the test runner via env var means tests that run late in the suite
hit an expired token and 401 on /api/trigger-pages (and any other
trusted-sources protected endpoint).
Move minting into scripts/trusted-sources-headers.mjs:
- getTrustedSourcesHeaders() is now async.
- It calls the runner's ACTIONS_ID_TOKEN_REQUEST_URL endpoint directly
(the env vars GHA exposes when permissions: id-token: write is on)
and re-mints 60s before the cached token's exp.
- Falls back to process.env.VERCEL_OIDC_TOKEN for non-GHA contexts
(Vercel runtime, local dev).
Workflow files drop the now-redundant 'Mint OIDC token' step and the
VERCEL_OIDC_TOKEN env-var passthrough on the test step. The runner env
vars propagate to subsequent steps automatically.
Updates all 17 callers in e2e.test.ts / bench.bench.ts / utils.ts /
docs/scripts/check-docs-smoke.mjs to await the now-async call.
Comment thread.changeset/world-vercel-protection-bypass.md
Comment thread.changeset/world-vercel-trusted-sources.md Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
- Drop `statuses: read` from the three workflow permission blocks (the
wait-for-vercel-project action works without it on a public repo).
- Revert the `x-vercel-id` debug logging in `startWorkflowViaHttp`.
- Delete `packages/world-vercel/src/jwt-claims.ts` (debug-only helper).
- Drop the JWT claims diagnostic logging from `getHttpConfig`.
- Tighten the auth-flow comment in `getHttpConfig` and remove the
historical 'no longer attaches' note from `getHeaders`/its test.
- Restore `.changeset/world-vercel-protection-bypass.md` (already
shipped in a beta release per .changeset/pre.json).
- Trim the `.changeset/world-vercel-trusted-sources.md` description to
one short paragraph.
Configured trustedSources.projects on all 11 workbench app projects so
each one accepts a Vercel-issued OIDC token from any of the others. A
developer running e2e locally can now do `vercel env pull` from any
workbench app's directory and use the resulting VERCEL_OIDC_TOKEN to
bypass Deployment Protection on any of the workbench preview/prod
deployments — no need to disable protection on the project just to run
the suite locally.
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.

3 participants

@TooTallNate@pranaygp
, '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

ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources - #1882

Merged
TooTallNate merged 24 commits into
mainfrom
ci/oidc-trusted-sources
May 2, 2026
Merged

ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources#1882
TooTallNate merged 24 commits into
mainfrom
ci/oidc-trusted-sources

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 30, 2026

Copy link
Copy Markdown
Member

Summary

Switch the e2e, benchmark, and docs-smoke CI jobs from the static VERCEL_AUTOMATION_BYPASS_SECRET deployment-protection bypass token over to Vercel's OIDC Trusted Sources flow. Same conceptual swap is applied in the runtime world-vercel SDK: outbound requests to a protected workflow-server preview now carry an x-vercel-trusted-oidc-idp-token header instead of x-vercel-protection-bypass.

What changes

CI workflows

.github/workflows/tests.yml, benchmarks.yml, docs-checks.yml:

  • Add permissions: id-token: write (and explicit contents: read, deployments: read) to the affected jobs so the runner can mint OIDC tokens.
  • Tests/benchmarks/smoke checks pick up the ACTIONS_ID_TOKEN_REQUEST_URL / ACTIONS_ID_TOKEN_REQUEST_TOKEN env vars that GitHub injects on id-token: write–enabled jobs and mint OIDC tokens on demand (see helper below). The VERCEL_AUTOMATION_BYPASS_SECRET env var and the (now no-op) VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS env var are removed.

Shared on-demand minting helper — scripts/trusted-sources-headers.mjs

GitHub Actions OIDC tokens have a hard 5-minute lifetime. Pre-minting once at the start of the job and forwarding the result down doesn't work for long e2e suites — tests that run late in the run hit an expired token and 401. The new getTrustedSourcesHeaders() is async, mints fresh tokens from the runner endpoint on demand, caches them in-process, and re-mints 60 s before the JWT's exp. Falls back to process.env.VERCEL_OIDC_TOKEN for non-CI contexts (Vercel runtime, local dev).

Used by packages/core/e2e/{e2e.test.ts,bench.bench.ts,utils.ts} and docs/scripts/check-docs-smoke.mjs — all callers now await the helper.

@workflow/world-vercel runtime

packages/world-vercel/src/utils.ts:getHttpConfig:

  • Proxied path (usingProxy=true, hits api.vercel.com/v1/workflow): bearer auth from config.token only. The api-workflow proxy authenticates the caller with a regular Vercel auth token, so the trusted-sources header is meaningless on this hop. We now throw a clear error if config.token is missing instead of letting an opaque 401 bubble up.
  • Direct path (usingProxy=false, hits the workflow-server URL directly from a Vercel deployment): bearer auth prefers config.token and falls back to getVercelOidcToken(). The trusted-sources bypass header always uses getVercelOidcToken() (per-request token Vercel injects on every invocation). This avoids the trap of reading process.env.VERCEL_OIDC_TOKEN directly, which carries the deployment's bake-time token and goes stale after project-config changes.

packages/world-vercel/src/{encryption,resolve-latest-deployment}.ts: drop the (now-unused) getProtectionBypassHeader calls — both go to the public api.vercel.com, which never needs the bypass.

Vercel-side configuration (out of band)

Each of the 11 workbench app projects has its trust configuration updated:

  • trustedSources.projects — entries for all 11 workbench projects with a customAllow: [{ from: any-env, to: any-env }] rule, so any workbench's Vercel-issued OIDC token can reach any other (and itself) on any environment. This is what enables the local-dev recipe in AGENTS.md: vercel env pull from any workbench app yields a VERCEL_OIDC_TOKEN that bypasses Deployment Protection on every workbench preview/prod URL.
  • trustedSources.oidcProviders["https://token.actions.githubusercontent.com"] — accepts GitHub Actions OIDC tokens from vercel/workflow CI for the direct-fetch tests against the deployed workbench apps.

Local-development recipe (AGENTS.md)

Updated the local-against-preview e2e recipe to document vercel env pull as the source of VERCEL_OIDC_TOKEN. Replaces the old VERCEL_AUTOMATION_BYPASS_SECRET instruction.

Files changed

FileChange
.github/workflows/tests.ymlpermissions: id-token: write, dropped pre-mint step + VERCEL_AUTOMATION_BYPASS_SECRET / VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS
.github/workflows/benchmarks.ymlsame
.github/workflows/docs-checks.ymlsame
scripts/trusted-sources-headers.mjs (new)async on-demand minting helper
packages/core/e2e/e2e.test.tsawait getTrustedSourcesHeaders() at every callsite
packages/core/e2e/bench.bench.tssame
packages/core/e2e/utils.tssame
docs/scripts/check-docs-smoke.mjssame
packages/world-vercel/src/utils.tsbearer/trusted-sources split between proxied and direct paths; throw on missing config.token for proxied path; removed dead getProtectionBypassHeader
packages/world-vercel/src/utils.test.tstests for the new direct/proxied behaviour
packages/world-vercel/src/encryption.tsdrop bypass header (api.vercel.com is public)
packages/world-vercel/src/resolve-latest-deployment.tsdrop bypass header (api.vercel.com is public)
AGENTS.mdlocal-dev recipe uses vercel env pull for VERCEL_OIDC_TOKEN
.changeset/world-vercel-trusted-sources.md (new)minor bump for @workflow/world-vercel

Untouched (separate concerns)

  • WORKFLOW_VERCEL_AUTH_TOKEN (secrets.VERCEL_LABS_TOKEN) — Vercel API auth, still needed for the proxied path's config.token.
  • VERCEL_WORKFLOW_SERVER_URL env var — still used to point CI at the workflow-server preview on PR runs.

Cleanup follow-up

Once a CI run on main confirms everything works, the VERCEL_AUTOMATION_BYPASS_SECRET and VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS repo secrets can be deleted from GitHub.

The e2e, benchmark, and docs-smoke CI jobs previously used the static
`VERCEL_AUTOMATION_BYPASS_SECRET` deployment-protection bypass token
to reach protected Vercel deployments. Switch them over to the new OIDC
Trusted Sources flow: the GitHub Actions runner mints a short-lived
OIDC token via `core.getIDToken()` and forwards it on requests in the
`x-vercel-trusted-oidc-idp-token` header.
Each workbench project (and `workflow-docs`) has been configured with a
matching trusted-source rule:
aud=https://github.com/vercel, repository=vercel/workflow
The shared header helper now lives at `scripts/trusted-sources-headers.mjs`
and is imported by both the e2e/bench tests and the docs smoke script,
removing the previous duplication.
CopilotAI review requested due to automatic review settings April 30, 2026 17:50
@changeset-bot

changeset-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c125b54

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@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

@vercel

vercelBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.043s (-2.5%)1.006s (~)0.962s101.00x
💻 LocalNitro0.049s (+13.9% 🔺)1.011s (~)0.961s101.14x
🐘 PostgresNitro0.059s (-38.3% 🟢)1.010s (-3.1%)0.952s101.36x
🐘 PostgresExpress0.060s (+3.3%)1.011s (~)0.951s101.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.314s (+33.2% 🔺)2.234s (+4.6%)1.920s101.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.125s (~)2.005s (~)0.880s101.00x
💻 LocalNitro1.127s (~)2.006s (~)0.879s101.00x
🐘 PostgresExpress1.143s (~)2.010s (~)0.867s101.02x
🐘 PostgresNitro1.149s (+0.8%)2.010s (~)0.862s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.920s (+2.4%)3.677s (-3.4%)1.757s101.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.880s (~)11.019s (~)0.139s31.00x
💻 LocalNitro10.928s (~)11.023s (~)0.095s31.00x
💻 LocalExpress10.947s (~)11.022s (~)0.075s31.01x
🐘 PostgresExpress10.955s (~)11.353s (+3.0%)0.398s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.546s (+3.3%)19.315s (-3.5%)1.770s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.542s (~)15.022s (~)0.480s41.00x
🐘 PostgresNitro14.553s (~)15.025s (~)0.472s41.00x
💻 LocalExpress14.970s (~)15.280s (+1.7%)0.310s41.03x
💻 LocalNitro14.986s (-0.5%)15.279s (-4.7%)0.293s41.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express31.530s (-37.3% 🟢)33.430s (-36.4% 🟢)1.899s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.022s (~)14.447s (-1.0%)0.425s71.00x
🐘 PostgresNitro14.038s (+0.5%)14.737s (+3.0%)0.699s71.00x
💻 LocalExpress16.492s (-0.7%)17.031s (~)0.539s61.18x
💻 LocalNitro16.588s (-1.2%)17.029s (~)0.441s61.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express55.578s (-54.2% 🟢)58.406s (-52.8% 🟢)2.828s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.249s (-0.9%)2.010s (~)0.760s151.00x
🐘 PostgresNitro1.283s (+0.6%)2.011s (~)0.728s151.03x
💻 LocalExpress1.508s (+1.3%)2.006s (~)0.498s151.21x
💻 LocalNitro1.533s (-6.1% 🟢)2.006s (-3.3%)0.473s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.403s (-16.0% 🟢)3.950s (-14.6% 🟢)1.547s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.345s (-0.7%)3.009s (~)0.664s101.00x
🐘 PostgresNitro2.359s (~)3.011s (~)0.651s101.01x
💻 LocalExpress2.817s (-4.6%)3.108s (-10.0% 🟢)0.292s101.20x
💻 LocalNitro2.910s (-7.4% 🟢)3.207s (-17.5% 🟢)0.297s101.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.388s (-34.0% 🟢)4.195s (-17.9% 🟢)1.807s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.462s (-0.7%)4.010s (~)0.548s81.00x
🐘 PostgresNitro3.494s (~)4.014s (~)0.520s81.01x
💻 LocalExpress7.361s (-11.7% 🟢)8.020s (-11.1% 🟢)0.659s42.13x
💻 LocalNitro7.874s (-5.7% 🟢)8.269s (-8.3% 🟢)0.395s42.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.002s (-5.6% 🟢)6.045s (-1.3%)2.042s51.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.261s (~)2.009s (~)0.748s151.00x
🐘 PostgresExpress1.266s (+0.7%)2.008s (~)0.743s151.00x
💻 LocalExpress1.522s (-19.6% 🟢)2.006s (-15.1% 🟢)0.483s151.21x
💻 LocalNitro1.553s (-16.8% 🟢)2.006s (-14.3% 🟢)0.453s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.335s (-9.6% 🟢)3.671s (-15.6% 🟢)1.336s91.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.338s (~)3.010s (~)0.672s101.00x
🐘 PostgresExpress2.348s (~)3.011s (~)0.663s101.00x
💻 LocalExpress2.906s (-7.2% 🟢)3.008s (-20.1% 🟢)0.102s101.24x
💻 LocalNitro2.983s (-2.7%)3.565s (-8.3% 🟢)0.582s91.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.317s (-27.4% 🟢)3.822s (-20.3% 🟢)1.505s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.469s (-0.9%)4.010s (~)0.540s81.00x
🐘 PostgresNitro3.478s (~)4.011s (~)0.533s81.00x
💻 LocalExpress8.107s (-7.9% 🟢)9.025s (-2.7%)0.917s42.34x
💻 LocalNitro8.436s (-7.7% 🟢)9.024s (-10.0% 🟢)0.588s42.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.081s (-52.0% 🟢)4.799s (-41.3% 🟢)1.718s71.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.844s (+0.6%)1.023s (~)0.179s591.00x
🐘 PostgresNitro0.884s (+7.8% 🔺)1.041s (+3.5%)0.157s581.05x
💻 LocalNitro0.987s (+0.6%)1.181s (+8.0% 🔺)0.194s511.17x
💻 LocalExpress1.022s (+3.8%)1.941s (+80.4% 🔺)0.920s311.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.175s (-51.8% 🟢)10.768s (-49.5% 🟢)1.593s61.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.972s (~)2.402s (+6.4% 🔺)0.430s381.00x
🐘 PostgresNitro1.987s (+3.1%)2.403s (+14.4% 🔺)0.416s381.01x
💻 LocalNitro3.023s (~)3.649s (-2.9%)0.625s251.53x
💻 LocalExpress3.038s (+0.7%)3.759s (+4.8%)0.721s241.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express28.216s (-18.3% 🟢)30.015s (-18.5% 🟢)1.800s31.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.999s (~)4.456s (+2.0%)0.457s271.00x
🐘 PostgresNitro4.047s (-1.4%)4.627s (~)0.580s261.01x
💻 LocalExpress8.938s (-3.0%)9.325s (-6.9% 🟢)0.387s132.23x
💻 LocalNitro9.121s (-1.9%)9.710s (-3.1%)0.589s132.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express74.288s (-42.9% 🟢)76.387s (-42.2% 🟢)2.098s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.278s (-1.6%)1.007s (~)0.729s601.00x
🐘 PostgresNitro0.292s (+2.9%)1.007s (~)0.716s601.05x
💻 LocalExpress0.594s (+5.9% 🔺)1.005s (~)0.411s602.14x
💻 LocalNitro0.782s (+29.3% 🔺)1.205s (+17.9% 🔺)0.422s502.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.563s (-20.0% 🟢)3.059s (-15.9% 🟢)1.496s201.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.494s (-3.1%)1.007s (~)0.513s901.00x
🐘 PostgresNitro0.509s (+2.6%)1.008s (~)0.499s901.03x
💻 LocalExpress2.408s (-4.2%)3.009s (~)0.600s304.87x
💻 LocalNitro2.460s (-3.1%)3.009s (~)0.548s304.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.780s (-8.8% 🟢)4.406s (-8.4% 🟢)1.626s211.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.790s (-3.5%)1.007s (-1.0%)0.218s1201.00x
🐘 PostgresNitro0.798s (+1.0%)1.009s (~)0.210s1191.01x
💻 LocalExpress10.360s (-7.4% 🟢)11.028s (-7.6% 🟢)0.668s1113.12x
💻 LocalNitro10.737s (-4.1%)11.121s (-4.7%)0.384s1113.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express7.045s (-5.1% 🟢)8.589s (-7.1% 🟢)1.544s151.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.203s (-4.9%)1.004s (~)0.011s (-9.6% 🟢)1.017s (~)0.814s101.00x
🐘 PostgresNitro0.208s (+1.5%)0.999s (~)0.002s (~)1.011s (~)0.802s101.02x
💻 LocalExpress0.209s (+5.2% 🔺)1.004s (~)0.011s (-13.2% 🟢)1.016s (~)0.807s101.03x
🐘 PostgresExpress0.210s (+2.3%)0.998s (~)0.001s (-43.8% 🟢)1.010s (~)0.800s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.484s (-40.8% 🟢)3.012s (-26.4% 🟢)1.734s (+80.5% 🔺)5.137s (-8.1% 🟢)3.653s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.620s (-0.7%)1.005s (~)0.004s (-2.0%)1.023s (~)0.403s591.00x
🐘 PostgresExpress0.635s (+0.8%)1.005s (~)0.004s (+7.1% 🔺)1.023s (~)0.388s591.02x
💻 LocalExpress0.764s (+0.9%)1.013s (-1.6%)0.009s (-6.0% 🟢)1.023s (-1.6%)0.260s591.23x
💻 LocalNitro0.833s (-0.7%)1.011s (~)0.010s (+5.1% 🔺)1.116s (~)0.283s541.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.166s (-35.9% 🟢)5.514s (-31.2% 🟢)0.218s (-46.7% 🟢)6.081s (-31.2% 🟢)1.914s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.918s (-4.5%)1.149s (-10.1% 🟢)0.000s (-11.5% 🟢)1.168s (-10.6% 🟢)0.250s521.00x
🐘 PostgresNitro0.971s (~)1.264s (+1.3%)0.000s (-50.0% 🟢)1.290s (+2.5%)0.319s481.06x
💻 LocalNitro1.203s (-1.6%)2.020s (~)0.000s (+133.3% 🔺)2.021s (~)0.818s301.31x
💻 LocalExpress1.226s (~)2.021s (~)0.000s (-50.0% 🟢)2.023s (~)0.797s301.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.185s (-14.8% 🟢)4.482s (-12.2% 🟢)0.000s (-100.0% 🟢)4.856s (-12.2% 🟢)1.671s131.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.795s (+1.3%)2.177s (~)0.000s (NaN%)2.189s (~)0.394s281.00x
🐘 PostgresNitro1.832s (+2.3%)2.219s (+3.6%)0.000s (+3.7%)2.233s (+2.7%)0.400s271.02x
💻 LocalExpress3.392s (-2.2%)3.973s (-1.5%)0.001s (+25.0% 🔺)3.977s (-1.5%)0.584s161.89x
💻 LocalNitro3.499s (+3.3%)4.032s (~)0.000s (-12.5% 🟢)4.035s (~)0.536s151.95x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.518s (-1.5%)5.994s (~)0.000s (NaN%)6.413s (-0.7%)1.896s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress16/21
🐘 PostgresExpress15/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres15/21
Nitro🐘 Postgres18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production10000671067
✅ 💻 Local Development10780861164
✅ 📦 Local Production10780861164
✅ 🐘 Local Postgres10780861164
✅ 🪟 Windows970097
✅ 📋 Other273018291
Total460403434947

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9007
✅ example9007
✅ express9007
✅ fastify9007
✅ hono9007
✅ nextjs-turbopack9502
✅ nextjs-webpack9502
✅ nitro9007
✅ nuxt9007
✅ sveltekit9007
✅ vite9007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9700
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9106
✅ e2e-local-postgres-nest-stable9106
✅ e2e-local-prod-nest-stable9106

📋 View full workflow run

CopilotAI 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.

Pull request overview

Switches CI jobs that hit protected Vercel deployments from a long-lived bypass secret to Vercel Deployment Protection “Trusted Sources” using GitHub Actions OIDC, and centralizes the request header logic in a shared helper.

Changes:

  • Add OIDC token minting in relevant CI jobs and pass it via VERCEL_TRUSTED_OIDC_TOKEN.
  • Introduce scripts/trusted-sources-headers.mjs and update e2e/bench/docs smoke callers to use it.
  • Update local dev docs (AGENTS.md) to reflect the new bypass mechanism.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
scripts/trusted-sources-headers.mjsNew shared helper to attach the Vercel Trusted Sources OIDC header when present.
packages/core/e2e/utils.tsRemoves legacy bypass-secret helper; uses the shared Trusted Sources headers helper.
packages/core/e2e/e2e.test.tsUpdates HTTP calls to use Trusted Sources headers.
packages/core/e2e/bench.bench.tsUpdates manifest fetch to use Trusted Sources headers.
docs/scripts/check-docs-smoke.mjsUpdates docs smoke fetches to use Trusted Sources headers.
AGENTS.mdDocuments the new env var and local-development limitation.
.github/workflows/tests.ymlAdds OIDC token minting + env wiring for e2e Vercel prod tests.
.github/workflows/docs-checks.ymlAdds OIDC token minting + env wiring for docs preview smoke checks.
.github/workflows/benchmarks.ymlAdds OIDC token minting + env wiring for Vercel benchmarks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread.github/workflows/tests.yml
Comment thread.github/workflows/docs-checks.yml
Comment thread.github/workflows/benchmarks.yml
- Rename the env var from VERCEL_TRUSTED_OIDC_TOKEN to VERCEL_OIDC_TOKEN
to match Vercel's convention (also read by @vercel/oidc's
getVercelOidcToken()).
- In @workflow/world-vercel, replace the legacy
VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS / x-vercel-protection-bypass
flow with VERCEL_OIDC_TOKEN / x-vercel-trusted-oidc-idp-token. The
trusted-source header is attached on every outbound workflow-server
request (both proxied through api.vercel.com and direct).
- Drop the bypass header from the encryption-key and
resolve-latest-deployment fetches: those go to api.vercel.com which
is public.
- Drop VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS plumbing from tests.yml.
- Update the pending world-vercel changeset to describe the final
trusted-sources flow.
Adds a debug step that does two HEAD requests against the docs preview
deployment (with and without the OIDC trusted-sources header) and prints
the response status line plus `x-vercel-id` for each. The proxy-side
trusted-sources changes for GitHub Actions OIDC tokens are rolling out
gradually (~12+ hours), so the edge-node identifier in `x-vercel-id`
helps explain why a request might succeed or fail during the rollout
window.
Also includes `x-vercel-id` in the `waitForServer` timeout error so
post-mortem analysis of failing runs has the same edge-node info.
…ix reaches the serving edge node
The probe served its purpose: confirmed the bypass is functional once
the request lands on a region that has the proxy-side trusted-sources
fix rolled out. The waitForServer error message still surfaces
x-vercel-id for any future rollout-window debugging.
Adds a one-shot diagnostic that prints the non-sensitive claims of the
OIDC token (`iss`, `aud`, `owner_id`, `project_id`, `environment`,
`sub`, `scope`, `exp`) on the first request that uses bearer auth.
This is invaluable for debugging Vercel deployment-protection
trusted-source rule mismatches: a 401 from the edge tells you nothing
about why the rule didn't match, and the token's claims are the only
thing that determines that. The signature is never logged.
Gated to once per process — Vercel-issued tokens are process-stable for
the lambda's lifetime so further log lines would just be redundant
spam.
The Authorization bearer correctly preferred config.token (a static
Vercel auth token from CLI / Actions runner) and fell back to
getVercelOidcToken() inside a Vercel function. But the trusted-sources
bypass header (x-vercel-trusted-oidc-idp-token) was being read directly
from process.env.VERCEL_OIDC_TOKEN inside getHeaders(). That env var is
the bake-time token, frozen at deployment-creation time — on a project
that has been redeployed after a settings change, it carries stale
claims (e.g. an iss from when the project was briefly in 'global' mode)
that no longer match the workflow-server's trusted-sources rule.
Move trusted-sources header attachment from getHeaders() (sync) to
getHttpConfig() (async) and source it from getVercelOidcToken(). That
function reads getContext().headers['x-vercel-oidc-token'] first — a
freshly minted per-request token that always reflects current project
settings — and only falls back to the env var when that header is
missing.
Bearer auth source remains config.token-first.
Also expand the diagnostic to log claims from BOTH the per-request OIDC
token AND the bake-time env var so the divergence is visible in logs
when debugging future trusted-source mismatches.
Removes the now-misleading getProtectionBypassHeader() helper (its
'read env var directly' semantics were exactly the bug).
The two outbound flows have different auth requirements:
1. Proxied (usingProxy=true) — calls api.vercel.com/v1/workflow.
Public endpoint, authenticated with a static Vercel auth token via
config.token. The api-workflow proxy mints its own OIDC token
before forwarding to workflow-server, so the trusted-sources
bypass header on the SDK→proxy hop is meaningless. CLI, GitHub
Actions, and other API-client callers take this path.
2. Direct (usingProxy=false) — runs inside a Vercel deployment
talking straight to workflow-server. workflow-server validates a
Vercel OIDC bearer; Vercel's edge validates the trusted-sources
header. Both must come from getVercelOidcToken() (the per-request
fresh token), not process.env.VERCEL_OIDC_TOKEN (the bake-time
token that can be stale after a project config change).
Previously getHttpConfig attached x-vercel-trusted-oidc-idp-token on
both paths whenever getVercelOidcToken() resolved. That accidentally
forwarded the GitHub Actions OIDC token (when wired into
VERCEL_OIDC_TOKEN by the test runner) onto every SDK→proxy request,
which is harmless but wrong-by-design — the proxy is public, doesn't
look at that header on its inbound side, and the GHA token isn't its
intended audience.
Bearer auth source rules:
- Proxied: only config.token. (No fallback to OIDC; that auth
pathway doesn't go through the proxy's auth checks.)
- Direct: config.token (for tests / local dev), falling back to
getVercelOidcToken() (for Vercel-runtime calls).
The api-workflow proxy authenticates the caller with a regular Vercel
auth token (not OIDC), so reaching the proxied path with no
config.token is always wrong: the proxy will reject the request and
the SDK caller would see an opaque 401 with no actionable hint.
Throw at config-resolution time with a clear message that points to
the WORKFLOW_VERCEL_AUTH_TOKEN env var the SDK reads from. Adds tests
covering both the no-token-throws case and the with-token-attaches-
bearer-and-skips-trusted-sources case.
When the trusted-sources bypass returns 401, the error message now
surfaces the response's x-vercel-id header so we can identify which
edge node served the failure. Helps distinguish proxy-rollout
incompleteness from actual config errors during incremental
rollouts of edge-side changes.
Comment threadpackages/core/e2e/e2e.test.ts Outdated
Comment threadpackages/world-vercel/src/jwt-claims.ts Outdated
Comment threadpackages/world-vercel/src/utils.test.ts Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
GitHub Actions OIDC tokens have a hard 5-minute lifetime that cannot be
extended (no API to ask for a longer TTL — exp is always iat + ~300s).
Pre-minting once at the start of the job and shipping the result down
to the test runner via env var means tests that run late in the suite
hit an expired token and 401 on /api/trigger-pages (and any other
trusted-sources protected endpoint).
Move minting into scripts/trusted-sources-headers.mjs:
- getTrustedSourcesHeaders() is now async.
- It calls the runner's ACTIONS_ID_TOKEN_REQUEST_URL endpoint directly
(the env vars GHA exposes when permissions: id-token: write is on)
and re-mints 60s before the cached token's exp.
- Falls back to process.env.VERCEL_OIDC_TOKEN for non-GHA contexts
(Vercel runtime, local dev).
Workflow files drop the now-redundant 'Mint OIDC token' step and the
VERCEL_OIDC_TOKEN env-var passthrough on the test step. The runner env
vars propagate to subsequent steps automatically.
Updates all 17 callers in e2e.test.ts / bench.bench.ts / utils.ts /
docs/scripts/check-docs-smoke.mjs to await the now-async call.
Comment thread.changeset/world-vercel-protection-bypass.md
Comment thread.changeset/world-vercel-trusted-sources.md Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
- Drop `statuses: read` from the three workflow permission blocks (the
wait-for-vercel-project action works without it on a public repo).
- Revert the `x-vercel-id` debug logging in `startWorkflowViaHttp`.
- Delete `packages/world-vercel/src/jwt-claims.ts` (debug-only helper).
- Drop the JWT claims diagnostic logging from `getHttpConfig`.
- Tighten the auth-flow comment in `getHttpConfig` and remove the
historical 'no longer attaches' note from `getHeaders`/its test.
- Restore `.changeset/world-vercel-protection-bypass.md` (already
shipped in a beta release per .changeset/pre.json).
- Trim the `.changeset/world-vercel-trusted-sources.md` description to
one short paragraph.
Configured trustedSources.projects on all 11 workbench app projects so
each one accepts a Vercel-issued OIDC token from any of the others. A
developer running e2e locally can now do `vercel env pull` from any
workbench app's directory and use the resulting VERCEL_OIDC_TOKEN to
bypass Deployment Protection on any of the workbench preview/prod
deployments — no need to disable protection on the project just to run
the suite locally.
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.

3 participants

@TooTallNate@pranaygp
, '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

ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources - #1882

Merged
TooTallNate merged 24 commits into
mainfrom
ci/oidc-trusted-sources
May 2, 2026
Merged

ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources#1882
TooTallNate merged 24 commits into
mainfrom
ci/oidc-trusted-sources

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 30, 2026

Copy link
Copy Markdown
Member

Summary

Switch the e2e, benchmark, and docs-smoke CI jobs from the static VERCEL_AUTOMATION_BYPASS_SECRET deployment-protection bypass token over to Vercel's OIDC Trusted Sources flow. Same conceptual swap is applied in the runtime world-vercel SDK: outbound requests to a protected workflow-server preview now carry an x-vercel-trusted-oidc-idp-token header instead of x-vercel-protection-bypass.

What changes

CI workflows

.github/workflows/tests.yml, benchmarks.yml, docs-checks.yml:

  • Add permissions: id-token: write (and explicit contents: read, deployments: read) to the affected jobs so the runner can mint OIDC tokens.
  • Tests/benchmarks/smoke checks pick up the ACTIONS_ID_TOKEN_REQUEST_URL / ACTIONS_ID_TOKEN_REQUEST_TOKEN env vars that GitHub injects on id-token: write–enabled jobs and mint OIDC tokens on demand (see helper below). The VERCEL_AUTOMATION_BYPASS_SECRET env var and the (now no-op) VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS env var are removed.

Shared on-demand minting helper — scripts/trusted-sources-headers.mjs

GitHub Actions OIDC tokens have a hard 5-minute lifetime. Pre-minting once at the start of the job and forwarding the result down doesn't work for long e2e suites — tests that run late in the run hit an expired token and 401. The new getTrustedSourcesHeaders() is async, mints fresh tokens from the runner endpoint on demand, caches them in-process, and re-mints 60 s before the JWT's exp. Falls back to process.env.VERCEL_OIDC_TOKEN for non-CI contexts (Vercel runtime, local dev).

Used by packages/core/e2e/{e2e.test.ts,bench.bench.ts,utils.ts} and docs/scripts/check-docs-smoke.mjs — all callers now await the helper.

@workflow/world-vercel runtime

packages/world-vercel/src/utils.ts:getHttpConfig:

  • Proxied path (usingProxy=true, hits api.vercel.com/v1/workflow): bearer auth from config.token only. The api-workflow proxy authenticates the caller with a regular Vercel auth token, so the trusted-sources header is meaningless on this hop. We now throw a clear error if config.token is missing instead of letting an opaque 401 bubble up.
  • Direct path (usingProxy=false, hits the workflow-server URL directly from a Vercel deployment): bearer auth prefers config.token and falls back to getVercelOidcToken(). The trusted-sources bypass header always uses getVercelOidcToken() (per-request token Vercel injects on every invocation). This avoids the trap of reading process.env.VERCEL_OIDC_TOKEN directly, which carries the deployment's bake-time token and goes stale after project-config changes.

packages/world-vercel/src/{encryption,resolve-latest-deployment}.ts: drop the (now-unused) getProtectionBypassHeader calls — both go to the public api.vercel.com, which never needs the bypass.

Vercel-side configuration (out of band)

Each of the 11 workbench app projects has its trust configuration updated:

  • trustedSources.projects — entries for all 11 workbench projects with a customAllow: [{ from: any-env, to: any-env }] rule, so any workbench's Vercel-issued OIDC token can reach any other (and itself) on any environment. This is what enables the local-dev recipe in AGENTS.md: vercel env pull from any workbench app yields a VERCEL_OIDC_TOKEN that bypasses Deployment Protection on every workbench preview/prod URL.
  • trustedSources.oidcProviders["https://token.actions.githubusercontent.com"] — accepts GitHub Actions OIDC tokens from vercel/workflow CI for the direct-fetch tests against the deployed workbench apps.

Local-development recipe (AGENTS.md)

Updated the local-against-preview e2e recipe to document vercel env pull as the source of VERCEL_OIDC_TOKEN. Replaces the old VERCEL_AUTOMATION_BYPASS_SECRET instruction.

Files changed

FileChange
.github/workflows/tests.ymlpermissions: id-token: write, dropped pre-mint step + VERCEL_AUTOMATION_BYPASS_SECRET / VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS
.github/workflows/benchmarks.ymlsame
.github/workflows/docs-checks.ymlsame
scripts/trusted-sources-headers.mjs (new)async on-demand minting helper
packages/core/e2e/e2e.test.tsawait getTrustedSourcesHeaders() at every callsite
packages/core/e2e/bench.bench.tssame
packages/core/e2e/utils.tssame
docs/scripts/check-docs-smoke.mjssame
packages/world-vercel/src/utils.tsbearer/trusted-sources split between proxied and direct paths; throw on missing config.token for proxied path; removed dead getProtectionBypassHeader
packages/world-vercel/src/utils.test.tstests for the new direct/proxied behaviour
packages/world-vercel/src/encryption.tsdrop bypass header (api.vercel.com is public)
packages/world-vercel/src/resolve-latest-deployment.tsdrop bypass header (api.vercel.com is public)
AGENTS.mdlocal-dev recipe uses vercel env pull for VERCEL_OIDC_TOKEN
.changeset/world-vercel-trusted-sources.md (new)minor bump for @workflow/world-vercel

Untouched (separate concerns)

  • WORKFLOW_VERCEL_AUTH_TOKEN (secrets.VERCEL_LABS_TOKEN) — Vercel API auth, still needed for the proxied path's config.token.
  • VERCEL_WORKFLOW_SERVER_URL env var — still used to point CI at the workflow-server preview on PR runs.

Cleanup follow-up

Once a CI run on main confirms everything works, the VERCEL_AUTOMATION_BYPASS_SECRET and VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS repo secrets can be deleted from GitHub.

The e2e, benchmark, and docs-smoke CI jobs previously used the static
`VERCEL_AUTOMATION_BYPASS_SECRET` deployment-protection bypass token
to reach protected Vercel deployments. Switch them over to the new OIDC
Trusted Sources flow: the GitHub Actions runner mints a short-lived
OIDC token via `core.getIDToken()` and forwards it on requests in the
`x-vercel-trusted-oidc-idp-token` header.
Each workbench project (and `workflow-docs`) has been configured with a
matching trusted-source rule:
aud=https://github.com/vercel, repository=vercel/workflow
The shared header helper now lives at `scripts/trusted-sources-headers.mjs`
and is imported by both the e2e/bench tests and the docs smoke script,
removing the previous duplication.
CopilotAI review requested due to automatic review settings April 30, 2026 17:50
@changeset-bot

changeset-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c125b54

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@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

@vercel

vercelBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.043s (-2.5%)1.006s (~)0.962s101.00x
💻 LocalNitro0.049s (+13.9% 🔺)1.011s (~)0.961s101.14x
🐘 PostgresNitro0.059s (-38.3% 🟢)1.010s (-3.1%)0.952s101.36x
🐘 PostgresExpress0.060s (+3.3%)1.011s (~)0.951s101.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.314s (+33.2% 🔺)2.234s (+4.6%)1.920s101.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.125s (~)2.005s (~)0.880s101.00x
💻 LocalNitro1.127s (~)2.006s (~)0.879s101.00x
🐘 PostgresExpress1.143s (~)2.010s (~)0.867s101.02x
🐘 PostgresNitro1.149s (+0.8%)2.010s (~)0.862s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.920s (+2.4%)3.677s (-3.4%)1.757s101.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.880s (~)11.019s (~)0.139s31.00x
💻 LocalNitro10.928s (~)11.023s (~)0.095s31.00x
💻 LocalExpress10.947s (~)11.022s (~)0.075s31.01x
🐘 PostgresExpress10.955s (~)11.353s (+3.0%)0.398s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.546s (+3.3%)19.315s (-3.5%)1.770s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.542s (~)15.022s (~)0.480s41.00x
🐘 PostgresNitro14.553s (~)15.025s (~)0.472s41.00x
💻 LocalExpress14.970s (~)15.280s (+1.7%)0.310s41.03x
💻 LocalNitro14.986s (-0.5%)15.279s (-4.7%)0.293s41.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express31.530s (-37.3% 🟢)33.430s (-36.4% 🟢)1.899s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.022s (~)14.447s (-1.0%)0.425s71.00x
🐘 PostgresNitro14.038s (+0.5%)14.737s (+3.0%)0.699s71.00x
💻 LocalExpress16.492s (-0.7%)17.031s (~)0.539s61.18x
💻 LocalNitro16.588s (-1.2%)17.029s (~)0.441s61.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express55.578s (-54.2% 🟢)58.406s (-52.8% 🟢)2.828s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.249s (-0.9%)2.010s (~)0.760s151.00x
🐘 PostgresNitro1.283s (+0.6%)2.011s (~)0.728s151.03x
💻 LocalExpress1.508s (+1.3%)2.006s (~)0.498s151.21x
💻 LocalNitro1.533s (-6.1% 🟢)2.006s (-3.3%)0.473s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.403s (-16.0% 🟢)3.950s (-14.6% 🟢)1.547s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.345s (-0.7%)3.009s (~)0.664s101.00x
🐘 PostgresNitro2.359s (~)3.011s (~)0.651s101.01x
💻 LocalExpress2.817s (-4.6%)3.108s (-10.0% 🟢)0.292s101.20x
💻 LocalNitro2.910s (-7.4% 🟢)3.207s (-17.5% 🟢)0.297s101.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.388s (-34.0% 🟢)4.195s (-17.9% 🟢)1.807s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.462s (-0.7%)4.010s (~)0.548s81.00x
🐘 PostgresNitro3.494s (~)4.014s (~)0.520s81.01x
💻 LocalExpress7.361s (-11.7% 🟢)8.020s (-11.1% 🟢)0.659s42.13x
💻 LocalNitro7.874s (-5.7% 🟢)8.269s (-8.3% 🟢)0.395s42.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.002s (-5.6% 🟢)6.045s (-1.3%)2.042s51.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.261s (~)2.009s (~)0.748s151.00x
🐘 PostgresExpress1.266s (+0.7%)2.008s (~)0.743s151.00x
💻 LocalExpress1.522s (-19.6% 🟢)2.006s (-15.1% 🟢)0.483s151.21x
💻 LocalNitro1.553s (-16.8% 🟢)2.006s (-14.3% 🟢)0.453s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.335s (-9.6% 🟢)3.671s (-15.6% 🟢)1.336s91.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.338s (~)3.010s (~)0.672s101.00x
🐘 PostgresExpress2.348s (~)3.011s (~)0.663s101.00x
💻 LocalExpress2.906s (-7.2% 🟢)3.008s (-20.1% 🟢)0.102s101.24x
💻 LocalNitro2.983s (-2.7%)3.565s (-8.3% 🟢)0.582s91.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.317s (-27.4% 🟢)3.822s (-20.3% 🟢)1.505s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.469s (-0.9%)4.010s (~)0.540s81.00x
🐘 PostgresNitro3.478s (~)4.011s (~)0.533s81.00x
💻 LocalExpress8.107s (-7.9% 🟢)9.025s (-2.7%)0.917s42.34x
💻 LocalNitro8.436s (-7.7% 🟢)9.024s (-10.0% 🟢)0.588s42.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.081s (-52.0% 🟢)4.799s (-41.3% 🟢)1.718s71.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.844s (+0.6%)1.023s (~)0.179s591.00x
🐘 PostgresNitro0.884s (+7.8% 🔺)1.041s (+3.5%)0.157s581.05x
💻 LocalNitro0.987s (+0.6%)1.181s (+8.0% 🔺)0.194s511.17x
💻 LocalExpress1.022s (+3.8%)1.941s (+80.4% 🔺)0.920s311.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.175s (-51.8% 🟢)10.768s (-49.5% 🟢)1.593s61.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.972s (~)2.402s (+6.4% 🔺)0.430s381.00x
🐘 PostgresNitro1.987s (+3.1%)2.403s (+14.4% 🔺)0.416s381.01x
💻 LocalNitro3.023s (~)3.649s (-2.9%)0.625s251.53x
💻 LocalExpress3.038s (+0.7%)3.759s (+4.8%)0.721s241.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express28.216s (-18.3% 🟢)30.015s (-18.5% 🟢)1.800s31.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.999s (~)4.456s (+2.0%)0.457s271.00x
🐘 PostgresNitro4.047s (-1.4%)4.627s (~)0.580s261.01x
💻 LocalExpress8.938s (-3.0%)9.325s (-6.9% 🟢)0.387s132.23x
💻 LocalNitro9.121s (-1.9%)9.710s (-3.1%)0.589s132.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express74.288s (-42.9% 🟢)76.387s (-42.2% 🟢)2.098s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.278s (-1.6%)1.007s (~)0.729s601.00x
🐘 PostgresNitro0.292s (+2.9%)1.007s (~)0.716s601.05x
💻 LocalExpress0.594s (+5.9% 🔺)1.005s (~)0.411s602.14x
💻 LocalNitro0.782s (+29.3% 🔺)1.205s (+17.9% 🔺)0.422s502.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.563s (-20.0% 🟢)3.059s (-15.9% 🟢)1.496s201.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.494s (-3.1%)1.007s (~)0.513s901.00x
🐘 PostgresNitro0.509s (+2.6%)1.008s (~)0.499s901.03x
💻 LocalExpress2.408s (-4.2%)3.009s (~)0.600s304.87x
💻 LocalNitro2.460s (-3.1%)3.009s (~)0.548s304.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.780s (-8.8% 🟢)4.406s (-8.4% 🟢)1.626s211.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.790s (-3.5%)1.007s (-1.0%)0.218s1201.00x
🐘 PostgresNitro0.798s (+1.0%)1.009s (~)0.210s1191.01x
💻 LocalExpress10.360s (-7.4% 🟢)11.028s (-7.6% 🟢)0.668s1113.12x
💻 LocalNitro10.737s (-4.1%)11.121s (-4.7%)0.384s1113.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express7.045s (-5.1% 🟢)8.589s (-7.1% 🟢)1.544s151.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.203s (-4.9%)1.004s (~)0.011s (-9.6% 🟢)1.017s (~)0.814s101.00x
🐘 PostgresNitro0.208s (+1.5%)0.999s (~)0.002s (~)1.011s (~)0.802s101.02x
💻 LocalExpress0.209s (+5.2% 🔺)1.004s (~)0.011s (-13.2% 🟢)1.016s (~)0.807s101.03x
🐘 PostgresExpress0.210s (+2.3%)0.998s (~)0.001s (-43.8% 🟢)1.010s (~)0.800s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.484s (-40.8% 🟢)3.012s (-26.4% 🟢)1.734s (+80.5% 🔺)5.137s (-8.1% 🟢)3.653s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.620s (-0.7%)1.005s (~)0.004s (-2.0%)1.023s (~)0.403s591.00x
🐘 PostgresExpress0.635s (+0.8%)1.005s (~)0.004s (+7.1% 🔺)1.023s (~)0.388s591.02x
💻 LocalExpress0.764s (+0.9%)1.013s (-1.6%)0.009s (-6.0% 🟢)1.023s (-1.6%)0.260s591.23x
💻 LocalNitro0.833s (-0.7%)1.011s (~)0.010s (+5.1% 🔺)1.116s (~)0.283s541.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.166s (-35.9% 🟢)5.514s (-31.2% 🟢)0.218s (-46.7% 🟢)6.081s (-31.2% 🟢)1.914s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.918s (-4.5%)1.149s (-10.1% 🟢)0.000s (-11.5% 🟢)1.168s (-10.6% 🟢)0.250s521.00x
🐘 PostgresNitro0.971s (~)1.264s (+1.3%)0.000s (-50.0% 🟢)1.290s (+2.5%)0.319s481.06x
💻 LocalNitro1.203s (-1.6%)2.020s (~)0.000s (+133.3% 🔺)2.021s (~)0.818s301.31x
💻 LocalExpress1.226s (~)2.021s (~)0.000s (-50.0% 🟢)2.023s (~)0.797s301.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.185s (-14.8% 🟢)4.482s (-12.2% 🟢)0.000s (-100.0% 🟢)4.856s (-12.2% 🟢)1.671s131.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.795s (+1.3%)2.177s (~)0.000s (NaN%)2.189s (~)0.394s281.00x
🐘 PostgresNitro1.832s (+2.3%)2.219s (+3.6%)0.000s (+3.7%)2.233s (+2.7%)0.400s271.02x
💻 LocalExpress3.392s (-2.2%)3.973s (-1.5%)0.001s (+25.0% 🔺)3.977s (-1.5%)0.584s161.89x
💻 LocalNitro3.499s (+3.3%)4.032s (~)0.000s (-12.5% 🟢)4.035s (~)0.536s151.95x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.518s (-1.5%)5.994s (~)0.000s (NaN%)6.413s (-0.7%)1.896s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress16/21
🐘 PostgresExpress15/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres15/21
Nitro🐘 Postgres18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production10000671067
✅ 💻 Local Development10780861164
✅ 📦 Local Production10780861164
✅ 🐘 Local Postgres10780861164
✅ 🪟 Windows970097
✅ 📋 Other273018291
Total460403434947

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9007
✅ example9007
✅ express9007
✅ fastify9007
✅ hono9007
✅ nextjs-turbopack9502
✅ nextjs-webpack9502
✅ nitro9007
✅ nuxt9007
✅ sveltekit9007
✅ vite9007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9700
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9106
✅ e2e-local-postgres-nest-stable9106
✅ e2e-local-prod-nest-stable9106

📋 View full workflow run

CopilotAI 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.

Pull request overview

Switches CI jobs that hit protected Vercel deployments from a long-lived bypass secret to Vercel Deployment Protection “Trusted Sources” using GitHub Actions OIDC, and centralizes the request header logic in a shared helper.

Changes:

  • Add OIDC token minting in relevant CI jobs and pass it via VERCEL_TRUSTED_OIDC_TOKEN.
  • Introduce scripts/trusted-sources-headers.mjs and update e2e/bench/docs smoke callers to use it.
  • Update local dev docs (AGENTS.md) to reflect the new bypass mechanism.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
scripts/trusted-sources-headers.mjsNew shared helper to attach the Vercel Trusted Sources OIDC header when present.
packages/core/e2e/utils.tsRemoves legacy bypass-secret helper; uses the shared Trusted Sources headers helper.
packages/core/e2e/e2e.test.tsUpdates HTTP calls to use Trusted Sources headers.
packages/core/e2e/bench.bench.tsUpdates manifest fetch to use Trusted Sources headers.
docs/scripts/check-docs-smoke.mjsUpdates docs smoke fetches to use Trusted Sources headers.
AGENTS.mdDocuments the new env var and local-development limitation.
.github/workflows/tests.ymlAdds OIDC token minting + env wiring for e2e Vercel prod tests.
.github/workflows/docs-checks.ymlAdds OIDC token minting + env wiring for docs preview smoke checks.
.github/workflows/benchmarks.ymlAdds OIDC token minting + env wiring for Vercel benchmarks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread.github/workflows/tests.yml
Comment thread.github/workflows/docs-checks.yml
Comment thread.github/workflows/benchmarks.yml
- Rename the env var from VERCEL_TRUSTED_OIDC_TOKEN to VERCEL_OIDC_TOKEN
to match Vercel's convention (also read by @vercel/oidc's
getVercelOidcToken()).
- In @workflow/world-vercel, replace the legacy
VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS / x-vercel-protection-bypass
flow with VERCEL_OIDC_TOKEN / x-vercel-trusted-oidc-idp-token. The
trusted-source header is attached on every outbound workflow-server
request (both proxied through api.vercel.com and direct).
- Drop the bypass header from the encryption-key and
resolve-latest-deployment fetches: those go to api.vercel.com which
is public.
- Drop VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS plumbing from tests.yml.
- Update the pending world-vercel changeset to describe the final
trusted-sources flow.
Adds a debug step that does two HEAD requests against the docs preview
deployment (with and without the OIDC trusted-sources header) and prints
the response status line plus `x-vercel-id` for each. The proxy-side
trusted-sources changes for GitHub Actions OIDC tokens are rolling out
gradually (~12+ hours), so the edge-node identifier in `x-vercel-id`
helps explain why a request might succeed or fail during the rollout
window.
Also includes `x-vercel-id` in the `waitForServer` timeout error so
post-mortem analysis of failing runs has the same edge-node info.
…ix reaches the serving edge node
The probe served its purpose: confirmed the bypass is functional once
the request lands on a region that has the proxy-side trusted-sources
fix rolled out. The waitForServer error message still surfaces
x-vercel-id for any future rollout-window debugging.
Adds a one-shot diagnostic that prints the non-sensitive claims of the
OIDC token (`iss`, `aud`, `owner_id`, `project_id`, `environment`,
`sub`, `scope`, `exp`) on the first request that uses bearer auth.
This is invaluable for debugging Vercel deployment-protection
trusted-source rule mismatches: a 401 from the edge tells you nothing
about why the rule didn't match, and the token's claims are the only
thing that determines that. The signature is never logged.
Gated to once per process — Vercel-issued tokens are process-stable for
the lambda's lifetime so further log lines would just be redundant
spam.
The Authorization bearer correctly preferred config.token (a static
Vercel auth token from CLI / Actions runner) and fell back to
getVercelOidcToken() inside a Vercel function. But the trusted-sources
bypass header (x-vercel-trusted-oidc-idp-token) was being read directly
from process.env.VERCEL_OIDC_TOKEN inside getHeaders(). That env var is
the bake-time token, frozen at deployment-creation time — on a project
that has been redeployed after a settings change, it carries stale
claims (e.g. an iss from when the project was briefly in 'global' mode)
that no longer match the workflow-server's trusted-sources rule.
Move trusted-sources header attachment from getHeaders() (sync) to
getHttpConfig() (async) and source it from getVercelOidcToken(). That
function reads getContext().headers['x-vercel-oidc-token'] first — a
freshly minted per-request token that always reflects current project
settings — and only falls back to the env var when that header is
missing.
Bearer auth source remains config.token-first.
Also expand the diagnostic to log claims from BOTH the per-request OIDC
token AND the bake-time env var so the divergence is visible in logs
when debugging future trusted-source mismatches.
Removes the now-misleading getProtectionBypassHeader() helper (its
'read env var directly' semantics were exactly the bug).
The two outbound flows have different auth requirements:
1. Proxied (usingProxy=true) — calls api.vercel.com/v1/workflow.
Public endpoint, authenticated with a static Vercel auth token via
config.token. The api-workflow proxy mints its own OIDC token
before forwarding to workflow-server, so the trusted-sources
bypass header on the SDK→proxy hop is meaningless. CLI, GitHub
Actions, and other API-client callers take this path.
2. Direct (usingProxy=false) — runs inside a Vercel deployment
talking straight to workflow-server. workflow-server validates a
Vercel OIDC bearer; Vercel's edge validates the trusted-sources
header. Both must come from getVercelOidcToken() (the per-request
fresh token), not process.env.VERCEL_OIDC_TOKEN (the bake-time
token that can be stale after a project config change).
Previously getHttpConfig attached x-vercel-trusted-oidc-idp-token on
both paths whenever getVercelOidcToken() resolved. That accidentally
forwarded the GitHub Actions OIDC token (when wired into
VERCEL_OIDC_TOKEN by the test runner) onto every SDK→proxy request,
which is harmless but wrong-by-design — the proxy is public, doesn't
look at that header on its inbound side, and the GHA token isn't its
intended audience.
Bearer auth source rules:
- Proxied: only config.token. (No fallback to OIDC; that auth
pathway doesn't go through the proxy's auth checks.)
- Direct: config.token (for tests / local dev), falling back to
getVercelOidcToken() (for Vercel-runtime calls).
The api-workflow proxy authenticates the caller with a regular Vercel
auth token (not OIDC), so reaching the proxied path with no
config.token is always wrong: the proxy will reject the request and
the SDK caller would see an opaque 401 with no actionable hint.
Throw at config-resolution time with a clear message that points to
the WORKFLOW_VERCEL_AUTH_TOKEN env var the SDK reads from. Adds tests
covering both the no-token-throws case and the with-token-attaches-
bearer-and-skips-trusted-sources case.
When the trusted-sources bypass returns 401, the error message now
surfaces the response's x-vercel-id header so we can identify which
edge node served the failure. Helps distinguish proxy-rollout
incompleteness from actual config errors during incremental
rollouts of edge-side changes.
Comment threadpackages/core/e2e/e2e.test.ts Outdated
Comment threadpackages/world-vercel/src/jwt-claims.ts Outdated
Comment threadpackages/world-vercel/src/utils.test.ts Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
GitHub Actions OIDC tokens have a hard 5-minute lifetime that cannot be
extended (no API to ask for a longer TTL — exp is always iat + ~300s).
Pre-minting once at the start of the job and shipping the result down
to the test runner via env var means tests that run late in the suite
hit an expired token and 401 on /api/trigger-pages (and any other
trusted-sources protected endpoint).
Move minting into scripts/trusted-sources-headers.mjs:
- getTrustedSourcesHeaders() is now async.
- It calls the runner's ACTIONS_ID_TOKEN_REQUEST_URL endpoint directly
(the env vars GHA exposes when permissions: id-token: write is on)
and re-mints 60s before the cached token's exp.
- Falls back to process.env.VERCEL_OIDC_TOKEN for non-GHA contexts
(Vercel runtime, local dev).
Workflow files drop the now-redundant 'Mint OIDC token' step and the
VERCEL_OIDC_TOKEN env-var passthrough on the test step. The runner env
vars propagate to subsequent steps automatically.
Updates all 17 callers in e2e.test.ts / bench.bench.ts / utils.ts /
docs/scripts/check-docs-smoke.mjs to await the now-async call.
Comment thread.changeset/world-vercel-protection-bypass.md
Comment thread.changeset/world-vercel-trusted-sources.md Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
- Drop `statuses: read` from the three workflow permission blocks (the
wait-for-vercel-project action works without it on a public repo).
- Revert the `x-vercel-id` debug logging in `startWorkflowViaHttp`.
- Delete `packages/world-vercel/src/jwt-claims.ts` (debug-only helper).
- Drop the JWT claims diagnostic logging from `getHttpConfig`.
- Tighten the auth-flow comment in `getHttpConfig` and remove the
historical 'no longer attaches' note from `getHeaders`/its test.
- Restore `.changeset/world-vercel-protection-bypass.md` (already
shipped in a beta release per .changeset/pre.json).
- Trim the `.changeset/world-vercel-trusted-sources.md` description to
one short paragraph.
Configured trustedSources.projects on all 11 workbench app projects so
each one accepts a Vercel-issued OIDC token from any of the others. A
developer running e2e locally can now do `vercel env pull` from any
workbench app's directory and use the resulting VERCEL_OIDC_TOKEN to
bypass Deployment Protection on any of the workbench preview/prod
deployments — no need to disable protection on the project just to run
the suite locally.
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.

3 participants

@TooTallNate@pranaygp
, '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

ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources - #1882

Merged
TooTallNate merged 24 commits into
mainfrom
ci/oidc-trusted-sources
May 2, 2026
Merged

ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources#1882
TooTallNate merged 24 commits into
mainfrom
ci/oidc-trusted-sources

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 30, 2026

Copy link
Copy Markdown
Member

Summary

Switch the e2e, benchmark, and docs-smoke CI jobs from the static VERCEL_AUTOMATION_BYPASS_SECRET deployment-protection bypass token over to Vercel's OIDC Trusted Sources flow. Same conceptual swap is applied in the runtime world-vercel SDK: outbound requests to a protected workflow-server preview now carry an x-vercel-trusted-oidc-idp-token header instead of x-vercel-protection-bypass.

What changes

CI workflows

.github/workflows/tests.yml, benchmarks.yml, docs-checks.yml:

  • Add permissions: id-token: write (and explicit contents: read, deployments: read) to the affected jobs so the runner can mint OIDC tokens.
  • Tests/benchmarks/smoke checks pick up the ACTIONS_ID_TOKEN_REQUEST_URL / ACTIONS_ID_TOKEN_REQUEST_TOKEN env vars that GitHub injects on id-token: write–enabled jobs and mint OIDC tokens on demand (see helper below). The VERCEL_AUTOMATION_BYPASS_SECRET env var and the (now no-op) VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS env var are removed.

Shared on-demand minting helper — scripts/trusted-sources-headers.mjs

GitHub Actions OIDC tokens have a hard 5-minute lifetime. Pre-minting once at the start of the job and forwarding the result down doesn't work for long e2e suites — tests that run late in the run hit an expired token and 401. The new getTrustedSourcesHeaders() is async, mints fresh tokens from the runner endpoint on demand, caches them in-process, and re-mints 60 s before the JWT's exp. Falls back to process.env.VERCEL_OIDC_TOKEN for non-CI contexts (Vercel runtime, local dev).

Used by packages/core/e2e/{e2e.test.ts,bench.bench.ts,utils.ts} and docs/scripts/check-docs-smoke.mjs — all callers now await the helper.

@workflow/world-vercel runtime

packages/world-vercel/src/utils.ts:getHttpConfig:

  • Proxied path (usingProxy=true, hits api.vercel.com/v1/workflow): bearer auth from config.token only. The api-workflow proxy authenticates the caller with a regular Vercel auth token, so the trusted-sources header is meaningless on this hop. We now throw a clear error if config.token is missing instead of letting an opaque 401 bubble up.
  • Direct path (usingProxy=false, hits the workflow-server URL directly from a Vercel deployment): bearer auth prefers config.token and falls back to getVercelOidcToken(). The trusted-sources bypass header always uses getVercelOidcToken() (per-request token Vercel injects on every invocation). This avoids the trap of reading process.env.VERCEL_OIDC_TOKEN directly, which carries the deployment's bake-time token and goes stale after project-config changes.

packages/world-vercel/src/{encryption,resolve-latest-deployment}.ts: drop the (now-unused) getProtectionBypassHeader calls — both go to the public api.vercel.com, which never needs the bypass.

Vercel-side configuration (out of band)

Each of the 11 workbench app projects has its trust configuration updated:

  • trustedSources.projects — entries for all 11 workbench projects with a customAllow: [{ from: any-env, to: any-env }] rule, so any workbench's Vercel-issued OIDC token can reach any other (and itself) on any environment. This is what enables the local-dev recipe in AGENTS.md: vercel env pull from any workbench app yields a VERCEL_OIDC_TOKEN that bypasses Deployment Protection on every workbench preview/prod URL.
  • trustedSources.oidcProviders["https://token.actions.githubusercontent.com"] — accepts GitHub Actions OIDC tokens from vercel/workflow CI for the direct-fetch tests against the deployed workbench apps.

Local-development recipe (AGENTS.md)

Updated the local-against-preview e2e recipe to document vercel env pull as the source of VERCEL_OIDC_TOKEN. Replaces the old VERCEL_AUTOMATION_BYPASS_SECRET instruction.

Files changed

FileChange
.github/workflows/tests.ymlpermissions: id-token: write, dropped pre-mint step + VERCEL_AUTOMATION_BYPASS_SECRET / VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS
.github/workflows/benchmarks.ymlsame
.github/workflows/docs-checks.ymlsame
scripts/trusted-sources-headers.mjs (new)async on-demand minting helper
packages/core/e2e/e2e.test.tsawait getTrustedSourcesHeaders() at every callsite
packages/core/e2e/bench.bench.tssame
packages/core/e2e/utils.tssame
docs/scripts/check-docs-smoke.mjssame
packages/world-vercel/src/utils.tsbearer/trusted-sources split between proxied and direct paths; throw on missing config.token for proxied path; removed dead getProtectionBypassHeader
packages/world-vercel/src/utils.test.tstests for the new direct/proxied behaviour
packages/world-vercel/src/encryption.tsdrop bypass header (api.vercel.com is public)
packages/world-vercel/src/resolve-latest-deployment.tsdrop bypass header (api.vercel.com is public)
AGENTS.mdlocal-dev recipe uses vercel env pull for VERCEL_OIDC_TOKEN
.changeset/world-vercel-trusted-sources.md (new)minor bump for @workflow/world-vercel

Untouched (separate concerns)

  • WORKFLOW_VERCEL_AUTH_TOKEN (secrets.VERCEL_LABS_TOKEN) — Vercel API auth, still needed for the proxied path's config.token.
  • VERCEL_WORKFLOW_SERVER_URL env var — still used to point CI at the workflow-server preview on PR runs.

Cleanup follow-up

Once a CI run on main confirms everything works, the VERCEL_AUTOMATION_BYPASS_SECRET and VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS repo secrets can be deleted from GitHub.

The e2e, benchmark, and docs-smoke CI jobs previously used the static
`VERCEL_AUTOMATION_BYPASS_SECRET` deployment-protection bypass token
to reach protected Vercel deployments. Switch them over to the new OIDC
Trusted Sources flow: the GitHub Actions runner mints a short-lived
OIDC token via `core.getIDToken()` and forwards it on requests in the
`x-vercel-trusted-oidc-idp-token` header.
Each workbench project (and `workflow-docs`) has been configured with a
matching trusted-source rule:
aud=https://github.com/vercel, repository=vercel/workflow
The shared header helper now lives at `scripts/trusted-sources-headers.mjs`
and is imported by both the e2e/bench tests and the docs smoke script,
removing the previous duplication.
CopilotAI review requested due to automatic review settings April 30, 2026 17:50
@changeset-bot

changeset-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c125b54

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@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

@vercel

vercelBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.043s (-2.5%)1.006s (~)0.962s101.00x
💻 LocalNitro0.049s (+13.9% 🔺)1.011s (~)0.961s101.14x
🐘 PostgresNitro0.059s (-38.3% 🟢)1.010s (-3.1%)0.952s101.36x
🐘 PostgresExpress0.060s (+3.3%)1.011s (~)0.951s101.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.314s (+33.2% 🔺)2.234s (+4.6%)1.920s101.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.125s (~)2.005s (~)0.880s101.00x
💻 LocalNitro1.127s (~)2.006s (~)0.879s101.00x
🐘 PostgresExpress1.143s (~)2.010s (~)0.867s101.02x
🐘 PostgresNitro1.149s (+0.8%)2.010s (~)0.862s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.920s (+2.4%)3.677s (-3.4%)1.757s101.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.880s (~)11.019s (~)0.139s31.00x
💻 LocalNitro10.928s (~)11.023s (~)0.095s31.00x
💻 LocalExpress10.947s (~)11.022s (~)0.075s31.01x
🐘 PostgresExpress10.955s (~)11.353s (+3.0%)0.398s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.546s (+3.3%)19.315s (-3.5%)1.770s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.542s (~)15.022s (~)0.480s41.00x
🐘 PostgresNitro14.553s (~)15.025s (~)0.472s41.00x
💻 LocalExpress14.970s (~)15.280s (+1.7%)0.310s41.03x
💻 LocalNitro14.986s (-0.5%)15.279s (-4.7%)0.293s41.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express31.530s (-37.3% 🟢)33.430s (-36.4% 🟢)1.899s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.022s (~)14.447s (-1.0%)0.425s71.00x
🐘 PostgresNitro14.038s (+0.5%)14.737s (+3.0%)0.699s71.00x
💻 LocalExpress16.492s (-0.7%)17.031s (~)0.539s61.18x
💻 LocalNitro16.588s (-1.2%)17.029s (~)0.441s61.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express55.578s (-54.2% 🟢)58.406s (-52.8% 🟢)2.828s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.249s (-0.9%)2.010s (~)0.760s151.00x
🐘 PostgresNitro1.283s (+0.6%)2.011s (~)0.728s151.03x
💻 LocalExpress1.508s (+1.3%)2.006s (~)0.498s151.21x
💻 LocalNitro1.533s (-6.1% 🟢)2.006s (-3.3%)0.473s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.403s (-16.0% 🟢)3.950s (-14.6% 🟢)1.547s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.345s (-0.7%)3.009s (~)0.664s101.00x
🐘 PostgresNitro2.359s (~)3.011s (~)0.651s101.01x
💻 LocalExpress2.817s (-4.6%)3.108s (-10.0% 🟢)0.292s101.20x
💻 LocalNitro2.910s (-7.4% 🟢)3.207s (-17.5% 🟢)0.297s101.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.388s (-34.0% 🟢)4.195s (-17.9% 🟢)1.807s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.462s (-0.7%)4.010s (~)0.548s81.00x
🐘 PostgresNitro3.494s (~)4.014s (~)0.520s81.01x
💻 LocalExpress7.361s (-11.7% 🟢)8.020s (-11.1% 🟢)0.659s42.13x
💻 LocalNitro7.874s (-5.7% 🟢)8.269s (-8.3% 🟢)0.395s42.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.002s (-5.6% 🟢)6.045s (-1.3%)2.042s51.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.261s (~)2.009s (~)0.748s151.00x
🐘 PostgresExpress1.266s (+0.7%)2.008s (~)0.743s151.00x
💻 LocalExpress1.522s (-19.6% 🟢)2.006s (-15.1% 🟢)0.483s151.21x
💻 LocalNitro1.553s (-16.8% 🟢)2.006s (-14.3% 🟢)0.453s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.335s (-9.6% 🟢)3.671s (-15.6% 🟢)1.336s91.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.338s (~)3.010s (~)0.672s101.00x
🐘 PostgresExpress2.348s (~)3.011s (~)0.663s101.00x
💻 LocalExpress2.906s (-7.2% 🟢)3.008s (-20.1% 🟢)0.102s101.24x
💻 LocalNitro2.983s (-2.7%)3.565s (-8.3% 🟢)0.582s91.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.317s (-27.4% 🟢)3.822s (-20.3% 🟢)1.505s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.469s (-0.9%)4.010s (~)0.540s81.00x
🐘 PostgresNitro3.478s (~)4.011s (~)0.533s81.00x
💻 LocalExpress8.107s (-7.9% 🟢)9.025s (-2.7%)0.917s42.34x
💻 LocalNitro8.436s (-7.7% 🟢)9.024s (-10.0% 🟢)0.588s42.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.081s (-52.0% 🟢)4.799s (-41.3% 🟢)1.718s71.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.844s (+0.6%)1.023s (~)0.179s591.00x
🐘 PostgresNitro0.884s (+7.8% 🔺)1.041s (+3.5%)0.157s581.05x
💻 LocalNitro0.987s (+0.6%)1.181s (+8.0% 🔺)0.194s511.17x
💻 LocalExpress1.022s (+3.8%)1.941s (+80.4% 🔺)0.920s311.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.175s (-51.8% 🟢)10.768s (-49.5% 🟢)1.593s61.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.972s (~)2.402s (+6.4% 🔺)0.430s381.00x
🐘 PostgresNitro1.987s (+3.1%)2.403s (+14.4% 🔺)0.416s381.01x
💻 LocalNitro3.023s (~)3.649s (-2.9%)0.625s251.53x
💻 LocalExpress3.038s (+0.7%)3.759s (+4.8%)0.721s241.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express28.216s (-18.3% 🟢)30.015s (-18.5% 🟢)1.800s31.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.999s (~)4.456s (+2.0%)0.457s271.00x
🐘 PostgresNitro4.047s (-1.4%)4.627s (~)0.580s261.01x
💻 LocalExpress8.938s (-3.0%)9.325s (-6.9% 🟢)0.387s132.23x
💻 LocalNitro9.121s (-1.9%)9.710s (-3.1%)0.589s132.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express74.288s (-42.9% 🟢)76.387s (-42.2% 🟢)2.098s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.278s (-1.6%)1.007s (~)0.729s601.00x
🐘 PostgresNitro0.292s (+2.9%)1.007s (~)0.716s601.05x
💻 LocalExpress0.594s (+5.9% 🔺)1.005s (~)0.411s602.14x
💻 LocalNitro0.782s (+29.3% 🔺)1.205s (+17.9% 🔺)0.422s502.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.563s (-20.0% 🟢)3.059s (-15.9% 🟢)1.496s201.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.494s (-3.1%)1.007s (~)0.513s901.00x
🐘 PostgresNitro0.509s (+2.6%)1.008s (~)0.499s901.03x
💻 LocalExpress2.408s (-4.2%)3.009s (~)0.600s304.87x
💻 LocalNitro2.460s (-3.1%)3.009s (~)0.548s304.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.780s (-8.8% 🟢)4.406s (-8.4% 🟢)1.626s211.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.790s (-3.5%)1.007s (-1.0%)0.218s1201.00x
🐘 PostgresNitro0.798s (+1.0%)1.009s (~)0.210s1191.01x
💻 LocalExpress10.360s (-7.4% 🟢)11.028s (-7.6% 🟢)0.668s1113.12x
💻 LocalNitro10.737s (-4.1%)11.121s (-4.7%)0.384s1113.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express7.045s (-5.1% 🟢)8.589s (-7.1% 🟢)1.544s151.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.203s (-4.9%)1.004s (~)0.011s (-9.6% 🟢)1.017s (~)0.814s101.00x
🐘 PostgresNitro0.208s (+1.5%)0.999s (~)0.002s (~)1.011s (~)0.802s101.02x
💻 LocalExpress0.209s (+5.2% 🔺)1.004s (~)0.011s (-13.2% 🟢)1.016s (~)0.807s101.03x
🐘 PostgresExpress0.210s (+2.3%)0.998s (~)0.001s (-43.8% 🟢)1.010s (~)0.800s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.484s (-40.8% 🟢)3.012s (-26.4% 🟢)1.734s (+80.5% 🔺)5.137s (-8.1% 🟢)3.653s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.620s (-0.7%)1.005s (~)0.004s (-2.0%)1.023s (~)0.403s591.00x
🐘 PostgresExpress0.635s (+0.8%)1.005s (~)0.004s (+7.1% 🔺)1.023s (~)0.388s591.02x
💻 LocalExpress0.764s (+0.9%)1.013s (-1.6%)0.009s (-6.0% 🟢)1.023s (-1.6%)0.260s591.23x
💻 LocalNitro0.833s (-0.7%)1.011s (~)0.010s (+5.1% 🔺)1.116s (~)0.283s541.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.166s (-35.9% 🟢)5.514s (-31.2% 🟢)0.218s (-46.7% 🟢)6.081s (-31.2% 🟢)1.914s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.918s (-4.5%)1.149s (-10.1% 🟢)0.000s (-11.5% 🟢)1.168s (-10.6% 🟢)0.250s521.00x
🐘 PostgresNitro0.971s (~)1.264s (+1.3%)0.000s (-50.0% 🟢)1.290s (+2.5%)0.319s481.06x
💻 LocalNitro1.203s (-1.6%)2.020s (~)0.000s (+133.3% 🔺)2.021s (~)0.818s301.31x
💻 LocalExpress1.226s (~)2.021s (~)0.000s (-50.0% 🟢)2.023s (~)0.797s301.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.185s (-14.8% 🟢)4.482s (-12.2% 🟢)0.000s (-100.0% 🟢)4.856s (-12.2% 🟢)1.671s131.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.795s (+1.3%)2.177s (~)0.000s (NaN%)2.189s (~)0.394s281.00x
🐘 PostgresNitro1.832s (+2.3%)2.219s (+3.6%)0.000s (+3.7%)2.233s (+2.7%)0.400s271.02x
💻 LocalExpress3.392s (-2.2%)3.973s (-1.5%)0.001s (+25.0% 🔺)3.977s (-1.5%)0.584s161.89x
💻 LocalNitro3.499s (+3.3%)4.032s (~)0.000s (-12.5% 🟢)4.035s (~)0.536s151.95x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.518s (-1.5%)5.994s (~)0.000s (NaN%)6.413s (-0.7%)1.896s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress16/21
🐘 PostgresExpress15/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres15/21
Nitro🐘 Postgres18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production10000671067
✅ 💻 Local Development10780861164
✅ 📦 Local Production10780861164
✅ 🐘 Local Postgres10780861164
✅ 🪟 Windows970097
✅ 📋 Other273018291
Total460403434947

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9007
✅ example9007
✅ express9007
✅ fastify9007
✅ hono9007
✅ nextjs-turbopack9502
✅ nextjs-webpack9502
✅ nitro9007
✅ nuxt9007
✅ sveltekit9007
✅ vite9007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9700
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9106
✅ e2e-local-postgres-nest-stable9106
✅ e2e-local-prod-nest-stable9106

📋 View full workflow run

CopilotAI 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.

Pull request overview

Switches CI jobs that hit protected Vercel deployments from a long-lived bypass secret to Vercel Deployment Protection “Trusted Sources” using GitHub Actions OIDC, and centralizes the request header logic in a shared helper.

Changes:

  • Add OIDC token minting in relevant CI jobs and pass it via VERCEL_TRUSTED_OIDC_TOKEN.
  • Introduce scripts/trusted-sources-headers.mjs and update e2e/bench/docs smoke callers to use it.
  • Update local dev docs (AGENTS.md) to reflect the new bypass mechanism.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
scripts/trusted-sources-headers.mjsNew shared helper to attach the Vercel Trusted Sources OIDC header when present.
packages/core/e2e/utils.tsRemoves legacy bypass-secret helper; uses the shared Trusted Sources headers helper.
packages/core/e2e/e2e.test.tsUpdates HTTP calls to use Trusted Sources headers.
packages/core/e2e/bench.bench.tsUpdates manifest fetch to use Trusted Sources headers.
docs/scripts/check-docs-smoke.mjsUpdates docs smoke fetches to use Trusted Sources headers.
AGENTS.mdDocuments the new env var and local-development limitation.
.github/workflows/tests.ymlAdds OIDC token minting + env wiring for e2e Vercel prod tests.
.github/workflows/docs-checks.ymlAdds OIDC token minting + env wiring for docs preview smoke checks.
.github/workflows/benchmarks.ymlAdds OIDC token minting + env wiring for Vercel benchmarks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread.github/workflows/tests.yml
Comment thread.github/workflows/docs-checks.yml
Comment thread.github/workflows/benchmarks.yml
- Rename the env var from VERCEL_TRUSTED_OIDC_TOKEN to VERCEL_OIDC_TOKEN
to match Vercel's convention (also read by @vercel/oidc's
getVercelOidcToken()).
- In @workflow/world-vercel, replace the legacy
VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS / x-vercel-protection-bypass
flow with VERCEL_OIDC_TOKEN / x-vercel-trusted-oidc-idp-token. The
trusted-source header is attached on every outbound workflow-server
request (both proxied through api.vercel.com and direct).
- Drop the bypass header from the encryption-key and
resolve-latest-deployment fetches: those go to api.vercel.com which
is public.
- Drop VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS plumbing from tests.yml.
- Update the pending world-vercel changeset to describe the final
trusted-sources flow.
Adds a debug step that does two HEAD requests against the docs preview
deployment (with and without the OIDC trusted-sources header) and prints
the response status line plus `x-vercel-id` for each. The proxy-side
trusted-sources changes for GitHub Actions OIDC tokens are rolling out
gradually (~12+ hours), so the edge-node identifier in `x-vercel-id`
helps explain why a request might succeed or fail during the rollout
window.
Also includes `x-vercel-id` in the `waitForServer` timeout error so
post-mortem analysis of failing runs has the same edge-node info.
…ix reaches the serving edge node
The probe served its purpose: confirmed the bypass is functional once
the request lands on a region that has the proxy-side trusted-sources
fix rolled out. The waitForServer error message still surfaces
x-vercel-id for any future rollout-window debugging.
Adds a one-shot diagnostic that prints the non-sensitive claims of the
OIDC token (`iss`, `aud`, `owner_id`, `project_id`, `environment`,
`sub`, `scope`, `exp`) on the first request that uses bearer auth.
This is invaluable for debugging Vercel deployment-protection
trusted-source rule mismatches: a 401 from the edge tells you nothing
about why the rule didn't match, and the token's claims are the only
thing that determines that. The signature is never logged.
Gated to once per process — Vercel-issued tokens are process-stable for
the lambda's lifetime so further log lines would just be redundant
spam.
The Authorization bearer correctly preferred config.token (a static
Vercel auth token from CLI / Actions runner) and fell back to
getVercelOidcToken() inside a Vercel function. But the trusted-sources
bypass header (x-vercel-trusted-oidc-idp-token) was being read directly
from process.env.VERCEL_OIDC_TOKEN inside getHeaders(). That env var is
the bake-time token, frozen at deployment-creation time — on a project
that has been redeployed after a settings change, it carries stale
claims (e.g. an iss from when the project was briefly in 'global' mode)
that no longer match the workflow-server's trusted-sources rule.
Move trusted-sources header attachment from getHeaders() (sync) to
getHttpConfig() (async) and source it from getVercelOidcToken(). That
function reads getContext().headers['x-vercel-oidc-token'] first — a
freshly minted per-request token that always reflects current project
settings — and only falls back to the env var when that header is
missing.
Bearer auth source remains config.token-first.
Also expand the diagnostic to log claims from BOTH the per-request OIDC
token AND the bake-time env var so the divergence is visible in logs
when debugging future trusted-source mismatches.
Removes the now-misleading getProtectionBypassHeader() helper (its
'read env var directly' semantics were exactly the bug).
The two outbound flows have different auth requirements:
1. Proxied (usingProxy=true) — calls api.vercel.com/v1/workflow.
Public endpoint, authenticated with a static Vercel auth token via
config.token. The api-workflow proxy mints its own OIDC token
before forwarding to workflow-server, so the trusted-sources
bypass header on the SDK→proxy hop is meaningless. CLI, GitHub
Actions, and other API-client callers take this path.
2. Direct (usingProxy=false) — runs inside a Vercel deployment
talking straight to workflow-server. workflow-server validates a
Vercel OIDC bearer; Vercel's edge validates the trusted-sources
header. Both must come from getVercelOidcToken() (the per-request
fresh token), not process.env.VERCEL_OIDC_TOKEN (the bake-time
token that can be stale after a project config change).
Previously getHttpConfig attached x-vercel-trusted-oidc-idp-token on
both paths whenever getVercelOidcToken() resolved. That accidentally
forwarded the GitHub Actions OIDC token (when wired into
VERCEL_OIDC_TOKEN by the test runner) onto every SDK→proxy request,
which is harmless but wrong-by-design — the proxy is public, doesn't
look at that header on its inbound side, and the GHA token isn't its
intended audience.
Bearer auth source rules:
- Proxied: only config.token. (No fallback to OIDC; that auth
pathway doesn't go through the proxy's auth checks.)
- Direct: config.token (for tests / local dev), falling back to
getVercelOidcToken() (for Vercel-runtime calls).
The api-workflow proxy authenticates the caller with a regular Vercel
auth token (not OIDC), so reaching the proxied path with no
config.token is always wrong: the proxy will reject the request and
the SDK caller would see an opaque 401 with no actionable hint.
Throw at config-resolution time with a clear message that points to
the WORKFLOW_VERCEL_AUTH_TOKEN env var the SDK reads from. Adds tests
covering both the no-token-throws case and the with-token-attaches-
bearer-and-skips-trusted-sources case.
When the trusted-sources bypass returns 401, the error message now
surfaces the response's x-vercel-id header so we can identify which
edge node served the failure. Helps distinguish proxy-rollout
incompleteness from actual config errors during incremental
rollouts of edge-side changes.
Comment threadpackages/core/e2e/e2e.test.ts Outdated
Comment threadpackages/world-vercel/src/jwt-claims.ts Outdated
Comment threadpackages/world-vercel/src/utils.test.ts Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
GitHub Actions OIDC tokens have a hard 5-minute lifetime that cannot be
extended (no API to ask for a longer TTL — exp is always iat + ~300s).
Pre-minting once at the start of the job and shipping the result down
to the test runner via env var means tests that run late in the suite
hit an expired token and 401 on /api/trigger-pages (and any other
trusted-sources protected endpoint).
Move minting into scripts/trusted-sources-headers.mjs:
- getTrustedSourcesHeaders() is now async.
- It calls the runner's ACTIONS_ID_TOKEN_REQUEST_URL endpoint directly
(the env vars GHA exposes when permissions: id-token: write is on)
and re-mints 60s before the cached token's exp.
- Falls back to process.env.VERCEL_OIDC_TOKEN for non-GHA contexts
(Vercel runtime, local dev).
Workflow files drop the now-redundant 'Mint OIDC token' step and the
VERCEL_OIDC_TOKEN env-var passthrough on the test step. The runner env
vars propagate to subsequent steps automatically.
Updates all 17 callers in e2e.test.ts / bench.bench.ts / utils.ts /
docs/scripts/check-docs-smoke.mjs to await the now-async call.
Comment thread.changeset/world-vercel-protection-bypass.md
Comment thread.changeset/world-vercel-trusted-sources.md Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
- Drop `statuses: read` from the three workflow permission blocks (the
wait-for-vercel-project action works without it on a public repo).
- Revert the `x-vercel-id` debug logging in `startWorkflowViaHttp`.
- Delete `packages/world-vercel/src/jwt-claims.ts` (debug-only helper).
- Drop the JWT claims diagnostic logging from `getHttpConfig`.
- Tighten the auth-flow comment in `getHttpConfig` and remove the
historical 'no longer attaches' note from `getHeaders`/its test.
- Restore `.changeset/world-vercel-protection-bypass.md` (already
shipped in a beta release per .changeset/pre.json).
- Trim the `.changeset/world-vercel-trusted-sources.md` description to
one short paragraph.
Configured trustedSources.projects on all 11 workbench app projects so
each one accepts a Vercel-issued OIDC token from any of the others. A
developer running e2e locally can now do `vercel env pull` from any
workbench app's directory and use the resulting VERCEL_OIDC_TOKEN to
bypass Deployment Protection on any of the workbench preview/prod
deployments — no need to disable protection on the project just to run
the suite locally.
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.

3 participants

@TooTallNate@pranaygp
, '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

ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources - #1882

Merged
TooTallNate merged 24 commits into
mainfrom
ci/oidc-trusted-sources
May 2, 2026
Merged

ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources#1882
TooTallNate merged 24 commits into
mainfrom
ci/oidc-trusted-sources

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 30, 2026

Copy link
Copy Markdown
Member

Summary

Switch the e2e, benchmark, and docs-smoke CI jobs from the static VERCEL_AUTOMATION_BYPASS_SECRET deployment-protection bypass token over to Vercel's OIDC Trusted Sources flow. Same conceptual swap is applied in the runtime world-vercel SDK: outbound requests to a protected workflow-server preview now carry an x-vercel-trusted-oidc-idp-token header instead of x-vercel-protection-bypass.

What changes

CI workflows

.github/workflows/tests.yml, benchmarks.yml, docs-checks.yml:

  • Add permissions: id-token: write (and explicit contents: read, deployments: read) to the affected jobs so the runner can mint OIDC tokens.
  • Tests/benchmarks/smoke checks pick up the ACTIONS_ID_TOKEN_REQUEST_URL / ACTIONS_ID_TOKEN_REQUEST_TOKEN env vars that GitHub injects on id-token: write–enabled jobs and mint OIDC tokens on demand (see helper below). The VERCEL_AUTOMATION_BYPASS_SECRET env var and the (now no-op) VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS env var are removed.

Shared on-demand minting helper — scripts/trusted-sources-headers.mjs

GitHub Actions OIDC tokens have a hard 5-minute lifetime. Pre-minting once at the start of the job and forwarding the result down doesn't work for long e2e suites — tests that run late in the run hit an expired token and 401. The new getTrustedSourcesHeaders() is async, mints fresh tokens from the runner endpoint on demand, caches them in-process, and re-mints 60 s before the JWT's exp. Falls back to process.env.VERCEL_OIDC_TOKEN for non-CI contexts (Vercel runtime, local dev).

Used by packages/core/e2e/{e2e.test.ts,bench.bench.ts,utils.ts} and docs/scripts/check-docs-smoke.mjs — all callers now await the helper.

@workflow/world-vercel runtime

packages/world-vercel/src/utils.ts:getHttpConfig:

  • Proxied path (usingProxy=true, hits api.vercel.com/v1/workflow): bearer auth from config.token only. The api-workflow proxy authenticates the caller with a regular Vercel auth token, so the trusted-sources header is meaningless on this hop. We now throw a clear error if config.token is missing instead of letting an opaque 401 bubble up.
  • Direct path (usingProxy=false, hits the workflow-server URL directly from a Vercel deployment): bearer auth prefers config.token and falls back to getVercelOidcToken(). The trusted-sources bypass header always uses getVercelOidcToken() (per-request token Vercel injects on every invocation). This avoids the trap of reading process.env.VERCEL_OIDC_TOKEN directly, which carries the deployment's bake-time token and goes stale after project-config changes.

packages/world-vercel/src/{encryption,resolve-latest-deployment}.ts: drop the (now-unused) getProtectionBypassHeader calls — both go to the public api.vercel.com, which never needs the bypass.

Vercel-side configuration (out of band)

Each of the 11 workbench app projects has its trust configuration updated:

  • trustedSources.projects — entries for all 11 workbench projects with a customAllow: [{ from: any-env, to: any-env }] rule, so any workbench's Vercel-issued OIDC token can reach any other (and itself) on any environment. This is what enables the local-dev recipe in AGENTS.md: vercel env pull from any workbench app yields a VERCEL_OIDC_TOKEN that bypasses Deployment Protection on every workbench preview/prod URL.
  • trustedSources.oidcProviders["https://token.actions.githubusercontent.com"] — accepts GitHub Actions OIDC tokens from vercel/workflow CI for the direct-fetch tests against the deployed workbench apps.

Local-development recipe (AGENTS.md)

Updated the local-against-preview e2e recipe to document vercel env pull as the source of VERCEL_OIDC_TOKEN. Replaces the old VERCEL_AUTOMATION_BYPASS_SECRET instruction.

Files changed

FileChange
.github/workflows/tests.ymlpermissions: id-token: write, dropped pre-mint step + VERCEL_AUTOMATION_BYPASS_SECRET / VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS
.github/workflows/benchmarks.ymlsame
.github/workflows/docs-checks.ymlsame
scripts/trusted-sources-headers.mjs (new)async on-demand minting helper
packages/core/e2e/e2e.test.tsawait getTrustedSourcesHeaders() at every callsite
packages/core/e2e/bench.bench.tssame
packages/core/e2e/utils.tssame
docs/scripts/check-docs-smoke.mjssame
packages/world-vercel/src/utils.tsbearer/trusted-sources split between proxied and direct paths; throw on missing config.token for proxied path; removed dead getProtectionBypassHeader
packages/world-vercel/src/utils.test.tstests for the new direct/proxied behaviour
packages/world-vercel/src/encryption.tsdrop bypass header (api.vercel.com is public)
packages/world-vercel/src/resolve-latest-deployment.tsdrop bypass header (api.vercel.com is public)
AGENTS.mdlocal-dev recipe uses vercel env pull for VERCEL_OIDC_TOKEN
.changeset/world-vercel-trusted-sources.md (new)minor bump for @workflow/world-vercel

Untouched (separate concerns)

  • WORKFLOW_VERCEL_AUTH_TOKEN (secrets.VERCEL_LABS_TOKEN) — Vercel API auth, still needed for the proxied path's config.token.
  • VERCEL_WORKFLOW_SERVER_URL env var — still used to point CI at the workflow-server preview on PR runs.

Cleanup follow-up

Once a CI run on main confirms everything works, the VERCEL_AUTOMATION_BYPASS_SECRET and VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS repo secrets can be deleted from GitHub.

The e2e, benchmark, and docs-smoke CI jobs previously used the static
`VERCEL_AUTOMATION_BYPASS_SECRET` deployment-protection bypass token
to reach protected Vercel deployments. Switch them over to the new OIDC
Trusted Sources flow: the GitHub Actions runner mints a short-lived
OIDC token via `core.getIDToken()` and forwards it on requests in the
`x-vercel-trusted-oidc-idp-token` header.
Each workbench project (and `workflow-docs`) has been configured with a
matching trusted-source rule:
aud=https://github.com/vercel, repository=vercel/workflow
The shared header helper now lives at `scripts/trusted-sources-headers.mjs`
and is imported by both the e2e/bench tests and the docs smoke script,
removing the previous duplication.
CopilotAI review requested due to automatic review settings April 30, 2026 17:50
@changeset-bot

changeset-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c125b54

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@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

@vercel

vercelBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.043s (-2.5%)1.006s (~)0.962s101.00x
💻 LocalNitro0.049s (+13.9% 🔺)1.011s (~)0.961s101.14x
🐘 PostgresNitro0.059s (-38.3% 🟢)1.010s (-3.1%)0.952s101.36x
🐘 PostgresExpress0.060s (+3.3%)1.011s (~)0.951s101.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.314s (+33.2% 🔺)2.234s (+4.6%)1.920s101.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.125s (~)2.005s (~)0.880s101.00x
💻 LocalNitro1.127s (~)2.006s (~)0.879s101.00x
🐘 PostgresExpress1.143s (~)2.010s (~)0.867s101.02x
🐘 PostgresNitro1.149s (+0.8%)2.010s (~)0.862s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.920s (+2.4%)3.677s (-3.4%)1.757s101.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.880s (~)11.019s (~)0.139s31.00x
💻 LocalNitro10.928s (~)11.023s (~)0.095s31.00x
💻 LocalExpress10.947s (~)11.022s (~)0.075s31.01x
🐘 PostgresExpress10.955s (~)11.353s (+3.0%)0.398s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.546s (+3.3%)19.315s (-3.5%)1.770s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.542s (~)15.022s (~)0.480s41.00x
🐘 PostgresNitro14.553s (~)15.025s (~)0.472s41.00x
💻 LocalExpress14.970s (~)15.280s (+1.7%)0.310s41.03x
💻 LocalNitro14.986s (-0.5%)15.279s (-4.7%)0.293s41.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express31.530s (-37.3% 🟢)33.430s (-36.4% 🟢)1.899s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.022s (~)14.447s (-1.0%)0.425s71.00x
🐘 PostgresNitro14.038s (+0.5%)14.737s (+3.0%)0.699s71.00x
💻 LocalExpress16.492s (-0.7%)17.031s (~)0.539s61.18x
💻 LocalNitro16.588s (-1.2%)17.029s (~)0.441s61.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express55.578s (-54.2% 🟢)58.406s (-52.8% 🟢)2.828s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.249s (-0.9%)2.010s (~)0.760s151.00x
🐘 PostgresNitro1.283s (+0.6%)2.011s (~)0.728s151.03x
💻 LocalExpress1.508s (+1.3%)2.006s (~)0.498s151.21x
💻 LocalNitro1.533s (-6.1% 🟢)2.006s (-3.3%)0.473s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.403s (-16.0% 🟢)3.950s (-14.6% 🟢)1.547s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.345s (-0.7%)3.009s (~)0.664s101.00x
🐘 PostgresNitro2.359s (~)3.011s (~)0.651s101.01x
💻 LocalExpress2.817s (-4.6%)3.108s (-10.0% 🟢)0.292s101.20x
💻 LocalNitro2.910s (-7.4% 🟢)3.207s (-17.5% 🟢)0.297s101.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.388s (-34.0% 🟢)4.195s (-17.9% 🟢)1.807s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.462s (-0.7%)4.010s (~)0.548s81.00x
🐘 PostgresNitro3.494s (~)4.014s (~)0.520s81.01x
💻 LocalExpress7.361s (-11.7% 🟢)8.020s (-11.1% 🟢)0.659s42.13x
💻 LocalNitro7.874s (-5.7% 🟢)8.269s (-8.3% 🟢)0.395s42.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.002s (-5.6% 🟢)6.045s (-1.3%)2.042s51.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.261s (~)2.009s (~)0.748s151.00x
🐘 PostgresExpress1.266s (+0.7%)2.008s (~)0.743s151.00x
💻 LocalExpress1.522s (-19.6% 🟢)2.006s (-15.1% 🟢)0.483s151.21x
💻 LocalNitro1.553s (-16.8% 🟢)2.006s (-14.3% 🟢)0.453s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.335s (-9.6% 🟢)3.671s (-15.6% 🟢)1.336s91.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.338s (~)3.010s (~)0.672s101.00x
🐘 PostgresExpress2.348s (~)3.011s (~)0.663s101.00x
💻 LocalExpress2.906s (-7.2% 🟢)3.008s (-20.1% 🟢)0.102s101.24x
💻 LocalNitro2.983s (-2.7%)3.565s (-8.3% 🟢)0.582s91.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.317s (-27.4% 🟢)3.822s (-20.3% 🟢)1.505s81.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.469s (-0.9%)4.010s (~)0.540s81.00x
🐘 PostgresNitro3.478s (~)4.011s (~)0.533s81.00x
💻 LocalExpress8.107s (-7.9% 🟢)9.025s (-2.7%)0.917s42.34x
💻 LocalNitro8.436s (-7.7% 🟢)9.024s (-10.0% 🟢)0.588s42.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.081s (-52.0% 🟢)4.799s (-41.3% 🟢)1.718s71.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.844s (+0.6%)1.023s (~)0.179s591.00x
🐘 PostgresNitro0.884s (+7.8% 🔺)1.041s (+3.5%)0.157s581.05x
💻 LocalNitro0.987s (+0.6%)1.181s (+8.0% 🔺)0.194s511.17x
💻 LocalExpress1.022s (+3.8%)1.941s (+80.4% 🔺)0.920s311.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.175s (-51.8% 🟢)10.768s (-49.5% 🟢)1.593s61.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.972s (~)2.402s (+6.4% 🔺)0.430s381.00x
🐘 PostgresNitro1.987s (+3.1%)2.403s (+14.4% 🔺)0.416s381.01x
💻 LocalNitro3.023s (~)3.649s (-2.9%)0.625s251.53x
💻 LocalExpress3.038s (+0.7%)3.759s (+4.8%)0.721s241.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express28.216s (-18.3% 🟢)30.015s (-18.5% 🟢)1.800s31.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.999s (~)4.456s (+2.0%)0.457s271.00x
🐘 PostgresNitro4.047s (-1.4%)4.627s (~)0.580s261.01x
💻 LocalExpress8.938s (-3.0%)9.325s (-6.9% 🟢)0.387s132.23x
💻 LocalNitro9.121s (-1.9%)9.710s (-3.1%)0.589s132.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express74.288s (-42.9% 🟢)76.387s (-42.2% 🟢)2.098s21.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.278s (-1.6%)1.007s (~)0.729s601.00x
🐘 PostgresNitro0.292s (+2.9%)1.007s (~)0.716s601.05x
💻 LocalExpress0.594s (+5.9% 🔺)1.005s (~)0.411s602.14x
💻 LocalNitro0.782s (+29.3% 🔺)1.205s (+17.9% 🔺)0.422s502.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.563s (-20.0% 🟢)3.059s (-15.9% 🟢)1.496s201.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.494s (-3.1%)1.007s (~)0.513s901.00x
🐘 PostgresNitro0.509s (+2.6%)1.008s (~)0.499s901.03x
💻 LocalExpress2.408s (-4.2%)3.009s (~)0.600s304.87x
💻 LocalNitro2.460s (-3.1%)3.009s (~)0.548s304.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.780s (-8.8% 🟢)4.406s (-8.4% 🟢)1.626s211.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.790s (-3.5%)1.007s (-1.0%)0.218s1201.00x
🐘 PostgresNitro0.798s (+1.0%)1.009s (~)0.210s1191.01x
💻 LocalExpress10.360s (-7.4% 🟢)11.028s (-7.6% 🟢)0.668s1113.12x
💻 LocalNitro10.737s (-4.1%)11.121s (-4.7%)0.384s1113.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express7.045s (-5.1% 🟢)8.589s (-7.1% 🟢)1.544s151.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.203s (-4.9%)1.004s (~)0.011s (-9.6% 🟢)1.017s (~)0.814s101.00x
🐘 PostgresNitro0.208s (+1.5%)0.999s (~)0.002s (~)1.011s (~)0.802s101.02x
💻 LocalExpress0.209s (+5.2% 🔺)1.004s (~)0.011s (-13.2% 🟢)1.016s (~)0.807s101.03x
🐘 PostgresExpress0.210s (+2.3%)0.998s (~)0.001s (-43.8% 🟢)1.010s (~)0.800s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.484s (-40.8% 🟢)3.012s (-26.4% 🟢)1.734s (+80.5% 🔺)5.137s (-8.1% 🟢)3.653s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.620s (-0.7%)1.005s (~)0.004s (-2.0%)1.023s (~)0.403s591.00x
🐘 PostgresExpress0.635s (+0.8%)1.005s (~)0.004s (+7.1% 🔺)1.023s (~)0.388s591.02x
💻 LocalExpress0.764s (+0.9%)1.013s (-1.6%)0.009s (-6.0% 🟢)1.023s (-1.6%)0.260s591.23x
💻 LocalNitro0.833s (-0.7%)1.011s (~)0.010s (+5.1% 🔺)1.116s (~)0.283s541.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.166s (-35.9% 🟢)5.514s (-31.2% 🟢)0.218s (-46.7% 🟢)6.081s (-31.2% 🟢)1.914s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.918s (-4.5%)1.149s (-10.1% 🟢)0.000s (-11.5% 🟢)1.168s (-10.6% 🟢)0.250s521.00x
🐘 PostgresNitro0.971s (~)1.264s (+1.3%)0.000s (-50.0% 🟢)1.290s (+2.5%)0.319s481.06x
💻 LocalNitro1.203s (-1.6%)2.020s (~)0.000s (+133.3% 🔺)2.021s (~)0.818s301.31x
💻 LocalExpress1.226s (~)2.021s (~)0.000s (-50.0% 🟢)2.023s (~)0.797s301.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.185s (-14.8% 🟢)4.482s (-12.2% 🟢)0.000s (-100.0% 🟢)4.856s (-12.2% 🟢)1.671s131.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.795s (+1.3%)2.177s (~)0.000s (NaN%)2.189s (~)0.394s281.00x
🐘 PostgresNitro1.832s (+2.3%)2.219s (+3.6%)0.000s (+3.7%)2.233s (+2.7%)0.400s271.02x
💻 LocalExpress3.392s (-2.2%)3.973s (-1.5%)0.001s (+25.0% 🔺)3.977s (-1.5%)0.584s161.89x
💻 LocalNitro3.499s (+3.3%)4.032s (~)0.000s (-12.5% 🟢)4.035s (~)0.536s151.95x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.518s (-1.5%)5.994s (~)0.000s (NaN%)6.413s (-0.7%)1.896s101.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress16/21
🐘 PostgresExpress15/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres15/21
Nitro🐘 Postgres18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production10000671067
✅ 💻 Local Development10780861164
✅ 📦 Local Production10780861164
✅ 🐘 Local Postgres10780861164
✅ 🪟 Windows970097
✅ 📋 Other273018291
Total460403434947

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9007
✅ example9007
✅ express9007
✅ fastify9007
✅ hono9007
✅ nextjs-turbopack9502
✅ nextjs-webpack9502
✅ nitro9007
✅ nuxt9007
✅ sveltekit9007
✅ vite9007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9700
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9106
✅ e2e-local-postgres-nest-stable9106
✅ e2e-local-prod-nest-stable9106

📋 View full workflow run

CopilotAI 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.

Pull request overview

Switches CI jobs that hit protected Vercel deployments from a long-lived bypass secret to Vercel Deployment Protection “Trusted Sources” using GitHub Actions OIDC, and centralizes the request header logic in a shared helper.

Changes:

  • Add OIDC token minting in relevant CI jobs and pass it via VERCEL_TRUSTED_OIDC_TOKEN.
  • Introduce scripts/trusted-sources-headers.mjs and update e2e/bench/docs smoke callers to use it.
  • Update local dev docs (AGENTS.md) to reflect the new bypass mechanism.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
scripts/trusted-sources-headers.mjsNew shared helper to attach the Vercel Trusted Sources OIDC header when present.
packages/core/e2e/utils.tsRemoves legacy bypass-secret helper; uses the shared Trusted Sources headers helper.
packages/core/e2e/e2e.test.tsUpdates HTTP calls to use Trusted Sources headers.
packages/core/e2e/bench.bench.tsUpdates manifest fetch to use Trusted Sources headers.
docs/scripts/check-docs-smoke.mjsUpdates docs smoke fetches to use Trusted Sources headers.
AGENTS.mdDocuments the new env var and local-development limitation.
.github/workflows/tests.ymlAdds OIDC token minting + env wiring for e2e Vercel prod tests.
.github/workflows/docs-checks.ymlAdds OIDC token minting + env wiring for docs preview smoke checks.
.github/workflows/benchmarks.ymlAdds OIDC token minting + env wiring for Vercel benchmarks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread.github/workflows/tests.yml
Comment thread.github/workflows/docs-checks.yml
Comment thread.github/workflows/benchmarks.yml
- Rename the env var from VERCEL_TRUSTED_OIDC_TOKEN to VERCEL_OIDC_TOKEN
to match Vercel's convention (also read by @vercel/oidc's
getVercelOidcToken()).
- In @workflow/world-vercel, replace the legacy
VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS / x-vercel-protection-bypass
flow with VERCEL_OIDC_TOKEN / x-vercel-trusted-oidc-idp-token. The
trusted-source header is attached on every outbound workflow-server
request (both proxied through api.vercel.com and direct).
- Drop the bypass header from the encryption-key and
resolve-latest-deployment fetches: those go to api.vercel.com which
is public.
- Drop VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS plumbing from tests.yml.
- Update the pending world-vercel changeset to describe the final
trusted-sources flow.
Adds a debug step that does two HEAD requests against the docs preview
deployment (with and without the OIDC trusted-sources header) and prints
the response status line plus `x-vercel-id` for each. The proxy-side
trusted-sources changes for GitHub Actions OIDC tokens are rolling out
gradually (~12+ hours), so the edge-node identifier in `x-vercel-id`
helps explain why a request might succeed or fail during the rollout
window.
Also includes `x-vercel-id` in the `waitForServer` timeout error so
post-mortem analysis of failing runs has the same edge-node info.
…ix reaches the serving edge node
The probe served its purpose: confirmed the bypass is functional once
the request lands on a region that has the proxy-side trusted-sources
fix rolled out. The waitForServer error message still surfaces
x-vercel-id for any future rollout-window debugging.
Adds a one-shot diagnostic that prints the non-sensitive claims of the
OIDC token (`iss`, `aud`, `owner_id`, `project_id`, `environment`,
`sub`, `scope`, `exp`) on the first request that uses bearer auth.
This is invaluable for debugging Vercel deployment-protection
trusted-source rule mismatches: a 401 from the edge tells you nothing
about why the rule didn't match, and the token's claims are the only
thing that determines that. The signature is never logged.
Gated to once per process — Vercel-issued tokens are process-stable for
the lambda's lifetime so further log lines would just be redundant
spam.
The Authorization bearer correctly preferred config.token (a static
Vercel auth token from CLI / Actions runner) and fell back to
getVercelOidcToken() inside a Vercel function. But the trusted-sources
bypass header (x-vercel-trusted-oidc-idp-token) was being read directly
from process.env.VERCEL_OIDC_TOKEN inside getHeaders(). That env var is
the bake-time token, frozen at deployment-creation time — on a project
that has been redeployed after a settings change, it carries stale
claims (e.g. an iss from when the project was briefly in 'global' mode)
that no longer match the workflow-server's trusted-sources rule.
Move trusted-sources header attachment from getHeaders() (sync) to
getHttpConfig() (async) and source it from getVercelOidcToken(). That
function reads getContext().headers['x-vercel-oidc-token'] first — a
freshly minted per-request token that always reflects current project
settings — and only falls back to the env var when that header is
missing.
Bearer auth source remains config.token-first.
Also expand the diagnostic to log claims from BOTH the per-request OIDC
token AND the bake-time env var so the divergence is visible in logs
when debugging future trusted-source mismatches.
Removes the now-misleading getProtectionBypassHeader() helper (its
'read env var directly' semantics were exactly the bug).
The two outbound flows have different auth requirements:
1. Proxied (usingProxy=true) — calls api.vercel.com/v1/workflow.
Public endpoint, authenticated with a static Vercel auth token via
config.token. The api-workflow proxy mints its own OIDC token
before forwarding to workflow-server, so the trusted-sources
bypass header on the SDK→proxy hop is meaningless. CLI, GitHub
Actions, and other API-client callers take this path.
2. Direct (usingProxy=false) — runs inside a Vercel deployment
talking straight to workflow-server. workflow-server validates a
Vercel OIDC bearer; Vercel's edge validates the trusted-sources
header. Both must come from getVercelOidcToken() (the per-request
fresh token), not process.env.VERCEL_OIDC_TOKEN (the bake-time
token that can be stale after a project config change).
Previously getHttpConfig attached x-vercel-trusted-oidc-idp-token on
both paths whenever getVercelOidcToken() resolved. That accidentally
forwarded the GitHub Actions OIDC token (when wired into
VERCEL_OIDC_TOKEN by the test runner) onto every SDK→proxy request,
which is harmless but wrong-by-design — the proxy is public, doesn't
look at that header on its inbound side, and the GHA token isn't its
intended audience.
Bearer auth source rules:
- Proxied: only config.token. (No fallback to OIDC; that auth
pathway doesn't go through the proxy's auth checks.)
- Direct: config.token (for tests / local dev), falling back to
getVercelOidcToken() (for Vercel-runtime calls).
The api-workflow proxy authenticates the caller with a regular Vercel
auth token (not OIDC), so reaching the proxied path with no
config.token is always wrong: the proxy will reject the request and
the SDK caller would see an opaque 401 with no actionable hint.
Throw at config-resolution time with a clear message that points to
the WORKFLOW_VERCEL_AUTH_TOKEN env var the SDK reads from. Adds tests
covering both the no-token-throws case and the with-token-attaches-
bearer-and-skips-trusted-sources case.
When the trusted-sources bypass returns 401, the error message now
surfaces the response's x-vercel-id header so we can identify which
edge node served the failure. Helps distinguish proxy-rollout
incompleteness from actual config errors during incremental
rollouts of edge-side changes.
Comment threadpackages/core/e2e/e2e.test.ts Outdated
Comment threadpackages/world-vercel/src/jwt-claims.ts Outdated
Comment threadpackages/world-vercel/src/utils.test.ts Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
GitHub Actions OIDC tokens have a hard 5-minute lifetime that cannot be
extended (no API to ask for a longer TTL — exp is always iat + ~300s).
Pre-minting once at the start of the job and shipping the result down
to the test runner via env var means tests that run late in the suite
hit an expired token and 401 on /api/trigger-pages (and any other
trusted-sources protected endpoint).
Move minting into scripts/trusted-sources-headers.mjs:
- getTrustedSourcesHeaders() is now async.
- It calls the runner's ACTIONS_ID_TOKEN_REQUEST_URL endpoint directly
(the env vars GHA exposes when permissions: id-token: write is on)
and re-mints 60s before the cached token's exp.
- Falls back to process.env.VERCEL_OIDC_TOKEN for non-GHA contexts
(Vercel runtime, local dev).
Workflow files drop the now-redundant 'Mint OIDC token' step and the
VERCEL_OIDC_TOKEN env-var passthrough on the test step. The runner env
vars propagate to subsequent steps automatically.
Updates all 17 callers in e2e.test.ts / bench.bench.ts / utils.ts /
docs/scripts/check-docs-smoke.mjs to await the now-async call.
Comment thread.changeset/world-vercel-protection-bypass.md
Comment thread.changeset/world-vercel-trusted-sources.md Outdated
Comment threadpackages/world-vercel/src/utils.ts Outdated
- Drop `statuses: read` from the three workflow permission blocks (the
wait-for-vercel-project action works without it on a public repo).
- Revert the `x-vercel-id` debug logging in `startWorkflowViaHttp`.
- Delete `packages/world-vercel/src/jwt-claims.ts` (debug-only helper).
- Drop the JWT claims diagnostic logging from `getHttpConfig`.
- Tighten the auth-flow comment in `getHttpConfig` and remove the
historical 'no longer attaches' note from `getHeaders`/its test.
- Restore `.changeset/world-vercel-protection-bypass.md` (already
shipped in a beta release per .changeset/pre.json).
- Trim the `.changeset/world-vercel-trusted-sources.md` description to
one short paragraph.
Configured trustedSources.projects on all 11 workbench app projects so
each one accepts a Vercel-issued OIDC token from any of the others. A
developer running e2e locally can now do `vercel env pull` from any
workbench app's directory and use the resulting VERCEL_OIDC_TOKEN to
bypass Deployment Protection on any of the workbench preview/prod
deployments — no need to disable protection on the project just to run
the suite locally.
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.

3 participants

@TooTallNate@pranaygp