Skip to content

Commit 2f88cf4

Browse files
committed
feat: add Vercel automation bypass for protected CMS previews
1 parent b741321 commit 2f88cf4

6 files changed

Lines changed: 49 additions & 10 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@
1616
#
1717
# Prerequisites:
1818
# - Disable automatic Vercel deployments for both projects to avoid race conditions
19-
# - Disable Vercel Authentication for CMS preview deployments (Settings → Deployment Protection)
20-
#
21-
# TODO: Migrate to using VERCEL_AUTOMATION_BYPASS_SECRET instead of disabling authentication.
22-
# This would require passing the bypass header in getRedirects.ts and the PayloadSDK.
19+
# - Set up CMS_VERCEL_AUTOMATION_BYPASS_SECRET to allow Web builds to fetch from protected CMS previews
2320

2421
name: Deploy
2522

@@ -169,9 +166,10 @@ jobs:
169166
if [ "$IS_PROD" == "true" ]; then
170167
DEPLOYMENT_URL=$(vercel deploy --prod --yes --token=${{ secrets.VERCEL_TOKEN }})
171168
elif [ -n "$CMS_URL" ]; then
172-
# Preview with CMS preview URL
169+
# Preview with CMS preview URL and bypass secret for Vercel Authentication
173170
DEPLOYMENT_URL=$(vercel deploy --yes --token=${{ secrets.VERCEL_TOKEN }} \
174171
--build-env CMS_URL=$CMS_URL \
172+
--build-env CMS_VERCEL_AUTOMATION_BYPASS_SECRET=${{ secrets.CMS_VERCEL_AUTOMATION_BYPASS_SECRET }} \
175173
--env CMS_URL=$CMS_URL)
176174
else
177175
# Preview without CMS override (uses production CMS from Vercel env vars)

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Example: `fix(web): update styles [skip-cms]`
5151
| `VERCEL_ORG_ID` | Vercel Team ID (Settings → General) |
5252
| `VERCEL_CMS_PROJECT_ID` | CMS project ID (Project Settings → General) |
5353
| `VERCEL_WEB_PROJECT_ID` | Web project ID (Project Settings → General) |
54+
| `CMS_VERCEL_AUTOMATION_BYPASS_SECRET` | CMS bypass secret for preview builds (see below) |
5455

5556
### Preview Deployments
5657

@@ -59,6 +60,13 @@ For pull requests, the workflow:
5960
2. Deploys Web to a preview URL, configured to use the CMS preview URL
6061
3. Posts a comment on the PR with both preview URLs
6162

62-
**Note:** Disable Vercel Authentication for CMS preview deployments to allow the Web build to fetch data:
63-
- Go to **Vercel Dashboard****CMS Project****Settings****Deployment Protection**
64-
- Set Vercel Authentication to **Disabled** or **Only Production**
63+
#### CMS Authentication Bypass
64+
65+
To allow the Web build to fetch data from protected CMS preview deployments:
66+
67+
1. Go to **Vercel Dashboard****CMS Project****Settings****Deployment Protection**
68+
2. Enable **Vercel Authentication** (requires Pro plan with Advanced Deployment Protection)
69+
3. Copy the **Protection Bypass for Automation** secret
70+
4. Add it as `CMS_VERCEL_AUTOMATION_BYPASS_SECRET` in GitHub repository secrets
71+
72+
This allows the Web build to authenticate with the CMS preview while keeping it protected from public access.

web/astro.config.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ export default defineConfig({
2828
context: 'server',
2929
access: 'public',
3030
}),
31+
CMS_VERCEL_AUTOMATION_BYPASS_SECRET: envField.string({
32+
context: 'server',
33+
access: 'secret',
34+
optional: true,
35+
}),
3136
},
3237
},
3338
})

web/src/cms/getRedirects.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import { PayloadSDK } from '@payloadcms/sdk'
22
import type { RedirectConfig } from 'astro'
33
import type { Config } from 'cms/src/payload-types'
44
import 'dotenv/config'
5+
import { addBypassHeader } from './sdk/bypassHeader'
56

67
/** Fetches the redirects from the CMS and converts them to the Astro `RedirectConfig` format. */
78
export async function getRedirects(): Promise<Record<string, RedirectConfig>> {
89
// Because import.meta.env and astro:env is not available in Astro config files and this method is called from
910
// the Astro config file, use process.env to access the environment variable instead.
11+
const bypassSecret = process.env.CMS_VERCEL_AUTOMATION_BYPASS_SECRET
12+
1013
const payloadSDK = new PayloadSDK<Config>({
1114
baseURL: process.env.CMS_URL! + '/api',
15+
fetch: (input, init) => fetch(input, addBypassHeader(init, bypassSecret)),
1216
})
1317

1418
const redirectsCms = await payloadSDK.find({

web/src/cms/sdk/bypassHeader.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Adds the Vercel automation bypass header to a request if the secret is configured.
3+
* This allows fetching from CMS preview deployments that have Vercel Authentication enabled.
4+
*/
5+
export function addBypassHeader(
6+
init: RequestInit | undefined,
7+
bypassSecret: string | undefined,
8+
): RequestInit {
9+
if (!bypassSecret) {
10+
return init ?? {}
11+
}
12+
13+
const headers = new Headers(init?.headers)
14+
headers.set('x-vercel-protection-bypass', bypassSecret)
15+
16+
return {
17+
...init,
18+
headers,
19+
}
20+
}

web/src/cms/sdk/cachedFetch.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { CMS_VERCEL_AUTOMATION_BYPASS_SECRET } from 'astro:env/server'
2+
import { addBypassHeader } from './bypassHeader'
13
import { cache } from './cache'
24

35
/**
@@ -20,6 +22,8 @@ export function createCachedFetch(baseFetch: typeof fetch): typeof fetch {
2022
input: RequestInfo | URL,
2123
init?: RequestInit,
2224
): Promise<Response> {
25+
// Add bypass header for Vercel Authentication
26+
const initWithBypass = addBypassHeader(init, CMS_VERCEL_AUTOMATION_BYPASS_SECRET)
2327
// Convert input to URL string for caching
2428
const url =
2529
typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
@@ -55,7 +59,7 @@ export function createCachedFetch(baseFetch: typeof fetch): typeof fetch {
5559
}
5660

5761
// Make the actual request
58-
const response = await baseFetch(input, init)
62+
const response = await baseFetch(input, initWithBypass)
5963

6064
if (response.ok) {
6165
// Clone the response so we can read it and still return it
@@ -72,6 +76,6 @@ export function createCachedFetch(baseFetch: typeof fetch): typeof fetch {
7276
}
7377

7478
// For non-GET requests or when cache is not explicitly enabled, just forward the request
75-
return baseFetch(input, init)
79+
return baseFetch(input, initWithBypass)
7680
}
7781
}

0 commit comments

Comments
 (0)