From ca537736ad40295d68954b9c18cc43d0e5961b74 Mon Sep 17 00:00:00 2001 From: Vladislav Panin Date: Fri, 16 Oct 2026 01:05:28 +0300 Subject: [PATCH 1/4] ci: try the Unity CLI in place of GameCI for one EditMode job An experimental workflow that runs the EditMode tests of 6000.0.53f1 on the bare runner: the Unity CLI installs the Editor (cached as an installed folder), the Licensing Client bundled in the Editor takes the Personal seat, and `unity test` runs the tests and writes NUnit and JUnit reports. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/unity-cli-experiment.yml | 170 +++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 .github/workflows/unity-cli-experiment.yml diff --git a/.github/workflows/unity-cli-experiment.yml b/.github/workflows/unity-cli-experiment.yml new file mode 100644 index 00000000..ec7a2e93 --- /dev/null +++ b/.github/workflows/unity-cli-experiment.yml @@ -0,0 +1,170 @@ +name: Unity CLI experiment + +# An experiment, not a required check: can the official Unity CLI replace game-ci/unity-test-runner in the Unity +# EditMode job of ci.yml, and does it make the job simpler or faster? It runs the same throwaway project and tests on +# the bare runner, without a container. The CLI installs the Editor, the Licensing Client inside that Editor takes the +# Personal seat (`unity license activate --personal` rejects CI credentials), and `unity test` runs the tests. +# This file does not match the Unity plan regex of ci.yml, so a PR that changes only this file skips the GameCI matrix +# and the two never compete for the Personal seats of one account. + +on: + pull_request: + paths: + - .github/workflows/unity-cli-experiment.yml + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + unity-tests: + name: Unity CLI EditMode (${{ matrix.unity }}) + runs-on: ubuntu-latest + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + unity: [6000.0.53f1] + env: + UNITY: ${{ matrix.unity }} + # The CLI release this job was measured with. install.sh verifies the binary against that release's manifest. + UNITY_CLI_VERSION: 1.0.0-beta.13 + # A restored cache holds the Editor folder only: the CLI's install path is set to this root on every run. + EDITORS: ${{ github.workspace }}/../unity-editors + UNITY_NON_INTERACTIVE: 1 + UNITY_NO_CONSENT_PROMPT: 1 + UNITY_NO_CRASH_REPORT: 1 + UNITY_NO_UPDATE_CHECK: 1 + UNITY_NO_BANNER: 1 + steps: + # What the test project reads: the package and the script that builds the project. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + sparse-checkout: | + Aspid.FastTools/Packages/tech.aspid.fasttools + scripts + + - name: Install the Unity CLI + run: | + curl -fsSL https://unity.com/install.sh | bash + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + "$HOME/.local/bin/unity" --version + + # The installed Editor, not the downloaded archives: restoring the folder skips both the download and the unpack. + - id: editor-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ env.EDITORS }}/${{ matrix.unity }} + key: unity-editor-${{ runner.os }}-${{ matrix.unity }} + + - name: Install the Unity Editor + run: | + mkdir -p "$EDITORS" + unity install-path --set "$EDITORS" + if [ ! -x "$EDITORS/$UNITY/Editor/Unity" ]; then + unity install "$UNITY" --yes --accept-eula + fi + unity editors -i --format json + editor="$EDITORS/$UNITY/Editor/Unity" + if [ ! -x "$editor" ]; then + echo "::error::No Editor at $editor after the install; see the list above." + exit 1 + fi + echo "UNITY_EDITOR=$editor" >> "$GITHUB_ENV" + du -sh "$EDITORS/$UNITY" + df -h "$EDITORS" + + # Saved right after the install, so a failed test run still leaves the Editor cached for the next one. + - if: steps.editor-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ env.EDITORS }}/${{ matrix.unity }} + key: ${{ steps.editor-cache.outputs.cache-primary-key }} + + - name: Create the test project + run: scripts/make-unity-test-project.sh ci-unity-project "$UNITY" + + # The client exits 0 also when Unity assigned no seat; only a "Seat ID:" line in its output means a seat. Unity + # sometimes assigns none when several jobs activate one account at once, so that case gets two more attempts. + # GitHub masks both secrets in the log. + - name: Activate the Personal license + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + run: | + client="$(dirname "$UNITY_EDITOR")/Data/Resources/Licensing/Client/Unity.Licensing.Client" + log="$RUNNER_TEMP/activate.log" + for attempt in 1 2 3; do + "$client" --activate-all --include-personal --username "$UNITY_EMAIL" --password "$UNITY_PASSWORD" 2>&1 | tee "$log" || true + if grep -q 'Seat ID:' "$log"; then exit 0; fi + if grep -qiE 'invalid (username|password|credentials)|two.?factor|verification code' "$log"; then break; fi + if [ "$attempt" -lt 3 ]; then + echo "::warning::No Personal seat on attempt $attempt; trying again." + sleep $((30 * attempt)) + fi + done + echo "::error::The Licensing Client assigned no Personal seat; see its output above." + exit 1 + + # Exit 8: the tests ran and some failed. Exit 6: no verdict (license, compile error, crash, timeout); that one + # gets a second attempt, as in ci.yml. The Editor log goes to a file next to the results. + - name: Run the EditMode tests + run: | + results="$GITHUB_WORKSPACE/unity-test-results" + for attempt in 1 2; do + code=0 + unity test ci-unity-project --editor-path "$UNITY_EDITOR" --mode EditMode \ + --report-format nunit,junit --output "$results/editmode-results.xml" --timeout 1200 --format github \ + -- -nographics -aspidFastToolsStrictSetup -logFile "$results/editor-$attempt.log" || code=$? + echo "unity test exited with $code." + if [ "$code" -ne 0 ]; then + echo "::group::Editor log, last 200 lines" + tail -n 200 "$results/editor-$attempt.log" || true + echo "::endgroup::" + fi + if [ "$code" -ne 6 ] || [ "$attempt" -eq 2 ]; then exit "$code"; fi + echo "::warning::The test run gave no verdict (exit 6); trying once more." + sleep 30 + done + + # A Personal seat is an entitlement, not a .ulf: GameCI's log shows `--return-ulf` failing with "Ulf license file + # not found (1404)" and the Editor's -returnlicense handing the seat back. Both are tried in that order, and + # `unity license status` shows on either side whether the CLI itself sees the seat. + - name: Return the Personal license + if: always() && env.UNITY_EDITOR != '' + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + run: | + client="$(dirname "$UNITY_EDITOR")/Data/Resources/Licensing/Client/Unity.Licensing.Client" + log="$RUNNER_TEMP/return.log" + unity license status || true + "$client" --return-ulf 2>&1 | tee "$log" || true + if ! grep -q 'Successfully returned' "$log"; then + mkdir -p "$RUNNER_TEMP/return-license" + "$UNITY_EDITOR" -batchmode -nographics -quit -returnlicense -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" \ + -projectPath "$RUNNER_TEMP/return-license" -logFile - 2>&1 | tee "$log" || true + fi + unity license status || true + if ! grep -q 'Successfully returned' "$log"; then + echo "::warning::The Personal seat may still be held; release it at https://id.unity.com." + fi + + # The Editor log can name the account; the uploaded copy must not. + - name: Remove the account from the Editor logs + if: always() + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + run: | + [ -n "$UNITY_EMAIL" ] || exit 0 + shopt -s nullglob + for f in unity-test-results/*.log; do perl -pi -e 's/\Q$ENV{UNITY_EMAIL}\E/***/g' "$f"; done + + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + if: always() + with: + name: unity-cli-test-results-${{ matrix.unity }} + path: unity-test-results From fbb55dc826a822f84e816a9566f96501208024ba Mon Sep 17 00:00:00 2001 From: Vladislav Panin Date: Fri, 16 Oct 2026 01:07:19 +0300 Subject: [PATCH 2/4] ci: set the Editor install path without .. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/unity-cli-experiment.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unity-cli-experiment.yml b/.github/workflows/unity-cli-experiment.yml index ec7a2e93..d27fd545 100644 --- a/.github/workflows/unity-cli-experiment.yml +++ b/.github/workflows/unity-cli-experiment.yml @@ -32,8 +32,6 @@ jobs: UNITY: ${{ matrix.unity }} # The CLI release this job was measured with. install.sh verifies the binary against that release's manifest. UNITY_CLI_VERSION: 1.0.0-beta.13 - # A restored cache holds the Editor folder only: the CLI's install path is set to this root on every run. - EDITORS: ${{ github.workspace }}/../unity-editors UNITY_NON_INTERACTIVE: 1 UNITY_NO_CONSENT_PROMPT: 1 UNITY_NO_CRASH_REPORT: 1 @@ -51,6 +49,9 @@ jobs: run: | curl -fsSL https://unity.com/install.sh | bash echo "$HOME/.local/bin" >> "$GITHUB_PATH" + # A restored cache holds the Editor folder only: the CLI's install path is set to this root on every run. + # `unity install-path --set` rejects a path with `..`, so it is not built from github.workspace. + echo "EDITORS=$HOME/unity-editors" >> "$GITHUB_ENV" "$HOME/.local/bin/unity" --version # The installed Editor, not the downloaded archives: restoring the folder skips both the download and the unpack. From 65c219efada2ea4a536cdba1d2e534cc0fbeba8f Mon Sep 17 00:00:00 2001 From: Vladislav Panin Date: Fri, 16 Oct 2026 01:21:10 +0300 Subject: [PATCH 3/4] ci: run the CLI tests on a virtual display and return the seat via the CLI Co-Authored-By: Claude Opus 5.5 --- .github/workflows/unity-cli-experiment.yml | 35 ++++++++++++---------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/.github/workflows/unity-cli-experiment.yml b/.github/workflows/unity-cli-experiment.yml index d27fd545..ecb5819f 100644 --- a/.github/workflows/unity-cli-experiment.yml +++ b/.github/workflows/unity-cli-experiment.yml @@ -75,7 +75,7 @@ jobs: exit 1 fi echo "UNITY_EDITOR=$editor" >> "$GITHUB_ENV" - du -sh "$EDITORS/$UNITY" + du -h -d 4 "$EDITORS/$UNITY" | sort -h | tail -n 25 df -h "$EDITORS" # Saved right after the install, so a failed test run still leaves the Editor cached for the next one. @@ -112,14 +112,18 @@ jobs: # Exit 8: the tests ran and some failed. Exit 6: no verdict (license, compile error, crash, timeout); that one # gets a second attempt, as in ci.yml. The Editor log goes to a file next to the results. + # The tests that open an EditorWindow or paint IMGUI need a graphics device: with -nographics 68 of them fail + # with "No graphic device is available to initialize the view". The Editor runs on a virtual X display instead, + # as in the GameCI image, whose unity-editor wrapper calls xvfb-run. - name: Run the EditMode tests run: | results="$GITHUB_WORKSPACE/unity-test-results" for attempt in 1 2; do code=0 - unity test ci-unity-project --editor-path "$UNITY_EDITOR" --mode EditMode \ + xvfb-run -a -s '-screen 0 1024x768x24' \ + unity test ci-unity-project --editor-path "$UNITY_EDITOR" --mode EditMode \ --report-format nunit,junit --output "$results/editmode-results.xml" --timeout 1200 --format github \ - -- -nographics -aspidFastToolsStrictSetup -logFile "$results/editor-$attempt.log" || code=$? + -- -aspidFastToolsStrictSetup -logFile "$results/editor-$attempt.log" || code=$? echo "unity test exited with $code." if [ "$code" -ne 0 ]; then echo "::group::Editor log, last 200 lines" @@ -131,27 +135,26 @@ jobs: sleep 30 done - # A Personal seat is an entitlement, not a .ulf: GameCI's log shows `--return-ulf` failing with "Ulf license file - # not found (1404)" and the Editor's -returnlicense handing the seat back. Both are tried in that order, and - # `unity license status` shows on either side whether the CLI itself sees the seat. + # `unity license status` exits 0 while a license is active and 4 when none is, so it tells whether the seat is + # back. The CLI's own return is tried first; the Editor's -returnlicense, which GameCI uses, is the fallback. + # `Unity.Licensing.Client --return-ulf` cannot hand it back: a Personal seat is an entitlement, not a .ulf, and the + # client fails with "Ulf license file not found (1404)". - name: Return the Personal license if: always() && env.UNITY_EDITOR != '' env: UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} run: | - client="$(dirname "$UNITY_EDITOR")/Data/Resources/Licensing/Client/Unity.Licensing.Client" - log="$RUNNER_TEMP/return.log" - unity license status || true - "$client" --return-ulf 2>&1 | tee "$log" || true - if ! grep -q 'Successfully returned' "$log"; then + unity license status || exit 0 + unity license return --yes || true + if unity license status; then + echo "::warning::unity license return left the seat active; returning it through the Editor." mkdir -p "$RUNNER_TEMP/return-license" "$UNITY_EDITOR" -batchmode -nographics -quit -returnlicense -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" \ - -projectPath "$RUNNER_TEMP/return-license" -logFile - 2>&1 | tee "$log" || true - fi - unity license status || true - if ! grep -q 'Successfully returned' "$log"; then - echo "::warning::The Personal seat may still be held; release it at https://id.unity.com." + -projectPath "$RUNNER_TEMP/return-license" -logFile - || true + if unity license status; then + echo "::warning::The Personal seat may still be held; release it at https://id.unity.com." + fi fi # The Editor log can name the account; the uploaded copy must not. From e0eada9de3ed854550821bd5425f5c001dedfb80 Mon Sep 17 00:00:00 2001 From: Vladislav Panin Date: Fri, 16 Oct 2026 01:43:18 +0300 Subject: [PATCH 4/4] ci: run the CLI experiment on every Unity version of ci.yml Co-Authored-By: Claude Opus 5.5 --- .github/workflows/unity-cli-experiment.yml | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/unity-cli-experiment.yml b/.github/workflows/unity-cli-experiment.yml index ecb5819f..58f18912 100644 --- a/.github/workflows/unity-cli-experiment.yml +++ b/.github/workflows/unity-cli-experiment.yml @@ -27,7 +27,9 @@ jobs: strategy: fail-fast: false matrix: - unity: [6000.0.53f1] + # The versions of ci.yml. All of them start at once, as there: on the bare runner each job is a new machine + # and asks for a seat of its own, while the GameCI container reuses one machine id ("SameMachine"). + unity: [6000.0.53f1, 6000.1.17f1, 6000.2.15f1, 6000.3.26f1, 6000.4.12f1, 6000.5.11f1, 6000.6.5f1] env: UNITY: ${{ matrix.unity }} # The CLI release this job was measured with. install.sh verifies the binary against that release's manifest. @@ -75,11 +77,13 @@ jobs: exit 1 fi echo "UNITY_EDITOR=$editor" >> "$GITHUB_ENV" - du -h -d 4 "$EDITORS/$UNITY" | sort -h | tail -n 25 + du -h -d 4 "$EDITORS/$UNITY" | sort -h | tail -n 8 + du -h -d 1 "$EDITORS/$UNITY/Editor/Data/Resources/PackageManager" | sort -h df -h "$EDITORS" # Saved right after the install, so a failed test run still leaves the Editor cached for the next one. - - if: steps.editor-cache.outputs.cache-hit != 'true' + # One entry is 4.5 GB and the repository holds 10 GB, so only the minimum version is cached here. + - if: steps.editor-cache.outputs.cache-hit != 'true' && matrix.unity == '6000.0.53f1' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.EDITORS }}/${{ matrix.unity }} @@ -136,9 +140,9 @@ jobs: done # `unity license status` exits 0 while a license is active and 4 when none is, so it tells whether the seat is - # back. The CLI's own return is tried first; the Editor's -returnlicense, which GameCI uses, is the fallback. - # `Unity.Licensing.Client --return-ulf` cannot hand it back: a Personal seat is an entitlement, not a .ulf, and the - # client fails with "Ulf license file not found (1404)". + # back. Neither CLI route returns a Personal seat: `unity license return` answers "free Personal licenses can't + # be returned", and `Unity.Licensing.Client --return-ulf` fails with "Ulf license file not found (1404)", since + # the seat is an entitlement and not a .ulf. The Editor's -returnlicense does it, as in GameCI. - name: Return the Personal license if: always() && env.UNITY_EDITOR != '' env: @@ -146,15 +150,11 @@ jobs: UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} run: | unity license status || exit 0 - unity license return --yes || true + mkdir -p "$RUNNER_TEMP/return-license" + "$UNITY_EDITOR" -batchmode -nographics -quit -returnlicense -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" \ + -projectPath "$RUNNER_TEMP/return-license" -logFile - || true if unity license status; then - echo "::warning::unity license return left the seat active; returning it through the Editor." - mkdir -p "$RUNNER_TEMP/return-license" - "$UNITY_EDITOR" -batchmode -nographics -quit -returnlicense -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" \ - -projectPath "$RUNNER_TEMP/return-license" -logFile - || true - if unity license status; then - echo "::warning::The Personal seat may still be held; release it at https://id.unity.com." - fi + echo "::warning::The Personal seat may still be held; release it at https://id.unity.com." fi # The Editor log can name the account; the uploaded copy must not.