Skip to content

feat(retention): retain content-hashed JS/CSS bundles across deploys (Workstream A) - #1658

Merged
jung-thomas merged 13 commits into
mainfrom
worktree-asset-hash-retention
Aug 11, 2026
Merged

feat(retention): retain content-hashed JS/CSS bundles across deploys (Workstream A)#1658
jung-thomas merged 13 commits into
mainfrom
worktree-asset-hash-retention

Conversation

@jung-thomas

Copy link
Copy Markdown
Contributor

Asset-hash retention (Workstream A of the 2026-08-11 durable-fix spec)

Implements the ship-first workstream from the merged design (#1645): retain prior content-hashed JS/CSS bundles across deploys so edge-cached HTML never 404s on a bundle hash a newer deploy deleted. This closes the stale-HTML → deleted-hash class — including the live JS-island staleness (validation.js, navigator.js, …) surfaced during the 2026-08-10/11 incident.

Mechanism

  • scripts/lib/asset-retention.cjs — pure mergeRetention({currentFiles, retainedManifest, nowMs, windowMs}): unions the current build's hashed bundles with in-window prior ones; prunes expired; never drops a hash still in the current build.
  • scripts/retain-asset-bundles.cjs — runs after build:hugo on hugo/public/{js,css}; fetches the live approuter's served /_retained-assets.json, downloads in-window carried bundles, re-emits hugo/public/_retained-assets.json. Fail-open on every network path.
  • Detects both hash formats: Vite <name>-<base62≥8>.{js,css} (dash) and Hugo-fingerprinted <name>.<hex≥32>.css (dot); ignores committed unhashed files.
  • Wired into build:all (local, after Hugo) and root mta.yaml before-all (CI, after the Hugo build); APPROUTER_URL exposed to the CI mbt build step.
  • 48h window (RETENTION_WINDOW_HOURS, overridable). Content-hashed filenames are immutable, so unioning is always safe.

Tests / verification

  • Unit: test/unit/asset-retention.test.js (4) + test/unit/retain-asset-bundles.test.js (7) — 11/11 pass. Full npm test green (only pre-existing load-sensitive check-srv-qa flakiness, unrelated).
  • Post-deploy smoke: test/smoke/asset-retention.smoke.test.js asserts every bundle in /_retained-assets.json serves 200; self-skips without SMOKE_BASE_URL.
  • Built subagent-driven with per-task review + a whole-branch opus review (clean — "ship it").

Notes

Plan: docs/superpowers/plans/2026-08-11-asset-hash-retention.md. Squash-merge recommended (history includes intermediate fix commits).

Adds test/smoke/asset-retention.smoke.test.js — post-deploy observability
check that every bundle listed in the live /_retained-assets.json responds
200. Self-skips when SMOKE_BASE_URL is unset (local / unit runs), exactly
like browse.smoke.test.js and other smoke-tier siblings. Uses fetchWithRetry
for consistent retry behavior on transient 502/503s.
…ention smoke

- Pass { redirect: 'follow' } to all three fetchWithRetry calls (manifest
  GET ×2, bundle HEAD) so an unexpected approuter rewrite doesn't surface
  as a false 3xx failure. fetchWithRetry defaults to redirect:'manual' per
  smoke.config.js contract; override is explicit here.
- Extract parseManifest() helper that reads the body as text first, then
  JSON.parse()s it inside a try/catch — on a 200 non-JSON body it calls
  expect.fail() with the first 120 chars so the failure is diagnostic
  rather than an uncaught SyntaxError.
Adds 'Asset-hash retention (#1604 follow-up)' subsection to
docs/developers/architecture/build.md describing what
hugo/static/_retained-assets.json is, the 48-hour retention window,
fail-open carry-forward from the live approuter, and why unioning
content-hashed bundles is safe.
…section

The retain-asset-bundles.cjs script operates on hugo/public/{js,css} and
writes hugo/public/_retained-assets.json (after build:hugo; the approuter
builder copies hugo/public → approuter/static so the manifest is served at
/_retained-assets.json). The original subsection incorrectly said hugo/static.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant