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
Copy link
Copy Markdown
Member

@TooTallNate TooTallNate commented Apr 30, 2026

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

File Change
.github/workflows/tests.yml permissions: id-token: write, dropped pre-mint step + VERCEL_AUTOMATION_BYPASS_SECRET / VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS
.github/workflows/benchmarks.yml same
.github/workflows/docs-checks.yml same
scripts/trusted-sources-headers.mjs (new) async on-demand minting helper
packages/core/e2e/e2e.test.ts await getTrustedSourcesHeaders() at every callsite
packages/core/e2e/bench.bench.ts same
packages/core/e2e/utils.ts same
docs/scripts/check-docs-smoke.mjs same
packages/world-vercel/src/utils.ts bearer/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.ts tests for the new direct/proxied behaviour
packages/world-vercel/src/encryption.ts drop bypass header (api.vercel.com is public)
packages/world-vercel/src/resolve-latest-deployment.ts drop bypass header (api.vercel.com is public)
AGENTS.md local-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.
Copilot AI review requested due to automatic review settings April 30, 2026 17:50
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 30, 2026

🦋 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
Name Type
@workflow/world-vercel Minor
@workflow/cli Patch
@workflow/core Patch
@workflow/web Patch
workflow Patch
@workflow/world-testing Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/ai Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

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
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 30, 2026

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

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment May 2, 2026 8:15am
example-nextjs-workflow-webpack Ready Ready Preview, Comment May 2, 2026 8:15am
example-workflow Ready Ready Preview, Comment May 2, 2026 8:15am
workbench-astro-workflow Ready Ready Preview, Comment May 2, 2026 8:15am
workbench-express-workflow Ready Ready Preview, Comment May 2, 2026 8:15am
workbench-fastify-workflow Ready Ready Preview, Comment May 2, 2026 8:15am
workbench-hono-workflow Ready Ready Preview, Comment May 2, 2026 8:15am
workbench-nitro-workflow Ready Ready Preview, Comment May 2, 2026 8:15am
workbench-nuxt-workflow Ready Ready Preview, Comment May 2, 2026 8:15am
workbench-sveltekit-workflow Ready Ready Preview, Comment May 2, 2026 8:15am
workbench-vite-workflow Ready Ready Preview, Comment May 2, 2026 8:15am
workflow-docs Ready Ready Preview, Comment, Open in v0 May 2, 2026 8:15am
workflow-swc-playground Ready Ready Preview, Comment May 2, 2026 8:15am
workflow-web Ready Ready Preview, Comment May 2, 2026 8:15am

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 30, 2026

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 0.043s (-2.5%) 1.006s (~) 0.962s 10 1.00x
💻 Local Nitro 0.049s (+13.9% 🔺) 1.011s (~) 0.961s 10 1.14x
🐘 Postgres Nitro 0.059s (-38.3% 🟢) 1.010s (-3.1%) 0.952s 10 1.36x
🐘 Postgres Express 0.060s (+3.3%) 1.011s (~) 0.951s 10 1.39x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 0.314s (+33.2% 🔺) 2.234s (+4.6%) 1.920s 10 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.125s (~) 2.005s (~) 0.880s 10 1.00x
💻 Local Nitro 1.127s (~) 2.006s (~) 0.879s 10 1.00x
🐘 Postgres Express 1.143s (~) 2.010s (~) 0.867s 10 1.02x
🐘 Postgres Nitro 1.149s (+0.8%) 2.010s (~) 0.862s 10 1.02x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.920s (+2.4%) 3.677s (-3.4%) 1.757s 10 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 10.880s (~) 11.019s (~) 0.139s 3 1.00x
💻 Local Nitro 10.928s (~) 11.023s (~) 0.095s 3 1.00x
💻 Local Express 10.947s (~) 11.022s (~) 0.075s 3 1.01x
🐘 Postgres Express 10.955s (~) 11.353s (+3.0%) 0.398s 3 1.01x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 17.546s (+3.3%) 19.315s (-3.5%) 1.770s 2 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 14.542s (~) 15.022s (~) 0.480s 4 1.00x
🐘 Postgres Nitro 14.553s (~) 15.025s (~) 0.472s 4 1.00x
💻 Local Express 14.970s (~) 15.280s (+1.7%) 0.310s 4 1.03x
💻 Local Nitro 14.986s (-0.5%) 15.279s (-4.7%) 0.293s 4 1.03x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 31.530s (-37.3% 🟢) 33.430s (-36.4% 🟢) 1.899s 2 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 14.022s (~) 14.447s (-1.0%) 0.425s 7 1.00x
🐘 Postgres Nitro 14.038s (+0.5%) 14.737s (+3.0%) 0.699s 7 1.00x
💻 Local Express 16.492s (-0.7%) 17.031s (~) 0.539s 6 1.18x
💻 Local Nitro 16.588s (-1.2%) 17.029s (~) 0.441s 6 1.18x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 55.578s (-54.2% 🟢) 58.406s (-52.8% 🟢) 2.828s 2 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.249s (-0.9%) 2.010s (~) 0.760s 15 1.00x
🐘 Postgres Nitro 1.283s (+0.6%) 2.011s (~) 0.728s 15 1.03x
💻 Local Express 1.508s (+1.3%) 2.006s (~) 0.498s 15 1.21x
💻 Local Nitro 1.533s (-6.1% 🟢) 2.006s (-3.3%) 0.473s 15 1.23x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.403s (-16.0% 🟢) 3.950s (-14.6% 🟢) 1.547s 8 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 2.345s (-0.7%) 3.009s (~) 0.664s 10 1.00x
🐘 Postgres Nitro 2.359s (~) 3.011s (~) 0.651s 10 1.01x
💻 Local Express 2.817s (-4.6%) 3.108s (-10.0% 🟢) 0.292s 10 1.20x
💻 Local Nitro 2.910s (-7.4% 🟢) 3.207s (-17.5% 🟢) 0.297s 10 1.24x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.388s (-34.0% 🟢) 4.195s (-17.9% 🟢) 1.807s 8 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 3.462s (-0.7%) 4.010s (~) 0.548s 8 1.00x
🐘 Postgres Nitro 3.494s (~) 4.014s (~) 0.520s 8 1.01x
💻 Local Express 7.361s (-11.7% 🟢) 8.020s (-11.1% 🟢) 0.659s 4 2.13x
💻 Local Nitro 7.874s (-5.7% 🟢) 8.269s (-8.3% 🟢) 0.395s 4 2.27x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.002s (-5.6% 🟢) 6.045s (-1.3%) 2.042s 5 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.261s (~) 2.009s (~) 0.748s 15 1.00x
🐘 Postgres Express 1.266s (+0.7%) 2.008s (~) 0.743s 15 1.00x
💻 Local Express 1.522s (-19.6% 🟢) 2.006s (-15.1% 🟢) 0.483s 15 1.21x
💻 Local Nitro 1.553s (-16.8% 🟢) 2.006s (-14.3% 🟢) 0.453s 15 1.23x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.335s (-9.6% 🟢) 3.671s (-15.6% 🟢) 1.336s 9 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 2.338s (~) 3.010s (~) 0.672s 10 1.00x
🐘 Postgres Express 2.348s (~) 3.011s (~) 0.663s 10 1.00x
💻 Local Express 2.906s (-7.2% 🟢) 3.008s (-20.1% 🟢) 0.102s 10 1.24x
💻 Local Nitro 2.983s (-2.7%) 3.565s (-8.3% 🟢) 0.582s 9 1.28x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.317s (-27.4% 🟢) 3.822s (-20.3% 🟢) 1.505s 8 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 3.469s (-0.9%) 4.010s (~) 0.540s 8 1.00x
🐘 Postgres Nitro 3.478s (~) 4.011s (~) 0.533s 8 1.00x
💻 Local Express 8.107s (-7.9% 🟢) 9.025s (-2.7%) 0.917s 4 2.34x
💻 Local Nitro 8.436s (-7.7% 🟢) 9.024s (-10.0% 🟢) 0.588s 4 2.43x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.081s (-52.0% 🟢) 4.799s (-41.3% 🟢) 1.718s 7 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.844s (+0.6%) 1.023s (~) 0.179s 59 1.00x
🐘 Postgres Nitro 0.884s (+7.8% 🔺) 1.041s (+3.5%) 0.157s 58 1.05x
💻 Local Nitro 0.987s (+0.6%) 1.181s (+8.0% 🔺) 0.194s 51 1.17x
💻 Local Express 1.022s (+3.8%) 1.941s (+80.4% 🔺) 0.920s 31 1.21x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 9.175s (-51.8% 🟢) 10.768s (-49.5% 🟢) 1.593s 6 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.972s (~) 2.402s (+6.4% 🔺) 0.430s 38 1.00x
🐘 Postgres Nitro 1.987s (+3.1%) 2.403s (+14.4% 🔺) 0.416s 38 1.01x
💻 Local Nitro 3.023s (~) 3.649s (-2.9%) 0.625s 25 1.53x
💻 Local Express 3.038s (+0.7%) 3.759s (+4.8%) 0.721s 24 1.54x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 28.216s (-18.3% 🟢) 30.015s (-18.5% 🟢) 1.800s 3 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 3.999s (~) 4.456s (+2.0%) 0.457s 27 1.00x
🐘 Postgres Nitro 4.047s (-1.4%) 4.627s (~) 0.580s 26 1.01x
💻 Local Express 8.938s (-3.0%) 9.325s (-6.9% 🟢) 0.387s 13 2.23x
💻 Local Nitro 9.121s (-1.9%) 9.710s (-3.1%) 0.589s 13 2.28x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 74.288s (-42.9% 🟢) 76.387s (-42.2% 🟢) 2.098s 2 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.278s (-1.6%) 1.007s (~) 0.729s 60 1.00x
🐘 Postgres Nitro 0.292s (+2.9%) 1.007s (~) 0.716s 60 1.05x
💻 Local Express 0.594s (+5.9% 🔺) 1.005s (~) 0.411s 60 2.14x
💻 Local Nitro 0.782s (+29.3% 🔺) 1.205s (+17.9% 🔺) 0.422s 50 2.81x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.563s (-20.0% 🟢) 3.059s (-15.9% 🟢) 1.496s 20 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.494s (-3.1%) 1.007s (~) 0.513s 90 1.00x
🐘 Postgres Nitro 0.509s (+2.6%) 1.008s (~) 0.499s 90 1.03x
💻 Local Express 2.408s (-4.2%) 3.009s (~) 0.600s 30 4.87x
💻 Local Nitro 2.460s (-3.1%) 3.009s (~) 0.548s 30 4.98x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.780s (-8.8% 🟢) 4.406s (-8.4% 🟢) 1.626s 21 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.790s (-3.5%) 1.007s (-1.0%) 0.218s 120 1.00x
🐘 Postgres Nitro 0.798s (+1.0%) 1.009s (~) 0.210s 119 1.01x
💻 Local Express 10.360s (-7.4% 🟢) 11.028s (-7.6% 🟢) 0.668s 11 13.12x
💻 Local Nitro 10.737s (-4.1%) 11.121s (-4.7%) 0.384s 11 13.59x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 7.045s (-5.1% 🟢) 8.589s (-7.1% 🟢) 1.544s 15 1.00x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 0.203s (-4.9%) 1.004s (~) 0.011s (-9.6% 🟢) 1.017s (~) 0.814s 10 1.00x
🐘 Postgres Nitro 0.208s (+1.5%) 0.999s (~) 0.002s (~) 1.011s (~) 0.802s 10 1.02x
💻 Local Express 0.209s (+5.2% 🔺) 1.004s (~) 0.011s (-13.2% 🟢) 1.016s (~) 0.807s 10 1.03x
🐘 Postgres Express 0.210s (+2.3%) 0.998s (~) 0.001s (-43.8% 🟢) 1.010s (~) 0.800s 10 1.03x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.484s (-40.8% 🟢) 3.012s (-26.4% 🟢) 1.734s (+80.5% 🔺) 5.137s (-8.1% 🟢) 3.653s 10 1.00x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.620s (-0.7%) 1.005s (~) 0.004s (-2.0%) 1.023s (~) 0.403s 59 1.00x
🐘 Postgres Express 0.635s (+0.8%) 1.005s (~) 0.004s (+7.1% 🔺) 1.023s (~) 0.388s 59 1.02x
💻 Local Express 0.764s (+0.9%) 1.013s (-1.6%) 0.009s (-6.0% 🟢) 1.023s (-1.6%) 0.260s 59 1.23x
💻 Local Nitro 0.833s (-0.7%) 1.011s (~) 0.010s (+5.1% 🔺) 1.116s (~) 0.283s 54 1.34x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.166s (-35.9% 🟢) 5.514s (-31.2% 🟢) 0.218s (-46.7% 🟢) 6.081s (-31.2% 🟢) 1.914s 10 1.00x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.918s (-4.5%) 1.149s (-10.1% 🟢) 0.000s (-11.5% 🟢) 1.168s (-10.6% 🟢) 0.250s 52 1.00x
🐘 Postgres Nitro 0.971s (~) 1.264s (+1.3%) 0.000s (-50.0% 🟢) 1.290s (+2.5%) 0.319s 48 1.06x
💻 Local Nitro 1.203s (-1.6%) 2.020s (~) 0.000s (+133.3% 🔺) 2.021s (~) 0.818s 30 1.31x
💻 Local Express 1.226s (~) 2.021s (~) 0.000s (-50.0% 🟢) 2.023s (~) 0.797s 30 1.34x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.185s (-14.8% 🟢) 4.482s (-12.2% 🟢) 0.000s (-100.0% 🟢) 4.856s (-12.2% 🟢) 1.671s 13 1.00x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express

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

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.795s (+1.3%) 2.177s (~) 0.000s (NaN%) 2.189s (~) 0.394s 28 1.00x
🐘 Postgres Nitro 1.832s (+2.3%) 2.219s (+3.6%) 0.000s (+3.7%) 2.233s (+2.7%) 0.400s 27 1.02x
💻 Local Express 3.392s (-2.2%) 3.973s (-1.5%) 0.001s (+25.0% 🔺) 3.977s (-1.5%) 0.584s 16 1.89x
💻 Local Nitro 3.499s (+3.3%) 4.032s (~) 0.000s (-12.5% 🟢) 4.035s (~) 0.536s 15 1.95x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.518s (-1.5%) 5.994s (~) 0.000s (NaN%) 6.413s (-0.7%) 1.896s 10 1.00x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Express 16/21
🐘 Postgres Express 15/21
▲ Vercel Express 21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 15/21
Nitro 🐘 Postgres 18/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
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 30, 2026

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 1000 0 67 1067
✅ 💻 Local Development 1078 0 86 1164
✅ 📦 Local Production 1078 0 86 1164
✅ 🐘 Local Postgres 1078 0 86 1164
✅ 🪟 Windows 97 0 0 97
✅ 📋 Other 273 0 18 291
Total 4604 0 343 4947

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 90 0 7
✅ example 90 0 7
✅ express 90 0 7
✅ fastify 90 0 7
✅ hono 90 0 7
✅ nextjs-turbopack 95 0 2
✅ nextjs-webpack 95 0 2
✅ nitro 90 0 7
✅ nuxt 90 0 7
✅ sveltekit 90 0 7
✅ vite 90 0 7
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 91 0 6
✅ express-stable 91 0 6
✅ fastify-stable 91 0 6
✅ hono-stable 91 0 6
✅ nextjs-turbopack-canary 78 0 19
✅ nextjs-turbopack-stable 97 0 0
✅ nextjs-webpack-canary 78 0 19
✅ nextjs-webpack-stable 97 0 0
✅ nitro-stable 91 0 6
✅ nuxt-stable 91 0 6
✅ sveltekit-stable 91 0 6
✅ vite-stable 91 0 6
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 91 0 6
✅ express-stable 91 0 6
✅ fastify-stable 91 0 6
✅ hono-stable 91 0 6
✅ nextjs-turbopack-canary 78 0 19
✅ nextjs-turbopack-stable 97 0 0
✅ nextjs-webpack-canary 78 0 19
✅ nextjs-webpack-stable 97 0 0
✅ nitro-stable 91 0 6
✅ nuxt-stable 91 0 6
✅ sveltekit-stable 91 0 6
✅ vite-stable 91 0 6
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 91 0 6
✅ express-stable 91 0 6
✅ fastify-stable 91 0 6
✅ hono-stable 91 0 6
✅ nextjs-turbopack-canary 78 0 19
✅ nextjs-turbopack-stable 97 0 0
✅ nextjs-webpack-canary 78 0 19
✅ nextjs-webpack-stable 97 0 0
✅ nitro-stable 91 0 6
✅ nuxt-stable 91 0 6
✅ sveltekit-stable 91 0 6
✅ vite-stable 91 0 6
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 97 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 91 0 6
✅ e2e-local-postgres-nest-stable 91 0 6
✅ e2e-local-prod-nest-stable 91 0 6

📋 View full workflow run

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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
File Description
scripts/trusted-sources-headers.mjs New shared helper to attach the Vercel Trusted Sources OIDC header when present.
packages/core/e2e/utils.ts Removes legacy bypass-secret helper; uses the shared Trusted Sources headers helper.
packages/core/e2e/e2e.test.ts Updates HTTP calls to use Trusted Sources headers.
packages/core/e2e/bench.bench.ts Updates manifest fetch to use Trusted Sources headers.
docs/scripts/check-docs-smoke.mjs Updates docs smoke fetches to use Trusted Sources headers.
AGENTS.md Documents the new env var and local-development limitation.
.github/workflows/tests.yml Adds OIDC token minting + env wiring for e2e Vercel prod tests.
.github/workflows/docs-checks.yml Adds OIDC token minting + env wiring for docs preview smoke checks.
.github/workflows/benchmarks.yml Adds 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.
The GitHub Actions OIDC trusted-sources bypass returns 401 on all tested
projects regardless of claim configuration (including workflow-docs which
was set up via the dashboard). This is not a per-project config issue.
Need to investigate with Vercel team before continuing.
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 thread packages/core/e2e/e2e.test.ts Outdated
Comment thread packages/world-vercel/src/jwt-claims.ts Outdated
Comment thread packages/world-vercel/src/utils.test.ts Outdated
Comment thread packages/world-vercel/src/utils.ts Outdated
Comment thread packages/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 thread packages/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 free to 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