Deploy to Cloudflare Workers: content-addressed assets, staged preview tests, rollback notes

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 17:23:34 -04:00
1 parent 83bc28c087
commit 3630bf2a27
10 files changed
+1667 -23

No files matched your search

+7 -1
View File
@@ -24,7 +24,7 @@ icon/ hand-placed sprites
gallery/ registry, demos (demos_<category>.go), app view, invariant tests
cmd/gallery/ native window
cmd/web/ js/wasm host
web/ shell, glue JS, build.sh, Playwright tests; web/dist is build output
web/ shell, glue JS, build.sh, deploy.sh, wrangler.jsonc, _headers, Playwright tests; web/dist is build output
tools/fontpack/ BDF to font12.bin
internal/archtest/ import and API rules
```
@@ -73,6 +73,12 @@ Each component adds one `Entry` in `gallery/demos_<category>.go`: ID, Name, Zh,
- `gallery` checks every entry under every palette: only palette colors, whole dots. `PIXEL_SHOTS=<dir> go test ./gallery -run TestInvariants` writes PNGs for visual review.
- Web: `npm install && npm test` in `web/` builds `web/dist` and runs Playwright on a local server; `BASE_URL=<url> npx playwright test` checks a deployed site. Tests read `window.__8bit` (`ready`, `stats()`, `texts()`, `page()`, `find(label)`, `log`).
## Deploy
- `npm run deploy` in `web/` (clean tree only): builds, uploads a Worker version, runs Playwright on its preview URL, promotes it to 100%, runs Playwright on https://8bitmygoui.zhangyanghaha0407.workers.dev.
- Rollback: `npx wrangler rollback <version-id>` in `web/`; `npx wrangler deployments list` shows versions.
- Scripts and wasm sit under `a/`, named by content hash, and `_headers` makes them immutable; `index.html` revalidates. Never cache a path whose content can change.
## Review
Each TASKS.md item is reviewed in a Herdr pane split to the right, by `codex --yolo -m gpt-6.1-sol -c model_reasoning_effort=high -c service_tier=fast`. At most three rounds. The reviewer reports defects against PRD and this file, ends with `VERDICT: PASS` or `VERDICT: FAIL`, and does not reject work for missing enhancements.
+2
View File
@@ -0,0 +1,2 @@
/a/*
Cache-Control: public, max-age=31536000, immutable
+21 -10
View File
@@ -1,13 +1,24 @@
#!/bin/sh
# Builds web/dist: the gallery as wasm plus the page shell.
# Builds web/dist; files under a/ are named by content.
set -eu
cd "$(dirname "$0")/.."
build=$(git rev-parse --short HEAD)-$(date +%s)
rm -rf web/dist
mkdir -p web/dist
GOOS=js GOARCH=wasm go build -trimpath -ldflags="-s -w" -o web/dist/app.wasm ./cmd/web
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" web/dist/
for f in index.html main.js; do
sed "s/__BUILD__/$build/g" "web/$f" > "web/dist/$f"
done
echo "web/dist built: $build, app.wasm $(wc -c < web/dist/app.wasm) bytes"
d=web/dist
rm -rf $d
mkdir -p $d/a
GOOS=js GOARCH=wasm go build -trimpath -ldflags="-s -w" -o $d/app.wasm ./cmd/web
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" $d/
size=$(wc -c < $d/app.wasm | tr -d ' ')
sed "s/__SIZE__/$size/g" web/main.js > $d/main.js
# name moves a file into a/ by its content hash.
name() {
to=a/$1.$(git hash-object "$d/$2" | cut -c1-12).$3
mv "$d/$2" "$d/$to"
echo "$to"
}
wasm=$(name app app.wasm wasm)
sed -i.bak "s#__WASM__#$wasm#g" $d/main.js && rm $d/main.js.bak
exec=$(name wasm_exec wasm_exec.js js)
main=$(name main main.js js)
sed "s#__EXEC__#$exec#g; s#__MAIN__#$main#g" web/index.html > $d/index.html
cp web/_headers $d/
echo "web/dist built: $main $wasm ($size bytes)"
Executable
+23
View File
@@ -0,0 +1,23 @@
#!/bin/sh
# Uploads a version, tests its preview URL, then promotes it.
set -eu
cd "$(dirname "$0")"
if [ -n "$(git status --porcelain)" ]; then
echo "deploy: commit first; each version names its commit" >&2
exit 1
fi
./build.sh
rev=$(git rev-parse --short HEAD)
out=$(npx wrangler versions upload --message "$rev")
echo "$out"
id=$(echo "$out" | sed -n 's/^Worker Version ID: //p')
preview=$(echo "$out" | sed -n 's/^Version Preview URL: //p')
if [ -z "$id" ] || [ -z "$preview" ]; then
echo "deploy: wrangler printed no version id or preview URL" >&2
exit 1
fi
BASE_URL=$preview npx playwright test
npx wrangler versions deploy "$id@100%" --yes --message "$rev"
site=$(echo "$preview" | sed 's#https://[0-9a-f]*-#https://#')
BASE_URL=$site npx playwright test
echo "deploy: $rev live at $site (version $id)"
+2 -2
View File
@@ -33,7 +33,7 @@
<div><h1 id="over-title">GAME OVER</h1><pre id="why"></pre><button id="continue">CONTINUE?</button></div>
</div>
<noscript>This gallery runs Go compiled to WebAssembly. Turn on JavaScript to play.</noscript>
<script src="wasm_exec.js?v=__BUILD__"></script>
<script src="main.js?v=__BUILD__"></script>
<script src="__EXEC__"></script>
<script src="__MAIN__"></script>
</body>
</html>
+4 -4
View File
@@ -184,9 +184,9 @@ window.addEventListener('resize', () => {
// load streams the wasm, filling the bar as bytes arrive.
async function load () {
const res = await fetch('app.wasm?v=__BUILD__')
if (!res.ok) throw new Error('app.wasm: HTTP ' + res.status)
const total = Number(res.headers.get('content-length')) || 0
const res = await fetch('__WASM__')
if (!res.ok) throw new Error(`${res.url}: HTTP ${res.status}`)
const total = Number('__SIZE__')
const reader = res.body.getReader()
const parts = []
let got = 0
@@ -195,7 +195,7 @@ async function load () {
if (done) break
parts.push(value)
got += value.length
const lit = total ? Math.floor(got / total * blocks.length) : Math.min(blocks.length, parts.length)
const lit = Math.min(blocks.length, Math.floor(got / total * blocks.length))
blocks.forEach((b, i) => b.classList.toggle('on', i < lit))
document.getElementById('loading').setAttribute('aria-valuenow', lit)
}
+1579 -1
View File
File diff suppressed because it is too large. Load diff
+4 -2
View File
@@ -4,9 +4,11 @@
"type": "module",
"scripts": {
"build": "./build.sh",
"test": "./build.sh && playwright test"
"test": "./build.sh && playwright test",
"deploy": "./deploy.sh"
},
"devDependencies": {
"@playwright/test": "1.64.0"
"@playwright/test": "1.64.0",
"wrangler": "4.149.0"
}
}
+17 -3
View File
@@ -234,7 +234,7 @@ test('a Go panic shows GAME OVER in pixels; CONTINUE reloads', async ({ page })
test('resizing while wasm loads is safe', async ({ page }) => {
const errors = []
page.on('pageerror', e => errors.push(e.message))
await page.route('**/app.wasm*', async route => {
await page.route('**/a/app.*.wasm', async route => {
await new Promise(resolve => setTimeout(resolve, 800))
await route.continue()
})
@@ -244,9 +244,23 @@ test('resizing while wasm loads is safe', async ({ page }) => {
expect(errors).toEqual([])
})
test('scripts and wasm have content-addressed, immutable URLs', async ({ page, request }) => {
await open(page, '#/')
const urls = await page.evaluate(() => performance.getEntriesByType('resource').map(r => r.name).filter(u => u.includes('/a/')))
expect(urls.map(u => new URL(u).pathname).sort()).toEqual([
expect.stringMatching(/^\/a\/app\.[0-9a-f]{12}\.wasm$/),
expect.stringMatching(/^\/a\/main\.[0-9a-f]{12}\.js$/),
expect.stringMatching(/^\/a\/wasm_exec\.[0-9a-f]{12}\.js$/)
])
// Cache headers come from Cloudflare, not the local server.
if (!process.env.BASE_URL) return
for (const u of urls) expect((await request.head(u)).headers()['cache-control']).toContain('immutable')
expect((await request.head('/')).headers()['cache-control']).toContain('must-revalidate')
})
test('a load failure shows the page GAME OVER', async ({ page }) => {
await page.route('**/app.wasm*', route => route.fulfill({ status: 404 }))
await page.route('**/a/app.*.wasm', route => route.fulfill({ status: 404 }))
await page.goto('/')
await expect(page.locator('#over')).toBeVisible()
await expect(page.locator('#why')).toContainText('app.wasm: HTTP 404')
await expect(page.locator('#why')).toContainText(/a\/app\.[0-9a-f]{12}\.wasm: HTTP 404/)
})
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "8bitmygoui",
"compatibility_date": "2026-10-01",
"workers_dev": true,
"preview_urls": true,
"assets": { "directory": "./dist" }
}