name: traffic # nigig-traffic CI: the MTB driving-theory game (sim + 3D view). # # Gates, in failure order cost: # 1. source-scanning greps (no toolchain): no unwrap/expect in src, # no lingering TODO/FIXME markers, no fictional commerce/persistence # copy (ADR 001), pinned third-party actions by SHA. # 2. compilation of lib AND bin (--all-targets: a binary that never # compiled once is how this repo got burned before, see email.yml). # 3. dependency coherence: Cargo exit status fails the job FIRST, # including dependency errors (TRAFFIC-P0-01 can never read as green). # 4. the lib unit suite plus a floor so it cannot silently shrink. # 5. headless integration + property/replay/adversarial/journey/fault/ # asset suites (TRAFFIC-02..13 gates). # 6. formatting (hard gate, zero baseline). # 7. clippy ratchet, traffic-owned diagnostics only, baseline 0, with # Cargo exit status preserved (dependency errors fail honestly). # 8. supply-chain: lockfile, allow-listed sources, declared licenses. on: push: paths: - 'crates/apps/nigig-traffic/**' - 'Cargo.lock' - 'Cargo.toml' - 'rust-toolchain.toml' - '.forgejo/workflows/traffic.yml' pull_request: paths: - 'crates/apps/nigig-traffic/**' - 'Cargo.lock' - 'Cargo.toml' - 'rust-toolchain.toml' - '.forgejo/workflows/traffic.yml' jobs: gates: runs-on: ubuntu-latest timeout-minutes: 10 steps: # Pinned by SHA, not tag: tags move, SHAs do not. Re-verify with # `git ls-remote https://github.com/actions/checkout` (v4 tag pointed # here on 2026-09-25). Bump deliberately, never to a floating ref. - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # Frame paths must never panic: no unwrap/expect anywhere in src. # Parse-back tests legitimately need fallible access, so if such a # test is ever added, this gate forces it to use explicit control # flow (match / let-else) instead of weakening the rule. - name: No unwrap or expect in traffic src run: | set -euo pipefail hits=$(grep -rnE '\.(unwrap|expect)\(' \ crates/apps/nigig-traffic/src || true) if [ -n "$hits" ]; then echo "$hits" echo echo "ERROR: unwrap()/expect() in frame-path code. A failed" echo "lookup in the render or tick path panics the app. Use" echo "Option combinators or explicit control flow instead." exit 1 fi echo "OK" # No unsafe in the game crate: nothing here justifies it. - name: No unsafe blocks in traffic src run: | set -euo pipefail hits=$(grep -rnE '(^|[^a-zA-Z_:])unsafe[[:space:]]*(\{|!)' \ crates/apps/nigig-traffic/src || true) if [ -n "$hits" ]; then echo "$hits" echo echo "ERROR: unsafe in nigig-traffic. Justify it in review or remove it." exit 1 fi echo "OK" # No TODO/FIXME debt markers: file an issue or do the work. - name: No TODO or FIXME markers in traffic src run: | set -euo pipefail hits=$(grep -rnE 'TODO|FIXME|todo!|unimplemented!' \ crates/apps/nigig-traffic/src || true) if [ -n "$hits" ]; then echo "$hits" echo echo "ERROR: debt markers above. Track the work in an issue" echo "instead of in comments nobody greps." exit 1 fi echo "OK" nigig-traffic: runs-on: ubuntu-latest timeout-minutes: 60 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # pinned SHA, see gates job - name: Install native dependencies run: | sudo apt-get update -qq sudo apt-get install -y -qq xvfb libgl1-mesa-dri \ pkg-config libwayland-dev libxcursor-dev libxrandr-dev \ libxi-dev libx11-dev libgl1-mesa-dev libasound2-dev \ libglib2.0-dev libssl-dev libsqlite3-dev libudev-dev \ libpulse-dev libxkbcommon-dev - name: Install the declared toolchain run: | set -e version="$(sed -n 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \ rust-toolchain.toml | head -n 1)" curl --fail --location --proto '=https' --tlsv1.2 https://sh.rustup.rs -o /tmp/rustup-init chmod 700 /tmp/rustup-init /tmp/rustup-init -y --profile minimal --default-toolchain "$version" \ --component rustfmt --component clippy --no-modify-path echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" # --all-targets on purpose: the lib can pass while main.rs rots. - name: Check (lib AND bin AND tests) run: cargo check --locked -p nigig-traffic --all-targets # Dependency coherence gate: the pinned Makepad game crates must agree. # Fails on ANY compiler error, including dependency errors that the # clippy ratchet below filters by package. Never suppress with `|| true`. - name: Dependency coherence (no suppressed Cargo failure) run: | set -euo pipefail cargo check --locked -p nigig-traffic --all-targets 2>&1 | tee /tmp/check-traffic.log status=${PIPESTATUS[0]} if [ "$status" -ne 0 ]; then echo "ERROR: cargo check failed with status $status." exit "$status" fi - name: Unit tests run: cargo test --locked -p nigig-traffic --lib -- --test-threads=1 # A floor, not a ratchet: cheap, pure, and the number should only # grow. 70 lib tests at the practice-preview completion; the floor # sits at 60 so ordinary test renames don't trip it while real loss # does. - name: The unit suite must not shrink run: | set -euo pipefail FLOOR=60 out="$(cargo test --locked -p nigig-traffic --lib -- --test-threads=1 2>&1)" echo "$out" | tail -3 n="$(echo "$out" | grep -E '^test result: ok\.' | head -1 \ | sed -E 's/.* ([0-9]+) passed.*/\1/')" if [ -z "$n" ] || [ "$n" -lt "$FLOOR" ]; then echo "ERROR: $n tests passed, floor is $FLOOR." exit 1 fi echo "OK ($n >= $FLOOR)" - name: Headless integration tests run: cargo test --locked -p nigig-traffic --test ui_basic -- --test-threads=1 - name: Coordinate-property tests run: cargo test --locked -p nigig-traffic --test coordinate_properties -- --test-threads=1 - name: Scenario replay tests run: cargo test --locked -p nigig-traffic --test scenario_replays -- --test-threads=1 - name: Adversarial rule tests run: cargo test --locked -p nigig-traffic --test rule_adversarial -- --test-threads=1 - name: Headless runtime-journey tests run: cargo test --locked -p nigig-traffic --test runtime_ui -- --test-threads=1 - name: GUI runtime tests (TRAFFIC-14, real Makepad app under Xvfb) run: xvfb-run -a cargo test --locked -p nigig-traffic --test ui -- --test-threads=1 - name: Persistence fault-injection tests run: cargo test --locked -p nigig-traffic --test persistence_faults -- --test-threads=1 - name: Asset validation tests run: cargo test --locked -p nigig-traffic --test asset_validation -- --test-threads=1 - name: Active-workload perf evidence (reported, not asserted) run: cargo test --locked -p nigig-traffic --test perf_release -- --test-threads=1 --nocapture - name: No forbidden product copy in sources run: | set -euo pipefail hits=$(grep -rnE 'MTB Premium|PaywallAction|Upgrade to Premium|subscription|unlock [0-9]+\+|locked content|locked scenarios' \ crates/apps/nigig-traffic/src crates/apps/nigig-traffic/tests \ | grep -v 'No subscription' | grep -v 'never gates' || true) if [ -n "$hits" ]; then echo "$hits" echo "ERROR: fictional commerce copy (ADR 001). Remove it." exit 1 fi save_hits=$(grep -rnE 'scores are saved|saved locally per learner|progress.*saved locally' \ crates/apps/nigig-traffic/src || true) if [ -n "$save_hits" ]; then echo "$save_hits" echo "ERROR: false persistence claim. Scores are session-only until TRAFFIC-11 persistence ships in the app." exit 1 fi echo "OK" - name: Formatting run: | cargo fmt -p nigig-traffic -- --check \ || { echo "run: cargo fmt -p nigig-traffic"; exit 1; } # Ratchet at the measured baseline, which is ZERO. # Traffic-owned diagnostics only (deduped on rendered text, as # --all-targets compiles lib and lib-test and duplicates each). # Cargo exit status is preserved and fails the job FIRST, including # dependency errors: a broken pinned Makepad revision must never be # reported as "zero owned diagnostics". - name: Clippy ratchet (nigig-traffic-owned diagnostics only) run: | set -euo pipefail BASELINE=0 set +e cargo clippy --locked -p nigig-traffic --all-targets \ --message-format=json > /tmp/clippy-traffic.json 2>/tmp/clippy-traffic.err cargo_status=$? set -e cat /tmp/clippy-traffic.err || true if [ "$cargo_status" -ne 0 ]; then echo "ERROR: cargo clippy exited with status $cargo_status (including dependency failures). Failing honestly." exit "$cargo_status" fi # A dependency compiler error with no Traffic-owned diagnostic is # still a hard failure: grep the raw stderr for rustc errors in # makepad-game-* before the ratchet below. if grep -qE '^error(\[|:)' /tmp/clippy-traffic.err; then echo "ERROR: compiler errors detected (possibly in dependencies). See above." exit 1 fi BASELINE="$BASELINE" python3 - <<'PY' import json, os, sys baseline = int(os.environ['BASELINE']) owned, seen = [], set() with open('/tmp/clippy-traffic.json') as fh: for line in fh: try: m = json.loads(line) except ValueError: continue if m.get('reason') != 'compiler-message': continue if 'nigig-traffic' not in m.get('package_id', ''): continue msg = m['message'] if msg.get('level') not in ('warning', 'error'): continue key = msg.get('rendered', '') if key in seen: continue seen.add(key) owned.append(msg) n = len(owned) print("found %d nigig-traffic diagnostics, baseline %d" % (n, baseline)) if n > baseline: for msg in owned: sys.stdout.write(msg.get('rendered', '')) print() print("ERROR: %d diagnostics, up from %d." % (n, baseline)) sys.exit(1) if n < baseline: print() print("Good: down to %d. Lower BASELINE in this file to %d " "so the progress cannot be undone." % (n, n)) sys.exit(1) print("OK") PY supply-chain: runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # pinned SHA, see gates job - name: Lockfile must be committed and current run: | set -euo pipefail test -f Cargo.lock || { echo "ERROR: Cargo.lock is not committed."; exit 1; } git diff --exit-code -- Cargo.lock - name: Reject whitespace errors run: git diff --check "$(git rev-list --max-parents=0 HEAD | tail -1)"..HEAD || git diff --check # Allowed sources: crates.io + the declared Makepad fork ONLY. A new # git host, alternate registry, or path-patched makepad-game crate # fails here until reviewed (TRAFFIC-13). - name: Dependency sources are allow-listed run: | set -euo pipefail python3 - <<'PY' import re, sys lock = open('Cargo.lock').read() allowed = { 'registry+https://github.com/rust-lang/crates.io-index', } bad = [] for m in re.finditer(r'\[\[package\]\]\nname = "([^"]+)"\nversion = "([^"]+)"\nsource = "([^"]+)"', lock): name, ver, src = m.groups() base = src.split('?')[0].split('#')[0] if base in allowed: continue if base.startswith('git+https://gitdab.com/andodeki/makepad'): continue bad.append(f'{name} {ver} from {src}') if bad: print('ERROR: non-allow-listed dependency sources:') print('\n'.join(bad)) sys.exit(1) print('OK: all locked sources allow-listed') PY # License presence for Traffic's dependency closure: every crate must # declare a license (or license-file). Unlicensed code fails until a # reviewed exception with owner/reason/expiry is recorded here. - name: Licenses declared for Traffic closure run: | set -euo pipefail cargo metadata --locked --format-version 1 --filter-platform x86_64-unknown-linux-gnu >/tmp/meta.json python3 - <<'PY' import json, subprocess, sys meta = json.load(open('/tmp/meta.json')) pkgs = {p['id']: p for p in meta['packages']} # Traffic closure: resolve node + recursive deps. resolve = {n['id']: n.get('deps', []) for n in meta['resolve']['nodes']} want = [p['id'] for p in meta['packages'] if p['name'] == 'nigig-traffic'] seen, stack = set(), list(want) while stack: pid = stack.pop() if pid in seen: continue seen.add(pid) for d in resolve.get(pid, []): stack.append(d['pkg']) bad = [] for pid in sorted(seen): p = pkgs[pid] if p.get('license') or p.get('license_file'): continue # Path-local workspace crates carry the repo license posture. src = (p.get('source') or '') if 'path+file://' in src and p['name'].startswith(('nigig-', 'makepad-')): continue bad.append(f"{p['name']} {p['version']}") if bad: print('ERROR: crates without declared license:') print('\n'.join(bad)) sys.exit(1) print(f"OK: licenses declared across {len(seen)} crates") PY # Duplicate major versions inside Traffic's closure are reported for # review (informational: the workspace legitimately carries several). # New duplicates vs the recorded baseline fail until reviewed. - name: Report duplicate crates in Traffic closure run: | set -euo pipefail cargo tree --locked -p nigig-traffic --prefix none --no-dedupe 2>/dev/null \ | sed -E 's/ v([0-9]+)\..*/ \1/' | sort | uniq -c | sort -rn | head -20 || true