-
Notifications
You must be signed in to change notification settings - Fork 17
659 lines (593 loc) · 26.5 KB
/
Copy pathtest.yml
File metadata and controls
659 lines (593 loc) · 26.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
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 }}
# Only cancel an in-flight run when the new event will actually produce a new
# useful run. Otherwise an incidental 'labeled' event (bots add labels like
# 'new-dependency', 'claude-approved' or 'no QA needed' mid-run) would cancel
# real in-progress tests and replace them with a run that skips every job.
cancel-in-progress: >-
${{
github.event_name != 'pull_request' ||
github.event.action == 'opened' ||
github.event.action == 'reopened' ||
github.event.action == 'synchronize' ||
github.event.action == 'ready_for_review' ||
(
github.event.action == 'labeled' &&
contains(fromJSON('["force-build", "clean-build", "force-lint"]'), github.event.label.name)
)
}}
jobs:
changes:
# Non-PR events (push to dev, merge_group, workflow_dispatch) always run.
# PR events run for non-draft PRs, except 'labeled', which only counts for
# the trigger labels — any other label add must not start a redundant run.
if: |
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && github.event.pull_request.draft == false) ||
(
github.event.action == 'labeled' &&
contains(fromJSON('["force-build", "clean-build", "force-lint"]'), github.event.label.name)
)
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
# BEGIN: C# Linting using JetBrains ReSharper CLI
- 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
# The ReSharper CLI needs a .NET runtime, which the Unity image lacks.
# Derive an image with the SDK (runtime + MSBuild for project-model evaluation)
- name: Build lint image with .NET SDK
id: lint-dotnet-img
run: |
base="${{ steps.img-lint.outputs.ghcr }}"
derived="${base}-dotnet"
cat > /tmp/Dockerfile.dotnet <<EOF
FROM $base
RUN apt-get update \
&& apt-get install -y --no-install-recommends dotnet-sdk-8.0 \
&& rm -rf /var/lib/apt/lists/*
EOF
docker build -t "$derived" -f /tmp/Dockerfile.dotnet /tmp
echo "image=$derived" >> "$GITHUB_OUTPUT"
# The solution generated by the GameCI step above bakes in absolute paths only valid there ('/opt/unity/...', and the
# workspace as '/github/workspace' — the exact path GameCI mounts it at, so the -v
# target below must match it). Running on the host leaves those references dangling
# and ReSharper resolves them heuristically per-run
- name: Run ReSharper InspectCode (inside Unity container)
run: |
docker run --rm \
-v "${{ github.workspace }}:/github/workspace" \
-w /github/workspace \
-e RSHARP_HOME=/github/workspace/rsharp \
-e HOME=/tmp \
"${{ steps.lint-dotnet-img.outputs.image }}" \
bash scripts/lint/run-inspectcode.sh Explorer/Explorer.sln "$INSPECT_REPORT_JSON_FILE_NAME"
# Filter false positives ('.CSharpErrors'/'.CppCompilerErrors', residual unresolved-ref noise) and count.
- name: Count warnings
id: warnings
uses: ./.github/actions/count-warnings
with:
report: ${{ env.INSPECT_REPORT_JSON_FILE_NAME }}
filtered_output: filtered.json
# With full in-container resolution, C# compiler-level diagnostics should be near
# zero (baseline: 11 stable Google.Protobuf Span-overload quirks). A spike means
# references stopped resolving (image change, GameCI mount path change) and the
# warning count is measuring environment breakage, not code - fail loudly instead
# of letting filter-warnings.sh discard the evidence. Only '.CSharpErrors' counts:
# '.CppCompilerErrors' is ReSharper's shader/C++ analyzer noise (~500 in .hlsl/
# .cginc files), unrelated to C# resolution health.
- name: Assert reference resolution is healthy
run: |
set -euo pipefail
errs=$(jq '[.runs[0].results[] | select(.ruleId == ".CSharpErrors")] | length' "$INSPECT_REPORT_JSON_FILE_NAME")
echo "C# compiler-error diagnostics in report: $errs (filtered warning count: ${{ steps.warnings.outputs.count }})"
if [ "$errs" -gt 20 ]; then
echo "::error::$errs C# compiler-level errors in the InspectCode report - references are not resolving, the warning count cannot be trusted."
exit 1
fi
# Keep the raw and filtered reports inspectable for investigation purposes in a case of failure from "Assert reference resolution is healthy"
- name: Upload InspectCode reports
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: inspect-code-reports
path: |
${{ env.INSPECT_REPORT_JSON_FILE_NAME }}
filtered.json
if-no-files-found: ignore
retention-days: 3
- 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).
# Same event gating as the 'changes' job, minus 'force-lint' (lint-only label).
if: |
!contains(github.event.pull_request.labels.*.name, 'perf_test') && (
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && github.event.pull_request.draft == false) ||
(
github.event.action == 'labeled' &&
contains(fromJSON('["force-build", "clean-build"]'), github.event.label.name)
)
)
# The resulting check names "Test (editmode)" / "Test (playmode)" are
# posted as skip statuses by build-unitycloud.yml ("Skip build and test
# checks" step) — keep both places in sync when renaming.
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
- 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]
# always() is required: without a status-check function GitHub wraps the
# condition in an implicit success(), which is false whenever a needed job
# fails — the exact case this job exists for — so it never ran at all.
if: |
always() && (
needs.lint.result == 'failure' ||
needs.test.result == 'failure'
)
steps:
- run: exit 1