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*" }