name: CI on: push: branches: [main] pull_request: branches: [main] env: CARGO_TERM_COLOR: always RUSTFLAGS: "-D warnings" jobs: check: name: Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo check --all-targets fmt: name: Format runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt - run: cargo fmt --all -- --check clippy: name: Clippy runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: clippy - uses: Swatinem/rust-cache@v2 - run: cargo clippy --all-targets -- -D warnings test: name: Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo test --all-targets examples: name: Build Examples runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo build --release - name: Verify committed ROMs match their .ne sources # Every example has a committed .nes file under examples/. The # compiler is deterministic (verified: same input → byte-identical # output), so rebuilding each example into a tmp directory and # diffing against the committed ROM catches "someone edited a .ne # file but forgot to regenerate the ROM" at PR time. run: | mkdir -p /tmp/reprotest fail=0 for f in examples/*.ne; do name=$(basename "$f" .ne) ./target/release/nescript build "$f" --output "/tmp/reprotest/$name.nes" if ! cmp -s "examples/$name.nes" "/tmp/reprotest/$name.nes"; then echo "::error file=examples/$name.ne::committed examples/$name.nes is stale; rerun \`cargo run --release -- build examples/$name.ne\` and commit the new ROM" fail=1 fi done if [ $fail -ne 0 ]; then exit 1 fi - name: Verify ROMs are valid iNES run: | for f in examples/*.nes; do echo "==> Checking $f" # iNES magic: first 4 bytes must be "NES\x1a" header=$(od -A n -t x1 -N 4 "$f" | tr -d ' ') if [ "$header" != "4e45531a" ]; then echo "FAIL: $f is not a valid iNES ROM (header: $header)" exit 1 fi size=$(stat -c%s "$f") echo " OK: $size bytes" done emulator: name: Emulator Smoke Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - uses: actions/setup-node@v4 with: node-version: '22' - name: Install Chrome runtime deps for puppeteer # puppeteer ships its own Chrome for Testing binary via the # package's postinstall hook, but the runner image is missing # a handful of shared libs that Chrome needs to launch. run: | sudo apt-get update sudo apt-get install -y --no-install-recommends \ libnss3 \ libatk-bridge2.0-0 \ libatk1.0-0 \ libatspi2.0-0 \ libcups2t64 \ libdbus-1-3 \ libdrm2 \ libgbm1 \ libpango-1.0-0 \ libxcomposite1 \ libxdamage1 \ libxfixes3 \ libxkbcommon0 \ libxrandr2 \ libasound2t64 - name: Build compiler run: cargo build --release - name: Compile all .ne examples # Overwrites the committed examples/*.nes in the CI workspace # (the workspace is ephemeral; the committed ROMs aren't # touched). This is load-bearing: the goldens must be diffed # against what *this* compiler produces, not against the ROMs # an earlier commit happened to ship, otherwise a broken # compiler change can still pass the emulator job as long as # the contributor forgot to rebuild the ROMs. The `examples` # job catches that second case via its reproducibility diff. run: | for f in examples/*.ne; do echo "==> Compiling $f" ./target/release/nescript build "$f" done - name: Install emulator harness dependencies working-directory: tests/emulator run: npm ci - name: Diff each ROM's framebuffer against its golden working-directory: tests/emulator run: node run_examples.mjs - name: Verify docs/platformer.gif is up to date # The README embeds docs/platformer.gif as the project demo. # gifenc + jsnes are deterministic, so rebuilding the gif from # the current compiler should byte-match the committed copy. # If it doesn't, the contributor changed the compiler / source # / harness in a way that affects the observable gameplay of # platformer.ne but forgot to regenerate the gif — the fix is # the exact command printed in the error. working-directory: tests/emulator run: | node record_gif.mjs platformer 360 2 /tmp/platformer.gif if ! cmp -s ../../docs/platformer.gif /tmp/platformer.gif; then echo "::error file=docs/platformer.gif::committed docs/platformer.gif is stale; rerun \`node tests/emulator/record_gif.mjs platformer 360 2 docs/platformer.gif\` and commit the new gif" exit 1 fi - name: Upload actual + diff PNGs on failure if: failure() uses: actions/upload-artifact@v4 with: name: emulator-diff # `actual/` holds the run's output and red-highlighted diff # PNGs for any ROM that didn't match its golden. `report.json` # has the per-example pass/fail/diff counts. `goldens/` is # included too so reviewers can compare the three side by side # without cloning the repo. path: | tests/emulator/actual/ tests/emulator/goldens/ tests/emulator/report.json