Repository navigation
ci: one toolchain image, verify every example, publish the ROMs - #22
Merged
Merged
Conversation
…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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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,romandverifydrop all their setup steps.verifygoes 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.shis the floor for those: a ROM exists, it resolved agb to our fork, it runs 300 frames headless, no crash string, noSWI 02halt 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 prefixesgen_examples_readme.pyalready uses to split games from diagnostics.3. The ROMs reach chuggie.dev
Which was the original task.
verifyuploads the ROM it already compiled;example_romsgathers them into the singleexample-romsartifact. 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— plusexample_roms. The sharded matrix and bundle from #15/#18 are gone.Verified
verify_default.shpasses onakari,anim-demoandasteroidsagainst real ROMs.