Skip to main content

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

StatusCountWho lives there
Playable1One Million Girlfriends (0.2.15)
Demo4torchlit-dungeon, isometric and birds-eye engine demos, birds-eye template
WIP3Capture Anime Girls, Deathrun, Endless Tower Defense
Candidate6Junk Runner, Hegemon, Darklord, Hellhunter, Neonbreak, Polis
Placeholder2canvas2d and isometric engine scaffolds
Total16Every entry desktop-only, DRM-free, no account
Real status counts from web/games/libs/data/games-catalog.json, computed August 1, 2026.

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.