diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d41c06f32..a0f3e1320 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1009,9 +1009,17 @@ jobs: npx vercel deploy --prebuilt --prod --yes --token=${{ secrets.VERCEL_TOKEN }} - name: Verify Website preview runtime embedding policy if: steps.freshness.outputs.stale != 'true' && steps.affected.outputs.website == 'true' - run: npx playwright test apps/website/e2e/platform-production-smoke.spec.ts --config apps/website/playwright.config.ts --grep "unified runtime embedding policy" --reporter=list + run: | + # Without the bypass the preview answers 302 -> vercel.com/sso-api and + # this check times out on an SSO page instead of the app. Say so. + if [ -z "${VERCEL_AUTOMATION_BYPASS_SECRET}" ]; then + echo "::error::VERCEL_AUTOMATION_BYPASS_SECRET is unset — the protected Website preview cannot be verified. Enable 'Protection Bypass for Automation' on the Vercel website project and store the value as this repository secret." + exit 1 + fi + npx playwright test apps/website/e2e/platform-production-smoke.spec.ts --config apps/website/playwright.config.ts --grep "unified runtime embedding policy" --reporter=list env: PRODUCTION_SMOKE: 'true' + VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} BASE_URL: ${{ steps.deploy_website.outputs.preview_origin }} WEBSITE_URL: ${{ steps.deploy_website.outputs.preview_origin }} EXAMPLES_URL: https://examples.threadplane.ai diff --git a/apps/website/playwright.config.ts b/apps/website/playwright.config.ts index 2aa28d9a9..56b8273ee 100644 --- a/apps/website/playwright.config.ts +++ b/apps/website/playwright.config.ts @@ -37,6 +37,20 @@ export const createWebsitePlaywrightConfig = ( retries: environment['CI'] ? 2 : 0, use: { baseURL, + // Vercel deployment protection answers 302 -> vercel.com/sso-api for every + // path on a preview, so a browser-driven check lands on an SSO page and + // times out. When CI supplies the project's automation bypass, send it so + // the preview is reachable. Every URL this suite touches is a first-party + // Threadplane origin. Unset locally and in production runs. + ...(environment['VERCEL_AUTOMATION_BYPASS_SECRET'] + ? { + extraHTTPHeaders: { + 'x-vercel-protection-bypass': + environment['VERCEL_AUTOMATION_BYPASS_SECRET'], + 'x-vercel-set-bypass-cookie': 'true', + }, + } + : {}), // Custom-target coverage carries an obvious fixture key. Keep browser // artifacts disabled so request headers and page state are never retained. trace: 'off', diff --git a/apps/website/src/playwright-config.spec.ts b/apps/website/src/playwright-config.spec.ts index 67b0f71f8..76b1b06a0 100644 --- a/apps/website/src/playwright-config.spec.ts +++ b/apps/website/src/playwright-config.spec.ts @@ -4,6 +4,37 @@ import { resolve } from 'node:path'; import { createWebsitePlaywrightConfig } from '../playwright.config'; describe('Website Playwright configuration', () => { + it('sends the Vercel automation bypass only when CI supplies it', () => { + const withoutSecret = createWebsitePlaywrightConfig({}); + expect(withoutSecret.use?.extraHTTPHeaders).toBeUndefined(); + + const withSecret = createWebsitePlaywrightConfig({ + VERCEL_AUTOMATION_BYPASS_SECRET: 'sentinel-value', + }); + expect(withSecret.use?.extraHTTPHeaders).toEqual({ + 'x-vercel-protection-bypass': 'sentinel-value', + 'x-vercel-set-bypass-cookie': 'true', + }); + }); + + it('keeps the production-smoke spec loadable under Playwright CJS transpilation', () => { + const smoke = readFileSync( + resolve(__dirname, '../e2e/platform-production-smoke.spec.ts'), + 'utf8' + ); + + // Playwright transpiles specs to CJS, so the ESM-only meta object compiles + // to a `require` the loaded module cannot resolve and the file silently + // fails to collect — the job then reports "No tests found" rather than + // failing. Strip comments first: the spec names the trap in prose so it is + // not reintroduced, and that mention must not trip this guard. + const code = smoke + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/(^|[^:])\/\/.*$/gm, '$1'); + + expect(code).not.toContain('import.meta'); + }); + it('derives the production embedding assertion from the authoritative origin source', () => { const smoke = readFileSync( resolve(__dirname, '../e2e/platform-production-smoke.spec.ts'),