diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5346c2c7..b87d4a60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -253,6 +253,17 @@ jobs: --changed-file artifacts/dist-purge-targets.txt \ --base-url "$CLOUDFLARE_PURGE_BASE_URL" + - name: Verify deployed index.html version markup + env: + CLOUDFLARE_PURGE_BASE_URL: ${{ secrets.CLOUDFLARE_PURGE_BASE_URL }} + run: | + if [ -z "$CLOUDFLARE_PURGE_BASE_URL" ]; then + echo "CLOUDFLARE_PURGE_BASE_URL is required to verify deployed index.html" + exit 1 + fi + + DEPLOY_INDEX_URL="${CLOUDFLARE_PURGE_BASE_URL%/}/index.html" npm run verify:deployed:index-html + unit-tests: runs-on: ubuntu-latest timeout-minutes: 30 diff --git a/package.json b/package.json index 91c55b2a..0889d107 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "hash:dist": "node scripts/hash-dist.mjs --dist dist --output artifacts/dist-hashes-current.json", "diff:dist-hashes": "node scripts/diff-dist-hashes.mjs", "verify:dist:deploy-artifact": "node scripts/verify-dist-deploy-artifact.mjs", + "verify:deployed:index-html": "node scripts/verify-deployed-index-html.mjs", "purge:cloudflare:changed": "node scripts/purge-cloudflare-changed.mjs", "smoke:dist-assets": "node scripts/check-asset-references.mjs", "test:e2e:cache-upgrade": "npx playwright test tests/e2e/cache-upgrade-smoke.spec.ts --project=e2e" diff --git a/scripts/verify-deployed-index-html.mjs b/scripts/verify-deployed-index-html.mjs new file mode 100644 index 00000000..36bcf5b2 --- /dev/null +++ b/scripts/verify-deployed-index-html.mjs @@ -0,0 +1,59 @@ +#!/usr/bin/env node + +const VERSION_MARKUP_PATTERN = /seed\.\s*zap\.\s*subscribe\.\s*<\/h2>\s*]*>\s*v:\s*[a-f0-9]{8}\s*•\s*\d{4}-\d{2}-\d{2}\s*<\/div>/i; + +function parseArgs(argv) { + const args = { + url: process.env.DEPLOY_INDEX_URL ?? '' + }; + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + + if (arg === '--url') { + args.url = argv[i + 1] ?? args.url; + i += 1; + continue; + } + + if (arg === '--help' || arg === '-h') { + console.log('Usage: node scripts/verify-deployed-index-html.mjs --url '); + process.exit(0); + } + + throw new Error(`Unknown argument: ${arg}`); + } + + if (!args.url) { + throw new Error('Missing deployed index URL. Pass --url or set DEPLOY_INDEX_URL.'); + } + + return args; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + const response = await fetch(args.url, { + headers: { + Accept: 'text/html' + } + }); + + if (!response.ok) { + throw new Error(`Unable to fetch deployed index.html (${args.url}): HTTP ${response.status}`); + } + + const html = await response.text(); + if (!VERSION_MARKUP_PATTERN.test(html)) { + throw new Error( + `Deployed index.html is missing version markup under the slogan at ${args.url} (expected: v: <8-char-hash> • ).` + ); + } + + console.log(`Deployed index verification passed: ${args.url} includes slogan version markup.`); +} + +main().catch((error) => { + console.error(error.message); + process.exit(1); +});