docs: provenance, licensing and attribution pass #1325
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
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ["v*"] | |
| pull_request: | |
| schedule: | |
| # Weekly drift net (v1.5.0 "Bedrock"): catches a regression in a dependency/toolchain/ROM-corpus | |
| # change that landed without a matching code diff to trigger the push/pull_request triggers | |
| # above. Offset from security.yml's own Monday-00:00-UTC cron to spread runner load. | |
| - cron: "0 7 * * 1" | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| # Cancel a superseded run (a new push to the same PR/branch) instead of letting stale, already- | |
| # obsolete runs burn minutes to completion — the single biggest per-PR cost lever besides scoping | |
| # the full battery to tags/main/schedule (below). | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # v1.5.0 "Bedrock": detect doc-only changes so the expensive jobs below can skip cleanly on a | |
| # pure-docs PR/push instead of burning minutes proving nothing changed. NOT a strict inversion of | |
| # security.yml's paths-ignore list (caught in PR review) — this is a curated, narrower positive | |
| # list of the specific paths that can affect a Rust build/lint/test/CI outcome, so it stays | |
| # correct even if security.yml's own ignore list grows for unrelated (e.g. asset) reasons. | |
| changes: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| code: ${{ steps.filter.outputs.code }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3 | |
| id: filter | |
| with: | |
| filters: | | |
| code: | |
| - "crates/**" | |
| - "tests/**" | |
| - "scripts/**" | |
| - "Cargo.toml" | |
| - "Cargo.lock" | |
| - "rust-toolchain.toml" | |
| - ".cargo/**" | |
| - ".github/workflows/**" | |
| - ".github/actions/**" | |
| # v1.5.0 "Bedrock": one place that decides "light" (PR / feature-branch push: fast gate only) vs | |
| # "full" (push to main, a release tag, the weekly cron, or a manual dispatch: the complete | |
| # battery) — mirrors RustyNES's own `changes`/`setup` split. Always-scheduled/dispatched/tag/main | |
| # runs are treated as "full" even on a doc-only diff (`always_full`), since a manual/scheduled | |
| # run's whole point is a from-scratch verification, not an incremental one. | |
| setup: | |
| needs: changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| mode: ${{ steps.mode.outputs.mode }} | |
| steps: | |
| - id: mode | |
| run: | | |
| always_full=false | |
| if [[ "${{ github.event_name }}" == "schedule" || "${{ github.event_name }}" == "workflow_dispatch" ]]; then | |
| always_full=true | |
| elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| always_full=true | |
| elif [[ "${{ github.ref }}" == "refs/heads/main" && "${{ github.event_name }}" == "push" ]]; then | |
| always_full=true | |
| fi | |
| if [[ "$always_full" == "true" ]]; then | |
| echo "mode=full" >> "$GITHUB_OUTPUT" | |
| elif [[ "${{ needs.changes.outputs.code }}" == "true" ]]; then | |
| echo "mode=light" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "mode=skip" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Fast gate: every PR event and every push to `main` with code changes. Formatting + lint | |
| # (Linux only) — the 3-OS matrix, the doc build's cross-platform legs, and the no_std/bench gates | |
| # stay reserved for `full` mode (below) so PR iteration stays quick. The tradeoff (a broken PR is | |
| # only caught by the FULL battery at merge-to-main/tag, not on every single push) is deliberate; | |
| # `test-light` below closes the specific gap that tradeoff used to leave (PR-time `cargo test` | |
| # never running at all, relying solely on CONTRIBUTING.md's manual pre-push checklist). | |
| lint: | |
| needs: setup | |
| if: needs.setup.outputs.mode != 'skip' | |
| env: | |
| CARGO_NET_RETRY: "10" | |
| CARGO_HTTP_MULTIPLEXING: "false" | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| # fmt/clippy only ever check the HOST target here (no `--target` is ever passed to either | |
| # command in this job) — installing the wasm32/thumbv7em cross targets bought nothing but | |
| # setup time on every single PR push. Only `no_std` (which actually cross-compiles) needs | |
| # thumbv7em-none-eabihf; nothing in ci.yml builds for wasm32 (that's `pages.yml`'s job). | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| components: rustfmt, clippy | |
| linux-frontend-deps: "true" | |
| - run: cargo fmt --all --check | |
| # The reviewer's comment-selection filter decides which PR comments the bot DELETES, and | |
| # it has been wrong twice in ways nothing observed (it posted its review fine both times). | |
| # This check is offline — fixtures and `jq`, no network, no `gh`, no self-hosted runner — | |
| # so it runs here on every PR rather than only where `agy` is installed. | |
| - run: bash scripts/agy-review-selftest.sh | |
| # `--exclude rustysnes-android` (`v1.15.0 "Sideload"`): that crate's `ndk-sys` dependency | |
| # hard-fails (`compile_error!`) on every non-Android target, unconditionally -- it's not | |
| # feature-gated, so no `--features`/`--no-default-features` combination avoids it. Its own | |
| # cross-compiled CI coverage is `android.yml`'s job (a later rung), not this host-target | |
| # workspace gate. | |
| - run: cargo clippy --workspace --exclude rustysnes-android --all-targets -- -D warnings | |
| # Explicit flags-off byte-identical gate (`v0.8.0` T-81-004): `default` is currently | |
| # exactly `["wasm-winit", "help-tui"]`, so this is redundant with the line above TODAY — | |
| # its value is as a named regression guard. If a future change ever folds `debug-hooks`/ | |
| # `scripting`/`cheats`/`emu-thread` into `default` without updating this line too, this still | |
| # locks a true flags-off build/lint as its own explicit, protected CI step, independent of | |
| # whatever `default` becomes. | |
| - run: cargo clippy --workspace --exclude rustysnes-android --all-targets --no-default-features --features wasm-winit,help-tui -- -D warnings | |
| # Per-feature-combo clippy (`v0.8.0` T-81-004, extended `v0.9.0` T-82-004) — each Phase 8 | |
| # `rustysnes-frontend` flag individually, then combined; NEVER `--all-features` (the | |
| # workspace-wide rule — `wasm-winit`/`wasm-canvas` are mutually exclusive, so an | |
| # all-features build doesn't even make sense). | |
| - run: cargo clippy -p rustysnes-frontend --all-targets --features debug-hooks -- -D warnings | |
| - run: cargo clippy -p rustysnes-frontend --all-targets --features scripting -- -D warnings | |
| - run: cargo clippy -p rustysnes-frontend --all-targets --features cheats -- -D warnings | |
| - run: cargo clippy -p rustysnes-frontend --all-targets --features netplay -- -D warnings | |
| - run: cargo clippy -p rustysnes-frontend --all-targets --features retroachievements -- -D warnings | |
| # `emu-thread` itself was never actually gated here despite being referenced in the comment | |
| # above (found in review, post-`v1.3.0`) — added now, plus its one functionally meaningful | |
| # combo: `netplay` (post-`v1.3.0`, `EmuControl::netplay_paused` + `NetplayState::drive` now | |
| # runs under `emu-thread` too, previously silently unreachable there). `emu-thread,scripting` | |
| # is NOT added — see `full`'s own comment below for why that combo doesn't compile. | |
| - run: cargo clippy -p rustysnes-frontend --all-targets --features emu-thread -- -D warnings | |
| - run: cargo clippy -p rustysnes-frontend --all-targets --features emu-thread,netplay -- -D warnings | |
| # `full` (`v1.0.0`, `cargo full-build`/`full-run`) IS this same combo (plus the no-op | |
| # `hd-pack` placeholder) — testing it directly instead of re-listing the flags keeps this | |
| # step and `full`'s own Cargo.toml definition from silently drifting apart. | |
| - run: cargo clippy -p rustysnes-frontend --all-targets --features full -- -D warnings | |
| # `rustysnes-test-harness` (T-CA-10): its ROM-oracle test targets are gated behind | |
| # `#![cfg(feature = "test-roms")]`, so the `--workspace` line above — which never passes | |
| # `test-roms` — compiles them to nothing and never lints them. Lint them explicitly. The harness | |
| # always renders through `rustysnes-core`'s default (per-dot) compositor — it has no | |
| # compositor feature of its own — so one `--features test-roms` step covers it. Without this the | |
| # whole accuracysnes/undisbeliever oracle suite is invisible to clippy and drifts (it had — a | |
| # backlog of pedantic findings that had never fired). NEVER `--all-features` (the workspace rule). | |
| - run: cargo clippy -p rustysnes-test-harness --all-targets --features test-roms -- -D warnings | |
| # `rustysnes-libretro` (`v1.2.0`): already covered by the `--workspace` clippy line above (a | |
| # regular workspace member, no non-default features to combo over), but clippy doesn't | |
| # necessarily exercise the FFI-crate-specific bit that matters most for a `cdylib`/ | |
| # `staticlib`: does it actually LINK. A build-only step (not `cargo test` -- this crate has | |
| # no tests of its own; its logic lives in and is tested by `rustysnes-core`) is cheap and | |
| # catches a broken libretro C-ABI export/link before it reaches a release artifact. | |
| - run: cargo build -p rustysnes-libretro | |
| # `fuzz/` is a SEPARATE workspace (its manifest declares an empty `[workspace]`, the | |
| # cargo-fuzz convention) so that cargo-fuzz's nightly requirement never leaks into an | |
| # ordinary `cargo check`. The cost of that isolation is that every `--workspace` command | |
| # above is blind to it, and a fuzz target calling an API that moved would rot silently until | |
| # someone next ran a campaign — which is scheduled weekly, not per-PR. | |
| # | |
| # This is a plain `cargo build`, deliberately, NOT `cargo fuzz build`: it needs no nightly | |
| # toolchain and no sanitizer, and it answers the only question a per-PR gate should ask — | |
| # do the targets still compile against the code this PR changes? The actual fuzzing is | |
| # `security.yml`'s weekly job, because per-commit fuzzing finds little; long campaigns do. | |
| # | |
| # Worth stating because the sibling RustyNES project's `fuzz/README.md` claims exactly this | |
| # gate and it does not actually exist there: its `fuzz/` is referenced by no workflow at all. | |
| - run: cargo build --manifest-path fuzz/Cargo.toml | |
| # Cheap locally (~4s) so it belongs on every PR, not just tag pushes -- catches broken | |
| # intra-doc links and rustdoc-specific warnings clippy's own lints don't cover. | |
| # `--exclude rustysnes-android`: see the clippy step above -- `cargo doc` still compiles the | |
| # crate, so it hits the same `ndk-sys` host-target failure. | |
| - run: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --exclude rustysnes-android --no-deps | |
| # v1.5.0 "Bedrock": the actual gap this release closes. Previously `cargo test --workspace` ran | |
| # ONLY on a tagged release (`full-test` below) — every PR/push-to-main merged on `lint`'s | |
| # fmt+clippy alone, trusting CONTRIBUTING.md's manual pre-push "Quality gate" checklist to have | |
| # been run by hand. This job makes that check programmatic without the cost of `full-test`'s 3-OS | |
| # release-mode matrix: one fast, cached, debug-mode, Linux-only `cargo test` pass, every PR/push. | |
| test-light: | |
| needs: setup | |
| if: needs.setup.outputs.mode != 'skip' | |
| env: | |
| CARGO_NET_RETRY: "10" | |
| CARGO_HTTP_MULTIPLEXING: "false" | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| cache-key-suffix: test-light | |
| linux-frontend-deps: "true" | |
| # `--exclude rustysnes-android`: see `lint`'s own clippy step comment above. | |
| - run: cargo test --workspace --exclude rustysnes-android | |
| # Full gate: release-tag pushes, a push to `main`, the weekly cron, or a manual dispatch — see | |
| # `setup` above. The complete verification battery — the 3-OS matrix, both test invocations, and | |
| # the doc-warnings gate. | |
| full-test: | |
| needs: setup | |
| if: needs.setup.outputs.mode == 'full' | |
| env: | |
| CARGO_NET_RETRY: "10" | |
| CARGO_HTTP_MULTIPLEXING: "false" | |
| strategy: | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| # Same rationale as `lint`: neither fmt nor clippy nor `cargo test` cross-compiles here, so | |
| # no extra targets are installed. | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| components: rustfmt, clippy | |
| cache-key-suffix: full-test-${{ matrix.os }} | |
| linux-frontend-deps: "true" | |
| # Formatting is platform-independent (pure source-text parsing) — one leg is enough. | |
| - run: cargo fmt --all --check | |
| if: runner.os == 'Linux' | |
| # `--exclude rustysnes-android`: see `lint`'s own clippy step comment above. | |
| - run: cargo clippy --workspace --exclude rustysnes-android --all-targets -- -D warnings | |
| - run: cargo test --workspace --exclude rustysnes-android | |
| - run: cargo test --workspace --exclude rustysnes-android --features test-roms | |
| # Exhaustive behavioral coverage (not just clippy) of every Phase 8 flag together, ahead of | |
| # a tagged release (`v0.8.0` T-81-004, extended `v0.9.0` T-82-004). Linux-only, matching | |
| # `lint`'s per-combo clippy: `scripting` vendors and compiles Lua 5.4 via `mlua`'s C source, | |
| # and `retroachievements` vendors and compiles `rcheevos` via `cc` -- both real | |
| # cross-platform build surface `lint` never exercises (Linux only, host target) — verifying | |
| # that specifically on macOS/Windows here too is out of this ticket's scope (it's a genuine | |
| # question of its own, not "is the byte-identical-off gate wired up"). | |
| - run: cargo test -p rustysnes-frontend --features full | |
| if: runner.os == 'Linux' | |
| # `emu-thread` was clippy-checked in `lint` (compiled, never run) but its own unit tests | |
| # (`EmuControl`/`PresentBuffer`, post-`v1.3.0`) never actually executed anywhere in CI until | |
| # now (found in review). `netplay` is its one functionally meaningful combo (see `lint`'s | |
| # own comment). | |
| - run: cargo test -p rustysnes-frontend --features emu-thread,netplay | |
| if: runner.os == 'Linux' | |
| - run: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --exclude rustysnes-android --no-deps | |
| if: runner.os == 'Linux' | |
| # Full-gate cadence only (see `setup` above) — cross-compiling the whole chip stack is cheap but | |
| # doesn't need to happen on every single PR push. | |
| # | |
| # `v1.14.0 "Foundry"`: expanded from a single `rustysnes-core --no-default-features` build into | |
| # a per-crate matrix. The old single job only proved the no_std posture TRANSITIVELY (every | |
| # chip crate is already `default-features = false` in `rustysnes-core`'s own `Cargo.toml`, and | |
| # `#![no_std]` is unconditional -- not feature-gated -- in each chip crate's `lib.rs`), never as | |
| # its own standalone build target. This matrix builds each chip crate directly, so a future | |
| # accidental `std`-only dependency added to just one of them fails on ITS OWN row instead of | |
| # only surfacing (if at all) through `rustysnes-core`'s aggregate build. | |
| no_std: | |
| needs: setup | |
| if: needs.setup.outputs.mode == 'full' | |
| env: | |
| CARGO_NET_RETRY: "10" | |
| CARGO_HTTP_MULTIPLEXING: "false" | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| crate: | |
| - rustysnes-cpu | |
| - rustysnes-ppu | |
| - rustysnes-apu | |
| - rustysnes-cart | |
| - rustysnes-core | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| targets: thumbv7em-none-eabihf | |
| cache-key-suffix: no_std-${{ matrix.crate }} | |
| - run: cargo build -p ${{ matrix.crate }} --target thumbv7em-none-eabihf --no-default-features | |
| # Full-gate cadence only (`v1.0.0`): a release-mode Criterion build + run is too costly for every | |
| # PR push, and the frame-time gate is an absolute ceiling that only needs to hold at | |
| # release/main/cron cadence, not on every iteration. Ported from RustyNES's own `bench` job | |
| # (`scripts/bench_regression_check.sh` + `docs/performance.md`). | |
| bench: | |
| name: frame-time regression gate | |
| needs: setup | |
| if: needs.setup.outputs.mode == 'full' | |
| env: | |
| CARGO_NET_RETRY: "10" | |
| CARGO_HTTP_MULTIPLEXING: "false" | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| # The `rustysnes-core` `headless_frame` bench is chip-stack-only (no wgpu/winit/egui), so | |
| # no Linux frontend system deps are needed here (unlike `lint`/`full-test`). | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| cache-key-suffix: bench | |
| - run: ./scripts/bench_regression_check.sh | |
| # AccuracySNES reproducibility gate. The cart's `.sfc` is committed so contributors can run the | |
| # battery without an assembler; this job proves the committed image is exactly what the source | |
| # produces, so the binary can never silently drift from `gen/`. | |
| accuracysnes: | |
| needs: setup | |
| if: needs.setup.outputs.mode != 'skip' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| cache-key-suffix: accuracysnes | |
| - name: Install cc65 (ca65/ld65) | |
| run: | | |
| dpkg -s cc65 >/dev/null 2>&1 || { | |
| sudo apt-get update | |
| sudo apt-get install -y cc65 | |
| } | |
| - name: Rebuild the cartridge from source | |
| run: cargo run -p accuracysnes-gen | |
| - name: Assert the committed artifacts are byte-identical | |
| run: | | |
| if ! git diff --exit-code -- tests/roms/AccuracySNES; then | |
| echo "::error::AccuracySNES build output differs from the committed artifacts." | |
| echo "::error::Run 'cargo run -p accuracysnes-gen' and commit the result." | |
| exit 1 | |
| fi | |
| - name: Run the battery | |
| run: | | |
| cargo test -p rustysnes-test-harness --features test-roms \ | |
| --test accuracysnes -- --nocapture | |
| # `v1.26.0`: the golden vectors were executed by NO pull-request job. | |
| # | |
| # Only `lint`, `test-light`, and this job run on a PR. `test-light` does not pass | |
| # `--features test-roms`, and the step above scopes itself to `--test accuracysnes` — so the | |
| # 53 rendered-scene goldens and every framebuffer golden were reached only by `full-test`, | |
| # which is `mode == 'full'` (main, tags, the weekly cron). A PR could shift PPU behaviour, | |
| # move a golden, and hear nothing about it until merge-to-main. | |
| # | |
| # That is the same structure that produced the coprocessor-golden staleness this project | |
| # already hit once (goldens left stale by post-bless PPU accuracy fixes): a golden vector | |
| # nothing executes only accumulates drift. Here it was not that nothing executed them ever, | |
| # but that nothing executed them when it could still be cheap to fix. | |
| # | |
| # Cost is bounded and known: the three suites with a committed corpus (~5 min total in a | |
| # debug build — scenes ~84s, undisbeliever ~158s, rainwarrior ~54s). The `*_oncart` | |
| # coprocessor suites need gitignored commercial dumps and firmware, so they self-skip here | |
| # for free; they are listed anyway so the intent is explicit rather than inferred from an | |
| # omission, and so they start gating the moment a runner ever has that corpus. | |
| # | |
| # `--no-fail-fast` is load-bearing, not decoration. Cargo's default stops after the FIRST | |
| # failing test binary, and it stops across binaries, not just within one — verified by | |
| # injecting a failure into `save_state_backward_compat` and watching | |
| # `save_state_determinism` never run at all. With ten suites here that would mean one broken | |
| # golden hides the status of the other nine, so an author fixes one, pushes, and only then | |
| # discovers the next: a serialized debug loop over a ~6-minute job. With the flag, every | |
| # suite reports and the step still fails. | |
| - name: Run the golden vectors | |
| run: | | |
| cargo test -p rustysnes-test-harness --features test-roms --no-fail-fast \ | |
| --test accuracysnes_scenes \ | |
| --test undisbeliever_golden \ | |
| --test rainwarrior_golden \ | |
| --test dsp1_oncart \ | |
| --test sa1_oncart \ | |
| --test superfx_oncart \ | |
| --test dsp3_st011_oncart \ | |
| --test srtc_st018_oncart \ | |
| --test save_state_determinism \ | |
| --test save_state_backward_compat | |
| # v1.5.0 "Bedrock": the one stable required-check name branch protection points at | |
| # (`docs/adr/0011`). Individual jobs above are conditionally skipped depending on `setup`'s mode, | |
| # which makes them unsuitable to require directly (a skipped required check blocks merging | |
| # forever). This job succeeds iff every job it depends on either succeeded or was cleanly | |
| # skipped, and fails if any of them failed or were cancelled. | |
| ci-success: | |
| if: always() | |
| needs: [changes, setup, lint, test-light, full-test, no_std, bench, accuracysnes] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check all required jobs | |
| run: | | |
| echo '${{ toJSON(needs) }}' | |
| for result in $(echo '${{ toJSON(needs.*.result) }}' | jq -r '.[]'); do | |
| if [[ "$result" != "success" && "$result" != "skipped" ]]; then | |
| echo "::error::A required job did not succeed (result: $result)" | |
| exit 1 | |
| fi | |
| done |