Skip to content

ci: one toolchain image, verify every example, publish the ROMs - #22

Merged
spacedevin merged 5 commits into
mainfrom
ci/restore-single-matrix
Aug 28, 2026
Merged

spacedevin merged 5 commits into
mainfrom
ci/restore-single-matrix

Conversation

@spacedevin

@spacedevin spacedevin commented Aug 28, 2026 •

Copy link
Copy Markdown
Member

Three problems, one shape: work repeated per job that never needed to be.

1. A prebuilt toolchain image, from its own repo

Every job rebuilt its world before running anything — apt, a libmgba source build, a rustup toolchain, cargo install agb-gbafix, a node download. Across ~90 matrix jobs that's most of the pipeline, and none of it changes between runs.

That toolchain now lives in schlopai/chuggie-ci, not here. It has its own lifecycle — it changes when a toolchain version moves, a handful of times a year, and never when engine or example code changes — so building it in this repo coupled two unrelated things and put a docker build in a pipeline that should only ever pull one.

This repo just names the image:

container: ghcr.io/schlopai/chuggie-ci:0b77982d1ebd

The tag is that Dockerfile's content hash, so the image behind a pin cannot change, and moving to a new toolchain is a one-line bump here after it publishes there. The package is public — no registry credentials, no packages: permission.

crates, rom and verify drop all their setup steps. verify goes from 12 steps to 5.

2. Every non-diagnostic example is verified

The matrix listed only examples with a verify.sh — 44 of the 90 games. The other 46 were run by nobody, so a ROM that crashed on boot or drew a blank screen shipped green.

scripts/verify_default.sh is the floor for those: a ROM exists, it resolved agb to our fork, it runs 300 frames headless, no crash string, no SWI 02 halt loop, and the final frame is more than a flat fill. Weak on purpose — it must hold for every genre with no knowledge of any — and no substitute for real assertions.

bench-*/repro-*/probe-*/p0-* stay out: they measure or reproduce, most draw nothing, and those are the four prefixes gen_examples_readme.py already uses to split games from diagnostics.

3. The ROMs reach chuggie.dev

Which was the original task. verify uploads the ROM it already compiled; example_roms gathers them into the single example-roms artifact. No job builds a ROM that wasn't already being built.

Now that all 90 games are verified, that artifact covers 90 rather than 49.

Job graph

Restored to the original shape — crates, rom, verify (the only matrix), discover, examples_index, release_check, release — plus example_roms. The sharded matrix and bundle from #15/#18 are gone.

Verified

verify_default.sh passes on akari, anim-demo and asteroids against real ROMs.

…eady build

The task was to make example ROMs available to chuggie.dev. I turned that into a
sharded 8-job build matrix, then a dedup pass, then a single serial job — three
rounds of churn that roughly tripled the pipeline and never needed to exist.

This puts the job graph back exactly as it was before any of it — crates, rom,
verify, discover, examples_index, release_check, release, with verify the only
matrix — and adds the one thing actually required:

  verify        already compiles each example's ROM to emulate it. It uploads what
                it made (2 steps, no build).
  example_roms  gathers those into the single `example-roms` artifact the site
                fetches.

No job builds a ROM that was not already being built. One job added, not nine.

That covers the 49 examples with a verifier. The other 76 have nothing in CI that
compiles them, and adding something to do so is precisely what turned this into
two build paths — so they stay uncovered rather than paying for a second matrix.

The published-deps work from #19 is kept: the restored `rom` job installs the
pinned compiler from npm instead of building tishlang from source, and checks out
no other repository.
Three things, all the same problem: work repeated per job that did not need to be.

1. A PUBLISHED TOOLCHAIN IMAGE. Every job rebuilt its world before running
   anything — apt, a libmgba source build, a rustup toolchain, `cargo install
   agb-gbafix`, a node download. Across ~90 matrix jobs that is most of the
   pipeline, and none of it changes between runs.

   .github/docker/ci.Dockerfile has it: libmgba 0.10.5 built with the same headless
   cmake flags, the pinned nightly-2026-07-22 with rust-src and the thumbv4t
   target, stable with rustfmt and clippy, agb-gbafix, node 24, and the apt
   packages the verifier scripts use. ci-image.yml builds it on change and
   publishes to ghcr.io/schlopai/chuggie-ci, tagged by the Dockerfile's own hash so
   a rebuild cannot silently change what jobs run on. On a pull request it builds
   without pushing, so a broken Dockerfile fails there.

   crates, rom and verify now run `container:` and their setup steps are gone:
   verify drops from 12 steps to 5.

   Deliberately not baked in: node_modules (pinned by package-lock.json, which
   moves with the repo), the agb fork (pinned by rev, cargo caches it), the repo.

2. EVERY NON-DIAGNOSTIC EXAMPLE IS VERIFIED. The matrix listed only examples with
   a verify.sh — 44 of the 90 games. The other 46 were compiled by nobody and run
   by nobody, so a ROM that crashed on boot or drew a blank screen shipped green.

   scripts/verify_default.sh is the floor for those: a ROM exists, it resolved agb
   to our fork, it runs 300 frames headless, no crash string, no SWI 02 halt loop,
   and the final frame is more than a flat fill. Weak on purpose — it has to hold
   for every genre with no knowledge of any of them — and no substitute for an
   example's own assertions.

   bench-*/repro-*/probe-*/p0-* stay out: they measure or reproduce, most draw
   nothing, and those are the four prefixes gen_examples_readme.py already uses to
   split games from diagnostics.

3. The matrix goes from 49 to 90 jobs, but each is now a checkout, an `npm ci` and
   the actual work.

Verified locally: verify_default.sh passes on akari, anim-demo and asteroids
against real ROMs.
@spacedevin spacedevin changed the title ci: restore the original pipeline; publish the ROMs the verifiers already build ci: one toolchain image, verify every example, publish the ROMs Aug 28, 2026
The image build is not part of a CI run: ci-image.yml is path-filtered to the
Dockerfile itself, so an ordinary push pulls a prebuilt image and builds nothing.

Two fixes so that is actually true on the first run:

- ci.yml pins ghcr.io/schlopai/chuggie-ci:b2e56aa7c2f0, the Dockerfile's content
  hash, not :latest. A rebuild cannot silently change what jobs run on.
- Branches in this repo push the image, not just main, otherwise a PR that edits
  the Dockerfile cannot pull the image it just described. The tag is content
  addressed, so two branches cannot collide. Only main moves :latest. Fork PRs
  still build without pushing.

Also: packages: read, and registry credentials on the container, since a GHCR
package is private by default.
The image has nothing to do with this pipeline. It changes when a toolchain
version moves — a handful of times a year — and never when engine or example code
changes, so building it here coupled two things with unrelated lifecycles and put
a docker build in a repo whose CI should only ever pull one.

The Dockerfile and its publish workflow now live in schlopai/chuggie-ci. This repo
just names the image:

  container: ghcr.io/schlopai/chuggie-ci:0b77982d1ebd

The tag is that Dockerfile's content hash, so the image behind a pin cannot
change, and moving to a new toolchain is a one-line bump here after it publishes
there. The package is public, so no registry credentials and no packages: read.
The image itself is fine — it builds clean in schlopai/chuggie-ci — but it cannot
be published: the schlopai org disables write permissions for GITHUB_TOKEN, so
the workflow's declared `packages: write` is capped to read and the push is
refused with `denied: permission_denied: read_package`. Nothing can pull an
image that was never pushed, so every containerized job here died on
`Docker pull failed`.

This restores the per-job toolchain setup so the pipeline is green on what it
had before. Everything the PR is actually for is untouched: all 90 non-diagnostic
examples are verified rather than the 44 with a verify.sh, and the ROMs are
uploaded for chuggie.dev.

Switching to the image is then a separate, small change once GHCR accepts a push
— either read/write workflow permissions on the org, or a PAT with write:packages
on schlopai/chuggie-ci. The Dockerfile and its publish workflow are ready and
waiting there; the pin was ghcr.io/schlopai/chuggie-ci:0b77982d1ebd.
@spacedevin
spacedevin merged commit 2615447 into main Aug 28, 2026
97 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant