A version number is pushed once, and every build addresses its CSS differently
gates / gates (push) Failing after 44s
gates / gates (push) Failing after 44s
MEASURED FIRST (audits/app-page-2026-10-15b/P0/VERDICT.md). The tab bar rendered unstyled through
the public address for one reason, and it was not the box: exactly one guest runs 0.315.0 and it
runs the registry's current digest (8887343011d6, identical to the registry's). The floor is still
0.314.0, so nothing else could have taken it.
What it was, in four measured facts:
1. the stylesheet URL is /static/style.css?v={{.Version}} -> ?v=0.315.0 for all THREE different
0.315.0 builds (e66d466, fe0228c, db52f5a), the first of which had no .app-tabs rules at all;
2. the origin sends NO validator at all (ETag present = 0, Last-Modified present = 0), so a
cached copy cannot be revalidated - it can only expire;
3. Cloudflare caches that URL (cf-cache-status HIT, age climbing) and rewrites the browser TTL
from the origin's 3600 to 14400 - four hours;
4. the session's first request returned cf-cache-status EXPIRED, which only happens when the edge
holds an entry that has aged out.
Origin, edge and box now agree byte-for-byte. The stale object itself is gone - that first probe
forced its revalidation - and the REPORT says so rather than claiming a HIT nobody can reproduce.
THE FIX IS IN TWO PARTS, NEITHER OF WHICH IS PURGING A CACHE:
build.sh refuses to push a version that already exists, before building, for --push and
--multiarch. A registry that cannot be asked is a refusal too: proving the tag is new is the point,
and "I could not ask" is not that proof. Local builds are untouched - reusing a version while
iterating is normal. scripts/test_build_push_once.sh drives the real build.sh with a fake docker on
PATH over six cases; red-proved twice (check deleted -> a present tag builds and pushes; an
unreachable registry treated as absent -> two cases fail).
assetTag() now hashes version + GitCommit + BuildTime, and EVERY shell's stylesheet link uses it
(seven of the nine asked for ?v={{.Version}}; only the two stranger-facing share pages already had
it). It stays opaque and 8 hex, because the share pages are reached by a capability URL and naming
the build to whoever holds one is a gift to somebody hunting a known fault. Cache-Control is
unchanged: the new address is the fix, not a shorter cache.
The parity harness normalises the asset tag on BOTH sides of the comparison, as it already does for
the per-session CSRF token, so all 148 fixtures stay byte-identical on disk and stop depending on
which build rendered them. The same normalisation is applied at the second comparison site in
i18n_wiring_test.go, which had the same assumption.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
This commit is contained in:
@@ -54,7 +54,9 @@ type Server struct {
|
||||
updater *selfupdate.Updater
|
||||
logger *log.Logger
|
||||
version string
|
||||
encKey []byte // AES-256 key for decrypting app.yaml values
|
||||
// buildID distinguishes two builds of the SAME version — see SetBuildIdentity and assetTag.
|
||||
buildID string
|
||||
encKey []byte // AES-256 key for decrypting app.yaml values
|
||||
// gate / gateClock (v0.280.0, decision 46): the setup gate's key + used tokens; the clock is a test seam.
|
||||
gate gateState
|
||||
fam familyState // v0.287.0: the family gate (family_gate.go)
|
||||
@@ -473,10 +475,10 @@ func (s *Server) hasSession(r *http.Request) bool {
|
||||
// predecessor got wrong twice. Idempotent: an existing Version is left alone, so a fixture case that
|
||||
// pins its own version keeps it.
|
||||
//
|
||||
// `AssetTag` is the buster for a page a STRANGER may open — the two guest share pages, reached by a
|
||||
// capability URL. `TestShareGuest_HeadersTilesNoAdminChrome` refuses the version string on those for a
|
||||
// reason worth keeping: telling anyone holding the link exactly which build is running is a gift to
|
||||
// somebody looking for a known fault. An opaque per-build tag busts the cache just as well.
|
||||
// `AssetTag` is the buster EVERY shell's stylesheet link uses (v0.316.0). It was only on the two
|
||||
// stranger-facing guest share pages; the rest asked for `?v={{.Version}}`, which does not change
|
||||
// between two builds of one version — the defect Part 0 measured. `Version` stays in the data for
|
||||
// the pages that display it.
|
||||
func (s *Server) addShellAssetData(data map[string]interface{}) {
|
||||
if data == nil {
|
||||
return
|
||||
@@ -487,11 +489,34 @@ func (s *Server) addShellAssetData(data map[string]interface{}) {
|
||||
data["AssetTag"] = s.assetTag()
|
||||
}
|
||||
|
||||
// assetTag is an opaque per-build cache-buster: it changes when the version changes and discloses
|
||||
// nothing about it. Used by the pages a stranger can open; everywhere else the version itself is
|
||||
// already on the page and the tag would only be indirection.
|
||||
// SetBuildIdentity records which BUILD this binary is, as opposed to which version it calls itself.
|
||||
// Init-only, called once from main before the HTTP server starts (the `SetStackProvider` contract).
|
||||
//
|
||||
// It exists because a version number is not a build. v0.315.0 was built and pushed three times from
|
||||
// three different commits; every one of them asked the browser for `/static/style.css?v=0.315.0`,
|
||||
// so a cache that already held the first build's stylesheet kept serving it — measured through
|
||||
// Cloudflare (`audits/app-page-2026-10-15b/P0/VERDICT.md`). build.sh now refuses a second push of a
|
||||
// version, and this makes the asset URL change even when the version does not.
|
||||
func (s *Server) SetBuildIdentity(gitCommit, buildTime string) {
|
||||
s.buildID = gitCommit + "/" + buildTime
|
||||
}
|
||||
|
||||
// assetTag is an opaque per-BUILD cache-buster: it changes whenever the binary changes, and
|
||||
// discloses neither the version nor the commit.
|
||||
//
|
||||
// Two jobs in one value, and both need it opaque:
|
||||
// - every shell's stylesheet link uses it, so no cache at any layer (browser, Cloudflare, or
|
||||
// anything between) can serve a previous build's CSS under an address the new build still asks
|
||||
// for. The origin sends no ETag and no Last-Modified, so a stale copy cannot be revalidated —
|
||||
// it can only expire, or be addressed differently. This is the "addressed differently".
|
||||
// - the pages a STRANGER can open use it too. `TestShareGuest_HeadersTilesNoAdminChrome` refuses
|
||||
// the version string there for a reason worth keeping: telling anyone holding a capability URL
|
||||
// exactly which build is running is a gift to somebody looking for a known fault.
|
||||
//
|
||||
// Deterministic for a given binary, so the i18n fixtures stay stable within a build; the parity
|
||||
// harness normalises it anyway, because a fixture pins COPY, not which build rendered it.
|
||||
func (s *Server) assetTag() string {
|
||||
sum := sha256.Sum256([]byte("felhom-asset-tag-v1:" + s.version))
|
||||
sum := sha256.Sum256([]byte("felhom-asset-tag-v1:" + s.version + ":" + s.buildID))
|
||||
return hex.EncodeToString(sum[:4])
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user