From 6fce2100445be361ffb7df2accc8035e22986c6f Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:52:05 -0500 Subject: [PATCH] [ci] verify dist deploy artifact and force HTML cache purge --- .github/workflows/ci.yml | 3 ++ docs/deployment-notes.md | 1 + netlify.toml | 2 +- package.json | 1 + scripts/plan-cache-purge.mjs | 5 ++- scripts/purge-cloudflare-changed.mjs | 8 +++- scripts/verify-dist-deploy-artifact.mjs | 50 +++++++++++++++++++++++++ 7 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 scripts/verify-dist-deploy-artifact.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77f31e56..5346c2c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,6 +58,9 @@ jobs: - name: Build assets run: npm run build + - name: Verify deploy artifact markup and hashed references + run: npm run verify:dist:deploy-artifact + - name: Smoke check hashed dist asset references run: npm run smoke:dist-assets diff --git a/docs/deployment-notes.md b/docs/deployment-notes.md index affeda90..5a1dc95b 100644 --- a/docs/deployment-notes.md +++ b/docs/deployment-notes.md @@ -10,4 +10,5 @@ - Static CSS/JS asset freshness is now driven by build-time manifest rewriting (`dist/asset-manifest.json`) instead of runtime `ASSET_VERSION` query-string mutation. - Keep source HTML entry points (`index.html`, `embed.html`) pointing at logical asset paths (for example, `js/index.js` and `css/tailwind.generated.css`). The build pipeline rewrites those references to content-hashed filenames in `dist/`. +- Deploy pipelines should always purge HTML entrypoints (`/` + `/index.html` and `/embed.html`) at the edge after publish so clients do not keep serving stale source shells. - App routing query parameters (such as `?v=` for video pointers) are unchanged and remain part of runtime URL behavior. diff --git a/netlify.toml b/netlify.toml index 4302d16e..ddfd011d 100644 --- a/netlify.toml +++ b/netlify.toml @@ -3,7 +3,7 @@ publish = "dist" [build.environment] - NODE_ENV = "development" + NODE_ENV = "production" [[headers]] for = "/*" diff --git a/package.json b/package.json index 3160b35c..91c55b2a 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "lint:sw-compat": "node scripts/check-sw-compat.mjs", "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", "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/plan-cache-purge.mjs b/scripts/plan-cache-purge.mjs index 341e34f7..d369998f 100644 --- a/scripts/plan-cache-purge.mjs +++ b/scripts/plan-cache-purge.mjs @@ -2,6 +2,7 @@ import fs from 'node:fs'; const ALWAYS_PUBLISH_PATHS = new Set(['index.html', 'embed.html', 'sw.min.js', 'site.webmanifest']); +const ALWAYS_PURGE_HTML_PATHS = ['index.html', 'embed.html']; const NON_ASSET_CHANGED_PATHS = new Set(['asset-manifest.json', '_headers', '_redirects']); function parseArgs(argv) { @@ -82,8 +83,8 @@ function main() { const hashedModeFullyActive = unhashedAssetPaths.length === 0; const purgePaths = hashedModeFullyActive - ? changedPaths.filter(isHtmlOrServiceWorkerPath) - : changedPaths; + ? [...changedPaths.filter(isHtmlOrServiceWorkerPath), ...ALWAYS_PURGE_HTML_PATHS] + : [...changedPaths, ...ALWAYS_PURGE_HTML_PATHS]; const uniquePurgePaths = uniqueSorted(purgePaths); diff --git a/scripts/purge-cloudflare-changed.mjs b/scripts/purge-cloudflare-changed.mjs index 0884bc3a..00d37728 100644 --- a/scripts/purge-cloudflare-changed.mjs +++ b/scripts/purge-cloudflare-changed.mjs @@ -66,7 +66,13 @@ function readChangedPaths(args) { output.push(...args.changedPaths); - return [...new Set(output.map((value) => value.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b)); + const normalized = output.map((value) => value.trim()).filter(Boolean); + + if (normalized.includes('index.html')) { + normalized.push('/'); + } + + return [...new Set(normalized)].sort((a, b) => a.localeCompare(b)); } function toAbsoluteUrl(baseUrl, relativePath) { diff --git a/scripts/verify-dist-deploy-artifact.mjs b/scripts/verify-dist-deploy-artifact.mjs new file mode 100644 index 00000000..fb1e3c39 --- /dev/null +++ b/scripts/verify-dist-deploy-artifact.mjs @@ -0,0 +1,50 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; + +const DIST_DIR = 'dist'; +const INDEX_PATH = path.join(DIST_DIR, 'index.html'); +const MANIFEST_PATH = path.join(DIST_DIR, 'asset-manifest.json'); +const VERSION_MARKUP_PATTERN = /seed\.\s*zap\.\s*subscribe\.\s*<\/h2>\s*