From 4e156c4c15a4f8d51253a00383912c53e2621667 Mon Sep 17 00:00:00 2001 From: jon Date: Sat, 18 Apr 2026 15:01:06 -0700 Subject: [PATCH] Harden Vercel production deploy path --- .github/workflows/vercel-web-deploy.yml | 34 ++++++++++++++++++- .../tests/vercel-web-deploy-workflow.test.mjs | 13 +++++++ web/vercel.json | 6 ++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/.github/workflows/vercel-web-deploy.yml b/.github/workflows/vercel-web-deploy.yml index eaa38eb8..28b256b9 100644 --- a/.github/workflows/vercel-web-deploy.yml +++ b/.github/workflows/vercel-web-deploy.yml @@ -49,5 +49,37 @@ jobs: run: npx vercel@latest build --prod --token=$VERCEL_TOKEN - name: Deploy web to Vercel production + id: deploy working-directory: web - run: npx vercel@latest deploy --prebuilt --prod --token=$VERCEL_TOKEN + run: | + deployment_url="$(npx vercel@latest deploy --prebuilt --prod --token=$VERCEL_TOKEN)" + echo "deployment_url=${deployment_url}" >> "$GITHUB_OUTPUT" + + - name: Smoke-check production deployment + env: + DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment_url }} + PRODUCTION_URL: https://blockdata.run + run: | + node --input-type=module - <<'EOF' + const targets = [ + ['deployment', process.env.DEPLOYMENT_URL?.trim()], + ['production', process.env.PRODUCTION_URL?.trim()], + ]; + + for (const [label, url] of targets) { + if (!url) { + throw new Error(`Missing ${label} url for smoke verification`); + } + + const response = await fetch(url, { redirect: 'follow' }); + const html = await response.text(); + + if (!response.ok) { + throw new Error(`${label} smoke check failed with ${response.status} for ${url}`); + } + + if (!html.includes('BlockData')) { + throw new Error(`${label} smoke check did not find the BlockData HTML title for ${url}`); + } + } + EOF diff --git a/scripts/tests/vercel-web-deploy-workflow.test.mjs b/scripts/tests/vercel-web-deploy-workflow.test.mjs index 2b255e39..56f59ba0 100644 --- a/scripts/tests/vercel-web-deploy-workflow.test.mjs +++ b/scripts/tests/vercel-web-deploy-workflow.test.mjs @@ -42,4 +42,17 @@ test('vercel web deploy workflow is repo-owned and production-scoped', () => { assert.match(workflow, /vercel@latest pull --yes --environment=production/, 'workflow must pull production config'); assert.match(workflow, /vercel@latest build --prod/, 'workflow must build through Vercel CLI'); assert.match(workflow, /vercel@latest deploy --prebuilt --prod/, 'workflow must deploy the prebuilt output'); + assert.match(workflow, /id:\s*deploy/, 'workflow must capture the production deploy step output'); + assert.match(workflow, /PRODUCTION_URL:\s*https:\/\/blockdata\.run/, 'workflow must smoke-check the official production domain'); + assert.match(workflow, /BlockData/, 'workflow smoke verification must assert the production shell marker'); +}); + +test('web vercel config disables direct master git deployments', () => { + const config = JSON.parse(read('web/vercel.json')); + + assert.equal( + config.git?.deploymentEnabled?.master, + false, + 'web/vercel.json must disable direct Vercel Git deployments for master so production flows through the repo-owned workflow', + ); }); diff --git a/web/vercel.json b/web/vercel.json index 8b166e22..a55eb51f 100644 --- a/web/vercel.json +++ b/web/vercel.json @@ -1,4 +1,10 @@ { + "$schema": "https://openapi.vercel.sh/vercel.json", + "git": { + "deploymentEnabled": { + "master": false + } + }, "outputDirectory": "dist", "rewrites": [ { "source": "/docs/:path*", "destination": "https://blockdata-ct.vercel.app/docs/:path*" }