Skip to content

Promote: the gateway follows the build it serves - #28

Merged
bitcoinuniverseadmin merged 5 commits into
mainfrom
develop
Aug 30, 2026
Merged

bitcoinuniverseadmin merged 5 commits into
mainfrom
develop

Conversation

@bitcoinuniverseadmin

Copy link
Copy Markdown

Promotes #27 to main,
and with it the release that was rolled back at 03:43:35Z.

What happened

ac2bcfaf5 was cut over at 03:42:50Z and rolled back 45 seconds later. The
chain page smoke against the public origin caught the reason:

FAIL  page:dogecoin: 1 console error(s): Executing inline script violates the
following Content Security Policy directive 'script-src 'self'
'sha256-TLx68/...''. Either the 'unsafe-inline' keyword, a hash
('sha256-F5AsGcf/...'), or a nonce is required.

Two different hashes: the one the gateway allowed and the one the document it
was serving actually needed. The theme bootstrap was blocked on every page, and
that console error was the entire visible symptom. The routes answered, the
component identities matched the manifest, the visual matrix was green, and the
page rendered.

ROOT is a fixed path whose contents a release swaps underneath it, which is
why release.sha leaves the gateway running when only the frontend changed.
The policy was computed once at start-up and did not follow that swap. #27
makes it read the file per document, with a test that fails against the
previous behaviour.

Release

ac2bcfaf5 is still installed on the host and is not being reused. The
artifact is rebuilt from this merge commit, installed beside the running
release, preflighted, cut over with a continuous probe, and then verified
through the public origin with synthetic-check.mjs,
release-manifest.mjs verify and visual-qa/chain-page-smoke.mjs. That last
one is the check that caught this, and it is the one that has to pass before
the cutover stands.

mempool-1b12b26f3 is the rollback target and has been serving cleanly for
ninety minutes with zero restarts and zero errors.

🤖 Generated with Claude Code

It failed on develop and passed on the pull request, on identical trees.
Both readings were honest; the check was not.

Largest paint. The gate did not threshold it, on the stated grounds that
the number swings with the load, and then failed a route for reporting no
paint at all on the stated grounds that "that answer does not move with
the load". It does. On the busy runner the Dogecoin transaction route
fired its load event at ten and a half seconds, and the six seconds the
probe waited after that were not enough for a contentful paint to land,
so it read zero on a build that measured twelve seconds on a quiet
machine.

Two changes. The paint is now waited for, up to twenty seconds, instead
of read once and taken as final. And when it still has not arrived the
page is asked what is on it: a screenful of text with no paint entry is a
measurement problem and is reported as one, while a page with nothing on
it is the fault the check exists for. That distinction genuinely does not
move with the load, which is the property the old comment claimed and did
not have.

Layout shift. The header called it "nearly deterministic, a property of
the stylesheet rather than of the clock". It is less deterministic than
that: the same tree measured 0.042, 0.046 and 0.069 on the blocks route
across three machines, because a busier machine delivers content later
and a shift that lands after the first paint counts while the same shift
before it does not.

Load can only add shifts, never remove one, so the smallest of several
readings is the closest available estimate of the shift the stylesheet is
actually responsible for. A route over its ceiling is now measured again,
up to twice, and judged on the minimum. The table says when a route was
measured more than once, because a gate that quietly retries until it
passes is worse than one that fails.

Neither change lowers a threshold. The eager payload budget, the 0.1
layout budget and the recorded exception on the transaction route are all
unchanged.
…the-runner

A gate that fails on a busy machine is measuring the machine
A release changed the document's inline theme bootstrap and left gateway.mjs
byte identical. The cutover therefore did the right thing and left the gateway
running, and the running gateway went on allowing the previous build's script
hash while refusing the one it was itself serving:

  Executing inline script violates the following Content Security Policy
  directive 'script-src 'self' 'sha256-TLx68/+2SeR3+dZreFBX1fiMo91olqe4aVTfKFd48Ik=''

Every page loaded with the theme bootstrap blocked. The only sign was a console
error, which is why this is the second defect this week whose whole visible
symptom was something nobody was looking at.

The cause is a contradiction inside this file. The static root is a fixed path
whose contents a release swaps underneath it, and that is deliberate: it is
exactly why a frontend change needs no gateway restart, and the release script
says so in as many words. The policy was computed once at start-up and did not
follow that swap. One half of the file tracked the build behind the path and
the other half remembered the build that was there when the process began.

It follows the file now, keyed on size and modification time, so a document
costs one stat and a hash is computed only when the file actually changes.

The test writes two builds behind the same path and asserts the policy names
the second and not the first, including the case where both land in the same
second, which is a window a release fits inside. Against the previous
start-up-pinned version it fails.
The first version of this fix cached the policy against the document's size and
modification time. That key is wrong in a way that would have been very hard to
find afterwards: two builds of the same length restored to the same instant
share it, which is exactly what a tar extraction preserving mtimes can produce,
and the resulting failure is a blocked script and nothing else.

The test asserted that case and passed for the wrong reason. `utimesSync` takes
a Date, so it wrote a millisecond-truncated time while `mtimeMs` carries finer
precision, and the keys differed by an accident of rounding rather than by
anything the code intended.

The document is 3.4 kilobytes and is the same file the response is about to
serve. The saving was never worth the class of bug it kept open, so it is read
each time.

The test now covers both dimensions on purpose: a build that changes the length
and the time, and a build of exactly the same length with the modification time
put back. Neither may be load bearing on its own. Against a start-up-pinned
policy it still fails.
…ows-the-build

The gateway refused the script it was serving
@bitcoinuniverseadmin
bitcoinuniverseadmin merged commit e21a26d into main Aug 30, 2026
17 checks passed
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