Skip to content

Alphabetize themes in code-only Themes doc (#3526) #3636

Alphabetize themes in code-only Themes doc (#3526)

Alphabetize themes in code-only Themes doc (#3526) #3636

Workflow file for this run

name: Build and Test Runtimes, Unit Tests, Generated Code Projects
on:
push:
branches: [main]
pull_request:
branches: [master, main]
workflow_dispatch:
jobs:
# The Gum tool (Gum.sln) builds with neither submodules nor .NET mobile
# workloads: it contains no FNA/Sokol projects and no mobile-TFM projects
# (MonoGameGum/RaylibGum are not in it; the in-solution KniGum defaults to
# net8.0). Isolating the tool build + its WPF unit tests onto their own
# Windows runner lets this lane skip the ~5 min submodule-init + workload-
# restore setup tax that the runtime lane (Build-and-Test) pays, shrinking
# overall PR wall-clock for free (standard runners cost nothing on a public
# repo). This lane installs no workloads on purpose; if it ever fails on a
# missing workload, the error names exactly which one to add here. (#3349)
#
# Skipped on push to main: the tool was already validated in the PR, and unlike
# Build-and-Test this lane seeds no cache, so there is nothing to do on push.
Build-Tool:
if: github.event_name != 'push'
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
submodules: false
- name: Setup .NET SDKs
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Build Gum (tool)
run: |
dotnet restore "Gum.sln" --verbosity quiet
dotnet build "Gum.sln" --configuration Release --no-restore --verbosity quiet -p:WarningLevel=0
- name: GumToolUnitTests
run: dotnet test "Tool/Tests/GumToolUnitTests" --configuration Release --no-build --logger "trx" --results-directory "TestResults"
- name: Publish Test Results
if: always()
uses: dorny/test-reporter@v2
with:
name: Tests Tool (Windows)
path: "TestResults/*.trx"
reporter: dotnet-trx
fail-on-error: true
Build-and-Test:
strategy:
matrix:
# We used to have ubuntu-latest here, but it can't build iOS, and it's easier to just use macos
os: [windows-latest, macos-15]
runs-on: ${{ matrix.os }}
steps:
# actions/checkout's `submodules: recursive` does shallow (--depth=1)
# submodule fetches. Sokol.NET's box2d submodule pins a SHA that lives
# on the `sokol-dev` branch and is not reachable from box2d's default
# branch (`main`), so a shallow fetch misses it. git then tries a
# direct-SHA fetch, falls back to credentialed cloning, and dies with
# "could not read Username" because GIT_TERMINAL_PROMPT=0 on runners.
# Doing `git submodule update --init --recursive` ourselves does full
# (non-shallow) clones, so the pinned SHA is always reachable.
- uses: actions/checkout@v4
with:
submodules: false
# Cache the submodule object stores (.git/modules) + working trees, keyed on
# the pinned submodule SHAs, so a hit turns the recursive non-shallow init
# below into a fast local verify instead of re-cloning FNA's nested native
# submodules (SDL/FAudio/FNA3D/Theorafile) and Sokol.NET/box2d every run.
# Keyed on the SHAs (not the workflow-file hash) so it re-caches only when a
# submodule pointer actually moves. Save on main only, mirroring the workload
# cache: a PR-scoped cache is not reused across PRs, and the 10 GB repo cache
# budget is dominated by the workload packs — limiting submodule caches to one
# per OS on main avoids evicting those. (#3349 item 2)
- name: Compute submodule cache key
id: submodule-key
shell: bash
run: echo "shas=$(git submodule status | cut -c2-41 | tr -d '\n')" >> "$GITHUB_OUTPUT"
- name: Cache submodules (main only)
id: cache-submodules
if: github.ref == 'refs/heads/main'
uses: actions/cache@v4
with:
path: |
.git/modules
fna
Sokol.NET
key: submodules-${{ runner.os }}-${{ steps.submodule-key.outputs.shas }}
- name: Restore cached submodules (PRs)
id: cache-submodules-restore
if: github.ref != 'refs/heads/main'
uses: actions/cache/restore@v4
with:
path: |
.git/modules
fna
Sokol.NET
key: submodules-${{ runner.os }}-${{ steps.submodule-key.outputs.shas }}
# --jobs parallelizes the (still non-shallow) fetch of independent submodules
# on a cache miss; on a hit the objects are already local so this is fast.
- name: Init submodules (non-shallow)
run: git submodule update --init --recursive --jobs 4
- name: Setup .NET SDKs
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
# On main: full cache (restore + save). On PRs: restore only, no save.
# PR branch caches are scoped to that branch and never reused, so saving
# the 3.6 GB cache on PRs is wasted upload time.
- name: Cache .NET workloads (main only)
id: cache-workloads
if: github.ref == 'refs/heads/main'
uses: actions/cache@v4
with:
path: |
C:\Program Files\dotnet\metadata
C:\Program Files\dotnet\packs
C:\Program Files\dotnet\sdk-manifests
/usr/local/share/dotnet/metadata
/usr/local/share/dotnet/packs
/usr/local/share/dotnet/sdk-manifests
key: workloads-${{ runner.os }}-${{ hashFiles('.github/workflows/build-and-test.yaml') }}
- name: Restore cached .NET workloads (PRs)
id: cache-workloads-restore
if: github.ref != 'refs/heads/main'
uses: actions/cache/restore@v4
with:
path: |
C:\Program Files\dotnet\metadata
C:\Program Files\dotnet\packs
C:\Program Files\dotnet\sdk-manifests
/usr/local/share/dotnet/metadata
/usr/local/share/dotnet/packs
/usr/local/share/dotnet/sdk-manifests
key: workloads-${{ runner.os }}-${{ hashFiles('.github/workflows/build-and-test.yaml') }}
- name: Install workloads
if: steps.cache-workloads.outputs.cache-hit != 'true' && steps.cache-workloads-restore.outputs.cache-hit != 'true'
run: dotnet workload install wasm-tools-net9 ios android maui
# Workload caching IS used above and saves ~2 minutes per run.
# I tried caching both NuGet packages and also cache on the actions/setup-dotnet@v4
# In both cases the caching slowed down the total execution time.
# Actually on further testing, it does seem like the NuGet cache can speed up the Windows
# build by around 20-30 seconds. However, the script for it is a little confusing and it's
# not a huge savings. Let's keep it simple and not use NuGet caching.
# On push to main, the only purpose is to seed the workload cache.
# If the cache already exists, skip the entire build and test.
# (The Gum tool is built + unit-tested in the separate Build-Tool job at
# the top of this file, which needs neither submodules nor workloads. #3349)
# IncludeAndroid=true forces KniGum's net9.0-android TFM to be included so CI
# actually validates the #if ANDROID code compiles. KniGum uses opt-in for Android
# (see KniGum.csproj); without this flag Android regressions would slip past CI and
# only surface when packing the Gum.KNI nupkg.
- name: Build AllLibraries
if: github.event_name != 'push' || steps.cache-workloads.outputs.cache-hit != 'true'
run: |
dotnet restore "AllLibraries.sln" -p:IncludeAndroid=true --verbosity quiet
dotnet build "AllLibraries.sln" --configuration Release --no-restore --verbosity quiet -p:WarningLevel=0 -p:IncludeAndroid=true
# Regenerate every harness project with GumCli and fail if the checked-in
# generated code is stale. This forces codegen behavior changes to ship with
# their regenerated output in the same PR, which the compile step below then
# proves still builds. GumCli exit code 1 means individual elements were
# skipped by the per-element error check — expected, several harness projects
# contain deliberately-broken fixture elements (e.g. DemoScreenGum's deleted
# component reference). Exit code 2+ means the project failed to load and
# must fail the build.
- name: Codegen Drift Check (Windows Only)
if: (github.event_name != 'push' || steps.cache-workloads.outputs.cache-hit != 'true') && runner.os == 'Windows'
run: |
dotnet build "Tools/Gum.Cli/Gum.Cli.csproj" --configuration Release --verbosity quiet -p:WarningLevel=0
$cli = "Tools/Gum.Cli/bin/Release/net8.0/GumCli.exe"
$gumxProjects = @(
"Tests/CodeGen_Maui_FullCodegen/Content/GumProject/CodeGenTestProject.gumx",
"Tests/CodeGen_MonoGame_ByReference/Content/GumProject/CodeGenTestProject.gumx",
"Tests/CodeGen_MonoGameForms_ByReference/Content/GumProject/CodeGenTestProject.gumx",
"Tests/CodeGen_MonoGameForms_Localization_ByReference/Content/GumProject/LocalizationCodeGenTestProject.gumx",
"Tests/CodeGen_MonoGameForms_FullCodegen/Content/CodeGenProject.gumx",
"Tests/CodeGen_Raylib_ByReference/Content/GumProject/CodeGenTestProject.gumx"
)
foreach ($gumxProject in $gumxProjects) {
& $cli codegen $gumxProject
if ($LASTEXITCODE -gt 1) { exit $LASTEXITCODE }
}
$drift = git status --porcelain -- "Tests/CodeGen_*"
if ($drift) {
Write-Host "::error::Checked-in generated code is stale. Run 'gumcli codegen' on each Tests/CodeGen_* project (or Tests/GenerateAllCodeGenProjects.bat) and commit the result."
Write-Host ($drift -join "`n")
git diff -- "Tests/CodeGen_*"
exit 1
}
- name: Build Generated Code Projects (Windows Only)
if: (github.event_name != 'push' || steps.cache-workloads.outputs.cache-hit != 'true') && runner.os == 'Windows'
run: |
dotnet restore "Tests/CodeGen_Maui_FullCodegen/CodeGen_Maui_FullCodegen.sln" --verbosity quiet
dotnet build "Tests/CodeGen_Maui_FullCodegen/CodeGen_Maui_FullCodegen.sln" --configuration Debug --no-restore --verbosity quiet -p:WarningLevel=0 -p:TargetFrameworks=net9.0-windows10.0.19041.0
dotnet restore "Tests/CodeGen_MonoGame_ByReference/CodeGen_MonoGame_ByReference.sln" --verbosity quiet
dotnet build "Tests/CodeGen_MonoGame_ByReference/CodeGen_MonoGame_ByReference.sln" --configuration Debug --no-restore --verbosity quiet -p:WarningLevel=0
dotnet restore "Tests/CodeGen_MonoGameForms_ByReference/CodeGenProject.sln" --verbosity quiet
dotnet build "Tests/CodeGen_MonoGameForms_ByReference/CodeGenProject.sln" --configuration Debug --no-restore --verbosity quiet -p:WarningLevel=0
dotnet restore "Tests/CodeGen_MonoGameForms_Localization_ByReference/CodeGen_MonoGameForms_Localization_ByReference.csproj" --verbosity quiet
dotnet build "Tests/CodeGen_MonoGameForms_Localization_ByReference/CodeGen_MonoGameForms_Localization_ByReference.csproj" --configuration Debug --no-restore --verbosity quiet -p:WarningLevel=0
dotnet restore "Tests/CodeGen_MonoGameForms_FullCodegen/CodeGen_MonoGameForms_FullCodegen.sln" --verbosity quiet
dotnet build "Tests/CodeGen_MonoGameForms_FullCodegen/CodeGen_MonoGameForms_FullCodegen.sln" --configuration Debug --no-restore --verbosity quiet -p:WarningLevel=0
dotnet restore "Tests/CodeGen_Raylib_ByReference/CodeGen_Raylib_ByReference.sln" --verbosity quiet
dotnet build "Tests/CodeGen_Raylib_ByReference/CodeGen_Raylib_ByReference.sln" --configuration Debug --no-restore --verbosity quiet -p:WarningLevel=0
# ---------------------------------------------------------------------
# Test allow-list policy
#
# GitHub Actions runners have no GPU. We can only run test projects whose
# tests do not instantiate a real GraphicsDevice / window / native renderer.
# Auto-discovery (`dotnet test <solution>`) is tempting but unsafe here —
# it would pull in projects whose tests open raylib windows, spin up
# MonoGame integration harnesses, etc. So we maintain an explicit list.
#
# When you add a new test project, decide which bucket it belongs to and
# update both this list and the comments below.
#
# Bucket A suites BLOCK the merge — a red test fails the job (no
# continue-on-error). Keep them green.
#
# Bucket A — RUN IN CI (headless: pure data, IO, codegen, analyzers, or
# MonoGame's headless TestGame harness):
# - MonoGameGum.Tests (headless TestGame harness)
# - SokolGum.Tests (Sokol; headless)
# - GumToolUnitTests (WPF tool unit tests; Windows only —
# now runs in the separate Build-Tool
# job at the top of this file, #3349)
# - Gum.Bundle.Tests (brotli/tar/format; pure IO)
# - Gum.ProjectServices.Tests (headless project loading, codegen)
# - Gum.Presentation.Tests (headless undo/redo + relocated tool
# ViewModels; references Gum.Presentation
# alone, no WPF/WinForms — ADR-0005)
# - Gum.ProjectServices.SkiaGum.Tests (SkiaGum SVG export — renders into
# SKSvgCanvas, CPU-only like SkiaGum.Tests)
# - Gum.Analyzers.Tests (Roslyn analyzers; pure compilation)
# - Gum.Cli.Tests (CLI subcommand smoke tests; fonts
# tests self-gate by OS, no screenshot
# tests exist yet — if one is added,
# fence it with a Trait and exclude here)
# - SkiaGum.Tests (renders into an in-memory CPU raster
# SKSurface — no GPU / window / GL. It
# was wrongly excluded as "needs a
# window"; it is fully headless. #3233)
# - RaylibGum.Tests (raylib runtime; Windows-only. Needs an
# OpenGL 3.3 context the GPU-less runners
# lack, supplied by Mesa llvmpipe software
# GL dropped in by the "Install Mesa" step
# right before it. #3250)
#
# Bucket B — DEFERRED, NEEDS INVESTIGATION (likely safe in part, but may
# mix headless and graphics-using tests in the same project; would need
# xUnit traits or filter-by-class to split):
# - MonoGameGum.Tests.V2 (V1 is headless; V2 likely is too —
# verify before enabling)
# - MonoGameGum.Tests.V3 (same — verify before enabling)
#
# Bucket C — (empty) RaylibGum.Tests used to live here as "cannot run in CI
# yet". It graduated to Bucket A in #3250: the macOS probe hung at GLFW/Cocoa
# window creation, but the Windows job supplies software GL via Mesa llvmpipe
# (Win32 window creation is not main-thread-coupled like Cocoa), so it now
# runs headless and blocks. See the Install Mesa + RaylibGum.Tests steps below.
#
# Bucket D — WILL NOT RUN IN CI (require a real graphics device / window;
# do NOT add these even if they look tempting):
# - MonoGameGum.IntegrationTests (integration → real GraphicsDevice)
# - MonoGameGum.Shapes.Tests (shape rendering → GraphicsDevice)
# - MonoGameFum.FromFile.Tests (instantiates real visuals)
# ---------------------------------------------------------------------
# continue-on-error was removed from the Bucket-A suites below (#3233): a red
# test now fails the job and blocks the merge. Note the trade-off — the first
# failing suite short-circuits the suites listed after it; the "Publish Test
# Results" step runs with always() so whatever trx were produced are still
# reported.
- name: MonoGameGum.Tests
if: github.event_name != 'push' || steps.cache-workloads.outputs.cache-hit != 'true'
run: dotnet test "MonoGameGum.Tests" --configuration Release --no-build --logger "trx" --results-directory "TestResults"
- name: SokolGum.Tests
if: github.event_name != 'push' || steps.cache-workloads.outputs.cache-hit != 'true'
run: dotnet test "Tests/SokolGum.Tests" --configuration Release --no-build --logger "trx" --results-directory "TestResults"
- name: Gum.Bundle.Tests
if: github.event_name != 'push' || steps.cache-workloads.outputs.cache-hit != 'true'
run: dotnet test "Tests/Gum.Bundle.Tests" --configuration Release --no-build --logger "trx" --results-directory "TestResults"
- name: Gum.ProjectServices.Tests
if: github.event_name != 'push' || steps.cache-workloads.outputs.cache-hit != 'true'
run: dotnet test "Tests/Gum.ProjectServices.Tests" --configuration Release --no-build --logger "trx" --results-directory "TestResults"
- name: Gum.Presentation.Tests
if: github.event_name != 'push' || steps.cache-workloads.outputs.cache-hit != 'true'
run: dotnet test "Tests/Gum.Presentation.Tests" --configuration Release --no-build --logger "trx" --results-directory "TestResults"
- name: Gum.Analyzers.Tests
if: github.event_name != 'push' || steps.cache-workloads.outputs.cache-hit != 'true'
run: dotnet test "Tests/Gum.Analyzers.Tests" --configuration Release --no-build --logger "trx" --results-directory "TestResults"
- name: Gum.Cli.Tests
if: github.event_name != 'push' || steps.cache-workloads.outputs.cache-hit != 'true'
run: dotnet test "Tests/Gum.Cli.Tests" --configuration Release --no-build --logger "trx" --results-directory "TestResults"
# SkiaGum.Tests renders into an in-memory CPU raster SKSurface (no GPU / window /
# GL), so it runs headless on CI like the suites above and blocks on failure. It
# was previously excluded by mistake — the old comment claimed it needed window
# plumbing, which is not true of these tests. #3233
- name: SkiaGum.Tests
if: github.event_name != 'push' || steps.cache-workloads.outputs.cache-hit != 'true'
run: dotnet test "Tests/SkiaGum.Tests" --configuration Release --no-build --logger "trx" --results-directory "TestResults"
# SkiaGum-backed SVG export service (gumcli svg / tool File ▸ Export). Drives
# SkiaGumSvgExportService end-to-end via SKSvgCanvas — CPU-only, fully headless
# like SkiaGum.Tests above, so it blocks on failure too. #3259
- name: Gum.ProjectServices.SkiaGum.Tests
if: github.event_name != 'push' || steps.cache-workloads.outputs.cache-hit != 'true'
run: dotnet test "Tests/Gum.ProjectServices.SkiaGum.Tests" --configuration Release --no-build --logger "trx" --results-directory "TestResults"
# --- RaylibGum.Tests via Mesa llvmpipe software GL (Windows, #3250) ---------
# raylib's InitWindow needs an OpenGL 3.3 context the GPU-less runners lack.
# Mesa's llvmpipe is a software GL rasterizer; raylib's native lib imports
# opengl32.dll by name, and Windows resolves that from the test host's own
# directory BEFORE System32, so dropping Mesa's opengl32.dll (+ its gallium/
# glapi siblings) next to the built test binaries overrides the GL-less system
# stub with a working software implementation. An earlier macOS probe (#3233)
# HUNG at window creation (GLFW/Cocoa needs the main thread); Win32 window
# creation is not main-thread-coupled, so it works headless here.
#
# This is a BLOCKING Bucket-A suite (promoted from an advisory probe in #3250
# after a clean 351/351 run). The install step feeds that blocking gate, so
# its download is retried against transient GitHub-releases hiccups; the
# timeout-minutes guard on the test step stays so a hang fails fast instead of
# burning the job budget, and the trx now lands in the shared TestResults dir
# so the "Publish Test Results" reporter gates on it like every other suite.
- name: "Install Mesa llvmpipe (Windows, #3250)"
if: (github.event_name != 'push' || steps.cache-workloads.outputs.cache-hit != 'true') && runner.os == 'Windows'
run: |
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue" # Invoke-WebRequest is far faster without progress rendering
$ver = "26.1.2" # pinned pal1000/mesa-dist-win release
$asset = "mesa3d-$ver-release-msvc.7z"
$url = "https://github.com/pal1000/mesa-dist-win/releases/download/$ver/$asset"
$archive = Join-Path $env:RUNNER_TEMP $asset
$extract = Join-Path $env:RUNNER_TEMP "mesa"
# Retry the download — this feeds a blocking gate, so a transient GitHub
# releases hiccup must not red the whole job on the first attempt.
for ($i = 1; $i -le 3; $i++) {
try { Write-Host "Downloading $url (attempt $i)"; Invoke-WebRequest -Uri $url -OutFile $archive; break }
catch {
if ($i -eq 3) { throw }
Write-Host "Download failed: $($_.Exception.Message). Retrying in $(5 * $i)s..."
Start-Sleep -Seconds (5 * $i)
}
}
7z x $archive "-o$extract" -y -bso0 -bsp0
# Find the x64 GL runtime in the archive and copy that folder's DLLs next
# to the RaylibGum.Tests binaries (the test host's exe directory).
$opengl = Get-ChildItem -Path $extract -Recurse -Filter opengl32.dll |
Where-Object { $_.FullName -match '\\x64\\' } | Select-Object -First 1
if (-not $opengl) { Write-Error "x64 opengl32.dll not found in Mesa archive"; exit 1 }
$dest = "Tests/RaylibGum.Tests/bin/Release/net8.0"
Copy-Item (Join-Path $opengl.Directory.FullName "*.dll") -Destination $dest -Force
Write-Host "Mesa GL runtime copied to $dest"
Get-ChildItem (Join-Path $dest "*.dll") |
Where-Object { $_.Name -match "opengl32|gallium|glapi|dxil" } |
Format-Table Name, Length
- name: "RaylibGum.Tests (Windows, #3250)"
if: (github.event_name != 'push' || steps.cache-workloads.outputs.cache-hit != 'true') && runner.os == 'Windows'
timeout-minutes: 10
env:
GALLIUM_DRIVER: llvmpipe # force Mesa's software rasterizer (no GPU on the runners)
run: dotnet test "Tests/RaylibGum.Tests" --configuration Release --no-build --logger "trx" --results-directory "TestResults"
- name: Publish Test Results
if: always() && (github.event_name != 'push' || steps.cache-workloads.outputs.cache-hit != 'true')
uses: dorny/test-reporter@v2
with:
name: Tests ${{ matrix.os }}
path: "TestResults/*.trx"
reporter: dotnet-trx
fail-on-error: true
# End-to-end verification for issue #731: Gum looked for loose content relative to the executable,
# which is wrong inside a macOS .app bundle (exe in Contents/MacOS/, content in Contents/Resources/).
# The unit tests above cover the path-rebasing logic on every OS; this step is the only thing that
# exercises the real bundle layout by publishing a tiny harness, assembling a genuine .app, and
# running it from Contents/MacOS/. The harness exits non-zero if Gum fails to resolve the content,
# so `set -e` turns that into a failed build. This is what removes the need to test #731 by hand.
- name: macOS .app bundle content resolution (issue #731)
if: runner.os == 'macOS' && (github.event_name != 'push' || steps.cache-workloads.outputs.cache-hit != 'true')
run: |
set -euo pipefail
ARCH=$(uname -m)
if [ "$ARCH" = "arm64" ]; then RID="osx-arm64"; else RID="osx-x64"; fi
echo "Publishing #731 harness for $RID"
dotnet publish Tests/MacOSBundle.Harness/MacOSBundle.Harness.csproj -c Release -r "$RID" --self-contained true -o "$RUNNER_TEMP/harness-publish"
APP="$RUNNER_TEMP/MacOSBundleHarness.app"
rm -rf "$APP"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources/Content"
# Executable + runtime live in Contents/MacOS/.
cp -R "$RUNNER_TEMP/harness-publish/." "$APP/Contents/MacOS/"
# Content ships ONLY in Contents/Resources/ — the layout that broke #731. The harness also
# asserts the file is absent next to the executable, so the bundle rebase is genuinely tested.
# The fix keys off the Contents/MacOS -> Contents/Resources path relationship via
# AppContext.BaseDirectory, so no Info.plist / NSBundle wiring is required.
printf 'gum-bundle-ok' > "$APP/Contents/Resources/Content/macos-bundle-test.txt"
chmod +x "$APP/Contents/MacOS/MacOSBundle.Harness"
echo "Running harness from inside the .app bundle (its exit code is the assertion)..."
"$APP/Contents/MacOS/MacOSBundle.Harness"