fix(deploy): make a deployment match the manifest it publishes - #47
Merged
Conversation
…nifest
`ARG VITE_MEDIA_URL=` defined the variable as an empty string, and Vite
distinguishes a variable that is absent from one that is defined and empty:
absent, the bundle keeps `const e={}, x=!!e?.VITE_MEDIA_URL`; defined and
empty, it folds to a constant and minification removes the branch.
So every deployment built from this image produced a bundle that did not
match the manifest `.github/workflows/release.yml` publishes — including a
deployment running no media server at all. That is backwards from what
`docs/THREAT-MODEL.md` § 4quinquies claims: the trade is "verifiable **or**
calls", and a deployment configuring no calls was paying it anyway. It made
the whole publishing mechanism inert in the one configuration it was built
for.
Reproduced here before committing, `dist/index.html`:
variable absent, as CI builds it 2a26c3f24aff3326
ARG VITE_MEDIA_URL= (before) 9b4c1178cc297802
unset when empty (after) 2a26c3f24aff3326
unset when empty, calls configured a13e1dcae6b3cbae
The last line is the one that must still differ, and does: a deployment that
configures calls names an origin in its policy and knowingly stops matching.
Mine to have missed. I measured that configuration no longer changes the
bytes, and then wrote a Dockerfile that reintroduced exactly that — because
the measurement was run without the variable while the image always set it.
Testing the thing rather than the thing as it is deployed.
…e image `.dockerignore` patterns match against paths relative to the context root unless they begin with `**/`. So `dist/` excluded the one at the top and copied `apps/web/dist` straight in, and `node_modules/` did the same with 683 MiB of `apps/web/node_modules`. The context carried 683 files where the source tree has 457. # What the stale dist did, which is the reason this is a fix and not a tidy-up Tailwind scans source files for class names. Given the previous bundle, it scanned that too and emitted a different stylesheet, so the chunk hashes moved and `index.html` with them. The deployment then no longer matched the manifest `.github/workflows/release.yml` publishes — which is precisely the property `scripts/verify-web.sh` exists to check. A build whose output depends on whether somebody ran `pnpm run build` beforehand is not reproducible, and nothing anywhere said so. It presented as five altered files out of 226, all of them the ones Vite produces. # Measured, before and after `scripts/verify-web.sh` against a `deploy/` stack, using the manifest from the v0.1.0 release: before 221 matched, 5 altered, 0 missing after 226 matched, 0 altered, 0 missing Context file count: 683 → 457, the same as the source tree.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
scripts/verify-web.shwas run against adeploy/stack for the first time, using the manifest from the v0.1.0 release. It failed:The five were exactly the files Vite produces —
index.html, twoassets/index-*.js, the stylesheet,PdfViewer-*.js. The 221 copied verbatim frompublic/matched byte for byte. So a deployment built with this repository's owndeploy/did not match the manifest this repository publishes, which empties the mechanism for anybody self-hosting.Two causes, found by changing one factor at a time.
apps/web/distwas shipping inside the build context.dockerignorepatterns match paths relative to the context root unless they start with**/.dist/excluded the one at the top and copiedapps/web/diststraight in;node_modules/did the same with 683 MiB. The context carried 683 files where the source tree has 457.Tailwind scans source files for class names. Given the previous bundle, it scanned that too and emitted a different stylesheet — so the chunk hashes moved, and
index.htmlwith them.A build whose output depends on whether somebody ran
pnpm run buildbeforehand is not reproducible, and nothing said so. It is also why this reproduced on a developer machine and not in CI, where the tree is clean.An empty
VITE_MEDIA_URLis not the same as an absent oneVite substitutes
import.meta.env.VITE_*statically and distinguishes the two. Absent, the bundle keepsconst e={}, x=!!e?.VITE_MEDIA_URL. Defined-and-empty, Vite folds it to a constant and minification deletes the branch — different bytes.ARG VITE_MEDIA_URL=put every deployment in the second case, so one running no media server still failed to match. That is backwards:docs/THREAT-MODEL.md§ 4quinquies makes the trade "verifiable or calls", and a deployment configuring no calls was paying it anyway.Measured on node 22.23.2 / pnpm 11.22.0,
sha256(dist/index.html):2a26c3f24aff3326← what the manifest listsARG VITE_MEDIA_URL=(before)9b4c1178cc2978022a26c3f24aff3326a13e1dcae6b3cbae← differs, and shouldWhat was ruled out
Build path — the first hypothesis, and wrong. Two builds at
/appand/a/much/deeper/build/directory/here, everything else equal, produced identical output. Worth recording becausescripts/release.shdocuments the opposite for the Rust binary (217 absolute paths, fixed with--remap-path-prefix), and the analogy does not carry to Vite.Node and pnpm versions were identical throughout (22.23.2, 11.22.0).
What was run
scripts/verify-web.sh http://localhost:8099 WEB-SHA256SUMSagainst adeploy/stack — 226 matched, 0 altered, 0 missinggh attestation verify WEB-SHA256SUMS --repo Sycatle/whispee— passes, and binds the manifest torelease.yml@refs/tags/v0.1.0and commit377e87edocker build -f deploy/Dockerfile.web— green