Deploy to Cloudflare Workers: content-addressed assets, staged preview tests, rollback notes
This commit is contained in:
10 files changed
+1667
-23
No files matched your search
@@ -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.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/a/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
+21
-10
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Generated
+1579
-1
File diff suppressed because it is too large.
Load diff
+4
-2
@@ -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
@@ -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/)
|
||||
})
|
||||
@@ -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" }
|
||||
}
|
||||
Reference in new issue
Block a user