# Magic Civilization — CI gate on push-to-main. # # Implements objective p2-10 (`.project/objectives/p2-10-regression-ci-gate.md`): # every push to `main` must pass the full regression suite on the self-hosted # apricot runner before the commit is considered "green" on the Forgejo commit page. # # Runner registration instructions: `.forgejo/RUNNER_SETUP.md`. # Forge: http://10.0.0.11:3000/magicciv/magicciv (Forgejo Actions, drone-compatible). # # Budget: median completion <= 15 min. If a stage consistently blows this, # demote it to a nightly workflow (see RUNNER_SETUP.md § "Budget overruns"). name: ci on: push: branches: - main # Allow a user to kick the gate manually from the Forgejo Actions UI or # via `POST /api/v1/repos/magicciv/magicciv/actions/workflows/ci.yml/dispatches`. # Useful for a first-run smoke on a freshly-registered runner without # needing a sacrificial commit. workflow_dispatch: # One CI run per commit on main; a later push supersedes an in-flight run. concurrency: group: ci-main cancel-in-progress: true env: # Keeps cargo/rustup/pip caches inside the repo workspace so the runner # can reuse them across runs without polluting $HOME on apricot. CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 jobs: regression: name: regression gate # Self-hosted apricot runner — see .forgejo/RUNNER_SETUP.md for registration. # Labels must match forgejo-runner config on apricot exactly. runs-on: [self-hosted, linux, apricot] timeout-minutes: 25 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 1 # ── Stage 1: Rust workspace tests ──────────────────────────────── # Covers all mc-* crates + api-wasm + api-gdext surface tests. # GPU-feature tests are attempted but don't fail the build if the # gpu feature fails to compile on this runner (CUDA/Vulkan env may # not be present); a clean "skipped" message is preferable to a # misleading red pipeline on an environment gap. - name: cargo test --workspace working-directory: src/simulator run: cargo test --workspace --locked - name: cargo test --workspace --features gpu (best-effort) working-directory: src/simulator # `cargo check` gates the attempt — if gpu deps won't even compile # on this runner, skip the full test run loudly but don't fail. run: | set -uo pipefail if cargo check --workspace --features gpu 2>/dev/null; then echo "gpu feature compiles; running gpu tests" cargo test --workspace --features gpu --locked else echo "::warning::gpu feature failed to compile on this runner — skipping gpu tests" fi # ── Stage 2: GDScript lint (advisory) ──────────────────────────── # gdtoolkit must be installed on the runner; see RUNNER_SETUP.md. # # Hard-gated 2026-04-25 (p2-10a). Backlog cleared via file-splitting: # unit_renderer_draw.gd, city_rust_bridge.gd, game_state_serialization_helpers.gd, # ai_turn_bridge_dispatch.gd, ai_turn_bridge_state.gd, turn_processor_helpers.gd, # turn_processor_city_helpers.gd extracted; stale src/generation/auto_play.gd # deleted (live autoload at scenes/tests/auto_play.gd was unaffected); # StatsTracker.CATEGORY_LABELS renamed to category_labels per # class-variable-name rule. `gdlint src/game/engine/src/` exits 0 on main HEAD. - name: gdlint run: gdlint src/game/engine/src/ # ── Stage 3: Game data JSON validator ──────────────────────────── - name: validate game data run: python3 tools/validate-game-data.py # ── Stage 4: Objectives dashboard sync check ───────────────────── # Fails if the dashboard in .project/objectives/ drifts from the # frontmatter in individual objective files. - name: objectives report --check run: python3 tools/objectives-report.py --check # ── Stage 4.5: Build GDExtension ───────────────────────────────── # Godot's GDScript compile depends on `GdClimatePhysics`, `GdGridState`, # `GdTechWeb` etc. which come from the Rust GDExtension. The .so is # gitignored (built per-host), so a fresh `actions/checkout@v4` # workspace has no binary — Godot's parser errors before any test runs. # We build release-profile `magic-civ-physics-gdext` into # `src/game/engine/addons/magic_civ_physics/libmagic_civ_physics.x86_64.so` # which is what `magic_civ_physics.gdextension` points at. - name: build GDExtension working-directory: src/simulator run: bash build-gdext.sh # ── Stage 4.75: Godot import — build class cache ───────────────── # Fresh checkouts have no `.godot/global_script_class_cache.cfg`; # without it Godot's parser cannot resolve GDExtension type names # (e.g. `GdClimatePhysics`), so even well-formed GDScript fails to # compile. `--headless --import` walks the project once to populate # the cache. - name: Godot import (build class cache) run: | flatpak run --filesystem=home org.godotengine.Godot \ --path src/game --headless --import # ── Stage 5: Headless GUT ──────────────────────────────────────── # Runs via Flatpak Godot on apricot. `tools/gut-headless.sh` wraps # the Flatpak invocation and parses JUnit XML exit code (Flatpak # swallows Godot's quit() exit code). # # Hard-gated 2026-04-26 (p2-10b). Original backlog: 40 failures out # of 439 tests. All 40 triaged: 32 fixed (stale assertions, wrong # API names, type mismatches, missing production_cost fixture field, # is_flying() keyword check, diplomacy null-partner guard) and 8 # spun out as pending with objective refs (p2-10c thru p2-10j). # `gut-headless.sh` exits 0 only when JUnit XML reports 0 failures. - name: headless GUT run: bash tools/gut-headless.sh # ── Stage 6: 1-seed T100 smoke batch (advisory) ────────────────── # Minimum-viable determinism + no-stall check. Confirms the commit # doesn't deadlock or crash the autoplay loop. PARALLEL=1 to keep # runtime predictable within the 15-minute budget. # # Currently advisory — autoplay-batch + flatpak sandbox plumbing # has remaining rough edges on fresh CI checkouts (meta.json / # turn_stats.jsonl not always landing even when the game completes). # The `cargo test` stage already covers the deeper simulation # determinism; this stage is the end-to-end dual-language smoke. # Flip to hard-fail when the last sandbox path bug is fixed. - name: autoplay smoke (seed 1, 100 turns) (advisory) continue-on-error: true env: PARALLEL: "1" run: | set -uo pipefail # Absolute path keeps flatpak's sandbox happy across checkout # layouts — relative paths break when the runner workdir is not # the CWD the sandbox resolves against. out_dir="$GITHUB_WORKSPACE/.local/iter/ci_smoke_${GITHUB_SHA:-unknown}" mkdir -p "$out_dir" bash tools/autoplay-batch.sh 1 100 "$out_dir" # Surface the smoke batch's per-turn stats + events on failure so a # red pipeline is debuggable without having to re-run locally. - name: Upload smoke artifacts if: failure() uses: actions/upload-artifact@v3 with: name: ci-smoke-${{ github.sha }} path: .local/iter/ci_smoke_*/** retention-days: 7