diff --git a/CLAUDE.md b/CLAUDE.md index 241031de..00dc61f8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -112,6 +112,8 @@ Subsystem one-liners: - **Fresh worktree setup needs `npm run setup` after `npm install`** — global npmrc has `ignore-scripts=true`. Without it, `hugo-apps/node_modules` won't be populated and `better-sqlite3`'s native binding won't build. Symptoms: hugo-apps tests fail resolving `@mediapipe/tasks-vision`, `npm test` hangs. +- **`ignore-scripts=true` silences `postbuild:apps` — build artifacts wired into it are NOT produced by local `npm run build:all`** — the global npmrc `ignore-scripts=true` (see above) means npm **lifecycle hooks never fire**. The `postbuild:apps` hook is where the #1604 island-fingerprint step (`build:island-manifest`, which writes `hugo/data/island_manifest.json`) and 8 static guards live. During a local `build:all`, none of them run. Symptom class: fresh JS/CSS **compiles** (Vite emits `navigator-.js`) but is **never referenced** — `hugo/layouts/partials/island-src.html` falls back to the unhashed `/js/.js`, Hugo bakes the stale path, and the approuter ships old bundles sitting next to the new ones. **Merged fixes look "not deployed" even though the deploy succeeded.** CI dodges this because `deploy.yml`/`unit-tests.yml` run `npm run postbuild:apps` as an **explicit step** (see deploy.yml:217-223 comment). Fix (2026-08-10): `build:all` now calls `npm run build:island-manifest` **explicitly** (not via the hook), and `scripts/deploy-mta.cjs` Step 2.5 fails the deploy if `hugo/public/index.html` bakes only unhashed island paths while a Vite manifest exists. Rule: any build **artifact** (not just a guard) needed for a correct ship must be an explicit step in `build:all`, never left to a `post*`/`pre*` lifecycle hook. + - **`hugo/content/tutorials/` is entirely generated** — Never edit; overwritten by `npm run fetch-tutorials`. Edit `scripts/parsers/` or source tutorials in `sap-tutorials` org. - **Never run `publish-content` from a workstation** — Use `gh workflow run rebuild-content.yml`. Workstation publishes skip CI validation; the server-side no-revert guard catches the worst stale-cache regressions but not everything. diff --git a/package.json b/package.json index 49a45836..96439303 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "build:display": "cd app/display-app && npm install && npm run build", "copy-joule-vendor": "node scripts/copy-joule-vendor.mjs", "check-deploy-cap-target": "node scripts/check-deploy-cap-target.cjs", - "build:all": "npm run prebuild && npm run fetch-tutorials -- --regenerate && npm run fetch-advocates && npm run fetch-homepage-shelves && npm run fetch-verb-definitions && npm run fetch-shelf-definitions && npm run fetch-featured-topics && npm run fetch-topic-clusters && npm run fetch-topics-gallery && npm run build:css && npm run build:apps && npm run build:analytics-explorer && npm run copy-joule-vendor && npm run build:explore && npm run build:hugo && npm run build:highlight && npm run build:display && npm run build:sdl", + "build:all": "npm run prebuild && npm run fetch-tutorials -- --regenerate && npm run fetch-advocates && npm run fetch-homepage-shelves && npm run fetch-verb-definitions && npm run fetch-shelf-definitions && npm run fetch-featured-topics && npm run fetch-topic-clusters && npm run fetch-topics-gallery && npm run build:css && npm run build:apps && npm run build:island-manifest && npm run build:analytics-explorer && npm run copy-joule-vendor && npm run build:explore && npm run build:hugo && npm run build:highlight && npm run build:display && npm run build:sdl", "build:deploy": "npm run check-deploy-cap-target && npm run build:all", "deploy": "node scripts/deploy-mta.cjs", "build:admin": "npm --prefix app/admin-shell run build", diff --git a/scripts/deploy-mta.cjs b/scripts/deploy-mta.cjs index 7dee62c1..97966b35 100644 --- a/scripts/deploy-mta.cjs +++ b/scripts/deploy-mta.cjs @@ -374,6 +374,37 @@ async function main() { ok('build:deploy complete (explore bundle + manifest included via build:all)'); } + // ---- Step 2.5: verify Hugo baked FINGERPRINTED island bundles --------- + // WHY (2026-08-10): the #1604 island-fingerprint pipeline builds + // hugo/data/island_manifest.json in `build:island-manifest`. That step was + // originally ONLY wired into the `postbuild:apps` npm lifecycle hook — but + // this repo runs with `ignore-scripts=true` globally, so the hook is SILENT + // during a local `npm run build:all`. Result: the manifest was never + // written, hugo/layouts/partials/island-src.html fell back to the UNHASHED + // /js/.js path, and the approuter shipped a stale bundle while the + // fresh (fingerprinted) one sat unreferenced next to it. The compiled fixes + // were present but never served. build:all now calls build:island-manifest + // explicitly; this guard is the belt-and-suspenders that fails the deploy if + // that ever regresses again (renamed step, reordered pipeline, etc.). + if (!args.dryRun && !args.skipBuild) { + const viteManifest = path.join(ROOT, 'hugo', 'static', 'js', '.vite', 'manifest.json'); + const homepage = path.join(ROOT, 'hugo', 'public', 'index.html'); + if (fs.existsSync(viteManifest) && fs.existsSync(homepage)) { + const html = fs.readFileSync(homepage, 'utf8'); + // A correctly fingerprinted homepage references at least one hashed + // island bundle: /js/-<8+hexish>.js. If EVERY island script tag is + // the bare unhashed fallback, the manifest never took effect. + const hashed = /\/js\/[a-zA-Z0-9-]+-[A-Za-z0-9_-]{8,}\.js/.test(html); + if (!hashed) { + die(1, 'Hugo baked only UNHASHED island bundle paths despite a Vite manifest existing.\n' + + ' hugo/data/island_manifest.json was likely not built (ignore-scripts=true\n' + + ' silences the postbuild:apps hook). Fresh JS fixes will NOT be served.\n' + + ' Fix: ensure `npm run build:island-manifest` runs inside build:all, then rebuild.'); + } + ok('island bundles fingerprinted in hugo/public (manifest took effect)'); + } + } + // ---- Step 3: mbt build (+ fresh-mtar verify) ------------------------- step(3, 'Package MTA (mbt build)'); if (args.skipBuild) {