Build Cache and Test Pipeline: Under 10 Minutes

  • Status: accepted
  • Deciders: V-Sekai, fire
  • Tags: V-Sekai, CI, SCons, Cache, Docker, Pipeline, Testing, BranchProtection, 20260425-build-cache-test-pipeline

The Context

The headless test matrix (20260425-headless-test-matrix.md) requires a Godot binary. Building from scratch takes > 10 minutes. The project already uses a two-layer scons cache via godot-cache-restore and godot-cache-save composite actions (.scons_cache/ in the workspace). Test jobs must never rebuild; they download a pre-built artifact.

The user wants tests to pass in CI before a branch can merge (branch protection rule), with local Docker as the gate before pushing.

The Problem Statement

Without separating build and test jobs, every test run rebuilds Godot. Without branch protection, a developer can merge a broken branch. Without scons cache, incremental builds still take > 10 minutes.

Design

Two-job pipeline

build-godot (runs when sources change)
  ├── godot-cache-restore   (.scons_cache/, two-layer: main branch + PR branch)
  ├── scons dev_build=yes … → bin/godot.linuxbsd.editor.dev.x86_64
  ├── godot-cache-save
  └── upload-artifact: godot-headless-bin

headless-tests (runs after build-godot)
  ├── download-artifact: godot-headless-bin
  ├── start zone server (downloaded binary, --headless)
  ├── run shell-based matrix (GO + GP, phases 1 and 2; no Playwright)
  └── report results

The test job never calls scons. Under 10 minutes from artifact download to test results.

scons cache: two-layer strategy (existing)

The godot-cache-restore action already implements this:

key:           {cache-name}|{default_branch}|{sha}
restore-keys:  {cache-name}|{default_branch}   ← main-branch warm cache
               {cache-name}|{branch}            ← PR-specific warm cache

PRs restore the main-branch cache first so they inherit all previous incremental build objects, then overlay PR-specific objects. No changes needed here; the existing actions handle it.

Local Docker: mount pre-built binary

The test container does not build Godot. It mounts the binary from the developer’s local bin/ directory:

# docker-compose.test.yml
services:
  test-runner:
    image: ubuntu:24.04
    volumes:
      - ../multiplayer-fabric-godot/bin/godot.linuxbsd.editor.dev.x86_64:/godot/godot:ro
      - ../tests:/tests
    environment:
      GODOT_BIN: /godot/godot
      ZONE_HOST: zone-server
      ZONE_PORT: "17500"
    depends_on:
      zone-server:
        condition: service_healthy

The developer builds once (gescons on the host or pull from the latest CI artifact), then runs the Docker test suite as many times as needed without rebuilding.

Where the workflow lives

The headless-tests job is added to multiplayer-fabric-godot/.github/workflows/runner.yml as a fourth stage, after docker-images:

# runner.yml (excerpt)
headless-tests:
  name: 🧪 Headless matrix
  needs: linux-build
  uses: ./.github/workflows/headless_tests.yml
  with:
    godot-artifact: godot-headless-bin

headless_tests.yml is a new workflow file in the same directory. It downloads the artifact, starts the zone server, and runs the shell-based matrix (bash run_matrix.sh). No Playwright, no browser engine.

The branch protection rule applies to the multiplayer-fabric branch of github.com/V-Sekai-fire/multiplayer-fabric-godot, the assembled branch that gitassembly pushes to:

Repository: V-Sekai-fire/multiplayer-fabric-godot
Branch:     multiplayer-fabric
Settings → Branches → Add rule:
  Branch name pattern: multiplayer-fabric
  ✓ Require status checks to pass before merging
    ✓ 🧪 Headless / GO — Godot observer
    ✓ 🧪 Headless / GP — Godot player
    ✓ 🧪 Headless / GO+GP — dual cross-check

Three checks: two single-role (GO, GP) and one dual cross-check. Three.js roles are gone (the browser client is superseded), and the observer (GO) itself is currently deferred while the VR client lands; checks become live once the matrix unfreezes.

CI artifact retention

godot-headless-bin is retained for 3 days. Developers can download the latest green CI binary instead of building locally:

gh run download --repo V-Sekai-fire/multiplayer-fabric-godot \
  --name godot-headless-bin --dir multiplayer-fabric-godot/bin/

This eliminates the local build entirely for developers whose only goal is to run the test suite.

The Downsides

If no green CI artifact exists (first push, cache cold), the developer must build locally or wait for CI to produce one. The first build on a new branch takes the full compile time even with a warm cache (scons cache miss rate for new files).

The Road Not Taken

Build inside the test container: rejected; scons and all build dependencies would bloat the test image and add > 10 minutes to every test run regardless of source changes.

Baking the binary into the test Docker image: rejected; the image would need rebuilding on every source change, negating the cache benefit.

Status

Status: Accepted

Decision Makers

  • iFire

Further Reading

(godotcacherestore?): .github/actions/godot-cache-restore/action.yml in multiplayer-fabric-godot — existing two-layer scons cache restore.

(ghactionscache?): “Caching dependencies to speed up workflows.” GitHub Docs. https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows