ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources#1882
Conversation
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.
🦋 Changeset detectedLatest commit: c125b54 The changes in this PR will be included in the next version bump. This PR includes changesets to release 18 packages
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 |
📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 10 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 25 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 50 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 10 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 25 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 50 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express stream pipeline with 5 transform steps (1MB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express 10 parallel streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express fan-out fan-in 10 streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
|
🧪 E2E Test Results✅ All tests passed Summary
Details by Category✅ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
✅ 📋 Other
|
There was a problem hiding this comment.
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.mjsand 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.
- 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.
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.
- 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.
Summary
Switch the e2e, benchmark, and docs-smoke CI jobs from the static
VERCEL_AUTOMATION_BYPASS_SECRETdeployment-protection bypass token over to Vercel's OIDC Trusted Sources flow. Same conceptual swap is applied in the runtimeworld-vercelSDK: outbound requests to a protected workflow-server preview now carry anx-vercel-trusted-oidc-idp-tokenheader instead ofx-vercel-protection-bypass.What changes
CI workflows
.github/workflows/tests.yml,benchmarks.yml,docs-checks.yml:permissions: id-token: write(and explicitcontents: read,deployments: read) to the affected jobs so the runner can mint OIDC tokens.ACTIONS_ID_TOKEN_REQUEST_URL/ACTIONS_ID_TOKEN_REQUEST_TOKENenv vars that GitHub injects onid-token: write–enabled jobs and mint OIDC tokens on demand (see helper below). TheVERCEL_AUTOMATION_BYPASS_SECRETenv var and the (now no-op)VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASSenv var are removed.Shared on-demand minting helper —
scripts/trusted-sources-headers.mjsGitHub 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'sexp. Falls back toprocess.env.VERCEL_OIDC_TOKENfor non-CI contexts (Vercel runtime, local dev).Used by
packages/core/e2e/{e2e.test.ts,bench.bench.ts,utils.ts}anddocs/scripts/check-docs-smoke.mjs— all callers nowawaitthe helper.@workflow/world-vercelruntimepackages/world-vercel/src/utils.ts:getHttpConfig:usingProxy=true, hitsapi.vercel.com/v1/workflow): bearer auth fromconfig.tokenonly. 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 ifconfig.tokenis missing instead of letting an opaque 401 bubble up.usingProxy=false, hits the workflow-server URL directly from a Vercel deployment): bearer auth prefersconfig.tokenand falls back togetVercelOidcToken(). The trusted-sources bypass header always usesgetVercelOidcToken()(per-request token Vercel injects on every invocation). This avoids the trap of readingprocess.env.VERCEL_OIDC_TOKENdirectly, 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)getProtectionBypassHeadercalls — both go to the publicapi.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 acustomAllow: [{ 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 pullfrom any workbench app yields aVERCEL_OIDC_TOKENthat bypasses Deployment Protection on every workbench preview/prod URL.trustedSources.oidcProviders["https://token.actions.githubusercontent.com"]— accepts GitHub Actions OIDC tokens fromvercel/workflowCI 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 pullas the source ofVERCEL_OIDC_TOKEN. Replaces the oldVERCEL_AUTOMATION_BYPASS_SECRETinstruction.Files changed
.github/workflows/tests.ymlpermissions: id-token: write, dropped pre-mint step +VERCEL_AUTOMATION_BYPASS_SECRET/VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS.github/workflows/benchmarks.yml.github/workflows/docs-checks.ymlscripts/trusted-sources-headers.mjs(new)packages/core/e2e/e2e.test.tsawait getTrustedSourcesHeaders()at every callsitepackages/core/e2e/bench.bench.tspackages/core/e2e/utils.tsdocs/scripts/check-docs-smoke.mjspackages/world-vercel/src/utils.tsconfig.tokenfor proxied path; removed deadgetProtectionBypassHeaderpackages/world-vercel/src/utils.test.tspackages/world-vercel/src/encryption.tspackages/world-vercel/src/resolve-latest-deployment.tsAGENTS.mdvercel env pullforVERCEL_OIDC_TOKEN.changeset/world-vercel-trusted-sources.md(new)@workflow/world-vercelUntouched (separate concerns)
WORKFLOW_VERCEL_AUTH_TOKEN(secrets.VERCEL_LABS_TOKEN) — Vercel API auth, still needed for the proxied path'sconfig.token.VERCEL_WORKFLOW_SERVER_URLenv var — still used to point CI at the workflow-server preview on PR runs.Cleanup follow-up
Once a CI run on
mainconfirms everything works, theVERCEL_AUTOMATION_BYPASS_SECRETandVERCEL_WORKFLOW_SERVER_PROTECTION_BYPASSrepo secrets can be deleted from GitHub.