ci(audience): Linux PlayMode runs under xvfb (SDK-317) #183
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| 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 | |
| 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 | |
| # Tests now actually execute under xvfb instead of returning instantly | |
| # as inconclusive, so cells take ~5-10 min. The 30 min cap leaves | |
| # headroom for cold caches and the first image pull without leaving | |
| # a stuck job sitting on the runner for the default 6 hours. | |
| timeout-minutes: 30 | |
| 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 }} | |
| 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 \ | |
| --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 (the editor opens / closes / reopens | |
| # connections during scene load). | |
| xvfb-run -a --server-args="-screen 0 1280x720x24 -ac +extension GLX +render -noreset" -- \ | |
| unity-editor \ | |
| -batchmode \ | |
| -projectPath /github/workspace/examples/audience \ | |
| -runTests \ | |
| -testPlatform StandaloneLinux64 \ | |
| -testResults /github/workspace/artifacts/playmode-results.xml \ | |
| -logFile - 2>&1 | tee /github/workspace/artifacts/playmode.log | |
| test_rc=${PIPESTATUS[0]} | |
| # 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: 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 | |
| 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/** |