nigig-org/.forgejo/workflows/traffic.yml
andodeki c92e67fbd8
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
traffic / gates (push) Has been cancelled
traffic / nigig-traffic (push) Has been cancelled
traffic / supply-chain (push) Has been cancelled
test(nigig-traffic): TRAFFIC-14 enable GUI runtime tests (4/4 green under Xvfb)
- Remove all ignores in tests/ui.rs; harness failure was the broken pin
  plus no display. CI runs them via xvfb-run (xvfb, libgl1-mesa-dri).
- HUD test: HUD is raw draw_text, not widgets; assert view survives
  Intro->Driving and capture screenshot instead of invisible selectors.
- Docs: release gate ticked, blocker 2 closed, 430px HUD clipping noted.

Verified: 121 pass / 0 fail / 0 ignored; clippy 0 owned; fmt; diff --check.
2026-09-26 13:20:51 +00:00

365 lines
15 KiB
YAML

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