- 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.
365 lines
15 KiB
YAML
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
|