v0.7.7 — two releases shipped to a browser that never received them

buildStamp()'s no-git fallback was the literal "nogit", and the .s9pk
Dockerfile copies the tree in without .git — so git rev-parse fails on
every packaged build. That string is also the cache-bust key every
module URL carries, so v0.7.4, v0.7.5 and v0.7.6 all published
./web/main.js?v=nogit, byte-identical, and returning browsers refetched
nothing. v0.7.5's setup screen and v0.7.6's door fix were both correct
and neither arrived.

The fallback is now the package version plus the build timestamp, always
distinct. And serveStatic sent no Cache-Control at all, which is the
other half — a cached play.html pins a player to the whole build it
names. A request carrying ?v= is now immutable for a year; everything
else is no-cache. ?v= rather than "not HTML" because build-web.ts tags
the modules and nothing else.

Neither half is sufficient alone.

864 tests pass, two new.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX
This commit is contained in:
Jesse.Markowitz
2026-08-29 22:23:09 -04:00
co-authored by Claude Sonnet 5
parent b4f09f05cb
commit af68aac78d
6 changed files with 129 additions and 10 deletions
+30
View File
@@ -2628,6 +2628,36 @@ describe('the static build', () => {
assert.match(splash, /href="\.\/replays\.html"/, 'the splash does not link to the replays');
});
it('cache-busts with a tag that varies per build even where there is no git', () => {
/**
* THE BUG THIS PINS COST TWO RELEASES. `buildStamp`'s no-git fallback was the literal `nogit`,
* and the `.s9pk` Dockerfile copies the working tree in WITHOUT `.git` — so every packaged
* release published `?v=nogit`, byte-identical to the one before it, and a returning player's
* browser refetched nothing. v0.7.5's setup screen and v0.7.6's fix to it both installed
* correctly on `phoenix.local` and neither reached the browser that asked for them.
*
* Asserted against the SCRIPT rather than a built page, because the property is about what the
* fallback does when `git rev-parse` fails, which a normal build here never exercises.
*/
const src = readFileSync(join(root, 'scripts/build-web.ts'), 'utf8');
const fallback = /let git = ([^;]+);/.exec(src)?.[1] ?? '';
assert.ok(fallback !== '', 'the no-git fallback moved and this test cannot see it any more');
assert.doesNotMatch(fallback, /^'nogit'$|^"nogit"$/, 'the no-git fallback is a constant again');
assert.match(fallback, /Date\.now\(\)/, 'the no-git fallback carries nothing that varies per build');
});
it('lets a build-tagged URL be cached and nothing else', () => {
// The other half of the same bug: the pages carry the `?v=` tags but cannot be versioned in
// their own URL, so a cached `play.html` pins a player to the whole build it names. Only a
// request that actually carries `?v=` may be stored — an untagged image or the replay manifest
// has no way to announce a change.
const src = readFileSync(join(root, 'src/server/http.ts'), 'utf8');
assert.match(src, /'Cache-Control':\s*buildTagged\s*\?/, 'static responses no longer vary their caching');
assert.match(src, /immutable/, 'a tagged asset is not allowed to be cached at all');
assert.match(src, /serveStatic\(opts\.distDir, url\.pathname, res, url\.searchParams\.has\('v'\)\)/,
'the ?v= tag is not reaching serveStatic, so every response falls back to no-cache');
});
it('opens the multiplayer door from the splash, straight into the lobby', () => {
// This door sat `disabled` and labelled "Coming soon" from before the server existed until
// v0.5.2 — Phases 2-4 built a working lobby and nothing ever linked to it, so a player with a