Skip to content

chore(audience-sample): trim 30 unused packages and engine modules #213

chore(audience-sample): trim 30 unused packages and engine modules

chore(audience-sample): trim 30 unused packages and engine modules #213

name: Audience SDK — PlayMode (IL2CPP + Mono)
on:
pull_request:
paths:
- 'src/Packages/Audience/Runtime/**'
- 'src/Packages/Audience/Tests/**'
- 'src/Packages/Audience/package.json'
- 'src/Packages/Audience/Directory.Build.props'
- 'src/Packages/Audience/link.xml'
- 'examples/audience/Assets/**'
- 'examples/audience/Packages/**'
- 'examples/audience/ProjectSettings/**'
- '.github/workflows/test-audience-sample-app.yml'
schedule:
# Weekly full-matrix run on the default branch.
# Cron is UTC; cron has no DST awareness, so the local time shifts by one
# hour twice a year. Saturday 14:00 UTC maps to:
# Sun 00:00 Sydney AEST / Sun 02:00 NZ NZST (winter, Apr to Oct)
# Sun 01:00 Sydney AEDT / Sun 03:00 NZ NZDT (summer, Oct to Apr)
- cron: '0 14 * * 6'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# CI run identifier injected into the player so events on CDP can be
# filtered as test traffic. Run id is workflow-wide; matrix-aware cell
# id is set on the test jobs below. The marker test reads these env
# vars and Assert.Ignores when both are unset.
env:
AUDIENCE_TEST_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}
jobs:
# Reduced matrix on pull_request, full matrix on schedule and
# workflow_dispatch. The self-hosted Windows runner pool is small, so
# trimming PR cells keeps PR feedback fast. `matrix.exclude` below is
# the source of truth for which cells are dropped on pull_request.
playmode:
if: |
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
|| github.event_name == 'schedule'
|| github.event_name == 'workflow_dispatch'
name: ${{ matrix.target }} / ${{ matrix.backend }} / Unity ${{ matrix.unity }}
strategy:
fail-fast: false
matrix:
unity: ['2021.3.45f2', '6000.4.0f1', '2022.3.62f2']
target: [StandaloneWindows64, StandaloneOSX]
backend: [IL2CPP, Mono2x]
include:
- unity: '2021.3.45f2'
changeset: 88f88f591b2e
- unity: '6000.4.0f1'
changeset: 8cf496087c8f
- unity: '2022.3.62f2'
changeset: 7670c08855a9
- target: StandaloneWindows64
runner: [self-hosted, Windows, X64]
- target: StandaloneOSX
runner: [self-hosted, macOS, ARM64]
exclude: ${{ fromJSON(github.event_name == 'pull_request' && '[{"unity":"2022.3.62f2"}]' || '[]') }}
runs-on: ${{ matrix.runner }}
# Healthy cells finish in ~10 min. 30 min covers cold caches +
# IL2CPP + Unity 6 startup; anything past that is a hang. Capping
# short releases the self-hosted runner sooner so queued cells can
# progress instead of waiting 60 min on a stuck job.
timeout-minutes: 30
env:
AUDIENCE_TEST_CELL_ID: ${{ matrix.target }}-${{ matrix.backend }}-${{ matrix.unity }}
steps:
- name: Kill stale Unity processes (Windows pre-checkout)
if: runner.os == 'Windows'
shell: pwsh
continue-on-error: true
run: |
# actions/checkout@v4 deletes the prior workspace before cloning. If a
# previous run's Unity Editor / IL2CPP build process is still holding
# handles inside examples/audience, checkout dies with EBUSY. Kill any
# leftover Unity-family process here so checkout's cleanup succeeds.
Get-Process | Where-Object {
$_.Name -like 'Unity*' -or
$_.Name -like 'il2cpp*' -or
$_.Name -like 'UnityShaderCompiler*' -or
$_.Name -like 'UnityCrashHandler*'
} | ForEach-Object {
Write-Host "Killing stale process: $($_.Name) (pid $($_.Id))"
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
}
Start-Sleep -Seconds 2
- uses: actions/checkout@v4
with:
lfs: true
- name: Cache Unity Library
uses: actions/cache@v4
with:
path: examples/audience/Library
key: Library-${{ matrix.backend }}-${{ matrix.target }}-${{ matrix.unity }}-${{ hashFiles('examples/audience/Assets/**', 'examples/audience/Packages/**', 'examples/audience/ProjectSettings/**', 'src/Packages/Audience/**') }}
restore-keys: |
Library-${{ matrix.backend }}-${{ matrix.target }}-${{ matrix.unity }}-
Library-${{ matrix.backend }}-${{ matrix.target }}-
- name: Ensure MSVC + Windows 10 SDK (Windows IL2CPP)
if: runner.os == 'Windows' && matrix.backend == 'IL2CPP'
shell: pwsh
run: |
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
# Match Unity's detection logic exactly: vswhere requires VC.Tools
# (any version), registry probe for any Win10 SDK at v10.0/InstallationFolder.
# Pinning a specific SDK version in -requires is too strict — VCTools
# ships with whatever Win10 SDK is current, and Unity accepts any.
function Test-Toolchain {
$vc = if (Test-Path $vswhere) {
& $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2>$null
} else { '' }
$sdk = (Get-ItemProperty 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Microsoft SDKs\Windows\v10.0' -ErrorAction SilentlyContinue).InstallationFolder
return @{ VcTools = $vc; Win10Sdk = $sdk }
}
$state = Test-Toolchain
if ($state.VcTools -and $state.Win10Sdk) {
Write-Host "VC.Tools at: $($state.VcTools)"
Write-Host "Win10 SDK at: $($state.Win10Sdk)"
exit 0
}
Write-Host "Toolchain incomplete. VC.Tools='$($state.VcTools)' Win10Sdk='$($state.Win10Sdk)'"
Write-Host "::group::Install VS 2022 Build Tools (VCTools + Win10 SDK)"
$installer = "$env:RUNNER_TEMP\vs_BuildTools.exe"
Invoke-WebRequest -Uri 'https://aka.ms/vs/17/release/vs_BuildTools.exe' -OutFile $installer
$installArgs = @(
'--quiet','--wait','--norestart','--nocache',
'--add','Microsoft.VisualStudio.Workload.VCTools',
'--add','Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
'--add','Microsoft.VisualStudio.Component.Windows10SDK.20348',
'--includeRecommended'
)
$p = Start-Process -FilePath $installer -ArgumentList $installArgs -Wait -PassThru -NoNewWindow
# 3010 = success, reboot pending (tools are usable without reboot).
if ($p.ExitCode -ne 0 -and $p.ExitCode -ne 3010) {
Write-Host "::error::VS Build Tools installer exited $($p.ExitCode)"
exit $p.ExitCode
}
Write-Host "::endgroup::"
$state = Test-Toolchain
if (-not ($state.VcTools -and $state.Win10Sdk)) {
Write-Host "::group::diagnostic"
Write-Host "VC.Tools path (vswhere): '$($state.VcTools)'"
Write-Host "Win10 SDK (registry v10.0/InstallationFolder): '$($state.Win10Sdk)'"
Write-Host "--- all VS installations ---"
if (Test-Path $vswhere) { & $vswhere -all -products * -format json }
Write-Host "--- HKLM Win10 SDK roots ---"
Get-ChildItem 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Microsoft SDKs\Windows' -ErrorAction SilentlyContinue | Format-List
Write-Host "::endgroup::"
Write-Host "::error::Install reported success but VC.Tools or Win10 SDK still not detected — runner service account likely lacks admin to install system-wide. Install VS Build Tools manually on IMX_SDKBUILD: vs_BuildTools.exe --quiet --wait --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"
exit 1
}
Write-Host "Verified VC.Tools at: $($state.VcTools)"
Write-Host "Verified Win10 SDK at: $($state.Win10Sdk)"
- name: Resolve Unity ${{ matrix.unity }} (macOS)
if: runner.os == 'macOS'
shell: bash
env:
UNITY_VER: ${{ matrix.unity }}
UNITY_CS: ${{ matrix.changeset }}
run: |
set -uo pipefail
HUB="/Applications/Unity Hub.app/Contents/MacOS/Unity Hub"
echo "::group::install editor"
"$HUB" -- --headless install \
--version "$UNITY_VER" --changeset "$UNITY_CS" --architecture arm64 \
|| echo "(install non-zero — OK if 'Editor already installed in this location')"
echo "::endgroup::"
if [ "${{ matrix.backend }}" = "IL2CPP" ]; then
echo "::group::install mac-il2cpp module"
"$HUB" -- --headless install-modules \
--version "$UNITY_VER" --changeset "$UNITY_CS" --architecture arm64 \
--module mac-il2cpp \
|| echo "(install-modules non-zero — OK if 'No modules found to install')"
echo "::endgroup::"
fi
EDITOR_APP=""
for cand in \
"/Applications/Unity/Hub/Editor/$UNITY_VER-arm64/Unity.app" \
"/Applications/Unity/Hub/Editor/$UNITY_VER/Unity.app"; do
if [ -x "$cand/Contents/MacOS/Unity" ]; then EDITOR_APP="$cand"; break; fi
done
IL2CPP_DIR=""
if [ "${{ matrix.backend }}" = "IL2CPP" ] && [ -n "$EDITOR_APP" ]; then
for d in \
"$EDITOR_APP/Contents/PlaybackEngines/MacStandaloneSupport/Variations/macos_arm64_player_nondevelopment_il2cpp" \
"$EDITOR_APP/Contents/PlaybackEngines/MacStandaloneSupport/Variations/macos_x64_player_nondevelopment_il2cpp"; do
if [ -d "$d" ]; then IL2CPP_DIR="$d"; break; fi
done
fi
MISSING=""
[ -z "$EDITOR_APP" ] && MISSING="editor"
[ "${{ matrix.backend }}" = "IL2CPP" ] && [ -z "$IL2CPP_DIR" ] && MISSING="${MISSING:+$MISSING+}mac-il2cpp"
if [ -n "$MISSING" ]; then
echo "::error::Unity $UNITY_VER missing: $MISSING"
ls -la /Applications/Unity/Hub/Editor/ 2>&1 || true
"$HUB" -- --headless editors --installed 2>&1 || true
exit 1
fi
echo "Found Unity: $EDITOR_APP/Contents/MacOS/Unity"
[ -n "$IL2CPP_DIR" ] && echo "Found IL2CPP: $IL2CPP_DIR"
echo "UNITY_PATH=$EDITOR_APP/Contents/MacOS/Unity" >> "$GITHUB_ENV"
- name: Resolve Unity ${{ matrix.unity }} (Windows)
if: runner.os == 'Windows'
shell: pwsh
env:
UNITY_VER: ${{ matrix.unity }}
UNITY_CS: ${{ matrix.changeset }}
run: |
$hub = "C:\Program Files\Unity Hub\Unity Hub.exe"
Write-Host "::group::install editor"
$installArgs = @('--','--headless','install','--version',$env:UNITY_VER,'--changeset',$env:UNITY_CS,'--architecture','x86_64')
& $hub @installArgs 2>&1 | Write-Host
if ($LASTEXITCODE -ne 0) { Write-Host "(install non-zero — OK if 'Editor already installed in this location')" }
$global:LASTEXITCODE = 0
Write-Host "::endgroup::"
if ('${{ matrix.backend }}' -eq 'IL2CPP') {
Write-Host "::group::install windows-il2cpp module"
$modArgs = @('--','--headless','install-modules','--version',$env:UNITY_VER,'--changeset',$env:UNITY_CS,'--architecture','x86_64','--module','windows-il2cpp')
& $hub @modArgs 2>&1 | Write-Host
if ($LASTEXITCODE -ne 0) { Write-Host "(install-modules non-zero — OK if 'No modules found to install')" }
$global:LASTEXITCODE = 0
Write-Host "::endgroup::"
}
$editor = "C:\Program Files\Unity\Hub\Editor\$env:UNITY_VER\Editor\Unity.exe"
$il2cpp = "C:\Program Files\Unity\Hub\Editor\$env:UNITY_VER\Editor\Data\PlaybackEngines\windowsstandalonesupport\Variations\win64_player_nondevelopment_il2cpp"
$missing = @()
if (-not (Test-Path $editor)) { $missing += 'editor' }
if ('${{ matrix.backend }}' -eq 'IL2CPP' -and -not (Test-Path $il2cpp)) { $missing += 'windows-il2cpp' }
if ($missing.Count -gt 0) {
Write-Host "::error::Unity $env:UNITY_VER missing: $($missing -join '+')"
Get-ChildItem "C:\Program Files\Unity\Hub\Editor\" -ErrorAction SilentlyContinue | Format-Table
& $hub -- --headless editors --installed
exit 1
}
Write-Host "Found Unity: $editor"
if ('${{ matrix.backend }}' -eq 'IL2CPP') { Write-Host "Found IL2CPP: $il2cpp" }
"UNITY_PATH=$editor" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Run PlayMode tests (macOS)
if: runner.os == 'macOS'
shell: bash
env:
AUDIENCE_TEST_PUBLISHABLE_KEY: ${{ secrets.AUDIENCE_TEST_PUBLISHABLE_KEY }}
AUDIENCE_SCRIPTING_BACKEND: ${{ matrix.backend }}
run: |
set -euo pipefail
mkdir -p artifacts
# Tee Unity's stdout to artifacts/unity.log so the annotation step has a
# file to scan, while still streaming progress to the job log. pipefail
# propagates Unity's exit code through tee. The annotation step reads this
# file in-job; the actions/upload-artifact step below also uploads it so
# compile failures retain a full post-mortem (annotations are matched-line
# only and drop IL2CPP linker output, build config dumps, etc).
"$UNITY_PATH" \
-batchmode -nographics \
-projectPath examples/audience \
-runTests \
-testPlatform ${{ matrix.target }} \
-testResults "$(pwd)/artifacts/test-results.xml" \
-logFile - 2>&1 | tee "$(pwd)/artifacts/unity.log"
- name: Run PlayMode tests (Windows)
if: runner.os == 'Windows'
shell: pwsh
env:
AUDIENCE_TEST_PUBLISHABLE_KEY: ${{ secrets.AUDIENCE_TEST_PUBLISHABLE_KEY }}
AUDIENCE_SCRIPTING_BACKEND: ${{ matrix.backend }}
run: |
New-Item -ItemType Directory -Force -Path artifacts | Out-Null
$logFile = "$pwd\artifacts\unity.log"
$unityArgs = @(
'-batchmode','-nographics',
'-projectPath','examples/audience',
'-runTests',
'-testPlatform','${{ matrix.target }}',
'-testResults',"$pwd\artifacts\test-results.xml",
'-logFile',$logFile
)
Write-Host "Launching Unity: $env:UNITY_PATH $($unityArgs -join ' ')"
$p = Start-Process -FilePath $env:UNITY_PATH -ArgumentList $unityArgs -Wait -PassThru -NoNewWindow
$exit = $p.ExitCode
Write-Host "::group::Unity log"
Get-Content $logFile -ErrorAction SilentlyContinue | Write-Host
Write-Host "::endgroup::"
Write-Host "Unity exited with code $exit"
if ($exit -ne 0) { exit $exit }
- name: Mark workspace safe for git (Windows)
if: always() && runner.os == 'Windows'
shell: pwsh
run: |
git config --global --add safe.directory $env:GITHUB_WORKSPACE.Replace('\','/')
- name: Capture player log (macOS)
if: always() && runner.os == 'macOS'
shell: bash
run: |
# The test-runner builds + launches a player binary that writes its own
# log separately from Unity's editor log. When the editor reports
# "Test execution timed out. No activity received from the player ..."
# the editor unity.log alone cannot tell us whether the player crashed,
# hung, or never started. Copy whatever Player.log files Unity wrote
# into artifacts/ so the upload-artifact step preserves them.
mkdir -p artifacts
src="$HOME/Library/Logs"
if [ -d "$src" ]; then
find "$src" -name "Player.log" 2>/dev/null | while IFS= read -r f; do
cp "$f" "artifacts/Player-$(basename "$(dirname "$f")").log" 2>/dev/null || true
done
fi
- name: Capture player log (Windows)
if: always() && runner.os == 'Windows'
shell: pwsh
run: |
# See macOS counterpart for rationale. Windows player log location:
# %USERPROFILE%\AppData\LocalLow\<CompanyName>\<ProductName>\Player.log
New-Item -ItemType Directory -Force -Path artifacts | Out-Null
$src = "$env:USERPROFILE\AppData\LocalLow"
if (Test-Path $src) {
Get-ChildItem -Path $src -Recurse -Filter "Player.log" -ErrorAction SilentlyContinue |
ForEach-Object {
$name = $_.Directory.Name
Copy-Item -Path $_.FullName -Destination "artifacts/Player-$name.log" -ErrorAction SilentlyContinue
}
}
- name: Surface Unity compile errors as annotations (macOS)
if: always() && runner.os == 'macOS'
shell: bash
run: |
set -uo pipefail
# Unity writes compile errors as 'error CS####:' or 'Compilation failed: <n>'.
# When a cell fails compile (vs fails a test), the test-results.xml is empty
# and the only signal otherwise is the artifact zip. Promote those lines to
# ::error:: annotations so the PR UI shows the cause inline.
LOG_FILE="artifacts/unity.log"
if [ ! -f "$LOG_FILE" ]; then
echo "::notice::No Unity log file at $LOG_FILE."
exit 0
fi
# `|| true` guards the success path: with `pipefail`, grep exits 1 when no
# matches (the clean-build case), which would otherwise propagate as the
# step's exit code and falsely mark every green cell red.
grep -E '(error CS[0-9]+:|Compilation failed:)' "$LOG_FILE" | sort -u | while IFS= read -r line; do
trimmed="${line#"${line%%[![:space:]]*}"}"
# Sanitize '::' so log lines containing workflow commands (e.g. ::endgroup::)
# cannot terminate the annotation early or inject other commands.
sanitized="${trimmed//::/%3A%3A}"
echo "::error::$sanitized"
done || true
- name: Surface Unity compile errors as annotations (Windows)
if: always() && runner.os == 'Windows'
shell: pwsh
run: |
$logFile = "artifacts\unity.log"
if (-not (Test-Path $logFile)) {
Write-Host "::notice::No Unity log file at $logFile."
exit 0
}
Get-Content $logFile |
Select-String -Pattern '(error CS\d+:|Compilation failed:)' |
ForEach-Object { $_.Line.Trim() } |
Sort-Object -Unique |
ForEach-Object {
# Sanitize '::' so log lines containing workflow commands cannot
# terminate the annotation early or inject other commands.
$sanitized = $_ -replace '::', '%3A%3A'
Write-Host "::error::$sanitized"
}
- name: Publish test report
uses: dorny/test-reporter@v3
if: always()
with:
name: PlayMode (${{ matrix.backend }} / ${{ matrix.target }})
path: artifacts/test-results.xml
reporter: dotnet-nunit
fail-on-error: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: playmode-${{ matrix.backend }}-${{ matrix.target }}-${{ matrix.unity }}
path: |
artifacts/test-results.xml
artifacts/unity.log
artifacts/Player-*.log
examples/audience/Logs/**
playmode-linux:
if: |
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
|| github.event_name == 'schedule'
|| github.event_name == 'workflow_dispatch'
name: ${{ matrix.target }} / ${{ matrix.backend }} / Unity ${{ matrix.unity }}
runs-on: ubuntu-latest-8-cores
# Cells settle to ~5-7 min for Unity 2021.3 (both backends) and
# ~22-25 min for Unity 6000.4 (Mono). Unity 6 IL2CPP on Mesa
# software OpenGL is the slow path. The 45 min cap covers the inner
# 40 min watchdog cap plus post-Unity steps (license return,
# Player.log copy, artifact upload, dorny/test-reporter) without
# leaving a stuck job sitting on the runner for the default 6 hours.
timeout-minutes: 45
env:
AUDIENCE_TEST_CELL_ID: ${{ matrix.target }}-${{ matrix.backend }}-${{ matrix.unity }}
strategy:
fail-fast: false
matrix:
unity: ['2021.3.45f2', '6000.4.0f1', '2022.3.62f2']
target: [StandaloneLinux64]
backend: [IL2CPP, Mono2x]
exclude: ${{ fromJSON(github.event_name == 'pull_request' && '[{"unity":"2022.3.62f2"}]' || '[]') }}
steps:
- uses: actions/checkout@v4
with:
lfs: true
- uses: actions/cache@v4
with:
path: examples/audience/Library
key: Library-${{ matrix.backend }}-${{ matrix.target }}-${{ matrix.unity }}-${{ hashFiles('examples/audience/Assets/**', 'examples/audience/Packages/**', 'examples/audience/ProjectSettings/**', 'src/Packages/Audience/**') }}
restore-keys: |
Library-${{ matrix.backend }}-${{ matrix.target }}-${{ matrix.unity }}-
Library-${{ matrix.backend }}-${{ matrix.target }}-
- name: Run PlayMode tests under xvfb
# Manual `docker run` instead of game-ci/unity-test-runner@v4: the
# action hardcodes `-batchmode -nographics` and never starts a
# virtual display, so every [UnityTest] in SampleAppLiveFireTests
# came back "inconclusive" (passed=0, failed=0). NUnit does not
# treat inconclusive as failure and the action's USE_EXIT_CODE=false
# suppresses Unity's exit code 2, so the cells silently went green
# without executing a single test. See SDK-318 for the diagnosis.
shell: bash
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
AUDIENCE_TEST_PUBLISHABLE_KEY: ${{ secrets.AUDIENCE_TEST_PUBLISHABLE_KEY }}
AUDIENCE_SCRIPTING_BACKEND: ${{ matrix.backend }}
UNITY_VERSION: ${{ matrix.unity }}
AUDIENCE_LINUX_GLCORE_ONLY: "1"
AUDIENCE_PLAYER_PROFILE_PATH: "/github/workspace/artifacts/player-profile.raw"
run: |
set -uo pipefail
mkdir -p artifacts
# The unityci/editor:ubuntu-...-linux-il2cpp-3 image ships both
# the Mono and IL2CPP playback engines plus xvfb, so the same
# tag works for both backends. The previous GameCI runs proved
# this: the StandaloneLinux64/Mono2x cell pulled the il2cpp tag.
image="unityci/editor:ubuntu-${UNITY_VERSION}-linux-il2cpp-3"
docker run --rm \
--workdir /github/workspace \
--env UNITY_EMAIL --env UNITY_PASSWORD --env UNITY_SERIAL \
--env AUDIENCE_TEST_PUBLISHABLE_KEY --env AUDIENCE_SCRIPTING_BACKEND \
--env AUDIENCE_TEST_RUN_ID --env AUDIENCE_TEST_CELL_ID \
--env AUDIENCE_LINUX_GLCORE_ONLY \
--env AUDIENCE_PLAYER_PROFILE_PATH \
--volume "$PWD":/github/workspace:z \
--cpus=8 --memory=30487m \
"$image" \
/bin/bash -c '
set -uo pipefail
# Per-run license activation. Unity occasionally exits non-zero
# on a successful activation (warnings about prior cached state
# in /root), so swallow the rc and assert on the success marker
# in the log instead. Same approach as the dropped self-hosted
# Linux job from SDK-255.
unity-editor -batchmode -nographics -quit \
-username "$UNITY_EMAIL" \
-password "$UNITY_PASSWORD" \
-serial "$UNITY_SERIAL" \
-logFile - 2>&1 | tee /github/workspace/artifacts/activation.log || true
if grep -qE "License activation has failed|\[Licensing::Client\] Error: Code [0-9]+" \
/github/workspace/artifacts/activation.log; then
echo "::error::Unity license activation failed."
exit 1
fi
if ! grep -qE "Successfully activated the entitlement license" \
/github/workspace/artifacts/activation.log; then
echo "::error::Unity license activation: no success marker in log."
exit 1
fi
# xvfb-run gives Unity a virtual X display so PlayMode tests
# that load scenes and exercise UI Toolkit can actually launch
# the player. GLX + render are required for UIElements; the
# image already bundles mesa-llvmpipe for software OpenGL, so
# no GPU is needed. -noreset keeps the X server up across
# Unity client reconnects.
#
# Why the watchdog instead of a simple `timeout` wrapper:
# - Unity 6 on Linux has a known shutdown hang. After
# "Test run completed", the editor begins
# `Application is shutting down...` and never fully exits.
# Without intervention the cell sits idle until the cell
# timeout fires before the post-Unity steps can run.
# - Tests on Unity 6 + Mesa software OpenGL take ~22 min for
# Mono and longer for IL2CPP, vs ~2 min on Unity 2021.3.
# A fixed timeout that fits 2021.3 cuts Unity 6 off mid-run;
# a fixed timeout sized for Unity 6 makes 2021.3 cells wait
# up to 30+ min on a shutdown hang they would not have hit.
# - The watchdog adapts: it scans the log, sees
# "Test run completed" the moment Unity finishes the suite,
# gives the editor 30 s to flush playmode-results.xml, then
# sends SIGTERM. SIGKILL follows 15 s later if the editor
# refuses to exit. Cells finish as soon as their tests do,
# regardless of how slow the underlying Unity version is or
# what shutdown bug it happens to hit.
# - 40 min hard cap as a fallback for the case where
# "Test run completed" never appears (player hang, etc.).
log=/github/workspace/artifacts/playmode.log
# The player renders at 320x240 (Unity -screen-width and
# -screen-height below). The xvfb desktop size does not
# affect fragment fill because Unity creates a GL context
# at the requested window size, regardless of how big the
# underlying X desktop is. xvfb-run with -a picks an
# unused display number and uses a default screen
# geometry; the X11 extension flags below are the only
# --server-args we actually need. Earlier comments in
# this file claimed the xvfb -screen flag was the source
# of the per-frame fill reduction; it was not.
# UI Toolkit lays out fine because tests assert on the
# VisualElement tree, not on rendered pixels.
#
# Why -force-glcore: Unity 6 prefers Vulkan on Linux and
# falls back to OpenGL when Vulkan init fails. Each frame
# carries the negotiation overhead. -force-glcore tells the
# player to skip Vulkan entirely and open a GLX context
# directly, the same path Unity 2021.3 takes by default.
xvfb-run -a --server-args="-ac +extension GLX +render -noreset" -- \
unity-editor \
-batchmode \
-force-glcore \
-screen-fullscreen 0 \
-screen-width 320 \
-screen-height 240 \
-projectPath /github/workspace/examples/audience \
-runTests \
-testPlatform StandaloneLinux64 \
-testResults /github/workspace/artifacts/playmode-results.xml \
-logFile "$log" &
unity_pid=$!
# Stream the log to job stdout for live visibility while the
# editor is alive. tail --pid exits when unity_pid does.
tail --pid=$unity_pid -F "$log" 2>/dev/null &
deadline=$((SECONDS + 2400)) # 40 min hard cap
flush_deadline=0
kill_reason=""
while kill -0 $unity_pid 2>/dev/null; do
if [ "$SECONDS" -ge "$deadline" ]; then
kill_reason="hard-cap-40m"
break
fi
if [ "$flush_deadline" -eq 0 ] && grep -q "Test run completed" "$log" 2>/dev/null; then
flush_deadline=$((SECONDS + 30))
echo "[watchdog] saw \"Test run completed\" at ${SECONDS}s; SIGTERM after 30s flush window"
fi
if [ "$flush_deadline" -gt 0 ] && [ "$SECONDS" -ge "$flush_deadline" ]; then
kill_reason="flush-window-elapsed"
break
fi
sleep 5
done
if [ -n "$kill_reason" ]; then
echo "[watchdog] sending SIGTERM to Unity (reason: $kill_reason)"
kill -TERM $unity_pid 2>/dev/null || true
# 15 s grace, then SIGKILL if still alive.
for _ in 1 2 3; do
kill -0 $unity_pid 2>/dev/null || break
sleep 5
done
if kill -0 $unity_pid 2>/dev/null; then
echo "[watchdog] SIGTERM not honored, sending SIGKILL"
kill -KILL $unity_pid 2>/dev/null || true
fi
fi
wait $unity_pid 2>/dev/null
test_rc=$?
if [ "$kill_reason" = "hard-cap-40m" ]; then
echo "::warning::Unity hit the 40 min hard cap without logging \"Test run completed\". The player may have hung mid-suite. Inspect Player.log to see how far it got."
fi
# Capture the standalone test player log. PlayMode tests on
# StandaloneLinux64 build a player binary that runs the suite
# in its own process; the editor stdout we tee above never
# sees the player'\''s HTTP traces, OnError callbacks, or cert
# failures. Without this, "39 passed" tells us FlushAsync
# returned without throwing but says nothing about whether
# events actually reached CDP.
# Linux convention: ~/.config/unity3d/<Company>/<Product>/Player.log.
# Glob across companies / products so the capture survives any
# rename of the sample app.
find /root/.config/unity3d -name "Player.log" 2>/dev/null | while IFS= read -r f; do
co=$(basename "$(dirname "$(dirname "$f")")")
pr=$(basename "$(dirname "$f")")
cp "$f" "/github/workspace/artifacts/Player-${co}-${pr}.log" 2>/dev/null || true
done
# Always return the seat so reruns and parallel cells do not
# exhaust the activation pool. Tolerate non-zero in case the
# editor process is still mid-shutdown.
unity-editor -batchmode -nographics -quit -returnlicense -logFile - 2>&1 || true
# Unity exits 2 when any test fails or comes back inconclusive.
# Propagating the rc here means the step fails on real failures
# without needing the USE_EXIT_CODE=false hack the GameCI
# action applies.
exit $test_rc
'
- name: Fail when no tests actually executed
# Defense in depth: catches the silent-pass case if a future change
# accidentally re-disables the display, breaks the player launch,
# or restores USE_EXIT_CODE=false on the docker invocation. NUnit
# marks tests "inconclusive" rather than "failed" when the player
# never starts, and dorny/test-reporter does not flag inconclusive
# as failure, so the suite has to be inspected directly.
if: always()
shell: bash
run: |
set -euo pipefail
xml="artifacts/playmode-results.xml"
if [ ! -f "$xml" ]; then
echo "::error::No test-results.xml at $xml. Unity did not produce results."
exit 1
fi
# Unity's NUnit writer puts the root summary attributes on a
# single <test-run ...> line, so a grep parse is enough and
# avoids depending on libxml2-utils (which is not preinstalled
# on ubuntu-latest-8-cores; the previous xmllint guard failed
# with "command not found" while tests had actually passed).
line=$(grep -m1 '<test-run ' "$xml" || true)
if [ -z "$line" ]; then
echo "::error::No <test-run> element in $xml. The XML is malformed."
exit 1
fi
extract() {
printf %s "$1" | grep -oE " $2=\"[0-9]+\"" | head -1 | grep -oE '[0-9]+'
}
passed=$(extract "$line" passed)
failed=$(extract "$line" failed)
inconclusive=$(extract "$line" inconclusive)
echo "passed=${passed:-?} failed=${failed:-?} inconclusive=${inconclusive:-?}"
if [ "${inconclusive:-0}" -gt 0 ]; then
echo "::error::$inconclusive test(s) came back inconclusive. Unity could not actually execute them. Check that xvfb is running and the player launches."
exit 1
fi
if [ "${passed:-0}" -eq 0 ] && [ "${failed:-0}" -eq 0 ]; then
echo "::error::Zero tests passed and zero failed. The suite did not execute."
exit 1
fi
- name: Surface CI provenance to job summary
# The SampleApp prints `[CI] buildGuid=... runId=... cellId=...`
# to Player.log on player startup. buildGuid is per-build and
# already lands on every game_launch event the SDK emits, so a
# one-line grep here gives a 1:1 match between the CI cell and
# CDP rows. The job summary makes it copy-pasteable from the
# Actions UI without downloading the artifact.
if: always()
shell: bash
run: |
set -uo pipefail
log=""
for candidate in artifacts/Player-*.log; do
[ -f "$candidate" ] && { log="$candidate"; break; }
done
{
echo "## CI provenance"
if [ -z "$log" ]; then
echo "_No Player.log captured for this cell._"
exit 0
fi
line=$(grep -m1 '\[CI\]' "$log" || true)
if [ -z "$line" ]; then
echo "_No [CI] line found in \`$log\`. The SampleApp's RuntimeInitializeOnLoad hook may not have fired._"
exit 0
fi
echo
echo '```'
echo "$line"
echo '```'
echo
echo "Filter CDP for events with this \`buildGuid\` to see the rows from this cell."
} >> "$GITHUB_STEP_SUMMARY"
- name: Publish test report
uses: dorny/test-reporter@v3
if: always()
with:
name: PlayMode (${{ matrix.backend }} / ${{ matrix.target }})
path: artifacts/playmode-results.xml
reporter: dotnet-nunit
fail-on-error: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: playmode-${{ matrix.backend }}-${{ matrix.target }}-${{ matrix.unity }}
path: |
artifacts/playmode-results.xml
artifacts/playmode.log
artifacts/activation.log
artifacts/Player-*.log
artifacts/player-profile.raw
examples/audience/Logs/**
# Mobile IL2CPP build validation — runs on GitHub-hosted Ubuntu via GameCI Docker
# containers so self-hosted macOS/Windows machines are not occupied.
# Scope: IL2CPP compile pipeline only. Runtime tests require a real device and
# are out of scope until a device farm is available.
mobile-build:
if: |
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
|| github.event_name == 'schedule'
|| github.event_name == 'workflow_dispatch'
name: ${{ matrix.target }} / IL2CPP / Unity ${{ matrix.unity }}
runs-on: ubuntu-latest-8-cores
strategy:
fail-fast: false
matrix:
target: [Android, iOS]
unity: ['2021.3.45f2', '2022.3.62f2', '6000.4.0f1']
include:
- target: Android
method: AndroidBuilder.Build
- target: iOS
method: IosBuilder.Build
steps:
- uses: actions/checkout@v4
with:
lfs: true
- uses: actions/cache@v4
with:
path: examples/audience/Library
key: Library-mobile-${{ matrix.target }}-${{ matrix.unity }}-${{ hashFiles('examples/audience/Assets/**', 'examples/audience/Packages/**', 'examples/audience/ProjectSettings/**', 'src/Packages/Audience/**') }}
restore-keys: |
Library-mobile-${{ matrix.target }}-${{ matrix.unity }}-
Library-mobile-${{ matrix.target }}-
- uses: game-ci/unity-builder@v4
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
with:
unityVersion: ${{ matrix.unity }}
targetPlatform: ${{ matrix.target }}
projectPath: examples/audience
buildMethod: Immutable.Audience.Samples.SampleApp.Editor.${{ matrix.method }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: mobile-build-${{ matrix.target }}-${{ matrix.unity }}
if-no-files-found: ignore
path: |
examples/audience/Builds/Android/*.apk
examples/audience/Logs/**