Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# CI scripts run inside Linux containers, so they must never be checked out with CRLF.
*.sh text eol=lf
158 changes: 81 additions & 77 deletions .github/workflows/main.ci.cd.workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ on:
schedule:
- cron: "0 0 * * *" # run daily at midnight (UTC)

# Cancel superseded runs on the same ref (e.g. rapid PR pushes) so queued matrix
# jobs don't stack up against the org's concurrent-runner limit. The event name is
# part of the group because the nightly schedule runs on the default branch ref and
# would otherwise cancel (or be cancelled by) a push run on that same branch.
# Only PR runs are cancelled; push and schedule runs are always allowed to finish.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
actions: write
contents: read
Expand Down Expand Up @@ -38,13 +47,12 @@ jobs:
rm -rf Assets/Plugins/StreamChat/SampleProject
rm -rf Assets/Plugins/StreamChat/Samples

# The repo intentionally does not track Packages/manifest.json so the
# legacy Unity 2020/2021 build job can fall back to its image's default
# manifest. The IL2CPP runtime test job runs on Unity 6000.0 and must
# pin its own packages (notably com.unity.test-framework for NUnit).
# This job strips SampleProject and runs on Unity 6000.0, so it replaces the tracked
# Packages/manifest.json with a trimmed one pinning com.unity.test-framework 1.6.0
# for NUnit. Overwriting a tracked file is why the Unity steps below need
# allowDirtyBuild.
- name: Install runtime-tests Packages/manifest.json
run: |
mkdir -p Packages
cp .github/workflows/manifests/runtime-tests.manifest.json Packages/manifest.json
rm -f Packages/packages-lock.json

Expand Down Expand Up @@ -132,6 +140,9 @@ jobs:
name: Runtime_Test_Results_IL2CPP
path: artifacts

- name: Verify runtime tests actually ran
run: bash .github/workflows/scripts/assert-tests-ran.sh artifacts/playmode-results.xml

- name: Notify Slack if failed
uses: voxmedia/github-action-slack-notify-build@v1
if: always() && failure()
Expand All @@ -143,30 +154,44 @@ jobs:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_NOTIFICATIONS_BOT_TOKEN }}

build:
name: build (${{ matrix.unity_version }}, ${{ matrix.target_platform }}, ${{ matrix.dotnet_version }}, ${{ matrix.compiler }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
# Testing every version x platform x dotnet_version x compiler combination
# would explode the job count. Instead we pick a representative subset that
# keeps similar coverage with far fewer jobs: one android + one iOS config
# per Unity version, arranged so every platform/dotnet/compiler value is still
# exercised across the matrix.
# Patch releases must be public LTS builds from the Unity archive, not Extended
# LTS (xLTS) patches that require Industry/Enterprise licenses in CI.
# dataset_index must be unique per row and within 0-15 (test data set count).
matrix:
target_platform: [android, ios]
unity_version: [2020, 2021]
dotnet_version: [NET_4_x, STANDARD_2_x]
compiler: [mono, il2cpp]
include:
- { unity_version: "2019.4", target_platform: android, dotnet_version: NET_4_x, compiler: il2cpp, dataset_index: 0, image: "unityci/editor:ubuntu-2019.4.40f1-android-3.2.2" }
- { unity_version: "2019.4", target_platform: ios, dotnet_version: STANDARD_2_x, compiler: mono, dataset_index: 1, image: "unityci/editor:ubuntu-2019.4.40f1-ios-3.2.2" }
- { unity_version: "2020.3", target_platform: android, dotnet_version: NET_4_x, compiler: mono, dataset_index: 2, image: "unityci/editor:ubuntu-2020.3.40f1-android-3.1.0" }
- { unity_version: "2020.3", target_platform: ios, dotnet_version: STANDARD_2_x, compiler: il2cpp, dataset_index: 3, image: "unityci/editor:ubuntu-2020.3.40f1-ios-3.1.0" }
- { unity_version: "2021.3", target_platform: android, dotnet_version: STANDARD_2_x, compiler: il2cpp, dataset_index: 4, image: "unityci/editor:ubuntu-2021.3.36f1-android-3.1.0" }
- { unity_version: "2021.3", target_platform: ios, dotnet_version: NET_4_x, compiler: mono, dataset_index: 5, image: "unityci/editor:ubuntu-2021.3.36f1-ios-3.1.0" }
- { unity_version: "2022.3", target_platform: android, dotnet_version: STANDARD_2_x, compiler: mono, dataset_index: 6, image: "unityci/editor:ubuntu-2022.3.62f2-android-3.2.2" }
- { unity_version: "2022.3", target_platform: ios, dotnet_version: NET_4_x, compiler: il2cpp, dataset_index: 7, image: "unityci/editor:ubuntu-2022.3.62f2-ios-3.2.2" }
- { unity_version: "2023.2", target_platform: android, dotnet_version: STANDARD_2_x, compiler: mono, dataset_index: 8, image: "unityci/editor:ubuntu-2023.2.20f1-android-3.2.2" }
- { unity_version: "2023.2", target_platform: ios, dotnet_version: NET_4_x, compiler: il2cpp, dataset_index: 9, image: "unityci/editor:ubuntu-2023.2.20f1-ios-3.2.2" }
- { unity_version: "6000.0", target_platform: android, dotnet_version: STANDARD_2_x, compiler: il2cpp, dataset_index: 10, image: "unityci/editor:ubuntu-6000.0.63f1-android-3.2.2" }
- { unity_version: "6000.0", target_platform: ios, dotnet_version: NET_4_x, compiler: mono, dataset_index: 11, image: "unityci/editor:ubuntu-6000.0.63f1-ios-3.2.2" }
- { unity_version: "6000.1", target_platform: android, dotnet_version: NET_4_x, compiler: mono, dataset_index: 12, image: "unityci/editor:ubuntu-6000.1.17f1-android-3.2.2" }
- { unity_version: "6000.1", target_platform: ios, dotnet_version: STANDARD_2_x, compiler: il2cpp, dataset_index: 13, image: "unityci/editor:ubuntu-6000.1.17f1-ios-3.2.2" }
- { unity_version: "6000.2", target_platform: android, dotnet_version: NET_4_x, compiler: il2cpp, dataset_index: 14, image: "unityci/editor:ubuntu-6000.2.12f1-android-3.2.2" }
- { unity_version: "6000.2", target_platform: ios, dotnet_version: STANDARD_2_x, compiler: mono, dataset_index: 15, image: "unityci/editor:ubuntu-6000.2.12f1-ios-3.2.2" }

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Calculate Sequential Index
id: calculate-index
- name: Set Test Data Set Index
run: |
target_index=$([[ "${{ matrix.target_platform }}" == 'android' ]] && echo '0' || echo '1')
unity_index=$([[ "${{ matrix.unity_version }}" == '2020' ]] && echo '0' || echo '1')
dotnet_index=$([[ "${{ matrix.dotnet_version }}" == 'NET_4_x' ]] && echo '0' || echo '1')
compiler_index=$([[ "${{ matrix.compiler }}" == 'mono' ]] && echo '0' || echo '1')

index=$((target_index * 1 + unity_index * 2 + dotnet_index * 4 + compiler_index * 8))

echo "SEQUENTIAL_INDEX=$index" >> $GITHUB_ENV
echo "SEQUENTIAL_INDEX=${{ matrix.dataset_index }}" >> $GITHUB_ENV

- name: Print Sequential Index
run: |
Expand All @@ -183,7 +208,7 @@ jobs:
- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update

- name: Install dependencies (macOS)
if: runner.os == 'macOS'
run: brew update
Expand All @@ -201,29 +226,7 @@ jobs:
- name: Determine Docker Image
id: dockerImageSelector
run: |
if [ "${{ matrix.unity_version }}" == '2020' ]; then
if [ "${{ matrix.target_platform }}" == 'android' ]; then
TAG='unityci/editor:ubuntu-2020.3.40f1-android-3.1.0'
elif [ "${{ matrix.target_platform }}" == 'ios' ]; then
TAG='unityci/editor:ubuntu-2020.3.40f1-ios-3.1.0'
else
echo "Unsupported platform"
exit 1
fi
elif [ "${{ matrix.unity_version }}" == '2021' ]; then
if [ "${{ matrix.target_platform }}" == 'android' ]; then
TAG='unityci/editor:ubuntu-2021.3.36f1-android-3.1.0'
elif [ "${{ matrix.target_platform }}" == 'ios' ]; then
TAG='unityci/editor:ubuntu-2021.3.36f1-ios-3.1.0'
else
echo "Unsupported platform"
exit 1
fi
else
echo "Unsupported Unity version"
exit 1
fi
echo "DOCKER_TAG=$TAG" >> $GITHUB_ENV
echo "DOCKER_TAG=${{ matrix.image }}" >> $GITHUB_ENV

- name: Echo Docker Image
run: |
Expand All @@ -232,7 +235,7 @@ jobs:
- name: Determine Build Name
run: |
RUNNER_ID="${{ matrix.unity_version }}_${{ matrix.target_platform }}_${{ matrix.compiler }}_${{ matrix.dotnet_version }}"

if [ "${{ matrix.target_platform }}" == "android" ]; then
BUILD_NAME="${RUNNER_ID}.apk"
elif [ "${{ matrix.target_platform }}" == "ios" ]; then
Expand All @@ -241,10 +244,20 @@ jobs:
echo "Unsupported platform"
exit 1
fi

echo "RUNNER_ID=$RUNNER_ID" >> $GITHUB_ENV
echo "BUILD_NAME=$BUILD_NAME" >> $GITHUB_ENV


# The tracked Packages/manifest.json pins com.unity.textmeshpro@3.0.9, whose editor
# scripts reference VersionControlSettings — a 2020.1+ API. Swap in the same package
# set with TMP downgraded to the 2.1.x line that 2019.4 supports. This overwrites a
# tracked file, so every Unity step below needs allowDirtyBuild.
- name: Install 2019.4 Packages/manifest.json
if: matrix.unity_version == '2019.4'
run: |
cp .github/workflows/manifests/build-2019.4.manifest.json Packages/manifest.json
rm -f Packages/packages-lock.json

- name: Enable Tests
uses: game-ci/unity-builder@v4
env:
Expand All @@ -254,7 +267,9 @@ jobs:
with:
buildMethod: StreamChat.EditorTools.StreamEditorTools.EnableStreamTestsEnabledCompilerFlag
customImage: ${{ env.DOCKER_TAG }}
allowDirtyBuild: true

# StreamChat.Tests is Editor-only, so playmode always reports 0 tests.
- name: Run Tests (Attempt 1)
id: run_tests_1
uses: game-ci/unity-test-runner@v4
Expand All @@ -263,11 +278,17 @@ jobs:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
testMode: editmode
customParameters: -streamBase64TestDataSet "${{ secrets.STREAM_AUTH_TEST_DATA_BASE64 }}" -testDataSetIndex ${{ env.SEQUENTIAL_INDEX }}
customImage: ${{ env.DOCKER_TAG }}
allowDirtyBuild: true
timeout-minutes: 40
continue-on-error: true

- name: Reset Unity state before retry 2
if: steps.run_tests_1.outcome == 'failure'
run: bash .github/workflows/scripts/reset-unity-state.sh

- name: Run Tests (Attempt 2)
id: run_tests_2
if: steps.run_tests_1.outcome == 'failure'
Expand All @@ -277,11 +298,17 @@ jobs:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
testMode: editmode
customParameters: -streamBase64TestDataSet "${{ secrets.STREAM_AUTH_TEST_DATA_BASE64 }}" -testDataSetIndex ${{ env.SEQUENTIAL_INDEX }}
customImage: ${{ env.DOCKER_TAG }}
allowDirtyBuild: true
timeout-minutes: 50
continue-on-error: true

- name: Reset Unity state before retry 3
if: steps.run_tests_2.outcome == 'failure'
run: bash .github/workflows/scripts/reset-unity-state.sh

- name: Run Tests (Attempt 3)
id: run_tests_3
if: steps.run_tests_2.outcome == 'failure'
Expand All @@ -291,44 +318,22 @@ jobs:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
testMode: editmode
customParameters: -streamBase64TestDataSet "${{ secrets.STREAM_AUTH_TEST_DATA_BASE64 }}" -testDataSetIndex ${{ env.SEQUENTIAL_INDEX }}
customImage: ${{ env.DOCKER_TAG }}
timeout-minutes: 60
continue-on-error: true

- name: Run Tests (Attempt 4)
id: run_tests_4
if: steps.run_tests_3.outcome == 'failure'
uses: game-ci/unity-test-runner@v4
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
customParameters: -streamBase64TestDataSet "${{ secrets.STREAM_AUTH_TEST_DATA_BASE64 }}" -testDataSetIndex ${{ env.SEQUENTIAL_INDEX }}
customImage: ${{ env.DOCKER_TAG }}
timeout-minutes: 60
continue-on-error: true

- name: Run Tests (Attempt 5)
id: run_tests_5
if: steps.run_tests_4.outcome == 'failure'
uses: game-ci/unity-test-runner@v4
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
customParameters: -streamBase64TestDataSet "${{ secrets.STREAM_AUTH_TEST_DATA_BASE64 }}" -testDataSetIndex ${{ env.SEQUENTIAL_INDEX }}
customImage: ${{ env.DOCKER_TAG }}
allowDirtyBuild: true
timeout-minutes: 60

- name: Upload Test Results as Artifact
uses: actions/upload-artifact@v4
if: always()
with:
name: Test_Results_${{ env.RUNNER_ID }}
path: artifacts

- name: Verify tests actually ran
run: bash .github/workflows/scripts/assert-tests-ran.sh artifacts/editmode-results.xml

- name: Free Disk space
uses: jlumbroso/free-disk-space@v1.2.0
if: matrix.target_platform == 'android' || matrix.target_platform == 'ios'
Expand All @@ -347,15 +352,15 @@ jobs:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
with:
buildMethod: StreamChat.EditorTools.StreamEditorTools.BuildSampleApp
customParameters: -streamBase64TestDataSet ${{ secrets.STREAM_AUTH_TEST_DATA_BASE64 }} -testDataSetIndex ${{ env.SEQUENTIAL_INDEX }} -apiCompatibility ${{ matrix.dotnet_version }} -scriptingBackend ${{ matrix.compiler }} -buildTargetPlatform ${{ matrix.target_platform }} -buildTargetPath $(pwd)/SampleAppBuild/${{ env.BUILD_NAME }}
customParameters: -streamBase64TestDataSet ${{ secrets.STREAM_AUTH_TEST_DATA_BASE64 }} -testDataSetIndex ${{ env.SEQUENTIAL_INDEX }} -apiCompatibility ${{ matrix.dotnet_version }} -scriptingBackend ${{ matrix.compiler }} -buildTargetPlatform ${{ matrix.target_platform }} -buildTargetPath SampleAppBuild/${{ env.BUILD_NAME }}
customImage: ${{ env.DOCKER_TAG }}
allowDirtyBuild: true #Needed because the import process may update ProjectSettings

- name: Upload Build as Artifact
uses: actions/upload-artifact@v4
with:
name: Build_${{ env.BUILD_NAME }}
path: $(pwd)/SampleAppBuild/${{ env.BUILD_NAME }}
path: ${{ github.workspace }}/SampleAppBuild/${{ env.BUILD_NAME }}

- name: Notify Slack if failed
uses: voxmedia/github-action-slack-notify-build@v1
Expand All @@ -366,4 +371,3 @@ jobs:
status: FAILED
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_NOTIFICATIONS_BOT_TOKEN }}

25 changes: 25 additions & 0 deletions .github/workflows/manifests/build-2019.4.manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"dependencies": {
"com.unity.textmeshpro": "2.1.6",
"com.unity.test-framework": "1.1.33",
"com.unity.ugui": "1.0.0",
"com.unity.modules.androidjni": "1.0.0",
"com.unity.modules.animation": "1.0.0",
"com.unity.modules.assetbundle": "1.0.0",
"com.unity.modules.audio": "1.0.0",
"com.unity.modules.imageconversion": "1.0.0",
"com.unity.modules.imgui": "1.0.0",
"com.unity.modules.jsonserialize": "1.0.0",
"com.unity.modules.particlesystem": "1.0.0",
"com.unity.modules.physics": "1.0.0",
"com.unity.modules.physics2d": "1.0.0",
"com.unity.modules.ui": "1.0.0",
"com.unity.modules.uielements": "1.0.0",
"com.unity.modules.unitywebrequest": "1.0.0",
"com.unity.modules.unitywebrequestassetbundle": "1.0.0",
"com.unity.modules.unitywebrequestaudio": "1.0.0",
"com.unity.modules.unitywebrequesttexture": "1.0.0",
"com.unity.modules.unitywebrequestwww": "1.0.0",
"com.unity.modules.video": "1.0.0"
}
}
28 changes: 28 additions & 0 deletions .github/workflows/scripts/assert-tests-ran.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Fails the job when the test runner reported success without executing any test.
#
# Unity exits 0 when it cannot activate a license, and game-ci then prints
# "Run succeeded, no failures occurred" with an empty result file. Without this guard a
# job that ran zero tests is indistinguishable from a job where everything passed.
set -euo pipefail

results_file="${1:?path to the results xml is required}"

if [ ! -f "${results_file}" ]; then
echo "::error::${results_file} is missing - the test runner never produced results."
exit 1
fi

test_case_count="$(sed -n 's/.*<test-run [^>]*testcasecount="\([0-9]*\)".*/\1/p' "${results_file}" | head -n 1)"

if [ -z "${test_case_count}" ]; then
echo "::error::Could not read testcasecount from ${results_file}."
exit 1
fi

if [ "${test_case_count}" -eq 0 ]; then
echo "::error::The test runner executed 0 tests. Treating this as a failure - see the log for license or compilation errors."
exit 1
fi

echo "Test runner executed ${test_case_count} tests."
26 changes: 26 additions & 0 deletions .github/workflows/scripts/reset-unity-state.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Resets everything a killed Unity attempt leaves behind, so the next attempt can start.
#
# When a "Run Tests" step hits its step timeout, GitHub kills the action but not the
# docker container it started. Without this cleanup the next attempt dies immediately
# with either "Multiple Unity instances cannot open the same project" (the orphan still
# holds the project) or "Machine identification is invalid for current license" - and in
# the licensing case Unity exits 0 after running zero tests, which silently turns the
# job green.
set -uo pipefail

running_containers="$(docker ps -q)"
if [ -n "${running_containers}" ]; then
echo "Stopping leftover containers: ${running_containers}"
# shellcheck disable=SC2086
docker stop --time 10 ${running_containers} || true
fi

echo "Removing Unity project lock file"
sudo rm -f Temp/UnityLockfile || true

HOME_DIR="${RUNNER_TEMP}/_github_home"
echo "Removing Unity license state under ${HOME_DIR}"
sudo find "${HOME_DIR}" -iname '*.ulf' -delete -print || true
sudo rm -rf "${HOME_DIR}/.config/unity3d/Unity/licenses" \
"${HOME_DIR}/.local/share/unity3d/Unity" || true
Loading
Loading