DO NOT MERGE: fix: add mechanism to force stop stuck emotes #28417
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: Unity Test | |
| on: | |
| workflow_dispatch: | |
| pull_request: | |
| paths: | |
| - 'Explorer/**' | |
| types: | |
| - opened | |
| - reopened | |
| - synchronize | |
| - ready_for_review | |
| - labeled | |
| merge_group: {} | |
| push: { branches: [dev] } | |
| permissions: | |
| contents: read | |
| packages: write | |
| checks: write | |
| actions: read | |
| env: | |
| UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} | |
| UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} | |
| UNITY_LICENSE: ${{ secrets.UNITY_PERSONAL_LICENSE }} | |
| UNITY_IMAGE_FLAVOR: linux-il2cpp | |
| UNITY_IMAGE_VERSION: 3.2.0 | |
| GHCR_IMAGE_NAME: unityci-editor | |
| INSPECT_REPORT_JSON_FILE_NAME: InspectCodeReport.json | |
| WARNINGS_BASELINE_S3_KEY: '@dcl/unity-explorer/ci/warnings-baseline.json' | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| changes: | |
| if: (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev')) || (github.event.pull_request.draft == false) || (github.event.label.name == 'force-build') || (github.event.label.name == 'clean-build') || (github.event.label.name == 'force-lint') | |
| name: Detect C# changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| cs: ${{ steps.detect.outputs.cs }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| fetch-depth: 0 | |
| - name: Detect changed C# files | |
| id: detect | |
| env: | |
| EVENT: ${{ github.event_name }} | |
| BASE_REF: ${{ github.event.pull_request.base.ref }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| BEFORE_SHA: ${{ github.event.before }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$EVENT" = "pull_request" ]; then | |
| base=$(git merge-base "origin/$BASE_REF" "$HEAD_SHA" 2>/dev/null || true) | |
| if [ -z "$base" ]; then | |
| echo "merge-base with origin/$BASE_REF unavailable - linting to be safe." | |
| echo "cs=true" >> "$GITHUB_OUTPUT"; exit 0 | |
| fi | |
| range="$base $HEAD_SHA" | |
| elif [ "$EVENT" = "push" ] && [ -n "$BEFORE_SHA" ] && [ "$BEFORE_SHA" != "0000000000000000000000000000000000000000" ] && git cat-file -e "${BEFORE_SHA}^{commit}" 2>/dev/null; then | |
| range="$BEFORE_SHA ${{ github.sha }}" | |
| else | |
| echo "Cannot determine a diff base for event '$EVENT' - linting to be safe." | |
| echo "cs=true" >> "$GITHUB_OUTPUT"; exit 0 | |
| fi | |
| if git diff --name-only $range -- '*.cs' | grep -q .; then | |
| echo "C# files changed - lint will run." | |
| echo "cs=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "No C# files changed - skipping lint." | |
| echo "cs=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| lint: | |
| needs: changes | |
| if: needs.changes.outputs.cs == 'true' || github.event.label.name == 'force-lint' | |
| name: Lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout CI actions | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: .github/actions | |
| sparse-checkout-cone-mode: false | |
| path: ci-actions | |
| - name: Setup Docker disk | |
| uses: ./ci-actions/.github/actions/setup-docker-disk | |
| with: | |
| keep-dotnet: 'true' | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| lfs: true | |
| submodules: recursive | |
| - name: Create LFS file list | |
| run: git lfs ls-files -l | awk '{print $1}' | sort > .lfs-assets-id | |
| - name: Restore LFS cache | |
| uses: actions/cache@v4 | |
| id: lfs-cache-lint | |
| with: | |
| path: .git/lfs | |
| key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }} | |
| - name: Git LFS Pull | |
| run: | | |
| git lfs pull | |
| git add . | |
| git reset --hard | |
| - uses: webfactory/ssh-agent@v0.8.0 | |
| with: | |
| ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| - name: Add GitHub to the SSH known hosts file | |
| run: | | |
| mkdir -p -m 0700 /home/runner/.ssh | |
| curl --silent https://api.github.com/meta | jq --raw-output '"github.com "+.ssh_keys[]' >> /home/runner/.ssh/known_hosts | |
| chmod 600 /home/runner/.ssh/known_hosts | |
| - name: Prepare SSH for Unity | |
| run: | | |
| rm -rf /tmp/ssh-unity | |
| mkdir -p -m 0700 /tmp/ssh-unity | |
| cp /home/runner/.ssh/known_hosts /tmp/ssh-unity/known_hosts | |
| cp /home/runner/.ssh/key-* /tmp/ssh-unity/ 2>/dev/null || true | |
| chmod 600 /tmp/ssh-unity/* | |
| - name: Prepare Unity writable dirs | |
| run: | | |
| mkdir -p /tmp/unity-home/.local/share/unity3d | |
| mkdir -p /tmp/unity-home/.cache/unity3d | |
| mkdir -p /tmp/unity-home/.config/unity3d | |
| chmod -R 777 /tmp/unity-home | |
| - name: Read Unity version from ProjectVersion.txt | |
| id: unity-version-lint | |
| run: | | |
| verFile="Explorer/ProjectSettings/ProjectVersion.txt" | |
| ver=$(grep -o 'm_EditorVersion: [0-9a-f.]*' "$verFile" | cut -d' ' -f2) | |
| echo "version=$ver" >> $GITHUB_OUTPUT | |
| - name: Set image names | |
| id: img-lint | |
| run: | | |
| version="${{ steps.unity-version-lint.outputs.version }}" | |
| flavor="${{ env.UNITY_IMAGE_FLAVOR }}" | |
| imgVer="${{ env.UNITY_IMAGE_VERSION }}" | |
| tag="ubuntu-$version-$flavor-$imgVer" | |
| owner=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') | |
| upstream="unityci/editor:$tag" | |
| ghcr="ghcr.io/$owner/${{ env.GHCR_IMAGE_NAME }}:$tag" | |
| echo "tag=$tag" >> $GITHUB_OUTPUT | |
| echo "upstream=$upstream" >> $GITHUB_OUTPUT | |
| echo "ghcr=$ghcr" >> $GITHUB_OUTPUT | |
| - name: Log in to GitHub Container Registry | |
| run: | | |
| for i in 1 2 3; do | |
| echo "${{ github.token }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin && exit 0 | |
| echo "GHCR login failed, retry $i/3" | |
| sleep $((i * 10)) | |
| done | |
| exit 1 | |
| - name: Restore Unity image from GHCR or seed from Docker Hub | |
| run: | | |
| set -o pipefail | |
| ghcr="${{ steps.img-lint.outputs.ghcr }}" | |
| upstream="${{ steps.img-lint.outputs.upstream }}" | |
| if docker pull "$ghcr" 2>&1 | tee /tmp/ghcr-pull.log; then | |
| exit 0 | |
| fi | |
| if grep -qiE 'manifest unknown|not found' /tmp/ghcr-pull.log; then | |
| echo "GHCR image missing, trying upstream..." | |
| docker pull "$upstream" | |
| docker tag "$upstream" "$ghcr" | |
| docker push "$ghcr" | |
| else | |
| echo "GHCR pull failed, but not because the image is missing." | |
| cat /tmp/ghcr-pull.log | |
| exit 1 | |
| fi | |
| # - name: Activate Unity license | |
| # run: | | |
| # docker run --rm \ | |
| # -v /tmp/unity-home/.local/share/unity3d:/root/.local/share/unity3d \ | |
| # -v /tmp/unity-home/.cache/unity3d:/root/.cache/unity3d \ | |
| # -v /tmp/unity-home/.config/unity3d:/root/.config/unity3d \ | |
| # -e UNITY_EMAIL="$UNITY_EMAIL" \ | |
| # -e UNITY_PASSWORD="$UNITY_PASSWORD" \ | |
| # -e UNITY_LICENSE="$UNITY_LICENSE" \ | |
| # ${{ steps.img-lint.outputs.ghcr }} \ | |
| # /bin/bash -lc 'source /opt/unity/activate.sh 2>/dev/null || true; unity-editor -batchmode -nographics -quit -logFile /dev/stdout' | |
| # # C# Linting using JetBrains ReSharper CLI | |
| # - name: Generate solution inside Unity Docker container | |
| # run: | | |
| # docker run --rm \ | |
| # -v "${{ github.workspace }}:/workspace" \ | |
| # -v $SSH_AUTH_SOCK:$SSH_AUTH_SOCK \ | |
| # -v /tmp/ssh-unity:/root/.ssh:ro \ | |
| # -v /tmp/unity-home/.local/share/unity3d:/root/.local/share/unity3d \ | |
| # -v /tmp/unity-home/.cache/unity3d:/root/.cache/unity3d \ | |
| # -v /tmp/unity-home/.config/unity3d:/root/.config/unity3d \ | |
| # -e SSH_AUTH_SOCK=$SSH_AUTH_SOCK \ | |
| # -e UNITY_EMAIL="$UNITY_EMAIL" \ | |
| # -e UNITY_PASSWORD="$UNITY_PASSWORD" \ | |
| # -e UNITY_LICENSE="$UNITY_LICENSE" \ | |
| # -w /workspace \ | |
| # ${{ steps.img-lint.outputs.ghcr }} \ | |
| # unity-editor \ | |
| # -batchmode \ | |
| # -nographics \ | |
| # -projectPath /workspace/Explorer \ | |
| # -executeMethod UnityEditor.SyncVS.SyncSolution \ | |
| # -quit \ | |
| # -logFile - | |
| - name: Rewrite GitHub SSH URLs to HTTPS | |
| run: | | |
| git config --global url."https://x-access-token:${{ secrets.REPOS_READ_ONLY_TOKEN }}@github.com/".insteadOf "git@github.com:" | |
| git config --global url."https://x-access-token:${{ secrets.REPOS_READ_ONLY_TOKEN }}@github.com/".insteadOf "ssh://git@github.com/" | |
| - name: Generate solution with GameCI Builder | |
| uses: game-ci/unity-builder@v4 | |
| env: | |
| UNITY_LICENSE: ${{ secrets.UNITY_PERSONAL_LICENSE }} | |
| UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} | |
| UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} | |
| GIT_PRIVATE_TOKEN: ${{ secrets.REPOS_READ_ONLY_TOKEN }} | |
| # GIT_CONFIG_EXTENSIONS: true | |
| with: | |
| projectPath: Explorer | |
| customImage: ${{ steps.img-lint.outputs.ghcr }} | |
| targetPlatform: StandaloneLinux64 | |
| buildMethod: Packages.Rider.Editor.RiderScriptEditor.SyncSolution | |
| - name: Download ReSharper Command Line Tools | |
| run: bash scripts/lint/download-resharper.sh rsharp | |
| - name: Run ReSharper InspectCode (static analysis) | |
| run: bash scripts/lint/run-inspectcode.sh Explorer/Explorer.sln "$INSPECT_REPORT_JSON_FILE_NAME" | |
| # Filter false positives ('.CSharpErrors'/'.CppCompilerErrors' from '--no-build') and count. | |
| - name: Count warnings | |
| id: warnings | |
| uses: ./.github/actions/count-warnings | |
| with: | |
| report: ${{ env.INSPECT_REPORT_JSON_FILE_NAME }} | |
| filtered_output: filtered.json | |
| - name: Read warnings baseline from S3 | |
| id: baseline | |
| env: | |
| AWS_MAX_ATTEMPTS: 3 | |
| AWS_RETRY_MODE: standard | |
| AWS_ACCESS_KEY_ID: ${{ secrets.EXPLORER_TEAM_AWS_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.EXPLORER_TEAM_AWS_SECRET_ACCESS_KEY }} | |
| EXPLORER_TEAM_S3_BUCKET: ${{ secrets.EXPLORER_TEAM_S3_BUCKET }} | |
| run: | | |
| # Read directly from s3 to avoid cache stales | |
| baseline=$(aws s3 cp "s3://$EXPLORER_TEAM_S3_BUCKET/${{ env.WARNINGS_BASELINE_S3_KEY }}" - 2>/dev/null | jq -r '.count' 2>/dev/null || echo "") | |
| echo "baseline=$baseline" >> "$GITHUB_OUTPUT" | |
| echo "Baseline warnings: ${baseline:-<none>}" | |
| # Hard gate: a PR may only merge if it strictly reduces the warning count. | |
| # No baseline yet -> pass (the first dev push seeds it). Add the 'no-warning-ratchet' | |
| # label to bypass on a PR that legitimately cannot touch warnings. | |
| - name: Enforce warning reduction (PR) | |
| if: github.event_name == 'pull_request' | |
| env: | |
| COUNT: ${{ steps.warnings.outputs.count }} | |
| BASELINE: ${{ steps.baseline.outputs.baseline }} | |
| BYPASS: ${{ contains(github.event.pull_request.labels.*.name, 'no-warning-ratchet') }} | |
| run: | | |
| if [ -z "$BASELINE" ]; then | |
| echo "No baseline established yet - passing. The first push to dev will seed it." | |
| exit 0 | |
| fi | |
| if [ "$COUNT" -lt "$BASELINE" ]; then | |
| echo "Warnings reduced: $COUNT < $BASELINE" | |
| exit 0 | |
| fi | |
| if [ "$BYPASS" = "true" ]; then | |
| echo "::warning::Warnings not reduced ($COUNT >= $BASELINE) but 'no-warning-ratchet' label present - bypassing." | |
| exit 0 | |
| fi | |
| echo "::error::Compilation warnings ($COUNT) must be strictly LESS than the baseline ($BASELINE). Remove at least one warning to merge (or apply the 'no-warning-ratchet' label)." | |
| exit 1 | |
| # Lower the baseline on merge to dev. Only ever decreases it. | |
| - name: Update warnings baseline on S3 (dev) | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/dev' | |
| env: | |
| COUNT: ${{ steps.warnings.outputs.count }} | |
| AWS_MAX_ATTEMPTS: 3 | |
| AWS_RETRY_MODE: standard | |
| AWS_ACCESS_KEY_ID: ${{ secrets.EXPLORER_TEAM_AWS_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.EXPLORER_TEAM_AWS_SECRET_ACCESS_KEY }} | |
| EXPLORER_TEAM_S3_BUCKET: ${{ secrets.EXPLORER_TEAM_S3_BUCKET }} | |
| run: | | |
| set -euo pipefail | |
| # Read directly from s3 to avoid cache stales | |
| cur=$(aws s3 cp "s3://$EXPLORER_TEAM_S3_BUCKET/${{ env.WARNINGS_BASELINE_S3_KEY }}" - 2>/dev/null | jq -r '.count' 2>/dev/null || echo "") | |
| if [ -n "$cur" ] && [ "$COUNT" -ge "$cur" ]; then | |
| echo "No update: current count ($COUNT) is not less than existing baseline ($cur)." | |
| exit 0 | |
| fi | |
| echo "Updating baseline: ${cur:-<none>} => $COUNT" | |
| jq -n --argjson c "$COUNT" \ | |
| '{count: $c, comment: "read and modified by CI/CD of explorer repo"}' > baseline.json | |
| aws s3 cp baseline.json "s3://$EXPLORER_TEAM_S3_BUCKET/${{ env.WARNINGS_BASELINE_S3_KEY }}" \ | |
| --content-type application/json --cache-control no-cache | |
| # Hand the numbers to the trusted workflow_run companion that posts the PR comment. | |
| # Runs even when the gate above failed (!cancelled), so a failing run still refreshes the comment | |
| # instead of leaving a stale message. | |
| - name: Write warning result for PR comment | |
| if: ${{ !cancelled() && github.event_name == 'pull_request' }} | |
| env: | |
| COUNT: ${{ steps.warnings.outputs.count }} | |
| BASELINE: ${{ steps.baseline.outputs.baseline }} | |
| run: | | |
| if [ -z "$COUNT" ]; then | |
| echo "No warning count computed (lint produced no result) - skipping comment payload." | |
| exit 0 | |
| fi | |
| jq -n \ | |
| --argjson pr "${{ github.event.pull_request.number }}" \ | |
| --argjson count "$COUNT" \ | |
| --arg baseline "$BASELINE" \ | |
| '{pr: $pr, count: $count, baseline: (if $baseline == "" then null else ($baseline | tonumber) end)}' \ | |
| > warning-result.json | |
| cat warning-result.json | |
| - name: Upload warning result | |
| if: ${{ !cancelled() && github.event_name == 'pull_request' }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: warning-result | |
| path: warning-result.json | |
| if-no-files-found: ignore | |
| - name: Upload lint reports | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: csharp-lint-reports | |
| path: | | |
| ${{ env.INSPECT_REPORT_JSON_FILE_NAME }} | |
| filtered.json | |
| # CodeStyleReport.xml | |
| # END: C# Linting using JetBrains ReSharper CLI | |
| test: | |
| # Skip when PR has 'perf_test' label (performance tests run in separate workflow) | |
| if: | | |
| !contains(github.event.pull_request.labels.*.name, 'perf_test') && ( | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main') || | |
| (github.event.pull_request.draft == false) || | |
| (github.event.label.name == 'force-build') || | |
| (github.event.label.name == 'clean-build') | |
| ) | |
| name: Test (${{ matrix.testMode }}) | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| testMode: [playmode, editmode] | |
| steps: | |
| - name: Checkout CI actions | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: .github/actions | |
| sparse-checkout-cone-mode: false | |
| path: ci-actions | |
| - name: Setup Docker disk | |
| uses: ./ci-actions/.github/actions/setup-docker-disk | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| lfs: true | |
| submodules: recursive | |
| - name: Create LFS file list | |
| run: git lfs ls-files -l | awk '{print $1}' | sort > .lfs-assets-id | |
| - name: Restore LFS cache | |
| uses: actions/cache@v5 | |
| id: lfs-cache | |
| with: | |
| path: .git/lfs | |
| key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }} | |
| - name: Git LFS Pull | |
| run: | | |
| git lfs pull | |
| git add . | |
| git reset --hard | |
| - uses: webfactory/ssh-agent@v0.8.0 | |
| with: | |
| ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| - name: 'Add GitHub to the SSH known hosts file' | |
| run: | | |
| mkdir -p -m 0700 ~/.ssh | |
| echo 'github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl' >> ~/.ssh/known_hosts | |
| echo 'github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=' >> ~/.ssh/known_hosts | |
| echo 'github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZKAc/NKgzBh2bFNFMR0mVmYeeNR1cb+gZXH6N1gjEHGo2J3qjVg9rBJ0aM5st/22iqkAl7K6d9jMTi6COwJKqFHGmn7gwK+FBnfaYDwdOB8JqGz9eDNi2SegGaKie1jURLEuRPkLqlJrJ3AEdC5v7ibREzMQPGiHlIC5bE0GD/3mPN1Kul4a4EZhIC5RFxewRqDaK7be0wYMbSW7JzfLDMVtRAkKO5GjaKKv4RzGmrgVApFI/j6jJ0sTJZVgmmqODx0P+RcAfGV0RBCjMEdOPj4yr5ObHGjYCdaFEhXGTIlSGeFSEAFGFweZb9t80b8ABbeBRwx/dqI94aaFkMRkzT8VJ9B4yQvbWO7HP64Lxk6rab5Ygwypbcghmms/ATrc0CVh0zGw36VTzV2eBwboRa7G0sxgBudGrJ4BAOI/JrF4BnWcG5fvQewJ1Uc=' >> ~/.ssh/known_hosts | |
| chmod 600 ~/.ssh/known_hosts | |
| - name: Restore Library cache | |
| uses: actions/cache@v5 | |
| with: | |
| path: Explorer/Library | |
| key: Library-Explorer-Ubuntu-${{ hashFiles('Explorer/Packages/packages-lock.json', 'Explorer/ProjectSettings/ProjectVersion.txt') }} | |
| restore-keys: Library-Explorer-Ubuntu- | |
| # --- Detect Unity version from the project --- | |
| - name: Read Unity version from ProjectVersion.txt | |
| id: unity-version | |
| run: | | |
| verFile="Explorer/ProjectSettings/ProjectVersion.txt" | |
| if [ ! -f "$verFile" ]; then | |
| echo "File not found: $verFile" | |
| exit 1 | |
| fi | |
| ver=$(grep -o 'm_EditorVersion: [0-9a-f.]*' "$verFile" | cut -d' ' -f2) | |
| if [ -z "$ver" ]; then | |
| echo "Could not parse Unity version from $verFile" | |
| exit 1 | |
| fi | |
| echo "version=$ver" >> $GITHUB_OUTPUT | |
| echo "Detected Unity version: $ver" | |
| # --- Build image references (GHCR + upstream) --- | |
| - name: Set image names | |
| id: img | |
| run: | | |
| version="${{ steps.unity-version.outputs.version }}" | |
| flavor="${{ env.UNITY_IMAGE_FLAVOR }}" | |
| imgVer="${{ env.UNITY_IMAGE_VERSION }}" | |
| # ubuntu-<unityVersion>-<flavor>-<gameciVersion> | |
| tag="ubuntu-$version-$flavor-$imgVer" | |
| owner=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') | |
| upstream="unityci/editor:$tag" | |
| ghcr="ghcr.io/$owner/${{ env.GHCR_IMAGE_NAME }}:$tag" | |
| echo "tag=$tag" >> $GITHUB_OUTPUT | |
| echo "upstream=$upstream" >> $GITHUB_OUTPUT | |
| echo "ghcr=$ghcr" >> $GITHUB_OUTPUT | |
| echo "Upstream image: $upstream" | |
| echo "GHCR image: $ghcr" | |
| # --- Log into GHCR --- | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ github.token }} | |
| # --- Restore Unity image from GHCR (fast path) --- | |
| - name: Restore Unity image from GHCR | |
| id: restore-image | |
| continue-on-error: true | |
| run: | | |
| docker pull "${{ steps.img.outputs.ghcr }}" | |
| # --- Seed GHCR from Docker Hub if missing (one-time per tag) --- | |
| - name: Download from Docker Hub and upload to GHCR (if missing) | |
| if: steps.restore-image.outcome == 'failure' | |
| run: | | |
| upstream="${{ steps.img.outputs.upstream }}" | |
| ghcr="${{ steps.img.outputs.ghcr }}" | |
| echo "GHCR miss. Pulling upstream: $upstream" | |
| docker pull $upstream | |
| docker tag $upstream $ghcr | |
| docker push $ghcr | |
| echo "Seeded GHCR: $ghcr" | |
| - name: Install ripgrep for edit mode tests | |
| if: matrix.testMode == 'editmode' | |
| id: rg-img | |
| run: | | |
| base="${{ steps.img.outputs.ghcr }}" | |
| derived="${base}-rg" | |
| cat > /tmp/Dockerfile.rg <<EOF | |
| FROM $base | |
| RUN apt-get update \ | |
| && apt-get install -y --no-install-recommends ripgrep \ | |
| && rm -rf /var/lib/apt/lists/* | |
| EOF | |
| docker build -t "$derived" -f /tmp/Dockerfile.rg /tmp | |
| echo "image=$derived" >> $GITHUB_OUTPUT | |
| echo "Built derived image with ripgrep: $derived" | |
| # Configure test runner | |
| - uses: game-ci/unity-test-runner@v4.3.1 | |
| id: testRunner | |
| timeout-minutes: 80 | |
| continue-on-error: true | |
| env: | |
| SHELL: /bin/bash | |
| with: | |
| projectPath: Explorer | |
| testMode: ${{ matrix.testMode }} | |
| sshAgent: ${{ env.SSH_AUTH_SOCK }} | |
| customImage: ${{ matrix.testMode == 'editmode' && steps.rg-img.outputs.image || steps.img.outputs.ghcr }} | |
| customParameters: -buildTarget StandaloneLinux64 -testCategory "!Performance" -burst-disable-compilation -accept-apiupdate | |
| githubToken: ${{ env.GITHUB_TOKEN }} | |
| - name: Dump Unity Editor log on failure | |
| if: always() && steps.testRunner.outcome != 'success' | |
| env: | |
| ARTIFACTS_PATH: ${{ steps.testRunner.outputs.artifactsPath }} | |
| TEST_MODE: ${{ matrix.testMode }} | |
| run: | | |
| echo "::group::Unity Editor log (last 200 lines)" | |
| # game-ci runs the test editor with `-logFile <artifactsPath>/<mode>.log`, | |
| # where artifactsPath resolves to "artifacts" at the workspace root | |
| # (NOT Explorer/artifacts — projectPath does not prefix it). Prefer the | |
| # action's own resolved output (matches the Report/Upload steps), but | |
| # fall back to the literal path because on a hard timeout kill the action | |
| # is killed before exporting its outputs (the Report step then fails with | |
| # "Input required and not supplied: path"). Values are passed via env | |
| # rather than interpolated into the shell directly. | |
| LOG="" | |
| for candidate in \ | |
| "$ARTIFACTS_PATH/$TEST_MODE.log" \ | |
| "artifacts/$TEST_MODE.log"; do | |
| if [ -n "$candidate" ] && [ -f "$candidate" ]; then | |
| LOG="$candidate" | |
| break | |
| fi | |
| done | |
| if [ -n "$LOG" ]; then | |
| echo "Showing last 200 lines of: $LOG" | |
| tail -200 "$LOG" | |
| else | |
| echo "No $TEST_MODE.log found in known locations." | |
| echo "Searching workspace for any Unity logs (depth-bounded to skip Library/):" | |
| matches=$(find . -maxdepth 3 -name "*.log" 2>/dev/null || true) | |
| if [ -n "$matches" ]; then | |
| echo "$matches" | head -50 | |
| else | |
| echo "No .log files found" | |
| fi | |
| fi | |
| echo "::endgroup::" | |
| - name: Report test results | |
| if: always() | |
| uses: dorny/test-reporter@v2.1.1 | |
| with: | |
| name: Unity Tests (${{ matrix.testMode }}) | |
| path: ${{ steps.testRunner.outputs.artifactsPath }}/*.xml | |
| reporter: dotnet-nunit | |
| fail-on-error: true | |
| fail-on-empty: true | |
| use-actions-summary: true | |
| # Upload artifact | |
| - uses: actions/upload-artifact@v6 | |
| if: always() | |
| with: | |
| name: Test results (${{ matrix.testMode }}) | |
| path: ${{ steps.testRunner.outputs.artifactsPath }} | |
| if-no-files-found: error | |
| watchdog: | |
| runs-on: ubuntu-latest | |
| needs: [lint, test] | |
| if: | | |
| needs.lint.result == 'failure' || | |
| needs.test.result == 'failure' | |
| steps: | |
| - run: exit 1 |