How a game ships to the Dracon arcade: build, bucket, catalog flip
Every game on the Dracon arcade gets there the same way: an eight-step runbook that ends in a single field changing in a JSON file. We run it manually, on purpose, because the steps where it breaks are exactly the steps worth watching. Yesterday we ran it five times in one day. Here is the pipeline, the catalog it produces, and what is queued behind it.
The eight steps
- 1. Build the game with bun run build in its repo under web/games/wip/<slug>.
- 2. Rewrite the static artifact for bucket hosting with the game's postbuild (scripts/rewrite-static-artifact.mjs), which prepares asset paths for object storage.
- 3. verify-release must exit 0. Each game carries its own checks; Polis's were rewritten this week after we found them asserting a deleted Phaser architecture while the real game had moved to Svelte.
- 4. Publish to the OVH bucket with ovh:publish, which uploads under games/<slug>/<version>/ with a public-read ACL on every object. There is no bucket policy to widen; access is per-object, which we learned the hard way when a stale upload cost Junk Runner its ACLs and started serving 403 AccessDenied.
- 5. Screenshots: stage PNGs into the build BEFORE publishing, so they land at games/<slug>/<version>/screenshots/, captured with a headless Chromium probe.
- 6. Catalog flip: status, version, iframe_src, play_url, asset_base_url, screenshots with alts, and updated_at, all in games-catalog.json.
- 7. Hub rebuild and rsync deploy. The games hub is adapter-static, so there is no service to restart.
- 8. Live smoke through the real URL, for example /games/darklord/play: the iframe content renders and no asset answers 400 or above. (The hub's own 401 on /api/v1/me for anonymous visitors is expected and unrelated.)
Bucket facts that bite
Three properties of the OVH bucket shape everything above. It serves no directory index, so /games/<slug>/<version>/ is a 403 and every reference must name index.html explicitly; the catalog's iframe_src always does. Public access is a per-object canned ACL because OVH does not implement PutBucketPolicy. And root-absolute runtime references like /generated/... resolve against the bucket root, not the version prefix, which is why Darklord publishes its generated art and fonts to the bucket root as a deliberate, documented exception. The SvelteKit reload loop that this pipeline surfaced on July 31 got its own postmortem; the short version is that a two-character base path broke route matching in three games at once.
The catalog today
| Status | Count | Who lives there |
|---|---|---|
| Playable | 1 | One Million Girlfriends (0.2.15) |
| Demo | 4 | torchlit-dungeon, isometric and birds-eye engine demos, birds-eye template |
| WIP | 3 | Capture Anime Girls, Deathrun, Endless Tower Defense |
| Candidate | 6 | Junk Runner, Hegemon, Darklord, Hellhunter, Neonbreak, Polis |
| Placeholder | 2 | canvas2d and isometric engine scaffolds |
| Total | 16 | Every entry desktop-only, DRM-free, no account |
One honest wrinkle in that table: five of the six candidates, Junk Runner, Hellhunter, Neonbreak, Darklord, and Polis, were published to the bucket in the July 31 run at versions 0.8.0 through 1.0.0 and passed live smoke with zero failing assets. The runbook records Darklord and Polis as playable and the other three as bucket-playable, but the catalog status field still says candidate, because flipping that field is a quality call a human makes, not a fact the pipeline asserts. The catalog is the scoreboard; the runbook is the replay.
What ships next
The standing publish rules from yesterday are now in the runbook: the three SvelteKit games must be built with their base-path env vars before every publish, and Neonbreak's postbuild passes --absolute-assets. Next up the status ladder are the five hosted candidates, one quality gate at a time, with Endless Tower Defense and Deathrun behind them in WIP. Moves get recorded in the changelog as they happen, and this article is regenerated when the catalog changes, so the table above is a snapshot, not a monument.