Compare commits

..

1 Commits

Author SHA1 Message Date
David Finol
1e53060e37 Build dist 2023-02-10 08:58:29 -06:00
219 changed files with 397068 additions and 110988 deletions

View File

@@ -2,4 +2,3 @@ dist/
lib/
node_modules/
jest.config.js
src/types/

View File

@@ -1,5 +1,4 @@
{
"root": true,
"plugins": ["jest", "@typescript-eslint", "prettier", "unicorn"],
"extends": ["plugin:unicorn/recommended", "plugin:github/recommended", "plugin:prettier/recommended"],
"parser": "@typescript-eslint/parser",
@@ -15,8 +14,7 @@
"env": {
"node": true,
"es6": true,
"jest/globals": true,
"es2020": true
"jest/globals": true
},
"rules": {
// Error out for code formatting errors
@@ -40,21 +38,9 @@
// Mandatory spacing
"padding-line-between-statements": [
"error",
{
"blankLine": "always",
"prev": "*",
"next": "return"
},
{
"blankLine": "always",
"prev": "directive",
"next": "*"
},
{
"blankLine": "any",
"prev": "directive",
"next": "directive"
}
{ "blankLine": "always", "prev": "*", "next": "return" },
{ "blankLine": "always", "prev": "directive", "next": "*" },
{ "blankLine": "any", "prev": "directive", "next": "directive" }
],
// Enforce camelCase
"camelcase": "error",
@@ -67,25 +53,8 @@
// Unused vars are useful to keep method signatures consistent and documented
"@typescript-eslint/no-unused-vars": "off",
// For this project only use kebab-case
"unicorn/filename-case": [
"error",
{
"cases": {
"kebabCase": true
}
}
],
"unicorn/filename-case": ["error", { "cases": { "kebabCase": true } }],
// Allow Array.from(set) mitigate TS2569 which would require '--downlevelIteration'
"unicorn/prefer-spread": "off",
// Temp disable to prevent mixing changes with other PRs
"i18n-text/no-en": "off"
},
"overrides": [
{
"files": ["jest.setup.js"],
"rules": {
"import/no-commonjs": "off"
}
}
]
"unicorn/prefer-spread": "off"
}
}

View File

@@ -2,28 +2,10 @@
- ...
#### Related Issues
- ...
#### Related PRs
- ...
#### Successful Workflow Run Link
PRs don't have access to secrets so you will need to provide a link to a successful run of the workflows from your own
repo.
- ...
#### Checklist
<!-- please check all items and add your own -->
- [x] Read the contribution [guide](https://github.com/game-ci/unity-builder/blob/main/CONTRIBUTING.md) and accept the
[code](https://github.com/game-ci/unity-builder/blob/main/CODE_OF_CONDUCT.md) of conduct
- [ ] Docs (If new inputs or outputs have been added or changes to behavior that should be documented. Please make a PR
in the [documentation repo](https://github.com/game-ci/documentation))
- [x] Read the contribution [guide](../CONTRIBUTING.md) and accept the [code](../CODE_OF_CONDUCT.md) of conduct
- [ ] Readme (updated or not needed)
- [ ] Tests (added, updated or not needed)

View File

@@ -13,7 +13,7 @@ jobs:
id: requestActivationFile
uses: game-ci/unity-request-activation-file@v2.0-alpha-1
- name: Upload activation file
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: ${{ steps.requestActivationFile.outputs.filePath }}
path: ${{ steps.requestActivationFile.outputs.filePath }}

View File

@@ -1,87 +0,0 @@
name: Builds - MacOS
on:
workflow_dispatch:
push:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
buildForAllPlatformsMacOS:
name: ${{ matrix.targetPlatform }} on ${{ matrix.unityVersion }}
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
projectPath:
- test-project
unityVersion:
- 2021.3.45f2
- 2022.3.62f3
- 2023.2.22f1
targetPlatform:
- StandaloneOSX # Build a MacOS executable
- iOS # Build an iOS executable
include:
# Additionally test enableGpu build for a standalone windows target
- unityVersion: 6000.0.36f1
targetPlatform: StandaloneOSX
- unityVersion: 6000.0.36f1
targetPlatform: StandaloneOSX
buildProfile: 'Assets/Settings/Build Profiles/Sample macOS Build Profile.asset'
steps:
###########################
# Checkout #
###########################
- uses: actions/checkout@v4
with:
lfs: true
###########################
# Cache #
###########################
- uses: actions/cache@v4
with:
path: ${{ matrix.projectPath }}/Library
key: Library-${{ matrix.projectPath }}-macos-${{ matrix.targetPlatform }}
restore-keys: |
Library-${{ matrix.projectPath }}-macos-
Library-
###########################
# Set Scripting Backend #
###########################
- name: Set Scripting Backend To il2cpp
run: |
mv -f "./test-project/ProjectSettings/ProjectSettingsIl2cpp.asset" "./test-project/ProjectSettings/ProjectSettings.asset"
###########################
# Build #
###########################
- uses: ./
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
with:
buildName: 'GameCI Test Build'
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
targetPlatform: ${{ matrix.targetPlatform }}
buildProfile: ${{ matrix.buildProfile }}
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue
# We use dirty build because we are replacing the default project settings file above
allowDirtyBuild: true
###########################
# Upload #
###########################
- uses: actions/upload-artifact@v4
with:
name: Build ${{ matrix.targetPlatform }} on MacOS (${{ matrix.unityVersion }})${{ matrix.buildProfile && ' With Build Profile' || '' }}
path: build
retention-days: 14

View File

@@ -1,206 +0,0 @@
name: Builds - Ubuntu
on:
workflow_dispatch:
push:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
UNITY_LICENSE:
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n <License
id=\"Terms\">\n <MachineBindings>\n <Binding Key=\"1\"
Value=\"576562626572264761624c65526f7578\"/>\n <Binding Key=\"2\"
Value=\"576562626572264761624c65526f7578\"/>\n </MachineBindings>\n <MachineID
Value=\"D7nTUnjNAmtsUMcnoyrqkgIbYdM=\"/>\n <SerialHash
Value=\"2033b8ac3e6faa3742ca9f0bfae44d18f2a96b80\"/>\n <Features>\n <Feature
Value=\"33\"/>\n <Feature Value=\"1\"/>\n <Feature Value=\"12\"/>\n <Feature
Value=\"2\"/>\n <Feature Value=\"24\"/>\n <Feature Value=\"3\"/>\n <Feature
Value=\"36\"/>\n <Feature Value=\"17\"/>\n <Feature Value=\"19\"/>\n <Feature
Value=\"62\"/>\n </Features>\n <DeveloperData
Value=\"AQAAAEY0LUJHUlgtWEQ0RS1aQ1dWLUM1SlctR0RIQg==\"/>\n <SerialMasked
Value=\"F4-BGRX-XD4E-ZCWV-C5JW-XXXX\"/>\n <StartDate Value=\"2021-02-08T00:00:00\"/>\n <UpdateDate
Value=\"2021-02-09T00:34:57\"/>\n <InitialActivationDate
Value=\"2021-02-08T00:34:56\"/>\n <LicenseVersion Value=\"6.x\"/>\n <ClientProvidedVersion
Value=\"2018.4.30f1\"/>\n <AlwaysOnline Value=\"false\"/>\n <Entitlements>\n <Entitlement
Ns=\"unity_editor\" Tag=\"UnityPersonal\" Type=\"EDITOR\"
ValidTo=\"9999-12-31T00:00:00\"/>\n <Entitlement Ns=\"unity_editor\" Tag=\"DarkSkin\"
Type=\"EDITOR_FEATURE\" ValidTo=\"9999-12-31T00:00:00\"/>\n </Entitlements>\n </License>\n<Signature
xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><SignedInfo><CanonicalizationMethod
Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments\"/><SignatureMethod
Algorithm=\"http://www.w3.org/2000/09/xmldsig#rsa-sha1\"/><Reference URI=\"#Terms\"><Transforms><Transform
Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"/></Transforms><DigestMethod
Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/><DigestValue>m0Db8UK+ktnOLJBtHybkfetpcKo=</DigestValue></Reference></SignedInfo><SignatureValue>o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw==</SignatureValue></Signature></root>"
jobs:
buildForAllPlatformsUbuntu:
name:
"${{ matrix.targetPlatform }} on ${{ matrix.unityVersion}}${{startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
providerStrategy:
# - local-docker
- local
projectPath:
- test-project
unityVersion:
- 2021.3.45f2
- 2022.3.62f3
- 2023.2.22f1
targetPlatform:
- StandaloneOSX # Build a macOS standalone (Intel 64-bit) with mono backend.
- StandaloneWindows64 # Build a Windows 64-bit standalone with mono backend.
- StandaloneLinux64 # Build a Linux 64-bit standalone with mono/il2cpp backend.
- iOS # Build an iOS project.
- Android # Build an Android .apk.
- WebGL # WebGL.
buildWithIl2cpp:
- false
- true
additionalParameters:
- -param value
- -standaloneBuildSubtarget Server
# Skipping configurations that are not supported
exclude:
# No il2cpp support on Linux Host
- targetPlatform: StandaloneOSX
buildWithIl2cpp: true
- targetPlatform: StandaloneWindows64
buildWithIl2cpp: true
# Only builds with Il2cpp
- targetPlatform: iOS
buildWithIl2cpp: false
- targetPlatform: Android
buildWithIl2cpp: false
- targetPlatform: WebGL
buildWithIl2cpp: false
# No dedicated server support
- targetPlatform: WebGL
additionalParameters: -standaloneBuildSubtarget Server
- targetPlatform: Android
additionalParameters: -standaloneBuildSubtarget Server
- targetPlatform: iOS
additionalParameters: -standaloneBuildSubtarget Server
# No dedicated server support on Linux Host
- targetPlatform: StandaloneOSX
additionalParameters: -standaloneBuildSubtarget Server
# No il2cpp dedicated server support on Linux Host
- targetPlatform: StandaloneWindows64
additionalParameters: -standaloneBuildSubtarget Server
buildWithIl2cpp: true
include:
- unityVersion: 6000.0.36f1
targetPlatform: WebGL
- unityVersion: 6000.0.36f1
targetPlatform: WebGL
buildProfile: 'Assets/Settings/Build Profiles/Sample WebGL Build Profile.asset'
steps:
- name: Clear Space for Android Build
if: matrix.targetPlatform == 'Android'
uses: jlumbroso/free-disk-space@v1.3.1
###########################
# Checkout #
###########################
- uses: actions/checkout@v4
with:
lfs: true
###########################
# Cache #
###########################
- uses: actions/cache@v4
with:
path: ${{ matrix.projectPath }}/Library
key: Library-${{ matrix.projectPath }}-ubuntu-${{ matrix.targetPlatform }}
restore-keys: |
Library-${{ matrix.projectPath }}-ubuntu-
Library-
###########################
# Set Scripting Backend #
###########################
- name: Set Scripting Backend To il2cpp
if: matrix.buildWithIl2cpp == true
run: |
mv -f "./test-project/ProjectSettings/ProjectSettingsIl2cpp.asset" "./test-project/ProjectSettings/ProjectSettings.asset"
###########################
# Build #
###########################
- name: Build
uses: ./
id: build-1
continue-on-error: true
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
buildName: 'GameCI Test Build'
projectPath: ${{ matrix.projectPath }}
buildProfile: ${{ matrix.buildProfile }}
unityVersion: ${{ matrix.unityVersion }}
targetPlatform: ${{ matrix.targetPlatform }}
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue ${{ matrix.additionalParameters }}
providerStrategy: ${{ matrix.providerStrategy }}
allowDirtyBuild: true
- name: Sleep for Retry
if: ${{ steps.build-1.outcome == 'failure' }}
run: |
sleep 60
- name: Build (Retry 1)
uses: ./
id: build-2
if: ${{ steps.build-1.outcome == 'failure' }}
continue-on-error: true
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
buildName: 'GameCI Test Build'
projectPath: ${{ matrix.projectPath }}
buildProfile: ${{ matrix.buildProfile }}
unityVersion: ${{ matrix.unityVersion }}
targetPlatform: ${{ matrix.targetPlatform }}
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue ${{ matrix.additionalParameters }}
providerStrategy: ${{ matrix.providerStrategy }}
allowDirtyBuild: true
- name: Sleep for Retry
if: ${{ steps.build-2.outcome == 'failure' }}
run: |
sleep 240
- name: Build (Retry 2)
uses: ./
id: build-3
if: ${{ steps.build-2.outcome == 'failure' }}
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
buildName: 'GameCI Test Build'
projectPath: ${{ matrix.projectPath }}
buildProfile: ${{ matrix.buildProfile }}
unityVersion: ${{ matrix.unityVersion }}
targetPlatform: ${{ matrix.targetPlatform }}
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue ${{ matrix.additionalParameters }}
providerStrategy: ${{ matrix.providerStrategy }}
allowDirtyBuild: true
###########################
# Upload #
###########################
- uses: actions/upload-artifact@v4
with:
name:
"Build ${{ matrix.targetPlatform }}${{ startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }} on Ubuntu (${{ matrix.unityVersion }}_il2cpp_${{ matrix.buildWithIl2cpp }}_params_${{ matrix.additionalParameters }})"
path: build
retention-days: 14

View File

@@ -1,181 +0,0 @@
name: Builds - Windows
on:
workflow_dispatch:
push:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
buildForAllPlatformsWindows:
name: ${{ matrix.targetPlatform }} on ${{ matrix.unityVersion }}
runs-on: windows-2022
strategy:
fail-fast: false
matrix:
projectPath:
- test-project
unityVersion:
- 2021.3.45f2
- 2022.3.62f3
- 2023.2.22f1
targetPlatform:
- Android # Build an Android apk.
- StandaloneWindows64 # Build a Windows 64-bit standalone.
- WSAPlayer # Build a UWP App
- tvOS # Build an Apple TV XCode project
enableGpu:
- false
include:
# Additionally test enableGpu build for a standalone windows target
- projectPath: test-project
unityVersion: 2023.2.2f1
targetPlatform: StandaloneWindows64
enableGpu: true
- unityVersion: 6000.0.36f1
targetPlatform: StandaloneWindows64
- unityVersion: 6000.0.36f1
targetPlatform: StandaloneWindows64
buildProfile: 'Assets/Settings/Build Profiles/Sample Windows Build Profile.asset'
steps:
###########################
# Checkout #
###########################
- uses: actions/checkout@v4
with:
lfs: true
###########################
# Cache #
###########################
- uses: actions/cache@v4
with:
path: ${{ matrix.projectPath }}/Library
key: Library-${{ matrix.projectPath }}-windows-${{ matrix.targetPlatform }}
restore-keys: |
Library-${{ matrix.projectPath }}-windows-
Library-
###########################
# Set Scripting Backend #
###########################
- name: Set Scripting Backend To il2cpp
run: |
Move-Item -Path "./test-project/ProjectSettings/ProjectSettingsIl2cpp.asset" -Destination "./test-project/ProjectSettings/ProjectSettings.asset" -Force
###########################
# Docker Readiness #
###########################
- name: Ensure Docker daemon is ready
timeout-minutes: 2
shell: powershell
run: |
$maxRetries = 10
$retryDelay = 6
for ($i = 0; $i -lt $maxRetries; $i++) {
$svc = Get-Service docker -ErrorAction SilentlyContinue
if ($svc -and $svc.Status -eq 'Running') {
docker version 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Host "Docker is ready."
exit 0
}
}
if ($svc -and $svc.Status -eq 'Stopped') {
Write-Host "Docker service stopped, attempting to start..."
Start-Service docker -ErrorAction SilentlyContinue
}
Write-Host "Waiting for Docker daemon (attempt $($i+1)/$maxRetries)..."
Start-Sleep -Seconds $retryDelay
}
Write-Error "Docker daemon did not start within $($maxRetries * $retryDelay) seconds"
exit 1
###########################
# Build #
###########################
- name: Build
uses: ./
id: build-1
continue-on-error: true
timeout-minutes: 30
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
with:
buildName: 'GameCI Test Build'
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
targetPlatform: ${{ matrix.targetPlatform }}
buildProfile: ${{ matrix.buildProfile }}
enableGpu: ${{ matrix.enableGpu }}
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue
allowDirtyBuild: true
# We use dirty build because we are replacing the default project settings file above
- name: Sleep for Retry
if: ${{ steps.build-1.outcome == 'failure' }}
run: |
Start-Sleep -s 120
- name: Build Retry 1
uses: ./
id: build-2
continue-on-error: true
timeout-minutes: 30
if: steps.build-1.outcome == 'failure'
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
with:
buildName: 'GameCI Test Build'
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
targetPlatform: ${{ matrix.targetPlatform }}
enableGpu: ${{ matrix.enableGpu }}
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue
allowDirtyBuild: true
# We use dirty build because we are replacing the default project settings file above
- name: Sleep for Retry
if: ${{ steps.build-1.outcome == 'failure' && steps.build-2.outcome == 'failure' }}
run: |
Start-Sleep -s 240
- name: Build Retry 2
uses: ./
id: build-3
timeout-minutes: 30
if: ${{ steps.build-1.outcome == 'failure' && steps.build-2.outcome == 'failure' }}
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
with:
buildName: 'GameCI Test Build'
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
targetPlatform: ${{ matrix.targetPlatform }}
enableGpu: ${{ matrix.enableGpu }}
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue
allowDirtyBuild: true
# We use dirty build because we are replacing the default project settings file above
###########################
# Upload #
###########################
- uses: actions/upload-artifact@v4
with:
name:
Build ${{ matrix.targetPlatform }} on Windows (${{ matrix.unityVersion }})${{ matrix.enableGpu && ' With
GPU' || '' }}${{ matrix.buildProfile && ' With Build Profile' || '' }}
path: build
retention-days: 14

100
.github/workflows/build-tests.yml vendored Normal file
View File

@@ -0,0 +1,100 @@
name: Builds
on:
push: { branches: [main] }
pull_request:
paths-ignore:
- '.github/**'
env:
UNITY_LICENSE:
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n <License
id=\"Terms\">\n <MachineBindings>\n <Binding Key=\"1\"
Value=\"576562626572264761624c65526f7578\"/>\n <Binding Key=\"2\"
Value=\"576562626572264761624c65526f7578\"/>\n </MachineBindings>\n <MachineID
Value=\"D7nTUnjNAmtsUMcnoyrqkgIbYdM=\"/>\n <SerialHash
Value=\"2033b8ac3e6faa3742ca9f0bfae44d18f2a96b80\"/>\n <Features>\n <Feature
Value=\"33\"/>\n <Feature Value=\"1\"/>\n <Feature Value=\"12\"/>\n <Feature
Value=\"2\"/>\n <Feature Value=\"24\"/>\n <Feature Value=\"3\"/>\n <Feature
Value=\"36\"/>\n <Feature Value=\"17\"/>\n <Feature Value=\"19\"/>\n <Feature
Value=\"62\"/>\n </Features>\n <DeveloperData
Value=\"AQAAAEY0LUJHUlgtWEQ0RS1aQ1dWLUM1SlctR0RIQg==\"/>\n <SerialMasked
Value=\"F4-BGRX-XD4E-ZCWV-C5JW-XXXX\"/>\n <StartDate Value=\"2021-02-08T00:00:00\"/>\n <UpdateDate
Value=\"2021-02-09T00:34:57\"/>\n <InitialActivationDate
Value=\"2021-02-08T00:34:56\"/>\n <LicenseVersion Value=\"6.x\"/>\n <ClientProvidedVersion
Value=\"2018.4.30f1\"/>\n <AlwaysOnline Value=\"false\"/>\n <Entitlements>\n <Entitlement
Ns=\"unity_editor\" Tag=\"UnityPersonal\" Type=\"EDITOR\"
ValidTo=\"9999-12-31T00:00:00\"/>\n <Entitlement Ns=\"unity_editor\" Tag=\"DarkSkin\"
Type=\"EDITOR_FEATURE\" ValidTo=\"9999-12-31T00:00:00\"/>\n </Entitlements>\n </License>\n<Signature
xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><SignedInfo><CanonicalizationMethod
Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments\"/><SignatureMethod
Algorithm=\"http://www.w3.org/2000/09/xmldsig#rsa-sha1\"/><Reference URI=\"#Terms\"><Transforms><Transform
Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"/></Transforms><DigestMethod
Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/><DigestValue>m0Db8UK+ktnOLJBtHybkfetpcKo=</DigestValue></Reference></SignedInfo><SignatureValue>o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw==</SignatureValue></Signature></root>"
jobs:
buildForAllPlatformsUbuntu:
name: Build for ${{ matrix.targetPlatform }} on version ${{ matrix.unityVersion }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
cloudRunnerCluster:
# - local-docker
- local
projectPath:
- test-project
unityVersion:
- 2019.2.11f1
- 2019.3.15f1
targetPlatform:
- StandaloneOSX # Build a macOS standalone (Intel 64-bit).
- StandaloneWindows64 # Build a Windows 64-bit standalone.
- StandaloneLinux64 # Build a Linux 64-bit standalone.
- iOS # Build an iOS player.
- Android # Build an Android .apk.
- WebGL # WebGL.
# - StandaloneWindows # Build a Windows standalone.
# - WSAPlayer # Build an Windows Store Apps player.
# - PS4 # Build a PS4 Standalone.
# - XboxOne # Build a Xbox One Standalone.
# - tvOS # Build to Apple's tvOS platform.
# - Switch # Build a Nintendo Switch player
steps:
###########################
# Checkout #
###########################
- uses: actions/checkout@v3
with:
lfs: true
###########################
# Cache #
###########################
- uses: actions/cache@v3
with:
path: ${{ matrix.projectPath }}/Library
key: Library-${{ matrix.projectPath }}-ubuntu-${{ matrix.targetPlatform }}
restore-keys: |
Library-${{ matrix.projectPath }}-ubuntu-
Library-
###########################
# Build #
###########################
- uses: ./
with:
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
targetPlatform: ${{ matrix.targetPlatform }}
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue
cloudRunnerCluster: ${{ matrix.cloudRunnerCluster }}
###########################
# Upload #
###########################
- uses: actions/upload-artifact@v3
with:
name: Build Ubuntu (${{ matrix.unityVersion }})
path: build
retention-days: 14

37
.github/workflows/cleanup.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Cleanup (cron)
on:
schedule:
- cron: '30 10 * * SUN' # every sunday at 10:30
jobs:
deleteArtifacts:
runs-on: ubuntu-latest
steps:
- name: Delete old artifacts
uses: kolpav/purge-artifacts-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
expire-in: 21 days
cleanupCloudRunner:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
if: github.event.event_type != 'pull_request_target'
with:
lfs: true
- uses: actions/setup-node@v3
with:
node-version: 16.x
- run: yarn
- run: yarn run cli --help
env:
AWS_REGION: eu-west-2
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: eu-west-2
- run: yarn run cli -m list-resources
env:
AWS_REGION: eu-west-2
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: eu-west-2

View File

@@ -0,0 +1,58 @@
name: Async Checks API
on:
workflow_dispatch:
inputs:
checksObject:
description: ''
required: false
default: ''
permissions:
checks: write
env:
GKE_ZONE: 'us-central1'
GKE_REGION: 'us-central1'
GKE_PROJECT: 'unitykubernetesbuilder'
GKE_CLUSTER: 'game-ci-github-pipelines'
GCP_LOGGING: true
GCP_PROJECT: unitykubernetesbuilder
GCP_LOG_FILE: ${{ github.workspace }}/cloud-runner-logs.txt
AWS_REGION: eu-west-2
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: eu-west-2
AWS_BASE_STACK_NAME: game-ci-github-pipelines
CLOUD_RUNNER_BRANCH: ${{ github.ref }}
CLOUD_RUNNER_DEBUG: true
CLOUD_RUNNER_DEBUG_TREE: true
DEBUG: true
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
PROJECT_PATH: test-project
UNITY_VERSION: 2019.3.15f1
USE_IL2CPP: false
jobs:
asyncChecks:
name: Async Checks
if: github.event.event_type != 'pull_request_target'
runs-on: ubuntu-latest
steps:
- name: Checkout (default)
uses: actions/checkout@v3
with:
lfs: false
- run: yarn
- run: yarn run cli -m checks-update
timeout-minutes: 180
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
PROJECT_PATH: test-project
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TARGET_PLATFORM: StandaloneWindows64
cloudRunnerTests: true
versioning: None
CLOUD_RUNNER_CLUSTER: local-docker
AWS_BASE_STACK_NAME: game-ci-github-pipelines
CHECKS_UPDATE: ${{ github.event.inputs.checksObject }}

View File

@@ -0,0 +1,133 @@
name: Cloud Runner CI Pipeline
on:
push: { branches: [cloud-runner-develop, cloud-runner-preview, main] }
workflow_dispatch:
permissions:
checks: write
contents: read
actions: write
env:
GKE_ZONE: 'us-central1'
GKE_REGION: 'us-central1'
GKE_PROJECT: 'unitykubernetesbuilder'
GKE_CLUSTER: 'game-ci-github-pipelines'
GCP_LOGGING: true
GCP_PROJECT: unitykubernetesbuilder
GCP_LOG_FILE: ${{ github.workspace }}/cloud-runner-logs.txt
AWS_REGION: eu-west-2
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: eu-west-2
AWS_BASE_STACK_NAME: game-ci-team-pipelines
CLOUD_RUNNER_BRANCH: ${{ github.ref }}
CLOUD_RUNNER_DEBUG: true
CLOUD_RUNNER_DEBUG_TREE: true
DEBUG: true
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
PROJECT_PATH: test-project
UNITY_VERSION: 2019.3.15f1
USE_IL2CPP: false
USE_GKE_GCLOUD_AUTH_PLUGIN: true
jobs:
integrationTests:
name: Integration Tests
if: github.event.event_type != 'pull_request_target'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
cloudRunnerCluster:
- aws
- local-docker
- k8s
steps:
- name: Checkout (default)
uses: actions/checkout@v3
with:
lfs: false
- uses: google-github-actions/auth@v1
with:
credentials_json: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
- name: 'Set up Cloud SDK'
uses: 'google-github-actions/setup-gcloud@v1'
- name: Get GKE cluster credentials
run: |
export USE_GKE_GCLOUD_AUTH_PLUGIN=True
gcloud components install gke-gcloud-auth-plugin
gcloud container clusters get-credentials $GKE_CLUSTER --zone $GKE_ZONE --project $GKE_PROJECT
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-2
- run: yarn
- run: yarn run test "cloud-runner-async-workflow" --detectOpenHandles --forceExit --runInBand
if: matrix.CloudRunnerCluster != 'local-docker'
timeout-minutes: 180
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
PROJECT_PATH: test-project
GIT_PRIVATE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TARGET_PLATFORM: StandaloneWindows64
cloudRunnerTests: true
versioning: None
CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: yarn run test-i --detectOpenHandles --forceExit --runInBand
if: matrix.CloudRunnerCluster == 'local-docker'
timeout-minutes: 180
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
PROJECT_PATH: test-project
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TARGET_PLATFORM: StandaloneWindows64
cloudRunnerTests: true
versioning: None
CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }}
localBuildTests:
name: Local Build Target Tests
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
cloudRunnerCluster:
#- aws
- local-docker
#- k8s
targetPlatform:
- StandaloneOSX # Build a macOS standalone (Intel 64-bit).
- StandaloneWindows64 # Build a Windows 64-bit standalone.
- StandaloneLinux64 # Build a Linux 64-bit standalone.
- WebGL # WebGL.
- iOS # Build an iOS player.
- Android # Build an Android .apk.
steps:
- name: Checkout (default)
uses: actions/checkout@v3
with:
lfs: false
- run: yarn
- uses: ./
id: unity-build
timeout-minutes: 90
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
with:
cloudRunnerTests: true
versioning: None
projectPath: test-project
gitPrivateToken: ${{ secrets.GITHUB_TOKEN }}
targetPlatform: ${{ matrix.targetPlatform }}
cloudRunnerCluster: ${{ matrix.cloudRunnerCluster }}
- run: |
cp ./cloud-runner-cache/cache/${{ steps.unity-build.outputs.CACHE_KEY }}/build/${{ steps.unity-build.outputs.BUILD_ARTIFACT }} ${{ steps.unity-build.outputs.BUILD_ARTIFACT }}
- uses: actions/upload-artifact@v3
with:
name: ${{ matrix.cloudRunnerCluster }} Build (${{ matrix.targetPlatform }})
path: ${{ steps.unity-build.outputs.BUILD_ARTIFACT }}
retention-days: 14

View File

@@ -2,40 +2,23 @@ name: Integrity
on:
push: { branches: [main] }
pull_request:
types: [opened, synchronize, reopened, labeled]
permissions:
contents: read
checks: write
statuses: write
pull_request: {}
env:
CODECOV_TOKEN: '2f2eb890-30e2-4724-83eb-7633832cf0de'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
tests:
name: Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
node-version: '16'
- run: yarn
- run: yarn lint
- run: yarn test:ci --coverage
- run: yarn test --coverage
- run: bash <(curl -s https://codecov.io/bash)
- run: yarn build || { echo "build command should always succeed" ; exit 61; }
# - run: yarn build --quiet && git diff --quiet dist || { echo "dist should be auto generated" ; git diff dist ; exit 62; }
orchestrator-integration:
name: Orchestrator Integration
if: >-
github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'run-integration')
uses: ./.github/workflows/validate-orchestrator-integration.yml
secrets: inherit
# - run: yarn build --quiet && git diff --quiet dist || { echo "dist should be auto generated" ; git diff dist ; exit 62; }

74
.github/workflows/mac-build-tests.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
name: Mac Builds
on:
push:
branches:
- main
env:
UNITY_LICENSE: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n <License id=\"Terms\">\n <MachineBindings>\n <Binding Key=\"1\" Value=\"576562626572264761624c65526f7578\"/>\n <Binding Key=\"2\" Value=\"576562626572264761624c65526f7578\"/>\n </MachineBindings>\n <MachineID Value=\"D7nTUnjNAmtsUMcnoyrqkgIbYdM=\"/>\n <SerialHash Value=\"2033b8ac3e6faa3742ca9f0bfae44d18f2a96b80\"/>\n <Features>\n <Feature Value=\"33\"/>\n <Feature Value=\"1\"/>\n <Feature Value=\"12\"/>\n <Feature Value=\"2\"/>\n <Feature Value=\"24\"/>\n <Feature Value=\"3\"/>\n <Feature Value=\"36\"/>\n <Feature Value=\"17\"/>\n <Feature Value=\"19\"/>\n <Feature Value=\"62\"/>\n </Features>\n <DeveloperData Value=\"AQAAAEY0LUJHUlgtWEQ0RS1aQ1dWLUM1SlctR0RIQg==\"/>\n <SerialMasked Value=\"F4-BGRX-XD4E-ZCWV-C5JW-XXXX\"/>\n <StartDate Value=\"2021-02-08T00:00:00\"/>\n <UpdateDate Value=\"2021-02-09T00:34:57\"/>\n <InitialActivationDate Value=\"2021-02-08T00:34:56\"/>\n <LicenseVersion Value=\"6.x\"/>\n <ClientProvidedVersion Value=\"2018.4.30f1\"/>\n <AlwaysOnline Value=\"false\"/>\n <Entitlements>\n <Entitlement Ns=\"unity_editor\" Tag=\"UnityPersonal\" Type=\"EDITOR\" ValidTo=\"9999-12-31T00:00:00\"/>\n <Entitlement Ns=\"unity_editor\" Tag=\"DarkSkin\" Type=\"EDITOR_FEATURE\" ValidTo=\"9999-12-31T00:00:00\"/>\n </Entitlements>\n </License>\n<Signature xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><SignedInfo><CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments\"/><SignatureMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#rsa-sha1\"/><Reference URI=\"#Terms\"><Transforms><Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"/></Transforms><DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/><DigestValue>m0Db8UK+ktnOLJBtHybkfetpcKo=</DigestValue></Reference></SignedInfo><SignatureValue>o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw==</SignatureValue></Signature></root>"
jobs:
buildForAllPlatformsWindows:
name: Build for ${{ matrix.targetPlatform }} on version ${{ matrix.unityVersion }}
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
projectPath:
- test-project
unityVersion:
- 2020.3.24f1
targetPlatform:
- StandaloneOSX # Build a MacOS executable
steps:
###########################
# Checkout #
###########################
- uses: actions/checkout@v3
with:
lfs: true
###########################
# Cache #
###########################
- uses: actions/cache@v3
with:
path: ${{ matrix.projectPath }}/Library
key: Library-${{ matrix.projectPath }}-macos-${{ matrix.targetPlatform }}
restore-keys: |
Library-${{ matrix.projectPath }}-macos-
Library-
###########################
# Set Scripting Backend #
###########################
- name: Set Scripting Backend To il2cpp
run: |
mv -f "./test-project/ProjectSettings/ProjectSettingsIl2cpp.asset" "./test-project/ProjectSettings/ProjectSettings.asset"
###########################
# Build #
###########################
- uses: ./
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
with:
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
targetPlatform: ${{ matrix.targetPlatform }}
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue
# We use dirty build because we are replacing the default project settings file above
allowDirtyBuild: true
###########################
# Upload #
###########################
- uses: actions/upload-artifact@v3
with:
name: Build MacOS (${{ matrix.unityVersion }})
path: build
retention-days: 14

View File

@@ -1,91 +0,0 @@
name: Sync Secrets to Repositories
on:
workflow_dispatch:
inputs:
target_repo:
description: 'Target repository (org/repo format)'
required: true
default: 'game-ci/orchestrator'
type: choice
options:
- game-ci/orchestrator
- game-ci/cli
dry_run:
description: 'Dry run (list secrets to sync without writing)'
required: false
default: false
type: boolean
permissions:
contents: read
jobs:
sync-secrets:
name: Sync secrets to ${{ inputs.target_repo }}
runs-on: ubuntu-latest
steps:
- name: Sync secrets
env:
GH_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
TARGET_REPO: ${{ inputs.target_repo }}
DRY_RUN: ${{ inputs.dry_run }}
# Secrets to sync — values come from repo + org secrets available here
SECRET_UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
SECRET_UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
SECRET_UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
SECRET_GIT_PRIVATE_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
SECRET_GOOGLE_SERVICE_ACCOUNT_EMAIL: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }}
SECRET_GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
SECRET_CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
SECRET_UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
SECRET_NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
SECRETS=(
"UNITY_EMAIL:SECRET_UNITY_EMAIL"
"UNITY_PASSWORD:SECRET_UNITY_PASSWORD"
"UNITY_SERIAL:SECRET_UNITY_SERIAL"
"UNITY_LICENSE:SECRET_UNITY_LICENSE"
"GIT_PRIVATE_TOKEN:SECRET_GIT_PRIVATE_TOKEN"
"GOOGLE_SERVICE_ACCOUNT_EMAIL:SECRET_GOOGLE_SERVICE_ACCOUNT_EMAIL"
"GOOGLE_SERVICE_ACCOUNT_KEY:SECRET_GOOGLE_SERVICE_ACCOUNT_KEY"
"CODECOV_TOKEN:SECRET_CODECOV_TOKEN"
"NPM_TOKEN:SECRET_NPM_TOKEN"
)
synced=0
skipped=0
for entry in "${SECRETS[@]}"; do
name="${entry%%:*}"
env_var="${entry##*:}"
value="${!env_var}"
if [ -z "$value" ]; then
echo "⏭ SKIP: $name (not available in this repo's context)"
skipped=$((skipped + 1))
continue
fi
if [ "$DRY_RUN" = "true" ]; then
echo "🔍 DRY RUN: would sync $name → $TARGET_REPO"
else
if echo "$value" | gh secret set "$name" -R "$TARGET_REPO" --body - 2>/dev/null; then
echo "✅ SYNCED: $name → $TARGET_REPO"
else
echo "⚠️ FAILED: $name → $TARGET_REPO (continuing)"
skipped=$((skipped + 1))
synced=$((synced - 1))
fi
fi
synced=$((synced + 1))
done
echo ""
echo "=== Summary ==="
echo "Synced: $synced"
echo "Skipped (not available): $skipped"
echo "Target: $TARGET_REPO"
if [ "$DRY_RUN" = "true" ]; then
echo "Mode: DRY RUN (no secrets were written)"
fi

View File

@@ -1,203 +0,0 @@
name: Validate Community Plugins
on:
schedule:
# Run weekly on Sunday at 02:00 UTC
- cron: '0 2 * * 0'
workflow_dispatch:
inputs:
plugin_filter:
description: 'Filter plugins by name (regex pattern, empty = all)'
required: false
default: ''
unity_version:
description: 'Override Unity version (empty = use plugin default)'
required: false
default: ''
permissions:
contents: read
issues: write
jobs:
load-plugins:
name: Load Plugin Registry
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.parse.outputs.matrix }}
plugin_count: ${{ steps.parse.outputs.count }}
steps:
- uses: actions/checkout@v4
- name: Parse plugin registry
id: parse
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const yaml = require('js-yaml');
const registry = yaml.load(fs.readFileSync('community-plugins.yml', 'utf8'));
let plugins = registry.plugins || [];
// Apply name filter if provided
const filter = '${{ github.event.inputs.plugin_filter }}';
if (filter) {
const regex = new RegExp(filter, 'i');
plugins = plugins.filter(p => regex.test(p.name));
}
// Expand platform matrix
const matrix = [];
for (const plugin of plugins) {
const platforms = plugin.platforms || ['StandaloneLinux64'];
for (const platform of platforms) {
matrix.push({
name: plugin.name,
package: plugin.package,
source: plugin.source || 'git',
unity: '${{ github.event.inputs.unity_version }}' || plugin.unity || '2021.3',
platform: platform,
timeout: plugin.timeout || 30
});
}
}
core.setOutput('matrix', JSON.stringify({ include: matrix }));
core.setOutput('count', matrix.length);
console.log(`Found ${matrix.length} plugin-platform combinations to validate`);
validate:
name: '${{ matrix.name }} (${{ matrix.platform }})'
needs: load-plugins
if: needs.load-plugins.outputs.plugin_count > 0
runs-on: ubuntu-latest
timeout-minutes: ${{ fromJson(matrix.timeout) }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.load-plugins.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- name: Create test project
run: |
mkdir -p test-project/Assets
mkdir -p test-project/Packages
mkdir -p test-project/ProjectSettings
# Create minimal manifest.json
if [ "${{ matrix.source }}" = "git" ]; then
cat > test-project/Packages/manifest.json << 'MANIFEST'
{
"dependencies": {
"com.unity.modules.imgui": "1.0.0",
"com.unity.modules.jsonserialize": "1.0.0"
}
}
MANIFEST
# Add git package via manifest
cd test-project
cat Packages/manifest.json | python3 -c "
import sys, json
manifest = json.load(sys.stdin)
manifest['dependencies']['${{ matrix.name }}'] = '${{ matrix.package }}'
json.dump(manifest, sys.stdout, indent=2)
" > Packages/manifest.tmp && mv Packages/manifest.tmp Packages/manifest.json
cd ..
fi
# Create minimal ProjectSettings
cat > test-project/ProjectSettings/ProjectVersion.txt << EOF
m_EditorVersion: ${{ matrix.unity }}
EOF
- name: Build with unity-builder
uses: ./
id: build
with:
projectPath: test-project
targetPlatform: ${{ matrix.platform }}
unityVersion: ${{ matrix.unity }}
continue-on-error: true
- name: Record result
if: always()
run: |
STATUS="${{ steps.build.outcome }}"
echo "## ${{ matrix.name }} — ${{ matrix.platform }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$STATUS" = "success" ]; then
echo "✅ **PASSED** — Compiled and built successfully" >> $GITHUB_STEP_SUMMARY
else
echo "❌ **FAILED** — Build or compilation failed" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Unity: ${{ matrix.unity }}" >> $GITHUB_STEP_SUMMARY
echo "- Platform: ${{ matrix.platform }}" >> $GITHUB_STEP_SUMMARY
echo "- Source: ${{ matrix.source }}" >> $GITHUB_STEP_SUMMARY
echo "- Package: \`${{ matrix.package }}\`" >> $GITHUB_STEP_SUMMARY
report:
name: Validation Report
needs: [load-plugins, validate]
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate summary
uses: actions/github-script@v7
with:
script: |
const { data: run } = await github.rest.actions.listJobsForWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.runId
});
const validateJobs = run.jobs.filter(j => j.name.startsWith('validate'));
const passed = validateJobs.filter(j => j.conclusion === 'success').length;
const failed = validateJobs.filter(j => j.conclusion === 'failure').length;
const total = validateJobs.length;
let summary = `# Community Plugin Validation Report\n\n`;
summary += `**${passed}/${total} passed** | ${failed} failed\n\n`;
summary += `| Plugin | Platform | Status |\n|--------|----------|--------|\n`;
for (const job of validateJobs) {
const icon = job.conclusion === 'success' ? '✅' : '❌';
summary += `| ${job.name} | | ${icon} ${job.conclusion} |\n`;
}
await core.summary.addRaw(summary).write();
// Create or update issue if there are failures
if (failed > 0) {
const title = `Community Plugin Validation: ${failed} failure(s) — ${new Date().toISOString().split('T')[0]}`;
const body = summary + `\n\n[Workflow Run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`;
const { data: issues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'community-plugin-validation'
});
if (issues.length > 0) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issues[0].number,
body: body
});
} else {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: body,
labels: ['community-plugin-validation']
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,237 +0,0 @@
name: Validate Orchestrator Compatibility
# ==============================================================================
# Essential plugin health checks — runs on every PR and push.
# Fast (~5 min): compilation, unit tests, plugin interface, type declarations.
#
# For exhaustive integration tests (k8s, AWS, local-docker, rclone) see
# validate-orchestrator-integration.yml which runs on a daily cron.
# ==============================================================================
on:
workflow_dispatch:
push:
branches: [main, 'release/**', 'feature/**', 'refactor/**']
paths:
- 'src/model/orchestrator-plugin.ts'
- 'src/model/build-parameters.ts'
- 'src/model/input.ts'
- 'src/model/github.ts'
- 'src/model/cli/cli.ts'
- 'src/model/input-readers/**'
- 'src/index.ts'
- 'src/types/game-ci-orchestrator.d.ts'
- 'action.yml'
- 'package.json'
- 'yarn.lock'
- '.github/workflows/validate-orchestrator.yml'
pull_request:
branches: [main, 'release/**']
paths:
- 'src/model/orchestrator-plugin.ts'
- 'src/model/build-parameters.ts'
- 'src/model/input.ts'
- 'src/model/github.ts'
- 'src/model/cli/cli.ts'
- 'src/model/input-readers/**'
- 'src/index.ts'
- 'src/types/game-ci-orchestrator.d.ts'
- 'action.yml'
- 'package.json'
- 'yarn.lock'
- '.github/workflows/validate-orchestrator.yml'
permissions:
contents: read
packages: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
# ============================================================================
# PLUGIN ARCHITECTURE HEALTH CHECK
# ============================================================================
# Validates that:
# 1. unity-builder compiles and its unit tests pass
# 2. Plugin loader degrades gracefully without orchestrator
# 3. Orchestrator compiles and its unit tests pass
# 4. Plugin loader loads all services when orchestrator is installed
# 5. Type declarations match actual exports
# ============================================================================
plugin-health:
name: Plugin Architecture Health
runs-on: ubuntu-latest
steps:
- name: Checkout unity-builder
uses: actions/checkout@v4
- name: Checkout orchestrator
uses: actions/checkout@v4
with:
repository: game-ci/orchestrator
ref: ${{ github.head_ref || github.ref_name }}
path: orchestrator-standalone
continue-on-error: true
id: orchestrator-branch
- name: Fallback to orchestrator main branch
if: steps.orchestrator-branch.outcome == 'failure'
uses: actions/checkout@v4
with:
repository: game-ci/orchestrator
path: orchestrator-standalone
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
# --- unity-builder compilation and tests ---
- name: Install unity-builder dependencies
run: yarn install --frozen-lockfile
- name: Build unity-builder
run: |
echo "Building unity-builder TypeScript..."
npx tsc
echo "✓ unity-builder compiles successfully"
- name: Run orchestrator-plugin unit tests
run: |
echo "Running orchestrator-plugin unit tests..."
npx jest orchestrator-plugin --verbose --detectOpenHandles --forceExit
# --- Plugin loader without orchestrator ---
- name: Verify plugin loader returns undefined without orchestrator
run: |
echo "Checking plugin loader handles missing @game-ci/orchestrator..."
node -e "
const { loadOrchestratorPlugin } = require('./lib/model/orchestrator-plugin');
(async () => {
const plugin = await loadOrchestratorPlugin();
if (plugin !== undefined) {
console.error('ERROR: loadOrchestratorPlugin should return undefined when package not installed');
process.exit(1);
}
console.log('✓ loadOrchestratorPlugin() returns undefined when package not installed');
})();
"
- name: Verify orchestrator type declarations exist
run: |
if [ -f "src/types/game-ci-orchestrator.d.ts" ]; then
echo "✓ Type declarations for @game-ci/orchestrator exist"
else
echo "::error::Missing type declarations: src/types/game-ci-orchestrator.d.ts"
exit 1
fi
# --- Orchestrator compilation and tests ---
- name: Build and pack orchestrator
working-directory: orchestrator-standalone
run: |
yarn install --frozen-lockfile
echo "Building orchestrator..."
npx tsc
echo "✓ orchestrator compiles successfully"
echo "Packing orchestrator as tarball..."
npm pack
- name: Run orchestrator unit tests
working-directory: orchestrator-standalone
run: |
echo "Running orchestrator unit tests..."
npx jest --no-cache 2>&1 | tail -20
# --- Plugin loader with orchestrator installed ---
- name: Install orchestrator into unity-builder
run: |
echo "Installing orchestrator into unity-builder workspace..."
npm install ./orchestrator-standalone/game-ci-orchestrator-*.tgz --no-save --legacy-peer-deps
- name: Verify plugin loader returns exports with orchestrator installed
run: |
echo "Checking plugin loader returns defined exports..."
node -e "
const { loadOrchestratorPlugin } = require('./lib/model/orchestrator-plugin');
(async () => {
const plugin = await loadOrchestratorPlugin();
if (plugin === undefined) {
console.error('ERROR: loadOrchestratorPlugin should return defined plugin when package is installed');
process.exit(1);
}
const lifecycleMethods = [
'initialize', 'canHandleBuild', 'handleBuild',
'beforeLocalBuild', 'afterLocalBuild', 'handlePostBuild',
];
for (const method of lifecycleMethods) {
if (typeof plugin[method] !== 'function') {
console.error('ERROR: plugin.' + method + ' should be a function, got ' + typeof plugin[method]);
process.exit(1);
}
}
console.log('✓ loadOrchestratorPlugin() returns plugin with all ' + lifecycleMethods.length + ' lifecycle methods');
})();
"
- name: Verify type declarations match orchestrator exports
run: |
echo "Checking type declarations align with orchestrator exports..."
node -e "
const orch = require('@game-ci/orchestrator');
const expectedExports = [
'Orchestrator', 'BuildReliabilityService', 'TestWorkflowService',
'HotRunnerService', 'OutputService', 'OutputTypeRegistry',
'ArtifactUploadHandler', 'IncrementalSyncService',
'ChildWorkspaceService', 'LocalCacheService', 'SubmoduleProfileService',
'LfsAgentService', 'GitHooksService',
];
const missing = expectedExports.filter(e => orch[e] === undefined);
if (missing.length > 0) {
console.error('ERROR: Missing exports from @game-ci/orchestrator:', missing.join(', '));
process.exit(1);
}
console.log('✓ All ' + expectedExports.length + ' declared exports present in orchestrator package');
"
- name: Smoke test orchestrator build wiring
run: |
echo "Verifying orchestrator build wiring end-to-end..."
node -e "
const { loadOrchestratorPlugin } = require('./lib/model/orchestrator-plugin');
(async () => {
// Verify plugin loads successfully with orchestrator installed
const plugin = await loadOrchestratorPlugin();
if (plugin === undefined) {
console.error('ERROR: plugin should be defined when orchestrator is installed');
process.exit(1);
}
// Verify all lifecycle methods are callable
const lifecycleMethods = [
'initialize', 'canHandleBuild', 'handleBuild',
'beforeLocalBuild', 'afterLocalBuild', 'handlePostBuild',
];
for (const m of lifecycleMethods) {
if (typeof plugin[m] !== 'function') {
console.error('ERROR: plugin.' + m + ' should be a function, got ' + typeof plugin[m]);
process.exit(1);
}
}
console.log('✓ Plugin has all ' + lifecycleMethods.length + ' lifecycle methods');
// Verify canHandleBuild returns a boolean
const canHandle = plugin.canHandleBuild();
if (typeof canHandle !== 'boolean') {
console.error('ERROR: canHandleBuild() should return a boolean, got ' + typeof canHandle);
process.exit(1);
}
console.log('✓ canHandleBuild() returns boolean');
console.log('✓ Plugin architecture wiring verified');
})();
"

View File

@@ -0,0 +1,77 @@
name: Windows Builds
on:
push:
branches:
- main
env:
UNITY_LICENSE: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n <License id=\"Terms\">\n <MachineBindings>\n <Binding Key=\"1\" Value=\"576562626572264761624c65526f7578\"/>\n <Binding Key=\"2\" Value=\"576562626572264761624c65526f7578\"/>\n </MachineBindings>\n <MachineID Value=\"D7nTUnjNAmtsUMcnoyrqkgIbYdM=\"/>\n <SerialHash Value=\"2033b8ac3e6faa3742ca9f0bfae44d18f2a96b80\"/>\n <Features>\n <Feature Value=\"33\"/>\n <Feature Value=\"1\"/>\n <Feature Value=\"12\"/>\n <Feature Value=\"2\"/>\n <Feature Value=\"24\"/>\n <Feature Value=\"3\"/>\n <Feature Value=\"36\"/>\n <Feature Value=\"17\"/>\n <Feature Value=\"19\"/>\n <Feature Value=\"62\"/>\n </Features>\n <DeveloperData Value=\"AQAAAEY0LUJHUlgtWEQ0RS1aQ1dWLUM1SlctR0RIQg==\"/>\n <SerialMasked Value=\"F4-BGRX-XD4E-ZCWV-C5JW-XXXX\"/>\n <StartDate Value=\"2021-02-08T00:00:00\"/>\n <UpdateDate Value=\"2021-02-09T00:34:57\"/>\n <InitialActivationDate Value=\"2021-02-08T00:34:56\"/>\n <LicenseVersion Value=\"6.x\"/>\n <ClientProvidedVersion Value=\"2018.4.30f1\"/>\n <AlwaysOnline Value=\"false\"/>\n <Entitlements>\n <Entitlement Ns=\"unity_editor\" Tag=\"UnityPersonal\" Type=\"EDITOR\" ValidTo=\"9999-12-31T00:00:00\"/>\n <Entitlement Ns=\"unity_editor\" Tag=\"DarkSkin\" Type=\"EDITOR_FEATURE\" ValidTo=\"9999-12-31T00:00:00\"/>\n </Entitlements>\n </License>\n<Signature xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><SignedInfo><CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments\"/><SignatureMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#rsa-sha1\"/><Reference URI=\"#Terms\"><Transforms><Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"/></Transforms><DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/><DigestValue>m0Db8UK+ktnOLJBtHybkfetpcKo=</DigestValue></Reference></SignedInfo><SignatureValue>o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw==</SignatureValue></Signature></root>"
jobs:
buildForAllPlatformsWindows:
name: Build for ${{ matrix.targetPlatform }} on version ${{ matrix.unityVersion }}
runs-on: windows-2019
strategy:
fail-fast: false
matrix:
projectPath:
- test-project
unityVersion:
- 2020.3.24f1
targetPlatform:
- StandaloneWindows64 # Build a Windows 64-bit standalone.
- StandaloneWindows # Build a Windows 32-bit standalone.
- WSAPlayer # Build a UWP App
- tvOS # Build an Apple TV XCode project
steps:
###########################
# Checkout #
###########################
- uses: actions/checkout@v3
with:
lfs: true
###########################
# Cache #
###########################
- uses: actions/cache@v3
with:
path: ${{ matrix.projectPath }}/Library
key: Library-${{ matrix.projectPath }}-windows-${{ matrix.targetPlatform }}
restore-keys: |
Library-${{ matrix.projectPath }}-windows-
Library-
###########################
# Set Scripting Backend #
###########################
- name: Set Scripting Backend To il2cpp
run: |
Move-Item -Path "./test-project/ProjectSettings/ProjectSettingsIl2cpp.asset" -Destination "./test-project/ProjectSettings/ProjectSettings.asset" -Force
###########################
# Build #
###########################
- uses: ./
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
with:
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
targetPlatform: ${{ matrix.targetPlatform }}
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue
allowDirtyBuild: true
# We use dirty build because we are replacing the default project settings file above
###########################
# Upload #
###########################
- uses: actions/upload-artifact@v3
with:
name: Build Windows (${{ matrix.unityVersion }})
path: build
retention-days: 14

2
.gitignore vendored
View File

@@ -5,5 +5,3 @@ lib/
.vsconfig
yarn-error.log
.orig
$LOG_FILE
temp/

1
.npmrc
View File

@@ -1 +0,0 @@
engine-strict=true

32
.vscode/launch.json vendored
View File

@@ -1,32 +0,0 @@
{
"configurations": [
{
"name": "PowerShell Launch Current File",
"type": "PowerShell",
"request": "launch",
"script": "${file}",
"cwd": "${cwd}"
},
{
"type": "node",
"request": "launch",
"name": "Debug Jest Test",
"program": "${workspaceRoot}/node_modules/jest/bin/jest.js",
"args": [
"--collectCoverage=false",
"--colors",
"--config",
"${workspaceRoot}/jest.config.js",
"--runInBand",
"--runTestsByPath",
"${relativeFile}",
"--testPathPattern=${fileDirname}",
"--testTimeout=10000000"
],
"outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart",
"envFile": "${workspaceRoot}/.env",
"skipFiles": ["${workspaceRoot}/../../node_modules/**/*", "<node_internals>/**/*"]
}
]
}

View File

@@ -25,7 +25,7 @@ Steps to be performed to submit a pull request:
#### Pull Request Prerequisites
You have [Node](https://nodejs.org/) installed at v18+ and [Yarn](https://yarnpkg.com/) at v1.22.0+.
You have [Node](https://nodejs.org/) installed at v12.2.0+ and [Yarn](https://yarnpkg.com/) at v1.18.0+.
Please note that commit hooks will run automatically to perform some tasks;
@@ -36,8 +36,7 @@ Please note that commit hooks will run automatically to perform some tasks;
#### Windows users
Make sure your editor and terminal that run the tests are set to `Powershell 7` or above with
`Git's Unix tools for Windows` installed. This is because some tests require you to be able to run `sh` and other
unix commands.
`Git's Unix tools for Windows` installed. Some tests require you to be able to run `sh` and other unix commands.
#### License

View File

@@ -10,9 +10,8 @@ Part of the <a href="https://game.ci">GameCI</a> open source project.
<br />
<br />
[![Builds - Ubuntu](https://github.com/game-ci/unity-builder/actions/workflows/build-tests-ubuntu.yml/badge.svg)](https://github.com/game-ci/unity-builder/actions/workflows/build-tests-ubuntu.yml)
[![Builds - Windows](https://github.com/game-ci/unity-builder/actions/workflows/build-tests-windows.yml/badge.svg)](https://github.com/game-ci/unity-builder/actions/workflows/build-tests-windows.yml)
[![Builds - MacOS](https://github.com/game-ci/unity-builder/actions/workflows/build-tests-mac.yml/badge.svg)](https://github.com/game-ci/unity-builder/actions/workflows/build-tests-mac.yml)
[![Actions status](https://github.com/game-ci/unity-builder/workflows/Builds/badge.svg?event=push&branch=main)](https://github.com/game-ci/unity-builder/actions?query=branch%3Amain+event%3Apush+workflow%3A%22Builds)
[![lgtm - code quality](https://img.shields.io/lgtm/grade/javascript/g/webbertakken/unity-builder.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/webbertakken/unity-builder/context:javascript)
[![codecov - test coverage](https://codecov.io/gh/game-ci/unity-builder/branch/master/graph/badge.svg)](https://codecov.io/gh/game-ci/unity-builder)
<br />
<br />

View File

@@ -18,15 +18,11 @@ inputs:
projectPath:
required: false
default: ''
description: 'Path to the project to be built, relative to the repository root.'
buildProfile:
required: false
default: ''
description: 'Path to the build profile to activate, relative to the project root.'
description: 'Relative path to the project to be built.'
buildName:
required: false
default: ''
description: 'Name of the build. Should not include a file extension.'
description: 'Name of the build.'
buildsPath:
required: false
default: ''
@@ -35,22 +31,10 @@ inputs:
required: false
default: ''
description: 'Path to a Namespace.Class.StaticMethod to run to perform the build.'
manualExit:
required: false
default: ''
description: 'Suppresses `-quit`. Exit your build method using `EditorApplication.Exit(0)` instead.'
enableGpu:
required: false
default: ''
description: 'Launches unity without specifying `-nographics`.'
customParameters:
required: false
default: ''
description: 'Custom parameters to configure the build.'
useHostNetwork:
required: false
default: false
description: 'Initialises Docker using the host network. (Linux only)'
versioning:
required: false
default: 'Semantic'
@@ -63,12 +47,10 @@ inputs:
required: false
default: ''
description: 'The android versionCode'
androidExportType:
androidAppBundle:
required: false
default: 'androidPackage'
description:
'The android export type. Should be androidPackage for apk, androidAppBundle for aab, or androidStudioProject for
an android studio project.'
default: 'false'
description: 'Whether to build .aab instead of .apk'
androidKeystoreName:
required: false
default: ''
@@ -93,94 +75,118 @@ inputs:
required: false
default: ''
description: 'The android target API level.'
androidSymbolType:
required: false
default: 'none'
description: 'The android symbol type to export. Should be "none", "public" or "debugging".'
sshAgent:
required: false
default: ''
description: 'SSH Agent path to forward to the container'
sshPublicKeysDirectoryPath:
required: false
default: ''
description: 'Path to a directory containing SSH public keys to forward to the container.'
gitPrivateToken:
required: false
default: ''
description: 'Github private token to pull from github'
providerStrategy:
default: 'local'
description: '[CloudRunner] Github private token to pull from github'
githubOwner:
required: false
description:
'Build execution strategy. Use "local" for local Docker/Mac builds. For remote builds (aws, k8s, etc.), install
@game-ci/orchestrator and use the game-ci/orchestrator action which declares its own inputs.'
runAsHostUser:
required: false
default: 'false'
description:
'Whether to run as a user that matches the host system or the default root container user. Only applicable to
Linux hosts and containers. This is useful for fixing permission errors on Self-Hosted runners.'
default: ''
description: '[CloudRunner] GitHub owner name or organization/team name'
chownFilesTo:
required: false
default: ''
description:
'User and optionally group (user or user:group or uid:gid) to give ownership of the resulting build artifacts'
dockerCpuLimit:
required: false
default: ''
description: 'Number of CPU cores to assign the docker container. Defaults to all available cores on all platforms.'
dockerMemoryLimit:
required: false
default: ''
description:
'Amount of memory to assign the docker container. Defaults to 95% of total system memory rounded down to the
nearest megabyte on Linux and 80% on Windows. On unrecognized platforms, defaults to 75% of total system memory.
To manually specify a value, use the format <number><unit>, where unit is either m or g. ie: 512m = 512 megabytes'
dockerIsolationMode:
required: false
default: 'default'
description:
'Isolation mode to use for the docker container. Can be one of process, hyperv, or default. Default will pick the
default mode as described by Microsoft where server versions use process and desktop versions use hyperv. Only
applicable on Windows'
containerRegistryRepository:
required: false
default: 'unityci/editor'
description: 'Container registry and repository to pull image from. Only applicable if customImage is not set.'
containerRegistryImageVersion:
required: false
default: '3'
description: 'Container registry image version. Only applicable if customImage is not set.'
allowDirtyBuild:
required: false
default: ''
description: 'Allows the branch of the build to be dirty, and still generate the build.'
cacheUnityInstallationOnMac:
default: 'false'
description: '[CloudRunner] Allows the branch of the build to be dirty, and still generate the build.'
postBuildSteps:
required: false
description: 'Whether to cache the Unity hub and editor installation on MacOS'
unityHubVersionOnMac:
default: ''
description:
'[CloudRunner] run a post build job in yaml format with the keys image, secrets (name, value object array),
command string'
preBuildSteps:
required: false
default: ''
description:
'[CloudRunner] Run a pre build job after the repository setup but before the build job (in yaml format with the
keys image, secrets (name, value object array), command line string)'
customStepFiles:
required: false
default: ''
description:
'[CloudRunner] Specify the names (by file name) of custom steps to run before or after cloud runner jobs, must
match a yaml step file inside your repo in the folder .game-ci/steps/'
customHookFiles:
required: false
default: ''
description:
'[CloudRunner] Specify the names (by file name) of custom hooks to run before or after cloud runner jobs, must
match a yaml step file inside your repo in the folder .game-ci/hooks/'
customJobHooks:
required: false
default: ''
description: '[CloudRunner] Specify custom commands and trigger hooks (injects commands into jobs)'
customJob:
required: false
default: ''
description:
'[CloudRunner] Run a custom job instead of the standard build automation for cloud runner (in yaml format with the
keys image, secrets (name, value object array), command line string)'
awsBaseStackName:
default: 'game-ci'
required: false
description: '[CloudRunner] The Cloud Formation stack name that must be setup before using this option.'
cloudRunnerCluster:
default: 'local'
required: false
description:
'[CloudRunner] Either local, k8s or aws can be used to run builds on a remote cluster. Additional parameters must
be configured.'
cloudRunnerCpu:
default: ''
required: false
description: '[CloudRunner] Amount of CPU time to assign the remote build container'
cloudRunnerMemory:
default: ''
required: false
description: '[CloudRunner] Amount of memory to assign the remote build container'
readInputFromOverrideList:
default: ''
required: false
description: '[CloudRunner] Comma separated list of input value names to read from "input override command"'
readInputOverrideCommand:
default: ''
required: false
description:
'The version of Unity Hub to install on MacOS (e.g. 3.4.0). Defaults to latest available on brew if empty string
or nothing is specified.'
unityLicensingServer:
'[CloudRunner] Extend game ci by specifying a command to execute to pull input from external source e.g cloud
provider secret managers'
kubeConfig:
default: ''
required: false
description: 'The Unity licensing server address to use for activating Unity.'
dockerWorkspacePath:
default: '/github/workspace'
description:
'[CloudRunner] Supply a base64 encoded kubernetes config to run builds on kubernetes and stream logs until
completion.'
kubeVolume:
default: ''
required: false
description: '[CloudRunner] Supply a Persistent Volume Claim name to use for the Unity build.'
kubeStorageClass:
default: ''
required: false
description:
'The path to mount the workspace inside the docker container. For windows, leave out the drive letter. For example
c:/github/workspace should be defined as /github/workspace'
skipActivation:
default: 'false'
'[CloudRunner] Kubernetes storage class to use for cloud runner jobs, leave empty to install rook cluster.'
kubeVolumeSize:
default: '5Gi'
required: false
description: 'Skip the activation/deactivation of Unity. This assumes Unity is already activated.'
description: '[CloudRunner] Amount of disc space to assign the Kubernetes Persistent Volume'
cacheKey:
default: ''
required: false
description: '[CloudRunner] Cache key to indicate bucket for cache'
watchToEnd:
default: 'true'
required: false
description:
'[CloudRunner] Whether or not to watch the build to the end. Can be used for especially long running jobs e.g
imports or self-hosted ephemeral runners.'
outputs:
volume:
description: 'The Persistent Volume (PV) where the build artifacts have been stored by Kubernetes'
@@ -188,14 +194,9 @@ outputs:
description: 'The generated version used for the Unity build'
androidVersionCode:
description: 'The generated versionCode used for the Android Unity build'
engineExitCode:
description:
'Returns the exit code from the build scripts. This code is 0 if the build was successful. If there was an error
during activation, the code is from the activation step. If activation is successful, the code is from the project
build step.'
branding:
icon: 'box'
color: 'gray-dark'
runs:
using: 'node20'
using: 'node16'
main: 'dist/index.js'

View File

@@ -1,27 +0,0 @@
# Community Plugin Validation Registry
# Packages listed here are automatically tested on a schedule
# to ensure compatibility with unity-builder.
#
# Format:
# - name: Human-readable name
# package: UPM package name or git URL
# source: upm | git | asset-store
# unity: Minimum Unity version (optional, defaults to 2021.3)
# platforms: List of platforms to test (optional, defaults to [StandaloneLinux64])
# timeout: Build timeout in minutes (optional, defaults to 30)
plugins:
# Example entries — community members can submit PRs to add their packages
- name: UniTask
package: https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask
source: git
platforms: [StandaloneLinux64, StandaloneWindows64]
- name: NaughtyAttributes
package: https://github.com/dbrizov/NaughtyAttributes.git?path=Assets/NaughtyAttributes
source: git
- name: Unity Atoms
package: https://github.com/unity-atoms/unity-atoms.git
source: git
platforms: [StandaloneLinux64]

View File

@@ -0,0 +1,10 @@
{
"m_SettingKeys": [
"VR Device Disabled",
"VR Device User Alert"
],
"m_SettingValues": [
"False",
"False"
]
}

View File

@@ -0,0 +1,709 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<LangVersion>latest</LangVersion>
<_TargetFrameworkDirectories>non_empty_path_generated_by_unity.rider.package</_TargetFrameworkDirectories>
<_FullFrameworkReferenceAssemblyPaths>non_empty_path_generated_by_unity.rider.package</_FullFrameworkReferenceAssemblyPaths>
<DisableHandlePackageFileConflicts>true</DisableHandlePackageFileConflicts>
</PropertyGroup>
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>10.0.20506</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<RootNamespace></RootNamespace>
<ProjectGuid>{B7F8614B-1EC2-9D3A-DA1C-4D279A867D74}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<AssemblyName>Assembly-CSharp-Editor</AssemblyName>
<TargetFrameworkVersion>v4.7.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<BaseDirectory>.</BaseDirectory>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>Temp\bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;UNITY_2019_2_11;UNITY_2019_2;UNITY_2019;UNITY_5_3_OR_NEWER;UNITY_5_4_OR_NEWER;UNITY_5_5_OR_NEWER;UNITY_5_6_OR_NEWER;UNITY_2017_1_OR_NEWER;UNITY_2017_2_OR_NEWER;UNITY_2017_3_OR_NEWER;UNITY_2017_4_OR_NEWER;UNITY_2018_1_OR_NEWER;UNITY_2018_2_OR_NEWER;UNITY_2018_3_OR_NEWER;UNITY_2018_4_OR_NEWER;UNITY_2019_1_OR_NEWER;UNITY_2019_2_OR_NEWER;PLATFORM_ARCH_64;UNITY_64;UNITY_INCLUDE_TESTS;ENABLE_AUDIO;ENABLE_CACHING;ENABLE_CLOTH;ENABLE_MICROPHONE;ENABLE_MULTIPLE_DISPLAYS;ENABLE_PHYSICS;ENABLE_TEXTURE_STREAMING;ENABLE_UNET;ENABLE_LZMA;ENABLE_UNITYEVENTS;ENABLE_WEBCAM;ENABLE_WWW;ENABLE_CLOUD_SERVICES_COLLAB;ENABLE_CLOUD_SERVICES_COLLAB_SOFTLOCKS;ENABLE_CLOUD_SERVICES_ADS;ENABLE_CLOUD_SERVICES_USE_WEBREQUEST;ENABLE_CLOUD_SERVICES_UNET;ENABLE_CLOUD_SERVICES_BUILD;ENABLE_CLOUD_LICENSE;ENABLE_EDITOR_HUB_LICENSE;ENABLE_WEBSOCKET_CLIENT;ENABLE_DIRECTOR_AUDIO;ENABLE_DIRECTOR_TEXTURE;ENABLE_MANAGED_JOBS;ENABLE_MANAGED_TRANSFORM_JOBS;ENABLE_MANAGED_ANIMATION_JOBS;ENABLE_MANAGED_AUDIO_JOBS;INCLUDE_DYNAMIC_GI;ENABLE_MONO_BDWGC;ENABLE_SCRIPTING_GC_WBARRIERS;PLATFORM_SUPPORTS_MONO;RENDER_SOFTWARE_CURSOR;ENABLE_VIDEO;PLATFORM_STANDALONE_WIN;PLATFORM_STANDALONE;UNITY_STANDALONE_WIN;UNITY_STANDALONE;ENABLE_RUNTIME_GI;ENABLE_MOVIES;ENABLE_NETWORK;ENABLE_CRUNCH_TEXTURE_COMPRESSION;ENABLE_UNITYWEBREQUEST;ENABLE_CLOUD_SERVICES;ENABLE_CLOUD_SERVICES_ANALYTICS;ENABLE_CLOUD_SERVICES_PURCHASING;ENABLE_CLOUD_SERVICES_CRASH_REPORTING;ENABLE_OUT_OF_PROCESS_CRASH_HANDLER;ENABLE_EVENT_QUEUE;ENABLE_CLUSTER_SYNC;ENABLE_CLUSTERINPUT;ENABLE_VR;ENABLE_AR;ENABLE_WEBSOCKET_HOST;ENABLE_MONO;NET_STANDARD_2_0;ENABLE_PROFILER;UNITY_ASSERTIONS;UNITY_EDITOR;UNITY_EDITOR_64;UNITY_EDITOR_WIN;ENABLE_UNITY_COLLECTIONS_CHECKS;ENABLE_BURST_AOT;UNITY_TEAM_LICENSE;ENABLE_CUSTOM_RENDER_TEXTURE;ENABLE_DIRECTOR;ENABLE_LOCALIZATION;ENABLE_SPRITES;ENABLE_TERRAIN;ENABLE_TILEMAP;ENABLE_TIMELINE;ENABLE_LEGACY_INPUT_MANAGER;NET_4_6;CSHARP_7_OR_LATER;CSHARP_7_3_OR_NEWER</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<NoWarn>0169</NoWarn>
<AllowUnsafeBlocks>False</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>Temp\bin\Release\</OutputPath>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<NoWarn>0169</NoWarn>
<AllowUnsafeBlocks>False</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<NoConfig>true</NoConfig>
<NoStdLib>true</NoStdLib>
<AddAdditionalExplicitAssemblyReferences>false</AddAdditionalExplicitAssemblyReferences>
<ImplicitlyExpandNETStandardFacades>false</ImplicitlyExpandNETStandardFacades>
<ImplicitlyExpandDesignTimeFacades>false</ImplicitlyExpandDesignTimeFacades>
</PropertyGroup>
<ItemGroup>
<Reference Include="UnityEngine">
<HintPath>C:\Program Files\Unity\2019.2.11f1\Editor\Data\Managed/UnityEngine/UnityEngine.dll</HintPath>
</Reference>
<Reference Include="UnityEditor">
<HintPath>C:\Program Files\Unity\2019.2.11f1\Editor\Data\Managed/UnityEditor.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Assets\Editor\Builder.cs" />
<Compile Include="Assets\Editor\Input\ArgumentsParser.cs" />
<Compile Include="Assets\Editor\Reporting\StdOutReporter.cs" />
<Compile Include="Assets\Editor\System\ProcessExtensions.cs" />
<Compile Include="Assets\Editor\Versioning\VersionApplicator.cs" />
<Compile Include="Assets\Editor\Versioning\Git.cs" />
<Compile Include="Assets\Editor\Versioning\VersionGenerator.cs" />
<Compile Include="Assets\Editor\Versioning\GitException.cs" />
<Reference Include="UnityEditor.TestRunner">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/UnityEditor.TestRunner.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.TestRunner">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/UnityEngine.TestRunner.dll</HintPath>
</Reference>
<Reference Include="Unity.Timeline.Editor">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/Unity.Timeline.Editor.dll</HintPath>
</Reference>
<Reference Include="com.unity.multiplayer-hlapi.Editor">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/com.unity.multiplayer-hlapi.Editor.dll</HintPath>
</Reference>
<Reference Include="Unity.VSCode.Editor">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/Unity.VSCode.Editor.dll</HintPath>
</Reference>
<Reference Include="Unity.TextMeshPro.Editor">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/Unity.TextMeshPro.Editor.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UI">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/UnityEngine.UI.dll</HintPath>
</Reference>
<Reference Include="Unity.Timeline">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/Unity.Timeline.dll</HintPath>
</Reference>
<Reference Include="Unity.CollabProxy.Editor">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/Unity.CollabProxy.Editor.dll</HintPath>
</Reference>
<Reference Include="com.unity.multiplayer-weaver.Editor">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/com.unity.multiplayer-weaver.Editor.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.XR.LegacyInputHelpers">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/UnityEngine.XR.LegacyInputHelpers.dll</HintPath>
</Reference>
<Reference Include="Unity.Rider.Editor">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/Unity.Rider.Editor.dll</HintPath>
</Reference>
<Reference Include="Unity.2D.Sprite.Editor">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/Unity.2D.Sprite.Editor.dll</HintPath>
</Reference>
<Reference Include="Unity.2D.Tilemap.Editor">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/Unity.2D.Tilemap.Editor.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.SpatialTracking">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/UnityEditor.SpatialTracking.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.SpatialTracking">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/UnityEngine.SpatialTracking.dll</HintPath>
</Reference>
<Reference Include="Unity.TextMeshPro">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/Unity.TextMeshPro.dll</HintPath>
</Reference>
<Reference Include="Unity.Analytics.DataPrivacy">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/Unity.Analytics.DataPrivacy.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.XR.LegacyInputHelpers">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/UnityEditor.XR.LegacyInputHelpers.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.UI">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/UnityEditor.UI.dll</HintPath>
</Reference>
<Reference Include="com.unity.multiplayer-hlapi.Runtime">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/ScriptAssemblies/com.unity.multiplayer-hlapi.Runtime.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.AIModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.AIModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ARModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.ARModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.AccessibilityModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.AccessibilityModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.AndroidJNIModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.AndroidJNIModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.AnimationModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.AnimationModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.AssetBundleModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.AssetBundleModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.AudioModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.AudioModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ClothModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.ClothModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ClusterInputModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.ClusterInputModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ClusterRendererModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.ClusterRendererModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.CoreModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CrashReportingModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.CrashReportingModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.DSPGraphModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.DSPGraphModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.DirectorModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.DirectorModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.FileSystemHttpModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.FileSystemHttpModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.GameCenterModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.GameCenterModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.GridModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.GridModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.HotReloadModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.HotReloadModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.IMGUIModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.IMGUIModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ImageConversionModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.ImageConversionModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.InputModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.InputModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.InputLegacyModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.InputLegacyModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.JSONSerializeModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.JSONSerializeModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.LocalizationModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.LocalizationModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ParticleSystemModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.ParticleSystemModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.PerformanceReportingModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.PerformanceReportingModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.PhysicsModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.PhysicsModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.Physics2DModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.Physics2DModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ProfilerModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.ProfilerModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ScreenCaptureModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.ScreenCaptureModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.SharedInternalsModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.SharedInternalsModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.SpriteMaskModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.SpriteMaskModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.SpriteShapeModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.SpriteShapeModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.StreamingModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.StreamingModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.SubstanceModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.SubstanceModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.TLSModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.TLSModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.TerrainModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.TerrainModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.TerrainPhysicsModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.TerrainPhysicsModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.TextCoreModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.TextCoreModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.TextRenderingModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.TextRenderingModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.TilemapModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.TilemapModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UIModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.UIModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UIElementsModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.UIElementsModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UNETModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.UNETModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UmbraModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.UmbraModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UnityAnalyticsModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.UnityAnalyticsModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UnityConnectModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.UnityConnectModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UnityTestProtocolModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.UnityTestProtocolModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UnityWebRequestModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.UnityWebRequestModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UnityWebRequestAssetBundleModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.UnityWebRequestAssetBundleModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UnityWebRequestAudioModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.UnityWebRequestAudioModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UnityWebRequestTextureModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.UnityWebRequestTextureModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UnityWebRequestWWWModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.UnityWebRequestWWWModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.VFXModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.VFXModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.VRModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.VRModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.VehiclesModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.VehiclesModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.VideoModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.VideoModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.WindModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.WindModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.XRModule">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEngine/UnityEngine.XRModule.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.VR">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/UnityExtensions/Unity/UnityVR/Editor/UnityEditor.VR.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.Graphs">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/Managed/UnityEditor.Graphs.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.WindowsStandalone.Extensions">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/PlaybackEngines/WindowsStandaloneSupport/UnityEditor.WindowsStandalone.Extensions.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.WebGL.Extensions">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/PlaybackEngines/WebGLSupport/UnityEditor.WebGL.Extensions.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.Android.Extensions">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/PlaybackEngines/AndroidPlayer/UnityEditor.Android.Extensions.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.UWP.Extensions">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/PlaybackEngines/MetroSupport/UnityEditor.UWP.Extensions.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.Advertisements">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/PackageCache/com.unity.ads@2.0.8/Editor/UnityEditor.Advertisements.dll</HintPath>
</Reference>
<Reference Include="Unity.Analytics.Editor">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/PackageCache/com.unity.analytics@3.3.2/Unity.Analytics.Editor.dll</HintPath>
</Reference>
<Reference Include="Unity.Analytics.StandardEvents">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/PackageCache/com.unity.analytics@3.3.2/Unity.Analytics.StandardEvents.dll</HintPath>
</Reference>
<Reference Include="Unity.Analytics.Tracker">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/PackageCache/com.unity.analytics@3.3.2/Unity.Analytics.Tracker.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.Purchasing">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/PackageCache/com.unity.purchasing@2.0.6/Editor/UnityEditor.Purchasing.dll</HintPath>
</Reference>
<Reference Include="nunit.framework">
<HintPath>C:/Repositories/unity-builder/builder/default-build-script/Library/PackageCache/com.unity.ext.nunit@1.0.0/net35/unity-custom/nunit.framework.dll</HintPath>
</Reference>
<Reference Include="mscorlib">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/mscorlib.dll</HintPath>
</Reference>
<Reference Include="System">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/System.dll</HintPath>
</Reference>
<Reference Include="System.Core">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/System.Core.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/System.Runtime.Serialization.dll</HintPath>
</Reference>
<Reference Include="System.Xml">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/System.Xml.dll</HintPath>
</Reference>
<Reference Include="System.Xml.Linq">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/System.Xml.Linq.dll</HintPath>
</Reference>
<Reference Include="System.Numerics">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/System.Numerics.dll</HintPath>
</Reference>
<Reference Include="System.Numerics.Vectors">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/System.Numerics.Vectors.dll</HintPath>
</Reference>
<Reference Include="System.Net.Http">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/System.Net.Http.dll</HintPath>
</Reference>
<Reference Include="Microsoft.CSharp">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Microsoft.CSharp.dll</HintPath>
</Reference>
<Reference Include="System.Data">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/System.Data.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Win32.Primitives">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/Microsoft.Win32.Primitives.dll</HintPath>
</Reference>
<Reference Include="netstandard">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/netstandard.dll</HintPath>
</Reference>
<Reference Include="System.AppContext">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.AppContext.dll</HintPath>
</Reference>
<Reference Include="System.Collections.Concurrent">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Collections.Concurrent.dll</HintPath>
</Reference>
<Reference Include="System.Collections">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Collections.dll</HintPath>
</Reference>
<Reference Include="System.Collections.NonGeneric">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Collections.NonGeneric.dll</HintPath>
</Reference>
<Reference Include="System.Collections.Specialized">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Collections.Specialized.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.Annotations">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.ComponentModel.Annotations.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.ComponentModel.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.EventBasedAsync">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.ComponentModel.EventBasedAsync.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.Primitives">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.ComponentModel.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.TypeConverter">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.ComponentModel.TypeConverter.dll</HintPath>
</Reference>
<Reference Include="System.Console">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Console.dll</HintPath>
</Reference>
<Reference Include="System.Data.Common">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Data.Common.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.Contracts">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Diagnostics.Contracts.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.Debug">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Diagnostics.Debug.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.FileVersionInfo">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Diagnostics.FileVersionInfo.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.Process">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Diagnostics.Process.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.StackTrace">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Diagnostics.StackTrace.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.TextWriterTraceListener">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Diagnostics.TextWriterTraceListener.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.Tools">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Diagnostics.Tools.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.TraceSource">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Diagnostics.TraceSource.dll</HintPath>
</Reference>
<Reference Include="System.Drawing.Primitives">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Drawing.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.Dynamic.Runtime">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Dynamic.Runtime.dll</HintPath>
</Reference>
<Reference Include="System.Globalization.Calendars">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Globalization.Calendars.dll</HintPath>
</Reference>
<Reference Include="System.Globalization">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Globalization.dll</HintPath>
</Reference>
<Reference Include="System.Globalization.Extensions">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Globalization.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.IO.Compression.ZipFile">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.IO.Compression.ZipFile.dll</HintPath>
</Reference>
<Reference Include="System.IO">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.IO.dll</HintPath>
</Reference>
<Reference Include="System.IO.FileSystem">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.IO.FileSystem.dll</HintPath>
</Reference>
<Reference Include="System.IO.FileSystem.DriveInfo">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.IO.FileSystem.DriveInfo.dll</HintPath>
</Reference>
<Reference Include="System.IO.FileSystem.Primitives">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.IO.FileSystem.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.IO.FileSystem.Watcher">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.IO.FileSystem.Watcher.dll</HintPath>
</Reference>
<Reference Include="System.IO.IsolatedStorage">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.IO.IsolatedStorage.dll</HintPath>
</Reference>
<Reference Include="System.IO.MemoryMappedFiles">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.IO.MemoryMappedFiles.dll</HintPath>
</Reference>
<Reference Include="System.IO.Pipes">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.IO.Pipes.dll</HintPath>
</Reference>
<Reference Include="System.IO.UnmanagedMemoryStream">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.IO.UnmanagedMemoryStream.dll</HintPath>
</Reference>
<Reference Include="System.Linq">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Linq.dll</HintPath>
</Reference>
<Reference Include="System.Linq.Expressions">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Linq.Expressions.dll</HintPath>
</Reference>
<Reference Include="System.Linq.Parallel">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Linq.Parallel.dll</HintPath>
</Reference>
<Reference Include="System.Linq.Queryable">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Linq.Queryable.dll</HintPath>
</Reference>
<Reference Include="System.Net.Http.Rtc">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Net.Http.Rtc.dll</HintPath>
</Reference>
<Reference Include="System.Net.NameResolution">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Net.NameResolution.dll</HintPath>
</Reference>
<Reference Include="System.Net.NetworkInformation">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Net.NetworkInformation.dll</HintPath>
</Reference>
<Reference Include="System.Net.Ping">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Net.Ping.dll</HintPath>
</Reference>
<Reference Include="System.Net.Primitives">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Net.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.Net.Requests">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Net.Requests.dll</HintPath>
</Reference>
<Reference Include="System.Net.Security">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Net.Security.dll</HintPath>
</Reference>
<Reference Include="System.Net.Sockets">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Net.Sockets.dll</HintPath>
</Reference>
<Reference Include="System.Net.WebHeaderCollection">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Net.WebHeaderCollection.dll</HintPath>
</Reference>
<Reference Include="System.Net.WebSockets.Client">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Net.WebSockets.Client.dll</HintPath>
</Reference>
<Reference Include="System.Net.WebSockets">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Net.WebSockets.dll</HintPath>
</Reference>
<Reference Include="System.ObjectModel">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.ObjectModel.dll</HintPath>
</Reference>
<Reference Include="System.Reflection">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Reflection.dll</HintPath>
</Reference>
<Reference Include="System.Reflection.Emit">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Reflection.Emit.dll</HintPath>
</Reference>
<Reference Include="System.Reflection.Emit.ILGeneration">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Reflection.Emit.ILGeneration.dll</HintPath>
</Reference>
<Reference Include="System.Reflection.Emit.Lightweight">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Reflection.Emit.Lightweight.dll</HintPath>
</Reference>
<Reference Include="System.Reflection.Extensions">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Reflection.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.Reflection.Primitives">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Reflection.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.Resources.Reader">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Resources.Reader.dll</HintPath>
</Reference>
<Reference Include="System.Resources.ResourceManager">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Resources.ResourceManager.dll</HintPath>
</Reference>
<Reference Include="System.Resources.Writer">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Resources.Writer.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.CompilerServices.VisualC">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Runtime.CompilerServices.VisualC.dll</HintPath>
</Reference>
<Reference Include="System.Runtime">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Runtime.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Extensions">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Runtime.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Handles">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Runtime.Handles.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.InteropServices">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Runtime.InteropServices.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.InteropServices.RuntimeInformation">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Runtime.InteropServices.RuntimeInformation.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.InteropServices.WindowsRuntime">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Runtime.InteropServices.WindowsRuntime.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Numerics">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Runtime.Numerics.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization.Formatters">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Runtime.Serialization.Formatters.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization.Json">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Runtime.Serialization.Json.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization.Primitives">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Runtime.Serialization.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization.Xml">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Runtime.Serialization.Xml.dll</HintPath>
</Reference>
<Reference Include="System.Security.Claims">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Security.Claims.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.Algorithms">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Security.Cryptography.Algorithms.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.Csp">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Security.Cryptography.Csp.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.Encoding">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Security.Cryptography.Encoding.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.Primitives">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Security.Cryptography.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.X509Certificates">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Security.Cryptography.X509Certificates.dll</HintPath>
</Reference>
<Reference Include="System.Security.Principal">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Security.Principal.dll</HintPath>
</Reference>
<Reference Include="System.Security.SecureString">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Security.SecureString.dll</HintPath>
</Reference>
<Reference Include="System.ServiceModel.Duplex">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.ServiceModel.Duplex.dll</HintPath>
</Reference>
<Reference Include="System.ServiceModel.Http">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.ServiceModel.Http.dll</HintPath>
</Reference>
<Reference Include="System.ServiceModel.NetTcp">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.ServiceModel.NetTcp.dll</HintPath>
</Reference>
<Reference Include="System.ServiceModel.Primitives">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.ServiceModel.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.ServiceModel.Security">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.ServiceModel.Security.dll</HintPath>
</Reference>
<Reference Include="System.Text.Encoding">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Text.Encoding.dll</HintPath>
</Reference>
<Reference Include="System.Text.Encoding.Extensions">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Text.Encoding.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.Text.RegularExpressions">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Text.RegularExpressions.dll</HintPath>
</Reference>
<Reference Include="System.Threading">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Threading.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Overlapped">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Threading.Overlapped.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Tasks">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Threading.Tasks.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Tasks.Parallel">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Threading.Tasks.Parallel.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Thread">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Threading.Thread.dll</HintPath>
</Reference>
<Reference Include="System.Threading.ThreadPool">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Threading.ThreadPool.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Timer">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Threading.Timer.dll</HintPath>
</Reference>
<Reference Include="System.ValueTuple">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.ValueTuple.dll</HintPath>
</Reference>
<Reference Include="System.Xml.ReaderWriter">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Xml.ReaderWriter.dll</HintPath>
</Reference>
<Reference Include="System.Xml.XDocument">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Xml.XDocument.dll</HintPath>
</Reference>
<Reference Include="System.Xml.XmlDocument">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Xml.XmlDocument.dll</HintPath>
</Reference>
<Reference Include="System.Xml.XmlSerializer">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Xml.XmlSerializer.dll</HintPath>
</Reference>
<Reference Include="System.Xml.XPath">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Xml.XPath.dll</HintPath>
</Reference>
<Reference Include="System.Xml.XPath.XDocument">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/4.7.1-api/Facades/System.Xml.XPath.XDocument.dll</HintPath>
</Reference>
<Reference Include="UnityScript">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/unityscript/UnityScript.dll</HintPath>
</Reference>
<Reference Include="UnityScript.Lang">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/unityscript/UnityScript.Lang.dll</HintPath>
</Reference>
<Reference Include="Boo.Lang">
<HintPath>C:/Program Files/Unity/2019.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/unityscript/Boo.Lang.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Content Include=".editorconfig" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -6,9 +6,6 @@ using UnityBuilderAction.Reporting;
using UnityBuilderAction.Versioning;
using UnityEditor;
using UnityEditor.Build.Reporting;
#if UNITY_6000_0_OR_NEWER
using UnityEditor.Build.Profile;
#endif
using UnityEngine;
namespace UnityBuilderAction
@@ -20,9 +17,36 @@ namespace UnityBuilderAction
// Gather values from args
var options = ArgumentsParser.GetValidatedOptions();
// Gather values from project
var scenes = EditorBuildSettings.scenes.Where(scene => scene.enabled).Select(s => s.path).ToArray();
// Get all buildOptions from options
BuildOptions buildOptions = BuildOptions.None;
foreach (string buildOptionString in Enum.GetNames(typeof(BuildOptions))) {
if (options.ContainsKey(buildOptionString)) {
BuildOptions buildOptionEnum = (BuildOptions) Enum.Parse(typeof(BuildOptions), buildOptionString);
buildOptions |= buildOptionEnum;
}
}
// Define BuildPlayer Options
var buildPlayerOptions = new BuildPlayerOptions {
scenes = scenes,
locationPathName = options["customBuildPath"],
target = (BuildTarget) Enum.Parse(typeof(BuildTarget), options["buildTarget"]),
options = buildOptions
};
// Set version for this build
VersionApplicator.SetVersion(options["buildVersion"]);
// Apply Android settings
if (buildPlayerOptions.target == BuildTarget.Android)
{
VersionApplicator.SetAndroidVersionCode(options["androidVersionCode"]);
AndroidSettings.Apply(options);
}
// Execute default AddressableAsset content build, if the package is installed.
// Version defines would be the best solution here, but Unity 2018 doesn't support that,
// so we fall back to using reflection instead.
@@ -39,85 +63,10 @@ namespace UnityBuilderAction
}
catch (Exception e)
{
Debug.LogError("Failed to run default addressables build:\n" + e);
Debug.LogError($"Failed to run default addressables build:\n{e}");
}
}
// Get all buildOptions from options
BuildOptions buildOptions = BuildOptions.None;
foreach (string buildOptionString in Enum.GetNames(typeof(BuildOptions))) {
if (options.ContainsKey(buildOptionString)) {
BuildOptions buildOptionEnum = (BuildOptions) Enum.Parse(typeof(BuildOptions), buildOptionString);
buildOptions |= buildOptionEnum;
}
}
// Depending on whether the build is using a build profile, `buildPlayerOptions` will an instance
// of either `UnityEditor.BuildPlayerOptions` or `UnityEditor.BuildPlayerWithProfileOptions`
dynamic buildPlayerOptions;
if (options.TryGetValue("activeBuildProfile", out var buildProfilePath)) {
if (string.IsNullOrEmpty(buildProfilePath)) {
throw new Exception("`-activeBuildProfile` is set but with an empty value; this shouldn't happen");
}
#if UNITY_6000_0_OR_NEWER
// Load build profile from Assets folder
var buildProfile = AssetDatabase.LoadAssetAtPath<BuildProfile>(buildProfilePath)
?? throw new Exception("Build profile file not found at path: " + buildProfilePath);
// no need to set active profile, as already set by `-activeBuildProfile` CLI argument
// BuildProfile.SetActiveBuildProfile(buildProfile);
Debug.Log($"build profile: {buildProfile.name}");
// Define BuildPlayerWithProfileOptions
buildPlayerOptions = new BuildPlayerWithProfileOptions {
buildProfile = buildProfile,
locationPathName = options["customBuildPath"],
options = buildOptions,
};
#else // UNITY_6000_0_OR_NEWER
throw new Exception("Build profiles are not supported by this version of Unity (" + Application.unityVersion +")");
#endif // UNITY_6000_0_OR_NEWER
} else {
#if BUILD_PROFILE_LOADED
throw new Exception("Build profile's define symbol present; shouldn't happen");
#endif // BUILD_PROFILE_LOADED
// Gather values from project
var scenes = EditorBuildSettings.scenes.Where(scene => scene.enabled).Select(s => s.path).ToArray();
#if UNITY_2021_2_OR_NEWER
// Determine subtarget
StandaloneBuildSubtarget buildSubtarget;
if (!options.TryGetValue("standaloneBuildSubtarget", out var subtargetValue) || !Enum.TryParse(subtargetValue, out buildSubtarget)) {
buildSubtarget = default;
}
#endif
BuildTarget buildTarget = (BuildTarget) Enum.Parse(typeof(BuildTarget), options["buildTarget"]);
// Define BuildPlayerOptions
buildPlayerOptions = new BuildPlayerOptions {
scenes = scenes,
locationPathName = options["customBuildPath"],
target = buildTarget,
options = buildOptions,
#if UNITY_2021_2_OR_NEWER
subtarget = (int) buildSubtarget
#endif
};
// Apply Android settings
if (buildTarget == BuildTarget.Android) {
VersionApplicator.SetAndroidVersionCode(options["androidVersionCode"]);
AndroidSettings.Apply(options);
}
}
// Perform build
BuildReport buildReport = BuildPipeline.BuildPlayer(buildPlayerOptions);

View File

@@ -1,14 +1,14 @@
using System;
using System.Collections.Generic;
using UnityEditor;
using System.Reflection;
namespace UnityBuilderAction.Input
{
public static class AndroidSettings
public class AndroidSettings
{
public static void Apply(Dictionary<string, string> options)
{
EditorUserBuildSettings.buildAppBundle = options["customBuildPath"].EndsWith(".aab");
#if UNITY_2019_1_OR_NEWER
if (options.TryGetValue("androidKeystoreName", out string keystoreName) && !string.IsNullOrEmpty(keystoreName))
{
@@ -16,21 +16,13 @@ namespace UnityBuilderAction.Input
PlayerSettings.Android.keystoreName = keystoreName;
}
#endif
// Can't use out variable declaration as Unity 2018 doesn't support it
string keystorePass;
if (options.TryGetValue("androidKeystorePass", out keystorePass) && !string.IsNullOrEmpty(keystorePass))
if (options.TryGetValue("androidKeystorePass", out string keystorePass) && !string.IsNullOrEmpty(keystorePass))
PlayerSettings.Android.keystorePass = keystorePass;
string keyaliasName;
if (options.TryGetValue("androidKeyaliasName", out keyaliasName) && !string.IsNullOrEmpty(keyaliasName))
if (options.TryGetValue("androidKeyaliasName", out string keyaliasName) && !string.IsNullOrEmpty(keyaliasName))
PlayerSettings.Android.keyaliasName = keyaliasName;
string keyaliasPass;
if (options.TryGetValue("androidKeyaliasPass", out keyaliasPass) && !string.IsNullOrEmpty(keyaliasPass))
if (options.TryGetValue("androidKeyaliasPass", out string keyaliasPass) && !string.IsNullOrEmpty(keyaliasPass))
PlayerSettings.Android.keyaliasPass = keyaliasPass;
string androidTargetSdkVersion;
if (options.TryGetValue("androidTargetSdkVersion", out androidTargetSdkVersion) && !string.IsNullOrEmpty(androidTargetSdkVersion))
if (options.TryGetValue("androidTargetSdkVersion", out string androidTargetSdkVersion) && !string.IsNullOrEmpty(androidTargetSdkVersion))
{
var targetSdkVersion = AndroidSdkVersions.AndroidApiLevelAuto;
try
@@ -44,107 +36,6 @@ namespace UnityBuilderAction.Input
}
PlayerSettings.Android.targetSdkVersion = targetSdkVersion;
}
string androidExportType;
if (options.TryGetValue("androidExportType", out androidExportType) && !string.IsNullOrEmpty(androidExportType))
{
// Only exists in 2018.3 and above
PropertyInfo buildAppBundle = typeof(EditorUserBuildSettings)
.GetProperty("buildAppBundle", BindingFlags.Public | BindingFlags.Static);
switch (androidExportType)
{
case "androidStudioProject":
EditorUserBuildSettings.exportAsGoogleAndroidProject = true;
if (buildAppBundle != null)
buildAppBundle.SetValue(null, false, null);
break;
case "androidAppBundle":
EditorUserBuildSettings.exportAsGoogleAndroidProject = false;
if (buildAppBundle != null)
buildAppBundle.SetValue(null, true, null);
break;
case "androidPackage":
EditorUserBuildSettings.exportAsGoogleAndroidProject = false;
if (buildAppBundle != null)
buildAppBundle.SetValue(null, false, null);
break;
}
}
string symbolType;
if (options.TryGetValue("androidSymbolType", out symbolType) && !string.IsNullOrEmpty(symbolType))
{
#if UNITY_6000_0_OR_NEWER
switch (symbolType)
{
case "public":
SetDebugSymbols("SymbolTable");
break;
case "debugging":
SetDebugSymbols("Full");
break;
case "none":
SetDebugSymbols("None");
break;
}
#elif UNITY_2021_1_OR_NEWER
switch (symbolType)
{
case "public":
EditorUserBuildSettings.androidCreateSymbols = AndroidCreateSymbols.Public;
break;
case "debugging":
EditorUserBuildSettings.androidCreateSymbols = AndroidCreateSymbols.Debugging;
break;
case "none":
EditorUserBuildSettings.androidCreateSymbols = AndroidCreateSymbols.Disabled;
break;
}
#elif UNITY_2019_2_OR_NEWER
switch (symbolType)
{
case "public":
case "debugging":
EditorUserBuildSettings.androidCreateSymbolsZip = true;
break;
case "none":
EditorUserBuildSettings.androidCreateSymbolsZip = false;
break;
}
#endif
}
}
#if UNITY_6000_0_OR_NEWER
private static void SetDebugSymbols(string enumValueName)
{
// UnityEditor.Android.UserBuildSettings and Unity.Android.Types.DebugSymbolLevel are part of the Unity Android module.
// Reflection is used here to ensure the code works even if the module is not installed.
var debugSymbolsType = Type.GetType("UnityEditor.Android.UserBuildSettings+DebugSymbols, UnityEditor.Android.Extensions");
if (debugSymbolsType == null)
{
return;
}
var levelProp = debugSymbolsType.GetProperty("level", BindingFlags.Static | BindingFlags.Public);
if (levelProp == null)
{
return;
}
var enumType = Type.GetType("Unity.Android.Types.DebugSymbolLevel, Unity.Android.Types");
if (enumType == null)
{
return;
}
if (!Enum.TryParse(enumType, enumValueName, false , out var enumValue))
{
return;
}
levelProp.SetValue(null, enumValue);
}
#endif
}
}

View File

@@ -12,53 +12,33 @@ namespace UnityBuilderAction.Input
public static Dictionary<string, string> GetValidatedOptions()
{
Dictionary<string, string> validatedOptions;
ParseCommandLineArguments(out validatedOptions);
ParseCommandLineArguments(out var validatedOptions);
string projectPath;
if (!validatedOptions.TryGetValue("projectPath", out projectPath)) {
if (!validatedOptions.TryGetValue("projectPath", out var projectPath)) {
Console.WriteLine("Missing argument -projectPath");
EditorApplication.Exit(110);
}
#if UNITY_6000_0_OR_NEWER
var buildProfileSupport = true;
#else
var buildProfileSupport = false;
#endif // UNITY_6000_0_OR_NEWER
string buildProfile;
if (buildProfileSupport && validatedOptions.TryGetValue("activeBuildProfile", out buildProfile)) {
if (validatedOptions.ContainsKey("buildTarget")) {
Console.WriteLine("Extra argument -buildTarget");
EditorApplication.Exit(122);
}
} else {
string buildTarget;
if (!validatedOptions.TryGetValue("buildTarget", out buildTarget)) {
if (!validatedOptions.TryGetValue("buildTarget", out var buildTarget)) {
Console.WriteLine("Missing argument -buildTarget");
EditorApplication.Exit(120);
}
if (!Enum.IsDefined(typeof(BuildTarget), buildTarget)) {
Console.WriteLine(buildTarget + " is not a defined " + typeof(BuildTarget).Name);
EditorApplication.Exit(121);
}
}
string customBuildPath;
if (!validatedOptions.TryGetValue("customBuildPath", out customBuildPath)) {
if (!validatedOptions.TryGetValue("customBuildPath", out var customBuildPath)) {
Console.WriteLine("Missing argument -customBuildPath");
EditorApplication.Exit(130);
}
const string defaultCustomBuildName = "TestBuild";
string customBuildName;
if (!validatedOptions.TryGetValue("customBuildName", out customBuildName)) {
Console.WriteLine("Missing argument -customBuildName, defaulting to" + defaultCustomBuildName);
if (!validatedOptions.TryGetValue("customBuildName", out var customBuildName)) {
Console.WriteLine($"Missing argument -customBuildName, defaulting to {defaultCustomBuildName}.");
validatedOptions.Add("customBuildName", defaultCustomBuildName);
} else if (customBuildName == "") {
Console.WriteLine("Invalid argument -customBuildName, defaulting to" + defaultCustomBuildName);
Console.WriteLine($"Invalid argument -customBuildName, defaulting to {defaultCustomBuildName}.");
validatedOptions.Add("customBuildName", defaultCustomBuildName);
}
@@ -71,11 +51,11 @@ namespace UnityBuilderAction.Input
string[] args = Environment.GetCommandLineArgs();
Console.WriteLine(
EOL +
"###########################" + EOL +
"# Parsing settings #" + EOL +
"###########################" + EOL +
EOL
$"{EOL}" +
$"###########################{EOL}" +
$"# Parsing settings #{EOL}" +
$"###########################{EOL}" +
$"{EOL}"
);
// Extract flags with optional values
@@ -92,7 +72,7 @@ namespace UnityBuilderAction.Input
string displayValue = secret ? "*HIDDEN*" : "\"" + value + "\"";
// Assign
Console.WriteLine("Found flag \"" + flag + "\" with value " + displayValue);
Console.WriteLine($"Found flag \"{flag}\" with value {displayValue}.");
providedArguments.Add(flag, value);
}
}

View File

@@ -1,36 +0,0 @@
using System;
using UnityEngine;
using UnityEditor;
namespace UnityBuilderAction.Reporting
{
[InitializeOnLoad]
static class CompileListener
{
static CompileListener()
{
if (Application.isBatchMode)
{
Application.logMessageReceived += Application_logMessageReceived;
}
}
private static void Application_logMessageReceived(string condition, string stackTrace, LogType type)
{
string prefix = "";
switch (type)
{
case LogType.Error:
prefix = "error";
break;
case LogType.Warning:
prefix = "warning";
break;
case LogType.Exception:
prefix = "error";
break;
}
Console.WriteLine(Environment.NewLine + "::" + prefix + "::" + condition + Environment.NewLine + stackTrace);
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: fad44373fb7b61a4bb584e2675795aca
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -11,16 +11,16 @@ namespace UnityBuilderAction.Reporting
public static void ReportSummary(BuildSummary summary)
{
Console.WriteLine(
EOL +
"###########################" + EOL +
"# Build results #" + EOL +
"###########################" + EOL +
EOL +
"Duration: " + summary.totalTime.ToString() + EOL +
"Warnings: " + summary.totalWarnings.ToString() + EOL +
"Errors: " + summary.totalErrors.ToString() + EOL +
"Size: " + summary.totalSize.ToString() + " bytes" + EOL +
EOL
$"{EOL}" +
$"###########################{EOL}" +
$"# Build results #{EOL}" +
$"###########################{EOL}" +
$"{EOL}" +
$"Duration: {summary.totalTime.ToString()}{EOL}" +
$"Warnings: {summary.totalWarnings.ToString()}{EOL}" +
$"Errors: {summary.totalErrors.ToString()}{EOL}" +
$"Size: {summary.totalSize.ToString()} bytes{EOL}" +
$"{EOL}"
);
}

View File

@@ -21,11 +21,11 @@ namespace UnityBuilderAction.Versioning
version = GetSemanticCommitVersion();
Console.WriteLine("Repository has a valid version tag.");
} else {
version = "0.0." + GetTotalNumberOfCommits();
version = $"0.0.{GetTotalNumberOfCommits()}";
Console.WriteLine("Repository does not have tags to base the version on.");
}
Console.WriteLine("Version is " + version);
Console.WriteLine($"Version is {version}");
return version;
}
@@ -106,8 +106,7 @@ namespace UnityBuilderAction.Versioning
using (var process = new System.Diagnostics.Process()) {
string workingDirectory = UnityEngine.Application.dataPath;
string output, errors;
int exitCode = process.Run(application, arguments, workingDirectory, out output, out errors);
int exitCode = process.Run(application, arguments, workingDirectory, out string output, out string errors);
if (exitCode != 0) { throw new GitException(exitCode, errors); }
return output;

View File

@@ -0,0 +1,10 @@
{
"m_SettingKeys": [
"VR Device Disabled",
"VR Device User Alert"
],
"m_SettingValues": [
"False",
"False"
]
}

View File

@@ -0,0 +1,20 @@

Microsoft Visual Studio Solution File, Format Version 11.00
# Visual Studio 2010
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp-Editor", "Assembly-CSharp-Editor.csproj", "{B7F8614B-1EC2-9D3A-DA1C-4D279A867D74}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B7F8614B-1EC2-9D3A-DA1C-4D279A867D74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B7F8614B-1EC2-9D3A-DA1C-4D279A867D74}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B7F8614B-1EC2-9D3A-DA1C-4D279A867D74}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B7F8614B-1EC2-9D3A-DA1C-4D279A867D74}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Untracked/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Versioning/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

481704
dist/index.js generated vendored

File diff suppressed because one or more lines are too long

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

4593
dist/licenses.txt generated vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +1,28 @@
#!/usr/bin/env bash
#
# Perform Activation
# Create directories for license activation
#
if [ "$SKIP_ACTIVATION" != "true" ]; then
UNITY_LICENSE_PATH="/Library/Application Support/Unity"
sudo mkdir /Library/Application\ Support/Unity
sudo chmod -R 777 /Library/Application\ Support/Unity
if [ ! -d "$UNITY_LICENSE_PATH" ]; then
echo "Creating Unity License Directory"
sudo mkdir -p "$UNITY_LICENSE_PATH"
sudo chmod -R 777 "$UNITY_LICENSE_PATH"
fi;
ACTIVATE_LICENSE_PATH="$ACTION_FOLDER/BlankProject"
mkdir -p "$ACTIVATE_LICENSE_PATH"
source $ACTION_FOLDER/platforms/mac/steps/activate.sh
else
echo "Skipping activation"
fi
ACTIVATE_LICENSE_PATH="$ACTION_FOLDER/BlankProject"
mkdir -p "$ACTIVATE_LICENSE_PATH"
#
# Run Build
# Run steps
#
source $ACTION_FOLDER/platforms/mac/steps/activate.sh
source $ACTION_FOLDER/platforms/mac/steps/build.sh
source $ACTION_FOLDER/platforms/mac/steps/return_license.sh
#
# License Cleanup
# Remove license activation directory
#
if [ "$SKIP_ACTIVATION" != "true" ]; then
source $ACTION_FOLDER/platforms/mac/steps/return_license.sh
rm -r "$ACTIVATE_LICENSE_PATH"
fi
sudo rm -r /Library/Application\ Support/Unity
rm -r "$ACTIVATE_LICENSE_PATH"
#
# Instructions for debugging
@@ -49,7 +37,7 @@ echo ""
echo "Please note that the exit code is not very descriptive."
echo "Most likely it will not help you solve the issue."
echo ""
echo "To find the reason for failure: please search for errors in the log above and check for annotations in the summary view."
echo "To find the reason for failure: please search for errors in the log above."
echo ""
fi;

View File

@@ -4,69 +4,21 @@
echo "Changing to \"$ACTIVATE_LICENSE_PATH\" directory."
pushd "$ACTIVATE_LICENSE_PATH"
if [[ -n "$UNITY_SERIAL" && -n "$UNITY_EMAIL" && -n "$UNITY_PASSWORD" ]]; then
#
# SERIAL LICENSE MODE
#
# This will activate unity, using the serial activation process.
#
echo "Requesting activation"
echo "Requesting activation"
# Activate license
/Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/MacOS/Unity \
-logFile /dev/stdout \
-batchmode \
-nographics \
-quit \
-serial "$UNITY_SERIAL" \
-username "$UNITY_EMAIL" \
-password "$UNITY_PASSWORD" \
-projectPath "$ACTIVATE_LICENSE_PATH"
# Activate license
/Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/MacOS/Unity \
-logFile - \
-batchmode \
-nographics \
-quit \
-serial "$UNITY_SERIAL" \
-username "$UNITY_EMAIL" \
-password "$UNITY_PASSWORD" \
-projectPath "$ACTIVATE_LICENSE_PATH"
# Store the exit code from the verify command
UNITY_EXIT_CODE=$?
elif [[ -n "$UNITY_LICENSING_SERVER" ]]; then
#
# Custom Unity License Server
#
echo "Adding licensing server config"
mkdir -p "$UNITY_LICENSE_PATH/config/"
cp "$ACTION_FOLDER/unity-config/services-config.json" "$UNITY_LICENSE_PATH/config/services-config.json"
/Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/Frameworks/UnityLicensingClient.app/Contents/MacOS/Unity.Licensing.Client \
--acquire-floating > license.txt
# Store the exit code from the verify command
UNITY_EXIT_CODE=$?
if [ $UNITY_EXIT_CODE -eq 0 ]; then
PARSEDFILE=$(grep -oE '\"[^"]*\"' < license.txt | tr -d '"')
export FLOATING_LICENSE
FLOATING_LICENSE=$(sed -n 2p <<< "$PARSEDFILE")
FLOATING_LICENSE_TIMEOUT=$(sed -n 4p <<< "$PARSEDFILE")
echo "Acquired floating license: \"$FLOATING_LICENSE\" with timeout $FLOATING_LICENSE_TIMEOUT"
fi
else
#
# NO LICENSE ACTIVATION STRATEGY MATCHED
#
# This will exit since no activation strategies could be matched.
#
echo "License activation strategy could not be determined."
echo ""
echo "Visit https://game.ci/docs/github/activation for more"
echo "details on how to set up one of the possible activation strategies."
echo "::error ::No valid license activation strategy could be determined. Make sure to provide UNITY_EMAIL, UNITY_PASSWORD, and either a UNITY_SERIAL \
or UNITY_LICENSE. Otherwise please use UNITY_LICENSING_SERVER. See more info at https://game.ci/docs/github/activation"
# Immediately exit as no UNITY_EXIT_CODE can be derived.
exit 1;
fi
# Store the exit code from the verify command
UNITY_EXIT_CODE=$?
#
# Display information about the result
@@ -78,7 +30,6 @@ else
# Activation failed so exit with the code from the license verification step
echo "Unclassified error occured while trying to activate license."
echo "Exit code was: $UNITY_EXIT_CODE"
echo "::error ::There was an error while trying to activate the Unity license."
exit $UNITY_EXIT_CODE
fi

View File

@@ -19,23 +19,6 @@ echo "Using build name \"$BUILD_NAME\"."
echo "Using build target \"$BUILD_TARGET\"."
#
# Display the build profile
#
if [ -z "$BUILD_PROFILE" ]; then
# User has not provided a build profile
#
echo "Doing a default \"$BUILD_TARGET\" platform build."
#
else
# User has provided a path to a build profile `.asset` file
#
echo "Using build profile \"$BUILD_PROFILE\" relative to \"$UNITY_PROJECT_PATH\"."
#
fi
#
# Display build path and file
#
@@ -145,17 +128,16 @@ echo ""
# Reference: https://docs.unity3d.com/2019.3/Documentation/Manual/CommandLineArguments.html
/Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/MacOS/Unity \
-logFile - \
$( [ "${MANUAL_EXIT}" == "true" ] || echo "-quit" ) \
-quit \
-batchmode \
$( [ "${ENABLE_GPU}" == "true" ] || echo "-nographics" ) \
-nographics \
-username "$UNITY_EMAIL" \
-password "$UNITY_PASSWORD" \
-customBuildName "$BUILD_NAME" \
-projectPath "$UNITY_PROJECT_PATH" \
$( [ -z "$BUILD_PROFILE" ] && echo "-buildTarget $BUILD_TARGET") \
-buildTarget "$BUILD_TARGET" \
-customBuildTarget "$BUILD_TARGET" \
-customBuildPath "$CUSTOM_BUILD_PATH" \
-customBuildProfile "$BUILD_PROFILE" \
${BUILD_PROFILE:+-activeBuildProfile} ${BUILD_PROFILE:+"$BUILD_PROFILE"} \
-executeMethod "$BUILD_METHOD" \
-buildVersion "$VERSION" \
-androidVersionCode "$ANDROID_VERSION_CODE" \
@@ -164,9 +146,8 @@ echo ""
-androidKeyaliasName "$ANDROID_KEYALIAS_NAME" \
-androidKeyaliasPass "$ANDROID_KEYALIAS_PASS" \
-androidTargetSdkVersion "$ANDROID_TARGET_SDK_VERSION" \
-androidExportType "$ANDROID_EXPORT_TYPE" \
-androidSymbolType "$ANDROID_SYMBOL_TYPE" \
$CUSTOM_PARAMETERS
$CUSTOM_PARAMETERS \
> "$UNITY_PROJECT_PATH/out.log" 2>&1
# Catch exit code
BUILD_EXIT_CODE=$?

View File

@@ -4,29 +4,15 @@
echo "Changing to \"$ACTIVATE_LICENSE_PATH\" directory."
pushd "$ACTIVATE_LICENSE_PATH"
if [[ -n "$UNITY_LICENSING_SERVER" ]]; then
#
# Return any floating license used.
#
echo "Returning floating license: \"$FLOATING_LICENSE\""
/Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/Frameworks/UnityLicensingClient.app/Contents/MacOS/Unity.Licensing.Client \
--return-floating "$FLOATING_LICENSE"
elif [[ -n "$UNITY_SERIAL" ]]; then
#
# SERIAL LICENSE MODE
#
# This will return the license that is currently in use.
#
/Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/MacOS/Unity \
-logFile - \
-batchmode \
-nographics \
-quit \
-username "$UNITY_EMAIL" \
-password "$UNITY_PASSWORD" \
-returnlicense \
-projectPath "$ACTIVATE_LICENSE_PATH"
fi
/Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/MacOS/Unity \
-logFile /dev/stdout \
-batchmode \
-nographics \
-quit \
-username "$UNITY_EMAIL" \
-password "$UNITY_PASSWORD" \
-returnlicense \
-projectPath "$ACTIVATE_LICENSE_PATH"
# Return to previous working directory
popd

View File

@@ -1,83 +1,45 @@
#!/usr/bin/env bash
# Ensure machine ID is randomized for personal license activation
if [[ "$UNITY_SERIAL" = F* ]]; then
echo "Randomizing machine ID for personal license activation"
dbus-uuidgen > /etc/machine-id && mkdir -p /var/lib/dbus/ && ln -sf /etc/machine-id /var/lib/dbus/machine-id
fi
#
# Prepare Android SDK, if needed
# We do this here to ensure it has root permissions
# Create directory for license activation
#
fullProjectPath="$GITHUB_WORKSPACE/$PROJECT_PATH"
ACTIVATE_LICENSE_PATH="$GITHUB_WORKSPACE/_activate-license~"
mkdir -p "$ACTIVATE_LICENSE_PATH"
if [[ "$BUILD_TARGET" == "Android" ]]; then
export JAVA_HOME="$(awk -F'=' '/JAVA_HOME=/{print $2}' /usr/bin/unity-editor.d/*)"
ANDROID_HOME_DIRECTORY="$(awk -F'=' '/ANDROID_HOME=/{print $2}' /usr/bin/unity-editor.d/*)"
SDKMANAGER=$(find $ANDROID_HOME_DIRECTORY/cmdline-tools -name sdkmanager)
if [ -z "${SDKMANAGER}" ]
then
SDKMANAGER=$(find $ANDROID_HOME_DIRECTORY/tools/bin -name sdkmanager)
if [ -z "${SDKMANAGER}" ]
then
echo "No sdkmanager found"
exit 1
fi
fi
#
# Run steps
#
source /steps/set_gitcredential.sh
source /steps/activate.sh
source /steps/build.sh
source /steps/return_license.sh
if [[ -n "$ANDROID_SDK_MANAGER_PARAMETERS" ]]; then
echo "Updating Android SDK with parameters: $ANDROID_SDK_MANAGER_PARAMETERS"
$SDKMANAGER "$ANDROID_SDK_MANAGER_PARAMETERS"
else
echo "Updating Android SDK with auto detected target API version"
# Read the line containing AndroidTargetSdkVersion from the file
targetAPILine=$(grep 'AndroidTargetSdkVersion' "$fullProjectPath/ProjectSettings/ProjectSettings.asset")
#
# Remove license activation directory
#
# Extract the number after the semicolon
targetAPI=$(echo "$targetAPILine" | cut -d':' -f2 | tr -d '[:space:]')
rm -r "$ACTIVATE_LICENSE_PATH"
$SDKMANAGER "platforms;android-$targetAPI"
fi
#
# Instructions for debugging
#
echo "Updated Android SDK."
else
echo "Not updating Android SDK."
fi
if [[ $BUILD_EXIT_CODE -gt 0 ]]; then
echo ""
echo "###########################"
echo "# Failure #"
echo "###########################"
echo ""
echo "Please note that the exit code is not very descriptive."
echo "Most likely it will not help you solve the issue."
echo ""
echo "To find the reason for failure: please search for errors in the log above."
echo ""
fi;
if [[ "$RUN_AS_HOST_USER" == "true" ]]; then
echo "Running as host user"
#
# Exit with code from the build step.
#
# Stop on error if we can't set up the user
set -e
# Get host user/group info so we create files with the correct ownership
USERNAME=$(stat -c '%U' "$fullProjectPath")
USERID=$(stat -c '%u' "$fullProjectPath")
GROUPNAME=$(stat -c '%G' "$fullProjectPath")
GROUPID=$(stat -c '%g' "$fullProjectPath")
groupadd -g $GROUPID $GROUPNAME
useradd -u $USERID -g $GROUPID $USERNAME
usermod -aG $GROUPNAME $USERNAME
mkdir -p "/home/$USERNAME"
chown $USERNAME:$GROUPNAME "/home/$USERNAME"
# Normally need root permissions to access when using su
chmod 777 /dev/stdout
chmod 777 /dev/stderr
# Don't stop on error when running our scripts as error handling is baked in
set +e
# Switch to the host user so we can create files with the correct ownership
su $USERNAME -c "$SHELL -c 'source /steps/runsteps.sh'"
else
echo "Running as root"
# Run as root
source /steps/runsteps.sh
fi
exit $?
exit $BUILD_EXIT_CODE

View File

@@ -1,65 +1,78 @@
#!/usr/bin/env bash
# if blankproject folder doesn't exist create it
if [ ! -d "/BlankProject" ]; then
mkdir /BlankProject
fi
# if blankproject folder doesn't exist create it
if [ ! -d "/BlankProject/Assets" ]; then
mkdir /BlankProject/Assets
fi
# Run in ACTIVATE_LICENSE_PATH directory
echo "Changing to \"$ACTIVATE_LICENSE_PATH\" directory."
pushd "$ACTIVATE_LICENSE_PATH"
if [[ -n "$UNITY_SERIAL" && -n "$UNITY_EMAIL" && -n "$UNITY_PASSWORD" ]]; then
if [[ -n "$UNITY_LICENSE" ]] || [[ -n "$UNITY_LICENSE_FILE" ]]; then
#
# SERIAL LICENSE MODE
# PERSONAL LICENSE MODE
#
# This will activate unity, using the serial activation process.
# This will activate Unity, using a license file
#
echo "Requesting activation"
# Note that this is the ONLY WAY for PERSONAL LICENSES in 2020.
# * See for more details: https://gitlab.com/gableroux/unity3d-gitlab-ci-example/issues/5#note_72815478
#
# The license file can be acquired using `webbertakken/request-manual-activation-file` action.
echo "Requesting activation (personal license)"
# Loop the unity-editor call until the license is activated with exponential backoff and a maximum of 5 retries
retry_count=0
# Set the license file path
FILE_PATH=UnityLicenseFile.ulf
# Initialize delay to 15 seconds
delay=15
if [[ -n "$UNITY_LICENSE" ]]; then
# Copy license file from Github variables
echo "$UNITY_LICENSE" | tr -d '\r' > $FILE_PATH
elif [[ -n "$UNITY_LICENSE_FILE" ]]; then
# Copy license file from file system
cat "$UNITY_LICENSE_FILE" | tr -d '\r' > $FILE_PATH
fi
# Loop until UNITY_EXIT_CODE is 0 or retry count reaches 5
while [[ $retry_count -lt 5 ]]
do
# Activate license
unity-editor \
# Activate license
ACTIVATION_OUTPUT=$(unity-editor \
-logFile /dev/stdout \
-quit \
-serial "$UNITY_SERIAL" \
-username "$UNITY_EMAIL" \
-password "$UNITY_PASSWORD" \
-projectPath "/BlankProject"
-manualLicenseFile $FILE_PATH)
# Store the exit code from the verify command
UNITY_EXIT_CODE=$?
# Store the exit code from the verify command
UNITY_EXIT_CODE=$?
# Check if UNITY_EXIT_CODE is 0
if [[ $UNITY_EXIT_CODE -eq 0 ]]
then
echo "Activation successful"
break
else
# Increment retry count
((retry_count++))
# The exit code for personal activation is always 1;
# Determine whether activation was successful.
#
# Successful output should include the following:
#
# "LICENSE SYSTEM [2020120 18:51:20] Next license update check is after 2019-11-25T18:23:38"
#
ACTIVATION_SUCCESSFUL=$(echo $ACTIVATION_OUTPUT | grep 'Next license update check is after' | wc -l)
echo "::warning ::Activation failed, attempting retry #$retry_count"
echo "Activation failed, retrying in $delay seconds..."
sleep $delay
# Set exit code to 0 if activation was successful
if [[ $ACTIVATION_SUCCESSFUL -eq 1 ]]; then
UNITY_EXIT_CODE=0
fi;
# Double the delay for the next iteration
delay=$((delay * 2))
fi
done
# Remove license file
rm -f $FILE_PATH
if [[ $retry_count -eq 5 ]]
then
echo "Activation failed after 5 retries"
fi
elif [[ -n "$UNITY_SERIAL" && -n "$UNITY_EMAIL" && -n "$UNITY_PASSWORD" ]]; then
#
# PROFESSIONAL (SERIAL) LICENSE MODE
#
# This will activate unity, using the activating process.
#
# Note: This is the preferred way for PROFESSIONAL LICENSES.
#
echo "Requesting activation (professional license)"
# Activate license
unity-editor \
-logFile /dev/stdout \
-quit \
-serial "$UNITY_SERIAL" \
-username "$UNITY_EMAIL" \
-password "$UNITY_PASSWORD"
# Store the exit code from the verify command
UNITY_EXIT_CODE=$?
elif [[ -n "$UNITY_LICENSING_SERVER" ]]; then
#
@@ -68,18 +81,14 @@ elif [[ -n "$UNITY_LICENSING_SERVER" ]]; then
echo "Adding licensing server config"
/opt/unity/Editor/Data/Resources/Licensing/Client/Unity.Licensing.Client --acquire-floating > license.txt #is this accessible in a env variable?
PARSEDFILE=$(grep -oP '\".*?\"' < license.txt | tr -d '"')
export FLOATING_LICENSE
FLOATING_LICENSE=$(sed -n 2p <<< "$PARSEDFILE")
FLOATING_LICENSE_TIMEOUT=$(sed -n 4p <<< "$PARSEDFILE")
echo "Acquired floating license: \"$FLOATING_LICENSE\" with timeout $FLOATING_LICENSE_TIMEOUT"
# Store the exit code from the verify command
UNITY_EXIT_CODE=$?
if [ $UNITY_EXIT_CODE -eq 0 ]; then
PARSEDFILE=$(grep -oP '\".*?\"' < license.txt | tr -d '"')
export FLOATING_LICENSE
FLOATING_LICENSE=$(sed -n 2p <<< "$PARSEDFILE")
FLOATING_LICENSE_TIMEOUT=$(sed -n 4p <<< "$PARSEDFILE")
echo "Acquired floating license: \"$FLOATING_LICENSE\" with timeout $FLOATING_LICENSE_TIMEOUT"
fi
else
#
# NO LICENSE ACTIVATION STRATEGY MATCHED
@@ -88,13 +97,10 @@ else
#
echo "License activation strategy could not be determined."
echo ""
echo "Visit https://game.ci/docs/github/activation for more"
echo "Visit https://game.ci/docs/github/getting-started for more"
echo "details on how to set up one of the possible activation strategies."
echo "::error ::No valid license activation strategy could be determined. Make sure to provide UNITY_EMAIL, UNITY_PASSWORD, and either a UNITY_SERIAL \
or UNITY_LICENSE. Otherwise please use UNITY_LICENSING_SERVER. See more info at https://game.ci/docs/github/activation"
# Immediately exit as no UNITY_EXIT_CODE can be derived.
# Immediately exit as no UNITY_EXIT_CODE can be derrived.
exit 1;
fi
@@ -109,6 +115,8 @@ else
# Activation failed so exit with the code from the license verification step
echo "Unclassified error occured while trying to activate license."
echo "Exit code was: $UNITY_EXIT_CODE"
echo "::error ::There was an error while trying to activate the Unity license."
exit $UNITY_EXIT_CODE
fi
# Return to previous working directory
popd

View File

@@ -19,22 +19,6 @@ echo "Using build name \"$BUILD_NAME\"."
echo "Using build target \"$BUILD_TARGET\"."
#
# Display the build profile
#
if [ -z "$BUILD_PROFILE" ]; then
# User has not provided a build profile
#
echo "Doing a default \"$BUILD_TARGET\" platform build."
#
else
# User has provided a path to a build profile `.asset` file
#
echo "Using build profile \"$BUILD_PROFILE\" relative to \"$UNITY_PROJECT_PATH\"."
#
fi
#
# Display build path and file
#
@@ -78,6 +62,19 @@ else
#
fi
#
# Prepare Android SDK, if needed
#
if [[ "$BUILD_TARGET" == "Android" && -n "$ANDROID_SDK_MANAGER_PARAMETERS" ]]; then
echo "Updating Android SDK with parameters: $ANDROID_SDK_MANAGER_PARAMETERS"
export JAVA_HOME="$(awk -F'=' '/JAVA_HOME=/{print $2}' /usr/bin/unity-editor.d/*)"
"$(awk -F'=' '/ANDROID_HOME=/{print $2}' /usr/bin/unity-editor.d/*)/tools/bin/sdkmanager" "$ANDROID_SDK_MANAGER_PARAMETERS"
echo "Updated Android SDK."
else
echo "Not updating Android SDK."
fi
#
# Pre-build debug information
#
@@ -122,14 +119,12 @@ echo ""
unity-editor \
-logfile /dev/stdout \
$( [ "${MANUAL_EXIT}" == "true" ] || echo "-quit" ) \
-quit \
-customBuildName "$BUILD_NAME" \
-projectPath "$UNITY_PROJECT_PATH" \
$( [ -z "$BUILD_PROFILE" ] && echo "-buildTarget $BUILD_TARGET" ) \
-buildTarget "$BUILD_TARGET" \
-customBuildTarget "$BUILD_TARGET" \
-customBuildPath "$CUSTOM_BUILD_PATH" \
-customBuildProfile "$BUILD_PROFILE" \
${BUILD_PROFILE:+-activeBuildProfile} ${BUILD_PROFILE:+"$BUILD_PROFILE"} \
-executeMethod "$BUILD_METHOD" \
-buildVersion "$VERSION" \
-androidVersionCode "$ANDROID_VERSION_CODE" \
@@ -138,8 +133,6 @@ unity-editor \
-androidKeyaliasName "$ANDROID_KEYALIAS_NAME" \
-androidKeyaliasPass "$ANDROID_KEYALIAS_PASS" \
-androidTargetSdkVersion "$ANDROID_TARGET_SDK_VERSION" \
-androidExportType "$ANDROID_EXPORT_TYPE" \
-androidSymbolType "$ANDROID_SYMBOL_TYPE" \
$CUSTOM_PARAMETERS
# Catch exit code
@@ -158,7 +151,6 @@ fi
# Make a given user owner of all artifacts
if [[ -n "$CHOWN_FILES_TO" ]]; then
echo "Changing ownership of files to $CHOWN_FILES_TO for $BUILD_PATH_FULL and $UNITY_PROJECT_PATH"
chown -R "$CHOWN_FILES_TO" "$BUILD_PATH_FULL"
chown -R "$CHOWN_FILES_TO" "$UNITY_PROJECT_PATH"
fi

View File

@@ -1,6 +1,11 @@
#!/usr/bin/env bash
if [[ -n "$UNITY_LICENSING_SERVER" ]]; then
# Run in ACTIVATE_LICENSE_PATH directory
echo "Changing to \"$ACTIVATE_LICENSE_PATH\" directory."
pushd "$ACTIVATE_LICENSE_PATH"
if [[ -n "$UNITY_LICENSING_SERVER" ]]; then #
#
# Return any floating license used.
#
@@ -8,15 +13,15 @@ if [[ -n "$UNITY_LICENSING_SERVER" ]]; then
/opt/unity/Editor/Data/Resources/Licensing/Client/Unity.Licensing.Client --return-floating "$FLOATING_LICENSE"
elif [[ -n "$UNITY_SERIAL" ]]; then
#
# SERIAL LICENSE MODE
# PROFESSIONAL (SERIAL) LICENSE MODE
#
# This will return the license that is currently in use.
#
unity-editor \
-logFile /dev/stdout \
-quit \
-returnlicense \
-username "$UNITY_EMAIL" \
-password "$UNITY_PASSWORD" \
-projectPath "/BlankProject"
-returnlicense
fi
# Return to previous working directory
popd

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env bash
#
# Run steps
#
source /steps/set_extra_git_configs.sh
source /steps/set_gitcredential.sh
if [ "$SKIP_ACTIVATION" != "true" ]; then
source /steps/activate.sh
# If we didn't activate successfully, exit with the exit code from the activation step.
if [[ $UNITY_EXIT_CODE -ne 0 ]]; then
exit $UNITY_EXIT_CODE
fi
else
echo "Skipping activation"
fi
source /steps/build.sh
if [ "$SKIP_ACTIVATION" != "true" ]; then
source /steps/return_license.sh
fi
#
# Instructions for debugging
#
if [[ $BUILD_EXIT_CODE -gt 0 ]]; then
echo ""
echo "###########################"
echo "# Failure #"
echo "###########################"
echo ""
echo "Please note that the exit code is not very descriptive."
echo "Most likely it will not help you solve the issue."
echo ""
echo "To find the reason for failure: please search for errors in the log above and check for annotations in the summary view."
echo ""
fi;
#
# Exit with code from the build step.
#
# Exiting su
exit $BUILD_EXIT_CODE

View File

@@ -1,29 +0,0 @@
#!/usr/bin/env bash
if [ -z "${GIT_CONFIG_EXTENSIONS}" ]
then
echo "GIT_CONFIG_EXTENSIONS unset skipping"
else
echo "GIT_CONFIG_EXTENSIONS is set configuring extra git configs"
IFS=$'\n'
for config in $(echo "${GIT_CONFIG_EXTENSIONS}" | sed 's/\(.*\)=\(.*\)/"\1" "\2"/g'); do
if [[ $config =~ \"([^\"]+)\"\ \"([^\"]+)\" ]]; then
key="${BASH_REMATCH[1]}"
value="${BASH_REMATCH[2]}"
else
echo "Error parsing config: $config"
exit 1
fi
echo "Adding extra git config: \"$key\" = \"$value\""
git config --global --add "$key" "$value"
done
unset IFS
fi
echo "---------- git config --list -------------"
git config --list
echo "---------- git config --list --show-origin -------------"
git config --list --show-origin

View File

@@ -9,8 +9,8 @@ else
git config --global credential.helper store
git config --global --replace-all url."https://token:$GIT_PRIVATE_TOKEN@github.com/".insteadOf ssh://git@github.com/
git config --global --add url."https://token:$GIT_PRIVATE_TOKEN@github.com/".insteadOf git@github.com
git config --global --add url."https://token:$GIT_PRIVATE_TOKEN@github.com/".insteadOf "https://github.com/"
git config --global url."https://ssh:$GIT_PRIVATE_TOKEN@github.com/".insteadOf "ssh://git@github.com/"
git config --global url."https://git:$GIT_PRIVATE_TOKEN@github.com/".insteadOf "git@github.com:"

View File

@@ -1,93 +1,7 @@
# Activates Unity
Write-Output ""
Write-Output "###########################"
Write-Output "# Activating #"
Write-Output "###########################"
Write-Output ""
if ( ($null -ne ${env:UNITY_SERIAL}) -and ($null -ne ${env:UNITY_EMAIL}) -and ($null -ne ${env:UNITY_PASSWORD}) )
{
#
# SERIAL LICENSE MODE
#
# This will activate unity, using the serial activation process.
#
Write-Output "Requesting activation"
$ACTIVATION_OUTPUT = Start-Process -FilePath "$Env:UNITY_PATH/Editor/Unity.exe" `
-NoNewWindow `
-PassThru `
-ArgumentList "-batchmode `
-quit `
-nographics `
-username $Env:UNITY_EMAIL `
-password $Env:UNITY_PASSWORD `
-serial $Env:UNITY_SERIAL `
-projectPath c:/BlankProject `
-logfile -"
# Cache the handle so exit code works properly
# https://stackoverflow.com/questions/10262231/obtaining-exitcode-using-start-process-and-waitforexit-instead-of-wait
$unityHandle = $ACTIVATION_OUTPUT.Handle
while ($true) {
if ($ACTIVATION_OUTPUT.HasExited) {
$ACTIVATION_EXIT_CODE = $ACTIVATION_OUTPUT.ExitCode
# Display results
if ($ACTIVATION_EXIT_CODE -eq 0)
{
Write-Output "Activation Succeeded"
} else
{
Write-Output "Activation failed, with exit code $ACTIVATION_EXIT_CODE"
}
break
}
Start-Sleep -Seconds 3
}
}
elseif( ($null -ne ${env:UNITY_LICENSING_SERVER}))
{
#
# Custom Unity License Server
#
Write-Output "Adding licensing server config"
$ACTIVATION_OUTPUT = Start-Process -FilePath "$Env:UNITY_PATH\Editor\Data\Resources\Licensing\Client\Unity.Licensing.Client.exe" `
-ArgumentList "--acquire-floating" `
-NoNewWindow `
-PassThru `
-Wait `
-RedirectStandardOutput "license.txt"
$PARSEDFILE = (Get-Content "license.txt" | Select-String -AllMatches -Pattern '\".*?\"' | ForEach-Object { $_.Matches.Value }) -replace '"'
$env:FLOATING_LICENSE = $PARSEDFILE[1]
$FLOATING_LICENSE_TIMEOUT = $PARSEDFILE[3]
Write-Output "Acquired floating license: ""$env:FLOATING_LICENSE"" with timeout $FLOATING_LICENSE_TIMEOUT"
# Store the exit code from the verify command
$ACTIVATION_EXIT_CODE = $ACTIVATION_OUTPUT.ExitCode
}
else
{
#
# NO LICENSE ACTIVATION STRATEGY MATCHED
#
# This will exit since no activation strategies could be matched.
#
Write-Output "License activation strategy could not be determined."
Write-Output ""
Write-Output "Visit https://game.ci/docs/github/activation for more"
Write-Output "details on how to set up one of the possible activation strategies."
Write-Output "::error ::No valid license activation strategy could be determined. Make sure to provide UNITY_EMAIL, UNITY_PASSWORD, and either a UNITY_SERIAL \
or UNITY_LICENSE. See more info at https://game.ci/docs/github/activation"
$ACTIVATION_EXIT_CODE = 1;
}
& "C:\Program Files\Unity\Hub\Editor\$Env:UNITY_VERSION\Editor\Unity.exe" -batchmode -quit -nographics `
-username $Env:UNITY_EMAIL `
-password $Env:UNITY_PASSWORD `
-serial $Env:UNITY_SERIAL `
-projectPath "c:/BlankProject" `
-logfile | Out-Host

View File

@@ -16,25 +16,6 @@ Write-Output "$('Using build name "')$($Env:BUILD_NAME)$('".')"
Write-Output "$('Using build target "')$($Env:BUILD_TARGET)$('".')"
#
# Display the build profile
#
if ($Env:BUILD_PROFILE)
{
# User has provided a path to a build profile `.asset` file
#
Write-Output "$('Using build profile "')$($Env:BUILD_PROFILE)$('" relative to "')$($Env:UNITY_PROJECT_PATH)$('".')"
#
}
else
{
# User has not provided a build profile
#
Write-Output "$('Doing a default "')$($Env:BUILD_TARGET)$('" platform build.')"
#
}
#
# Display build path and file
#
@@ -85,26 +66,6 @@ else
Get-ChildItem -Path $Env:UNITY_PROJECT_PATH\Assets\Editor -Recurse
}
if ( "$Env:BUILD_TARGET" -eq "Android" -and -not ([string]::IsNullOrEmpty("$Env:ANDROID_KEYSTORE_BASE64")) )
{
Write-Output "Creating Android keystore."
# Write to consistent location as Windows Unity seems to have issues with pwd and can't find the keystore
$keystorePath = "C:/android.keystore"
[System.IO.File]::WriteAllBytes($keystorePath, [System.Convert]::FromBase64String($Env:ANDROID_KEYSTORE_BASE64))
# Ensure the project settings are pointed at the correct path
$unitySettingsPath = "$Env:UNITY_PROJECT_PATH\ProjectSettings\ProjectSettings.asset"
$fileContent = Get-Content -Path "$unitySettingsPath"
$fileContent = $fileContent -replace "AndroidKeystoreName:\s+.*", "AndroidKeystoreName: $keystorePath"
$fileContent | Set-Content -Path "$unitySettingsPath"
Write-Output "Created Android keystore."
}
else {
Write-Output "Not creating Android keystore."
}
#
# Pre-build debug information
#
@@ -148,84 +109,49 @@ Write-Output "# Building project #"
Write-Output "###########################"
Write-Output ""
$unityGraphics = "-nographics"
if ($LLVMPIPE_INSTALLED -eq "true")
{
$unityGraphics = "-force-opengl"
}
# If $Env:CUSTOM_PARAMETERS contains spaces and is passed directly on the command line to Unity, powershell will wrap it
# in double quotes. To avoid this, parse $Env:CUSTOM_PARAMETERS into an array, while respecting any quotations within the string.
$_, $customParametersArray = Invoke-Expression('Write-Output -- "" ' + $Env:CUSTOM_PARAMETERS)
$unityArgs = @(
"-quit",
"-batchmode",
$unityGraphics,
"-silent-crashes",
"-customBuildName", "`"$Env:BUILD_NAME`"",
"-projectPath", "`"$Env:UNITY_PROJECT_PATH`"",
"-executeMethod", "`"$Env:BUILD_METHOD`"",
"-customBuildTarget", "`"$Env:BUILD_TARGET`"",
"-customBuildPath", "`"$Env:CUSTOM_BUILD_PATH`"",
"-customBuildProfile", "`"$Env:BUILD_PROFILE`"",
"-buildVersion", "`"$Env:VERSION`"",
"-androidVersionCode", "`"$Env:ANDROID_VERSION_CODE`"",
"-androidKeystorePass", "`"$Env:ANDROID_KEYSTORE_PASS`"",
"-androidKeyaliasName", "`"$Env:ANDROID_KEYALIAS_NAME`"",
"-androidKeyaliasPass", "`"$Env:ANDROID_KEYALIAS_PASS`"",
"-androidTargetSdkVersion", "`"$Env:ANDROID_TARGET_SDK_VERSION`"",
"-androidExportType", "`"$Env:ANDROID_EXPORT_TYPE`"",
"-androidSymbolType", "`"$Env:ANDROID_SYMBOL_TYPE`"",
"-logfile", "-"
) + $customParametersArray
if (-not $Env:BUILD_PROFILE) {
$unityArgs += @("-buildTarget", "`"$Env:BUILD_TARGET`"")
}
if ($Env:BUILD_PROFILE) {
$unityArgs += @("-activeBuildProfile", "`"$Env:BUILD_PROFILE`"")
& "C:\Program Files\Unity\Hub\Editor\$Env:UNITY_VERSION\Editor\Unity.exe" -quit -batchmode -nographics `
-projectPath $Env:UNITY_PROJECT_PATH `
-executeMethod $Env:BUILD_METHOD `
-buildTarget $Env:BUILD_TARGET `
-customBuildTarget $Env:BUILD_TARGET `
-customBuildPath $Env:CUSTOM_BUILD_PATH `
-buildVersion $Env:VERSION `
-androidVersionCode $Env:ANDROID_VERSION_CODE `
-androidKeystoreName $Env:ANDROID_KEYSTORE_NAME `
-androidKeystorePass $Env:ANDROID_KEYSTORE_PASS `
-androidKeyaliasName $Env:ANDROID_KEYALIAS_NAME `
-androidKeyaliasPass $Env:ANDROID_KEYALIAS_PASS `
-androidTargetSdkVersion $Env:ANDROID_TARGET_SDK_VERSION `
$customParametersArray `
-logfile | Out-Host
# Catch exit code
$Env:BUILD_EXIT_CODE=$?
# Display results
if ($Env:BUILD_EXIT_CODE -eq 0)
{
Write-Output "Build Succeeded!"
} else
{
Write-Output "$('Build failed, with exit code ')$($Env:BUILD_EXIT_CODE)$('"')"
}
# Remove null items as that will fail the Start-Process call
$unityArgs = $unityArgs | Where-Object { $_ -ne $null }
# TODO: Determine if we need to set permissions on any files
$unityProcess = Start-Process -FilePath "$Env:UNITY_PATH/Editor/Unity.exe" `
-ArgumentList $unityArgs `
-PassThru `
-NoNewWindow
#
# Results
#
# Cache the handle so exit code works properly
# https://stackoverflow.com/questions/10262231/obtaining-exitcode-using-start-process-and-waitforexit-instead-of-wait
$unityHandle = $unityProcess.Handle
Write-Output ""
Write-Output "###########################"
Write-Output "# Build output #"
Write-Output "###########################"
Write-Output ""
while ($true) {
if ($unityProcess.HasExited) {
Start-Sleep -Seconds 3
Get-Process
$BUILD_EXIT_CODE = $unityProcess.ExitCode
# Display results
if ($BUILD_EXIT_CODE -eq 0)
{
Write-Output "Build Succeeded!!"
} else
{
Write-Output "Build failed, with exit code $BUILD_EXIT_CODE"
}
Write-Output ""
Write-Output "###########################"
Write-Output "# Build output #"
Write-Output "###########################"
Write-Output ""
Get-ChildItem $Env:BUILD_PATH_FULL
Write-Output ""
break
}
Start-Sleep -Seconds 3
}
Get-ChildItem $Env:BUILD_PATH_FULL
Write-Output ""

View File

@@ -1,55 +1,15 @@
Get-Process
# Copy .upmconfig.toml if it exists
if (Test-Path "C:\githubhome\.upmconfig.toml") {
Write-Host "Copying .upmconfig.toml to $Env:USERPROFILE\.upmconfig.toml"
Copy-Item -Path "C:\githubhome\.upmconfig.toml" -Destination "$Env:USERPROFILE\.upmconfig.toml" -Force
} else {
Write-Host "No .upmconfig.toml found at C:\githubhome"
}
# Activate Unity
& "c:\steps\activate.ps1"
# Import any necessary registry keys, ie: location of windows 10 sdk
# No guarantee that there will be any necessary registry keys, ie: tvOS
Get-ChildItem -Path c:\regkeys -File | ForEach-Object { reg import $_.fullname }
Get-ChildItem -Path c:\regkeys -File | Foreach {reg import $_.fullname}
# Register the Visual Studio installation so Unity can find it
regsvr32 C:\ProgramData\Microsoft\VisualStudio\Setup\x64\Microsoft.VisualStudio.Setup.Configuration.Native.dll
# Kill the regsvr process
Get-Process -Name regsvr32 | ForEach-Object { Stop-Process -Id $_.Id -Force }
# Install Visual C++ 2013 Redistributables
. "c:\steps\install_vcredist13.ps1"
# Setup Git Credentials
. "c:\steps\set_gitcredential.ps1"
if ($env:ENABLE_GPU -eq "true") {
# Install LLVMpipe software graphics driver
. "c:\steps\install_llvmpipe.ps1"
}
# Activate Unity
if ($env:SKIP_ACTIVATION -ne "true") {
. "c:\steps\activate.ps1"
# If we didn't activate successfully, exit with the exit code from the activation step.
if ($ACTIVATION_EXIT_CODE -ne 0) {
exit $ACTIVATION_EXIT_CODE
}
}
else {
Write-Host "Skipping activation"
}
# Build the project
. "c:\steps\build.ps1"
& "c:\steps\build.ps1"
# Free the seat for the activated license
if ($env:SKIP_ACTIVATION -ne "true") {
. "c:\steps\return_license.ps1"
}
Get-Process
exit $BUILD_EXIT_CODE
& "c:\steps\return_license.ps1"

View File

@@ -1,56 +0,0 @@
$Private:repo = "mmozeiko/build-mesa"
$Private:downloadPath = "$Env:TEMP\mesa.zip"
$Private:extractPath = "$Env:TEMP\mesa"
$Private:destinationPath = "$Env:UNITY_PATH\Editor\"
$Private:version = "25.1.0"
$LLVMPIPE_INSTALLED = "false"
try {
# Get the release info from GitHub API (version fixed to decrease probability of breakage)
$releaseUrl = "https://api.github.com/repos/$repo/releases/tags/$version"
$release = Invoke-RestMethod -Uri $releaseUrl -Headers @{ "User-Agent" = "PowerShell" }
# Get the download URL for the zip asset
$zipUrl = $release.assets | Where-Object { $_.name -like "mesa-llvmpipe-x64*.zip" } | Select-Object -First 1 -ExpandProperty browser_download_url
if (-not $zipUrl) {
throw "No zip file found in the latest release."
}
# Download the zip file
Write-Host "Downloading $zipUrl..."
Invoke-WebRequest -Uri $zipUrl -OutFile $downloadPath
# Create extraction directory if it doesn't exist
if (-not (Test-Path $extractPath)) {
New-Item -ItemType Directory -Path $extractPath | Out-Null
}
# Extract the zip file
Write-Host "Extracting $downloadPath to $extractPath..."
Expand-Archive -Path $downloadPath -DestinationPath $extractPath -Force
# Create destination directory if it doesn't exist
if (-not (Test-Path $destinationPath)) {
New-Item -ItemType Directory -Path $destinationPath | Out-Null
}
# Copy extracted files to destination
Write-Host "Copying files to $destinationPath..."
Copy-Item -Path "$extractPath\*" -Destination $destinationPath -Recurse -Force
Write-Host "Successfully downloaded, extracted, and copied Mesa files to $destinationPath"
$LLVMPIPE_INSTALLED = "true"
} catch {
Write-Error "An error occurred: $_"
} finally {
# Clean up temporary files
if (Test-Path $downloadPath) {
Remove-Item $downloadPath -Force
}
if (Test-Path $extractPath) {
Remove-Item $extractPath -Recurse -Force
}
}

View File

@@ -1,11 +0,0 @@
# For some reason, Unity is failing in github actions windows runners
# due to missing Visual C++ 2013 redistributables.
# This script downloads and installs the required redistributables.
Write-Output ""
Write-Output "#########################################################"
Write-Output "# Installing Visual C++ Redistributables (2013) #"
Write-Output "#########################################################"
Write-Output ""
choco install vcredist2013 -y --no-progress

View File

@@ -1,61 +1,7 @@
# Return the active Unity license
Write-Output ""
Write-Output "###########################"
Write-Output "# Return License #"
Write-Output "###########################"
Write-Output ""
if (($null -ne ${env:UNITY_LICENSING_SERVER}))
{
Write-Output "Returning floating license: ""$env:FLOATING_LICENSE"""
Start-Process -FilePath "$Env:UNITY_PATH\Editor\Data\Resources\Licensing\Client\Unity.Licensing.Client.exe" `
-ArgumentList "--return-floating ""$env:FLOATING_LICENSE"" " `
-NoNewWindow `
-Wait
}
elseif (($null -ne ${env:UNITY_SERIAL}) -and ($null -ne ${env:UNITY_EMAIL}) -and ($null -ne ${env:UNITY_PASSWORD}))
{
#
# SERIAL LICENSE MODE
#
# This will return the license that is currently in use.
#
$RETURN_LICENSE_OUTPUT = Start-Process -FilePath "$Env:UNITY_PATH/Editor/Unity.exe" `
-NoNewWindow `
-PassThru `
-ArgumentList "-batchmode `
-quit `
-nographics `
-username $Env:UNITY_EMAIL `
-password $Env:UNITY_PASSWORD `
-returnlicense `
-projectPath c:/BlankProject `
-logfile -"
# Cache the handle so exit code works properly
# https://stackoverflow.com/questions/10262231/obtaining-exitcode-using-start-process-and-waitforexit-instead-of-wait
$unityHandle = $RETURN_LICENSE_OUTPUT.Handle
while ($true) {
if ($RETURN_LICENSE_OUTPUT.HasExited) {
$RETURN_LICENSE_EXIT_CODE = $RETURN_LICENSE_OUTPUT.ExitCode
# Display results
if ($RETURN_LICENSE_EXIT_CODE -eq 0)
{
Write-Output "License Return Succeeded"
} else
{
Write-Output "License Return failed, with exit code $RETURN_LICENSE_EXIT_CODE"
Write-Output "::warning ::License Return failed! If this is a Pro License you might need to manually `
free the seat in your Unity admin panel or you might run out of seats to activate with."
}
break
}
Start-Sleep -Seconds 3
}
}
& "C:\Program Files\Unity\Hub\Editor\$Env:UNITY_VERSION\Editor\Unity.exe" -batchmode -quit -nographics `
-username $Env:UNITY_EMAIL `
-password $Env:UNITY_PASSWORD `
-returnlicense `
-projectPath "c:/BlankProject" `
-logfile | Out-Host

View File

@@ -1,20 +0,0 @@
if ($null -eq ${env:GIT_PRIVATE_TOKEN}) {
Write-Host "GIT_PRIVATE_TOKEN unset skipping"
}
else {
Write-Host "GIT_PRIVATE_TOKEN is set configuring git credentials"
git config --global credential.helper store
git config --global --replace-all url."https://token:$env:GIT_PRIVATE_TOKEN@github.com/".insteadOf "ssh://git@github.com/"
git config --global --add url."https://token:$env:GIT_PRIVATE_TOKEN@github.com/".insteadOf "git@github.com"
git config --global --add url."https://token:$env:GIT_PRIVATE_TOKEN@github.com/".insteadOf "https://github.com/"
git config --global url."https://ssh:$env:GIT_PRIVATE_TOKEN@github.com/".insteadOf "ssh://git@github.com/"
git config --global url."https://git:$env:GIT_PRIVATE_TOKEN@github.com/".insteadOf "git@github.com:"
}
Write-Host "---------- git config --list -------------"
git config --list
Write-Host "---------- git config --list --show-origin -------------"
git config --list --show-origin

2
dist/sourcemap-register.js generated vendored

File diff suppressed because one or more lines are too long

60
dist/xhr-sync-worker.js vendored Normal file
View File

@@ -0,0 +1,60 @@
"use strict";
/* eslint-disable no-process-exit */
const util = require("util");
const { JSDOM } = require("../../../..");
const { READY_STATES } = require("./xhr-utils");
const idlUtils = require("../generated/utils");
const tough = require("tough-cookie");
const dom = new JSDOM();
const xhr = new dom.window.XMLHttpRequest();
const xhrImpl = idlUtils.implForWrapper(xhr);
const chunks = [];
process.stdin.on("data", chunk => {
chunks.push(chunk);
});
process.stdin.on("end", () => {
const buffer = Buffer.concat(chunks);
const flag = JSON.parse(buffer.toString());
if (flag.body && flag.body.type === "Buffer" && flag.body.data) {
flag.body = Buffer.from(flag.body.data);
}
if (flag.cookieJar) {
flag.cookieJar = tough.CookieJar.fromJSON(flag.cookieJar);
}
flag.synchronous = false;
Object.assign(xhrImpl.flag, flag);
const { properties } = xhrImpl;
xhrImpl.readyState = READY_STATES.OPENED;
try {
xhr.addEventListener("loadend", () => {
if (properties.error) {
properties.error = properties.error.stack || util.inspect(properties.error);
}
process.stdout.write(JSON.stringify({
responseURL: xhrImpl.responseURL,
status: xhrImpl.status,
statusText: xhrImpl.statusText,
properties
}), () => {
process.exit(0);
});
}, false);
xhr.send(flag.body);
} catch (error) {
properties.error += error.stack || util.inspect(error);
process.stdout.write(JSON.stringify({
responseURL: xhrImpl.responseURL,
status: xhrImpl.status,
statusText: xhrImpl.statusText,
properties
}), () => {
process.exit(0);
});
}
});

View File

@@ -1,3 +1,3 @@
hook: after
hook: after-build
commands: |
echo "after-build hook test!"

View File

@@ -1,3 +1,3 @@
hook: before
hook: before-build
commands: |
echo "before-build hook test!!"

View File

@@ -1,11 +0,0 @@
const base = require('./jest.config.js');
module.exports = {
...base,
forceExit: true,
detectOpenHandles: true,
testTimeout: 120000,
maxWorkers: 1,
};

View File

@@ -18,6 +18,7 @@ module.exports = {
transform: {
'^.+\\.ts$': 'ts-jest',
},
autoRun: false,
// Indicates whether each individual test should be reported during the run
verbose: true,
@@ -25,6 +26,6 @@ module.exports = {
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
modulePathIgnorePatterns: ['<rootDir>/lib/', '<rootDir>/dist/'],
// Use jest.setup.js to polyfill fetch for all tests
setupFiles: ['<rootDir>/jest.setup.js'],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
setupFilesAfterEnv: ['<rootDir>/src/jest.setup.ts'],
};

View File

@@ -1,2 +0,0 @@
const fetch = require('node-fetch');
global.fetch = fetch;

View File

@@ -1,45 +1,57 @@
{
"name": "unity-builder",
"version": "3.0.0",
"version": "2.0.0",
"description": "Build Unity projects for different platforms.",
"main": "dist/index.js",
"repository": "git@github.com:game-ci/unity-builder.git",
"author": "Webber <webber@takken.io>",
"license": "MIT",
"scripts": {
"prepare": "lefthook install",
"prepare": "lefthook install && npx husky uninstall -y",
"build": "yarn && tsc && ncc build lib --source-map --license licenses.txt",
"lint": "prettier --check \"src/**/*.{js,ts}\" && eslint src/**/*.ts",
"format": "prettier --write \"src/**/*.{js,ts}\"",
"cli": "yarn ts-node src/index.ts -m cli",
"gcp-secrets-tests": "cross-env cloudRunnerCluster=aws cloudRunnerTests=true readInputOverrideCommand=\"gcp-secret-manager\" populateOverride=true readInputFromOverrideList=UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD yarn test -i -t \"cloud runner\"",
"gcp-secrets-cli": "cross-env cloudRunnerTests=true readInputOverrideCommand=\"gcp-secret-manager\" yarn ts-node src/index.ts -m cli --populateOverride true --readInputFromOverrideList UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD",
"aws-secrets-cli": "cross-env cloudRunnerTests=true readInputOverrideCommand=\"aws-secret-manager\" yarn ts-node src/index.ts -m cli --populateOverride true --readInputFromOverrideList UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD",
"cli-aws": "cross-env cloudRunnerCluster=aws yarn run test-cli",
"cli-k8s": "cross-env cloudRunnerCluster=k8s yarn run test-cli",
"test-cli": "cross-env cloudRunnerTests=true yarn ts-node src/index.ts -m cli --projectPath test-project",
"test": "jest",
"test:ci": "jest --config=jest.ci.config.js --runInBand"
},
"engines": {
"node": ">=18.x"
"test-i": "cross-env cloudRunnerTests=true yarn test -i -t \"cloud runner\"",
"test-i-*": "yarn run test-i-aws && yarn run test-i-k8s",
"test-i-aws": "cross-env cloudRunnerTests=true cloudRunnerCluster=aws yarn test -i -t \"cloud runner\"",
"test-i-k8s": "cross-env cloudRunnerTests=true cloudRunnerCluster=k8s yarn test -i -t \"cloud runner\""
},
"dependencies": {
"@actions/cache": "^4.0.0",
"@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
"@actions/core": "^1.10.0",
"@actions/exec": "^1.1.0",
"@actions/github": "^5.0.0",
"@kubernetes/client-node": "^0.16.3",
"@octokit/core": "^3.5.1",
"async-wait-until": "^2.0.12",
"aws-sdk": "^2.1081.0",
"base-64": "^1.0.0",
"commander": "^9.0.0",
"commander-ts": "^0.2.0",
"md5": "^2.3.0",
"reflect-metadata": "^0.1.13",
"kubernetes-client": "^9.0.0",
"nanoid": "^3.3.1",
"semver": "^7.5.2",
"ts-md5": "^1.3.1",
"unity-changeset": "^3.1.0",
"yaml": "^2.2.2"
"reflect-metadata": "^0.1.13",
"semver": "^7.3.5",
"unity-changeset": "^1.6.0",
"uuid": "^8.3.2",
"yaml": "^1.10.2"
},
"devDependencies": {
"@arkweid/lefthook": "^0.7.7",
"@types/jest": "^27.4.1",
"@types/node": "^17.0.23",
"@types/semver": "^7.3.9",
"@typescript-eslint/parser": "4.8.1",
"@vercel/ncc": "^0.36.1",
"@vercel/ncc": "^0.33.3",
"cross-env": "^7.0.3",
"eslint": "^7.23.0",
"eslint": "7.17.0",
"eslint-config-prettier": "8.1.0",
"eslint-plugin-github": "^4.1.1",
"eslint-plugin-jest": "24.1.3",
@@ -47,18 +59,11 @@
"eslint-plugin-unicorn": "28.0.2",
"jest": "^27.5.1",
"jest-circus": "^27.5.1",
"jest-fail-on-console": "^3.0.2",
"jest-fail-on-console": "^2.3.0",
"js-yaml": "^4.1.0",
"lefthook": "^1.6.1",
"node-fetch": "2",
"prettier": "^2.5.1",
"ts-jest": "^27.1.3",
"ts-node": "10.8.1",
"typescript": "4.7.4",
"yarn-audit-fix": "^9.3.8"
},
"volta": {
"node": "20.5.1",
"yarn": "1.22.19"
"ts-node": "10.4.0",
"typescript": "4.1.3"
}
}

View File

@@ -1,15 +0,0 @@
echo "installing game-ci cli"
if exist %UserProfile%\AppData\LocalLow\game-ci\ (
echo Installed Updating
git -C %UserProfile%\AppData\LocalLow\game-ci\ fetch
git -C %UserProfile%\AppData\LocalLow\game-ci\ reset --hard
git -C %UserProfile%\AppData\LocalLow\game-ci\ pull
git -C %UserProfile%\AppData\LocalLow\game-ci\ branch
) else (
echo Not Installed Downloading...
mkdir %UserProfile%\AppData\LocalLow\game-ci\
git clone https://github.com/game-ci/unity-builder %UserProfile%\AppData\LocalLow\game-ci\
)
call yarn --cwd %UserProfile%\AppData\LocalLow\game-ci\ install
call yarn --cwd %UserProfile%\AppData\LocalLow\game-ci\ run gcp-secrets-cli %* --projectPath %cd% --awsStackName game-ci-cli

View File

@@ -1,262 +0,0 @@
/**
* Integration wiring tests for the plugin lifecycle in index.ts
*
* These tests verify that:
* - The plugin lifecycle hooks are called in the correct order
* - Plugin canHandleBuild() controls the execution path
* - fallbackToLocal is handled correctly
* - When no plugin is installed, local builds still work
* - When providerStrategy is non-local without a plugin, an error is thrown
*/
import { BuildParameters } from './model';
// ---------------------------------------------------------------------------
// Mock plugin
// ---------------------------------------------------------------------------
const mockPlugin = {
initialize: jest.fn().mockResolvedValue(undefined),
canHandleBuild: jest.fn().mockReturnValue(false),
handleBuild: jest.fn().mockResolvedValue({ exitCode: 0 }),
beforeLocalBuild: jest.fn().mockResolvedValue(undefined),
afterLocalBuild: jest.fn().mockResolvedValue(undefined),
handlePostBuild: jest.fn().mockResolvedValue(undefined),
};
const mockLoadOrchestratorPlugin = jest.fn().mockResolvedValue(mockPlugin);
jest.mock('./model/orchestrator-plugin', () => ({
loadOrchestratorPlugin: mockLoadOrchestratorPlugin,
}));
jest.mock('@actions/core');
jest.mock('./model', () => ({
Action: {
checkCompatibility: jest.fn(),
workspace: '/workspace',
actionFolder: '/action',
},
BuildParameters: {
create: jest.fn(),
},
Cache: {
verify: jest.fn(),
},
Docker: {
run: jest.fn().mockResolvedValue(0),
},
ImageTag: jest.fn().mockImplementation(() => ({
toString: () => 'mock-image:latest',
})),
Output: {
setBuildVersion: jest.fn().mockResolvedValue(''),
setAndroidVersionCode: jest.fn().mockResolvedValue(''),
setEngineExitCode: jest.fn().mockResolvedValue(''),
},
}));
jest.mock('./model/cli/cli', () => ({
Cli: {
InitCliMode: jest.fn().mockReturnValue(false),
},
}));
jest.mock('./model/mac-builder', () => ({
__esModule: true,
default: {
run: jest.fn().mockResolvedValue(0),
},
}));
jest.mock('./model/platform-setup', () => ({
__esModule: true,
default: {
setup: jest.fn().mockResolvedValue(''),
},
}));
const mockedBuildParametersCreate = BuildParameters.create as jest.Mock;
function createMockBuildParameters(overrides: Record<string, any> = {}) {
return {
providerStrategy: 'local',
targetPlatform: 'StandaloneLinux64',
editorVersion: '2021.3.1f1',
buildVersion: '1.0.0',
androidVersionCode: '1',
projectPath: '.',
branch: 'main',
runnerTempPath: '/tmp',
...overrides,
};
}
async function runIndex(overrides: Record<string, any> = {}): Promise<void> {
mockedBuildParametersCreate.mockResolvedValue(createMockBuildParameters(overrides));
return new Promise<void>((resolve) => {
jest.isolateModules(() => {
require('./index');
});
setTimeout(resolve, 100);
});
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe('index.ts plugin lifecycle wiring', () => {
const originalPlatform = process.platform;
const originalEnvironment = { ...process.env };
beforeEach(() => {
jest.clearAllMocks();
process.env.GITHUB_WORKSPACE = '/workspace';
Object.defineProperty(process, 'platform', { value: 'linux' });
// Reset plugin to default behavior
mockPlugin.canHandleBuild.mockReturnValue(false);
mockPlugin.handleBuild.mockResolvedValue({ exitCode: 0 });
mockLoadOrchestratorPlugin.mockResolvedValue(mockPlugin);
});
afterEach(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform });
process.env = { ...originalEnvironment };
});
// -----------------------------------------------------------------------
// Local build with plugin
// -----------------------------------------------------------------------
describe('local build with plugin installed', () => {
it('should call lifecycle hooks in order: initialize → beforeLocalBuild → [build] → afterLocalBuild → handlePostBuild', async () => {
const callOrder: string[] = [];
mockPlugin.initialize.mockImplementation(async () => callOrder.push('initialize'));
mockPlugin.beforeLocalBuild.mockImplementation(async () => callOrder.push('beforeLocalBuild'));
mockPlugin.afterLocalBuild.mockImplementation(async () => callOrder.push('afterLocalBuild'));
mockPlugin.handlePostBuild.mockImplementation(async () => callOrder.push('handlePostBuild'));
await runIndex();
expect(callOrder).toEqual(['initialize', 'beforeLocalBuild', 'afterLocalBuild', 'handlePostBuild']);
});
it('should pass buildParameters and workspace to initialize', async () => {
await runIndex({ targetPlatform: 'WebGL' });
expect(mockPlugin.initialize).toHaveBeenCalledWith(
expect.objectContaining({ targetPlatform: 'WebGL' }),
'/workspace',
);
});
it('should pass workspace to beforeLocalBuild', async () => {
await runIndex();
expect(mockPlugin.beforeLocalBuild).toHaveBeenCalledWith('/workspace');
});
it('should pass workspace and exit code to afterLocalBuild', async () => {
await runIndex();
expect(mockPlugin.afterLocalBuild).toHaveBeenCalledWith('/workspace', 0);
});
it('should pass exit code to handlePostBuild', async () => {
await runIndex();
expect(mockPlugin.handlePostBuild).toHaveBeenCalledWith(0);
});
});
// -----------------------------------------------------------------------
// Plugin handles build entirely
// -----------------------------------------------------------------------
describe('plugin handles build (canHandleBuild = true)', () => {
it('should call handleBuild instead of Docker.run', async () => {
const { Docker } = require('./model');
mockPlugin.canHandleBuild.mockReturnValue(true);
mockPlugin.handleBuild.mockResolvedValue({ exitCode: 0 });
await runIndex();
expect(mockPlugin.handleBuild).toHaveBeenCalledWith('mock-image:latest');
expect(Docker.run).not.toHaveBeenCalled();
expect(mockPlugin.beforeLocalBuild).not.toHaveBeenCalled();
expect(mockPlugin.afterLocalBuild).not.toHaveBeenCalled();
});
it('should still call handlePostBuild after handleBuild', async () => {
mockPlugin.canHandleBuild.mockReturnValue(true);
mockPlugin.handleBuild.mockResolvedValue({ exitCode: 0 });
await runIndex();
expect(mockPlugin.handlePostBuild).toHaveBeenCalledWith(0);
});
});
// -----------------------------------------------------------------------
// Fallback to local
// -----------------------------------------------------------------------
describe('fallback to local build', () => {
it('should do a local build when handleBuild returns fallbackToLocal', async () => {
const { Docker } = require('./model');
mockPlugin.canHandleBuild.mockReturnValue(true);
mockPlugin.handleBuild.mockResolvedValue({ exitCode: -1, fallbackToLocal: true });
await runIndex();
expect(mockPlugin.handleBuild).toHaveBeenCalled();
expect(mockPlugin.beforeLocalBuild).toHaveBeenCalled();
expect(Docker.run).toHaveBeenCalled();
expect(mockPlugin.afterLocalBuild).toHaveBeenCalled();
});
});
// -----------------------------------------------------------------------
// No plugin installed
// -----------------------------------------------------------------------
describe('no plugin installed', () => {
it('should build locally without errors when providerStrategy is local', async () => {
const { Docker } = require('./model');
mockLoadOrchestratorPlugin.mockResolvedValue(undefined);
await runIndex({ providerStrategy: 'local' });
expect(Docker.run).toHaveBeenCalled();
});
it('should error when providerStrategy is non-local and no plugin', async () => {
const core = require('@actions/core');
mockLoadOrchestratorPlugin.mockResolvedValue(undefined);
await runIndex({ providerStrategy: 'aws' });
expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('requires @game-ci/orchestrator'));
});
});
// -----------------------------------------------------------------------
// canHandleBuild = false with non-local provider
// -----------------------------------------------------------------------
describe('plugin installed but canHandleBuild returns false with non-local provider', () => {
it('should error when providerStrategy is non-local', async () => {
const core = require('@actions/core');
mockPlugin.canHandleBuild.mockReturnValue(false);
await runIndex({ providerStrategy: 'aws' });
// The plugin is initialized but says it can't handle the build,
// and providerStrategy is not local, so it falls to the error case
expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('requires @game-ci/orchestrator'));
});
});
});

View File

@@ -1,10 +1,8 @@
import * as core from '@actions/core';
import { Action, BuildParameters, Cache, Docker, ImageTag, Output } from './model';
import { Action, BuildParameters, Cache, CloudRunner, Docker, ImageTag, Output } from './model';
import { Cli } from './model/cli/cli';
import MacBuilder from './model/mac-builder';
import PlatformSetup from './model/platform-setup';
import { loadOrchestratorPlugin, OrchestratorPlugin } from './model/orchestrator-plugin';
async function runMain() {
try {
if (Cli.InitCliMode()) {
@@ -16,69 +14,27 @@ async function runMain() {
Cache.verify();
const { workspace, actionFolder } = Action;
const buildParameters = await BuildParameters.create();
const baseImage = new ImageTag(buildParameters);
// Load orchestrator plugin (optional — only needed for remote builds and plugin features)
const plugin = await loadOrchestratorPlugin();
await plugin?.initialize(buildParameters, workspace);
let exitCode = -1;
if (plugin?.canHandleBuild()) {
// Plugin handles the build entirely (remote providers, hot runner, test workflows)
const result = await plugin.handleBuild(baseImage.toString());
exitCode = result.fallbackToLocal
? await runLocalBuild(buildParameters, baseImage, workspace, actionFolder, plugin)
: result.exitCode;
} else if (buildParameters.providerStrategy === 'local') {
exitCode = await runLocalBuild(buildParameters, baseImage, workspace, actionFolder, plugin);
if (buildParameters.cloudRunnerCluster !== 'local') {
await CloudRunner.run(buildParameters, baseImage.toString());
} else {
throw new Error(
`Provider strategy "${buildParameters.providerStrategy}" requires @game-ci/orchestrator. ` +
'Install it via the game-ci/orchestrator action, or use providerStrategy=local.',
);
core.info('Building locally');
await PlatformSetup.setup(buildParameters, actionFolder);
if (process.platform === 'darwin') {
MacBuilder.run(actionFolder, workspace, buildParameters);
} else {
await Docker.run(baseImage, { workspace, actionFolder, ...buildParameters });
}
}
// Set core outputs
// Set output
await Output.setBuildVersion(buildParameters.buildVersion);
await Output.setAndroidVersionCode(buildParameters.androidVersionCode);
await Output.setEngineExitCode(exitCode);
// Plugin handles post-build (artifacts, archiving, retention)
await plugin?.handlePostBuild(exitCode);
if (exitCode !== 0) {
core.setFailed(`Build failed with exit code ${exitCode}`);
}
} catch (error) {
core.setFailed((error as Error).message);
}
}
async function runLocalBuild(
buildParameters: BuildParameters,
baseImage: ImageTag,
workspace: string,
actionFolder: string,
plugin?: OrchestratorPlugin,
): Promise<number> {
await plugin?.beforeLocalBuild(workspace);
await PlatformSetup.setup(buildParameters, actionFolder);
const exitCode =
process.platform === 'darwin'
? await MacBuilder.run(actionFolder)
: await Docker.run(baseImage.toString(), {
workspace,
actionFolder,
...buildParameters,
});
await plugin?.afterLocalBuild(workspace, exitCode);
return exitCode;
}
runMain();

View File

@@ -1,4 +1,4 @@
import { stat } from 'node:fs/promises';
import { stat } from 'fs/promises';
describe('Integrity tests', () => {
describe('package-lock.json', () => {

View File

@@ -1,3 +0,0 @@
import { fetch as undiciFetch, Headers, Request, Response } from 'undici';
Object.assign(globalThis, { fetch: undiciFetch, Headers, Request, Response });

View File

@@ -1,5 +1,5 @@
import path from 'node:path';
import fs from 'node:fs';
import path from 'path';
import fs from 'fs';
import Action from './action';
describe('Action', () => {

View File

@@ -1,27 +1,23 @@
import path from 'node:path';
import path from 'path';
class Action {
static get supportedPlatforms(): string[] {
static get supportedPlatforms() {
return ['linux', 'win32', 'darwin'];
}
static get isRunningLocally(): boolean {
static get isRunningLocally() {
return process.env.RUNNER_WORKSPACE === undefined;
}
static get isRunningFromSource(): boolean {
static get isRunningFromSource() {
return path.basename(__dirname) === 'model';
}
static get canonicalName(): string {
if (Action.isRunningFromSource) {
return path.basename(path.dirname(path.join(path.dirname(__filename), '/..')));
}
static get canonicalName() {
return 'unity-builder';
}
static get rootFolder(): string {
static get rootFolder() {
if (Action.isRunningFromSource) {
return path.dirname(path.dirname(path.dirname(__filename)));
}
@@ -29,12 +25,12 @@ class Action {
return path.dirname(path.dirname(__filename));
}
static get actionFolder(): string {
static get actionFolder() {
return `${Action.rootFolder}/dist`;
}
static get workspace(): string {
return process.env.GITHUB_WORKSPACE!;
static get workspace() {
return process.env.GITHUB_WORKSPACE;
}
static checkCompatibility() {

View File

@@ -3,15 +3,15 @@ import AndroidVersioning from './android-versioning';
describe('Android Versioning', () => {
describe('versionToVersionCode', () => {
it('defaults to 0 when versioning strategy is none', () => {
expect(AndroidVersioning.versionToVersionCode('none')).toBe('0');
expect(AndroidVersioning.versionToVersionCode('none')).toBe(0);
});
it('defaults to 1 when version is not a valid semver', () => {
expect(AndroidVersioning.versionToVersionCode('abcd')).toBe('1');
expect(AndroidVersioning.versionToVersionCode('abcd')).toBe(1);
});
it('returns a number', () => {
expect(AndroidVersioning.versionToVersionCode('123.456.789')).toBe('123456789');
expect(AndroidVersioning.versionToVersionCode('123.456.789')).toBe(123456789);
});
it('throw when generated version code is too large', () => {
@@ -21,11 +21,11 @@ describe('Android Versioning', () => {
describe('determineVersionCode', () => {
it('defaults to parsed version', () => {
expect(AndroidVersioning.determineVersionCode('1.2.3', '')).toBe('1002003');
expect(AndroidVersioning.determineVersionCode('1.2.3', '')).toBe(1002003);
});
it('use specified code', () => {
expect(AndroidVersioning.determineVersionCode('1.2.3', '2')).toBe('2');
expect(AndroidVersioning.determineVersionCode('1.2.3', 2)).toBe(2);
});
});

View File

@@ -2,19 +2,19 @@ import * as core from '@actions/core';
import * as semver from 'semver';
export default class AndroidVersioning {
static determineVersionCode(version: string, inputVersionCode: string): string {
if (inputVersionCode === '') {
static determineVersionCode(version, inputVersionCode) {
if (!inputVersionCode) {
return AndroidVersioning.versionToVersionCode(version);
}
return inputVersionCode;
}
static versionToVersionCode(version: string): string {
static versionToVersionCode(version) {
if (version === 'none') {
core.info(`Versioning strategy is set to ${version}, so android version code should not be applied.`);
return '0';
return 0;
}
const parsedVersion = semver.parse(version);
@@ -22,7 +22,7 @@ export default class AndroidVersioning {
if (!parsedVersion) {
core.warning(`Could not parse "${version}" to semver, defaulting android version code to 1`);
return '1';
return 1;
}
// The greatest value Google Plays allows is 2100000000.
@@ -36,10 +36,10 @@ export default class AndroidVersioning {
}
core.info(`Using android versionCode ${versionCode}`);
return versionCode.toString();
return versionCode;
}
static determineSdkManagerParameters(targetSdkVersion: string) {
static determineSdkManagerParameters(targetSdkVersion) {
const parsedVersion = Number.parseInt(targetSdkVersion.slice(-2), 10);
return Number.isNaN(parsedVersion) ? '' : `platforms;android-${parsedVersion}`;

View File

@@ -33,7 +33,7 @@ describe('BuildParameters', () => {
it('determines the unity version only once', async () => {
jest.spyOn(UnityVersioning, 'determineUnityVersion').mockImplementation(() => '2019.2.11f1');
await BuildParameters.create();
expect(UnityVersioning.determineUnityVersion).toHaveBeenCalledTimes(1);
await expect(UnityVersioning.determineUnityVersion).toHaveBeenCalledTimes(1);
});
it('returns the android version code with provided input', async () => {
@@ -47,15 +47,13 @@ describe('BuildParameters', () => {
it('returns the android version code from version by default', async () => {
const mockValue = '';
jest.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ androidVersionCode: '1003037' }),
);
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidVersionCode: 1003037 }));
});
it('determines the android sdk manager parameters only once', async () => {
jest.spyOn(AndroidVersioning, 'determineSdkManagerParameters').mockImplementation(() => 'platforms;android-30');
await BuildParameters.create();
expect(AndroidVersioning.determineSdkManagerParameters).toHaveBeenCalledTimes(1);
await expect(AndroidVersioning.determineSdkManagerParameters).toHaveBeenCalledTimes(1);
});
it('returns the targetPlatform', async () => {
@@ -71,12 +69,6 @@ describe('BuildParameters', () => {
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ projectPath: mockValue }));
});
it('returns the build profile', async () => {
const mockValue = 'path/to/build_profile.asset';
jest.spyOn(Input, 'buildProfile', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildProfile: mockValue }));
});
it('returns the build name', async () => {
const mockValue = 'someBuildName';
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
@@ -103,41 +95,34 @@ describe('BuildParameters', () => {
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildFile: mockValue }));
});
test.each`
targetPlatform | expectedExtension | androidExportType
${Platform.types.Android} | ${'.apk'} | ${'androidPackage'}
${Platform.types.Android} | ${'.aab'} | ${'androidAppBundle'}
${Platform.types.Android} | ${''} | ${'androidStudioProject'}
${Platform.types.StandaloneWindows} | ${'.exe'} | ${'n/a'}
${Platform.types.StandaloneWindows64} | ${'.exe'} | ${'n/a'}
`(
'appends $expectedExtension for $targetPlatform with androidExportType $androidExportType',
async ({ targetPlatform, expectedExtension, androidExportType }) => {
test.each([Platform.types.StandaloneWindows, Platform.types.StandaloneWindows64])(
'appends exe for %s',
async (targetPlatform) => {
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
jest.spyOn(Input, 'androidExportType', 'get').mockReturnValue(androidExportType);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildFile: `${targetPlatform}${expectedExtension}` }),
expect.objectContaining({ buildFile: `${targetPlatform}.exe` }),
);
},
);
test.each`
targetPlatform | androidSymbolType
${Platform.types.Android} | ${'none'}
${Platform.types.Android} | ${'public'}
${Platform.types.Android} | ${'debugging'}
${Platform.types.StandaloneWindows} | ${'none'}
${Platform.types.StandaloneWindows64} | ${'none'}
`(
'androidSymbolType is set to $androidSymbolType when targetPlatform is $targetPlatform and input targetSymbolType is $androidSymbolType',
async ({ targetPlatform, androidSymbolType }) => {
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
jest.spyOn(Input, 'androidSymbolType', 'get').mockReturnValue(androidSymbolType);
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidSymbolType }));
},
);
test.each([Platform.types.Android])('appends apk for %s', async (targetPlatform) => {
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
jest.spyOn(Input, 'androidAppBundle', 'get').mockReturnValue(false);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildFile: `${targetPlatform}.apk` }),
);
});
test.each([Platform.types.Android])('appends aab for %s', async (targetPlatform) => {
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
jest.spyOn(Input, 'androidAppBundle', 'get').mockReturnValue(true);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildFile: `${targetPlatform}.aab` }),
);
});
it('returns the build method', async () => {
const mockValue = 'Namespace.ClassName.BuildMethod';
@@ -219,10 +204,5 @@ describe('BuildParameters', () => {
jest.spyOn(Input, 'customParameters', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ customParameters: mockValue }));
});
it.each([true, false])('returns the flag for useHostNetwork when %s', async (mockValue) => {
jest.spyOn(Input, 'useHostNetwork', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ useHostNetwork: mockValue }));
});
});
});

View File

@@ -1,5 +1,7 @@
import { customAlphabet } from 'nanoid';
import AndroidVersioning from './android-versioning';
import CloudRunnerConstants from './cloud-runner/services/cloud-runner-constants';
import CloudRunnerBuildGuid from './cloud-runner/services/cloud-runner-guid';
import Input from './input';
import Platform from './platform';
import UnityVersioning from './unity-versioning';
@@ -8,28 +10,21 @@ import { GitRepoReader } from './input-readers/git-repo';
import { GithubCliReader } from './input-readers/github-cli';
import { Cli } from './cli/cli';
import GitHub from './github';
import * as core from '@actions/core';
import CloudRunnerOptions from './cloud-runner/cloud-runner-options';
class BuildParameters {
// eslint-disable-next-line no-undef
[key: string]: any;
public editorVersion!: string;
public customImage!: string;
public unitySerial!: string;
public unityLicensingServer!: string;
public skipActivation!: string;
public runnerTempPath!: string;
public runnerTempPath: string | undefined;
public targetPlatform!: string;
public projectPath!: string;
public buildProfile!: string;
public buildName!: string;
public buildPath!: string;
public buildFile!: string;
public buildMethod!: string;
public buildVersion!: string;
public manualExit!: boolean;
public enableGpu!: boolean;
public androidVersionCode!: string;
public androidKeystoreName!: string;
public androidKeystoreBase64!: string;
@@ -38,99 +33,83 @@ class BuildParameters {
public androidKeyaliasPass!: string;
public androidTargetSdkVersion!: string;
public androidSdkManagerParameters!: string;
public androidExportType!: string;
public androidSymbolType!: string;
public dockerCpuLimit!: string;
public dockerMemoryLimit!: string;
public dockerIsolationMode!: string;
public containerRegistryRepository!: string;
public containerRegistryImageVersion!: string;
public customParameters!: string;
public useHostNetwork!: boolean;
public sshAgent!: string;
public sshPublicKeysDirectoryPath!: string;
public providerStrategy!: string;
public cloudRunnerCluster!: string;
public awsBaseStackName!: string;
public gitPrivateToken!: string;
public runAsHostUser!: string;
public awsStackName!: string;
public kubeConfig!: string;
public cloudRunnerMemory!: string;
public cloudRunnerCpu!: string;
public kubeVolumeSize!: string;
public kubeVolume!: string;
public kubeStorageClass!: string;
public chownFilesTo!: string;
public customJobHooks!: string;
public readInputFromOverrideList!: string;
public readInputOverrideCommand!: string;
public cacheKey!: string;
public postBuildSteps!: string;
public preBuildSteps!: string;
public customJob!: string;
public runNumber!: string;
public branch!: string;
public githubRepo!: string;
public gitSha!: string;
public logId!: string;
public buildGuid!: string;
public buildPlatform!: string | undefined;
public cloudRunnerBranch!: string;
public cloudRunnerDebug!: boolean;
public cloudRunnerBuilderPlatform!: string | undefined;
public isCliMode!: boolean;
public cacheUnityInstallationOnMac!: boolean;
public unityHubVersionOnMac!: string;
public dockerWorkspacePath!: string;
public retainWorkspace!: boolean;
public maxRetainedWorkspaces!: number;
public useSharedLargePackages!: boolean;
public useLz4Compression!: boolean;
public garbageCollectionMaxAge!: number;
public constantGarbageCollection!: boolean;
public githubChecks!: boolean;
static async create(): Promise<BuildParameters> {
const buildFile = this.parseBuildFile(Input.buildName, Input.targetPlatform, Input.androidExportType);
const buildFile = this.parseBuildFile(Input.buildName, Input.targetPlatform, Input.androidAppBundle);
const editorVersion = UnityVersioning.determineUnityVersion(Input.projectPath, Input.unityVersion);
const buildVersion = await Versioning.determineBuildVersion(Input.versioningStrategy, Input.specifiedVersion);
const androidVersionCode = AndroidVersioning.determineVersionCode(buildVersion, Input.androidVersionCode);
const androidSdkManagerParameters = AndroidVersioning.determineSdkManagerParameters(Input.androidTargetSdkVersion);
const androidSymbolExportType = Input.androidSymbolType;
if (Platform.isAndroid(Input.targetPlatform)) {
switch (androidSymbolExportType) {
case 'none':
case 'public':
case 'debugging':
break;
default:
throw new Error(
`Invalid androidSymbolType: ${Input.androidSymbolType}. Must be one of: none, public, debugging`,
);
}
}
// Todo - Don't use process.env directly, that's what the input model class is for.
// ---
let unitySerial = '';
if (Input.unityLicensingServer === '') {
if (!Input.unitySerial && GitHub.githubInputEnabled) {
if (!process.env.UNITY_SERIAL && GitHub.githubInputEnabled) {
// No serial was present, so it is a personal license that we need to convert
if (!Input.unityLicense) {
throw new Error(
`Missing Unity License File and no Serial was found. If this
if (!process.env.UNITY_LICENSE) {
throw new Error(`Missing Unity License File and no Serial was found. If this
is a personal license, make sure to follow the activation
steps and set the UNITY_LICENSE GitHub secret or enter a Unity
serial number inside the UNITY_SERIAL GitHub secret.`,
);
serial number inside the UNITY_SERIAL GitHub secret.`);
}
unitySerial = this.getSerialFromLicenseFile(Input.unityLicense);
unitySerial = this.getSerialFromLicenseFile(process.env.UNITY_LICENSE);
} else {
unitySerial = Input.unitySerial!;
unitySerial = process.env.UNITY_SERIAL!;
}
}
if (unitySerial !== undefined && unitySerial.length === 27) {
core.setSecret(unitySerial);
core.setSecret(`${unitySerial.slice(0, -4)}XXXX`);
}
const providerStrategy = Input.getInput('providerStrategy') || (Cli.isCliMode ? 'aws' : 'local');
return {
editorVersion,
customImage: Input.customImage,
unitySerial,
unityLicensingServer: Input.unityLicensingServer,
skipActivation: Input.skipActivation,
runnerTempPath: Input.runnerTempPath,
runnerTempPath: process.env.RUNNER_TEMP,
targetPlatform: Input.targetPlatform,
projectPath: Input.projectPath,
buildProfile: Input.buildProfile,
buildName: Input.buildName,
buildPath: `${Input.buildsPath}/${Input.targetPlatform}`,
buildFile,
buildMethod: Input.buildMethod,
buildVersion,
manualExit: Input.manualExit,
enableGpu: Input.enableGpu,
androidVersionCode,
androidKeystoreName: Input.androidKeystoreName,
androidKeystoreBase64: Input.androidKeystoreBase64,
@@ -139,62 +118,59 @@ class BuildParameters {
androidKeyaliasPass: Input.androidKeyaliasPass,
androidTargetSdkVersion: Input.androidTargetSdkVersion,
androidSdkManagerParameters,
androidExportType: Input.androidExportType,
androidSymbolType: androidSymbolExportType,
customParameters: Input.customParameters,
useHostNetwork: Input.useHostNetwork,
sshAgent: Input.sshAgent,
sshPublicKeysDirectoryPath: Input.sshPublicKeysDirectoryPath,
gitPrivateToken: Input.gitPrivateToken ?? (await GithubCliReader.GetGitHubAuthToken()),
runAsHostUser: Input.runAsHostUser,
gitPrivateToken: Input.gitPrivateToken || (await GithubCliReader.GetGitHubAuthToken()),
chownFilesTo: Input.chownFilesTo,
dockerCpuLimit: Input.dockerCpuLimit,
dockerMemoryLimit: Input.dockerMemoryLimit,
dockerIsolationMode: Input.dockerIsolationMode,
containerRegistryRepository: Input.containerRegistryRepository,
containerRegistryImageVersion: Input.containerRegistryImageVersion,
providerStrategy,
buildPlatform: providerStrategy !== 'local' ? 'linux' : process.platform,
cloudRunnerCluster: CloudRunnerOptions.cloudRunnerCluster,
cloudRunnerBuilderPlatform: CloudRunnerOptions.cloudRunnerBuilderPlatform,
awsBaseStackName: CloudRunnerOptions.awsBaseStackName,
kubeConfig: CloudRunnerOptions.kubeConfig,
cloudRunnerMemory: CloudRunnerOptions.cloudRunnerMemory,
cloudRunnerCpu: CloudRunnerOptions.cloudRunnerCpu,
kubeVolumeSize: CloudRunnerOptions.kubeVolumeSize,
kubeVolume: CloudRunnerOptions.kubeVolume,
postBuildSteps: CloudRunnerOptions.postBuildSteps,
preBuildSteps: CloudRunnerOptions.preBuildSteps,
customJob: CloudRunnerOptions.customJob,
runNumber: Input.runNumber,
branch: Input.branch.replace('/head', '') || (await GitRepoReader.GetBranch()),
githubRepo: (Input.githubRepo ?? (await GitRepoReader.GetRemote())) || 'game-ci/unity-builder',
gitSha: Input.gitSha,
logId: customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 9)(),
buildGuid: `${Input.runNumber}-${Input.targetPlatform.toLowerCase().replace('standalone', '')}-${customAlphabet(
'0123456789abcdefghijklmnopqrstuvwxyz',
4,
)()}`,
cloudRunnerBranch: CloudRunnerOptions.cloudRunnerBranch.split('/').reverse()[0],
cloudRunnerDebug: CloudRunnerOptions.cloudRunnerDebug,
githubRepo: Input.githubRepo || (await GitRepoReader.GetRemote()) || 'game-ci/unity-builder',
isCliMode: Cli.isCliMode,
cacheUnityInstallationOnMac: Input.cacheUnityInstallationOnMac,
unityHubVersionOnMac: Input.unityHubVersionOnMac,
dockerWorkspacePath: Input.dockerWorkspacePath,
awsStackName: CloudRunnerOptions.awsBaseStackName,
gitSha: Input.gitSha,
logId: customAlphabet(CloudRunnerConstants.alphabet, 9)(),
buildGuid: CloudRunnerBuildGuid.generateGuid(Input.runNumber, Input.targetPlatform),
customJobHooks: CloudRunnerOptions.customJobHooks(),
readInputOverrideCommand: CloudRunnerOptions.readInputOverrideCommand(),
readInputFromOverrideList: CloudRunnerOptions.readInputFromOverrideList(),
kubeStorageClass: CloudRunnerOptions.kubeStorageClass,
cacheKey: CloudRunnerOptions.cacheKey,
retainWorkspace: CloudRunnerOptions.retainWorkspaces,
useSharedLargePackages: CloudRunnerOptions.useSharedLargePackages,
useLz4Compression: CloudRunnerOptions.useLz4Compression,
maxRetainedWorkspaces: CloudRunnerOptions.maxRetainedWorkspaces,
constantGarbageCollection: CloudRunnerOptions.constantGarbageCollection,
garbageCollectionMaxAge: CloudRunnerOptions.garbageCollectionMaxAge,
githubChecks: CloudRunnerOptions.githubChecks,
};
}
static parseBuildFile(filename: string, platform: string, androidExportType: string): string {
static parseBuildFile(filename, platform, androidAppBundle) {
if (Platform.isWindows(platform)) {
return `${filename}.exe`;
}
if (Platform.isAndroid(platform)) {
switch (androidExportType) {
case `androidPackage`:
return `${filename}.apk`;
case `androidAppBundle`:
return `${filename}.aab`;
case `androidStudioProject`:
return filename;
default:
throw new Error(
`Unknown Android Export Type: ${androidExportType}. Must be one of androidPackage for apk, androidAppBundle for aab, androidStudioProject for android project`,
);
}
return androidAppBundle ? `${filename}.aab` : `${filename}.apk`;
}
return filename;
}
static getSerialFromLicenseFile(license: string) {
static getSerialFromLicenseFile(license) {
const startKey = `<DeveloperData Value="`;
const endKey = `"/>`;
const startIndex = license.indexOf(startKey) + startKey.length;

View File

@@ -1,5 +1,5 @@
import * as core from '@actions/core';
import fs from 'node:fs';
import fs from 'fs';
import Action from './action';
import Project from './project';

View File

@@ -16,7 +16,7 @@ export class CliFunctionsRepository {
});
}
public static GetCliFunctions(key: any) {
public static GetCliFunctions(key) {
const results = CliFunctionsRepository.targets.find((x) => x.key === key);
if (results === undefined || results.length === 0) {
throw new Error(`no CLI mode found for ${key}`);

View File

@@ -1,17 +1,25 @@
import { Command } from 'commander-ts';
import { Input } from '..';
import { BuildParameters, CloudRunner, ImageTag, Input } from '..';
import * as core from '@actions/core';
import { ActionYamlReader } from '../input-readers/action-yaml';
import CloudRunnerLogger from '../cloud-runner/services/cloud-runner-logger';
import CloudRunnerQueryOverride from '../cloud-runner/services/cloud-runner-query-override';
import { CliFunction, CliFunctionsRepository } from './cli-functions-repository';
import { OptionValues } from 'commander';
import { InputKey } from '../input';
import { Caching } from '../cloud-runner/remote-client/caching';
import { LfsHashing } from '../cloud-runner/services/lfs-hashing';
import { RemoteClient } from '../cloud-runner/remote-client';
import CloudRunnerOptionsReader from '../cloud-runner/services/cloud-runner-options-reader';
import GitHub from '../github';
import { TaskParameterSerializer } from '../cloud-runner/services/task-parameter-serializer';
import { CloudRunnerFolders } from '../cloud-runner/services/cloud-runner-folders';
import { CloudRunnerSystem } from '../cloud-runner/services/cloud-runner-system';
export class Cli {
public static options: OptionValues | undefined;
public static options;
static get isCliMode() {
return Cli.options !== undefined && Cli.options.mode !== undefined && Cli.options.mode !== '';
}
public static query(key: string, alternativeKey: string) {
public static query(key, alternativeKey) {
if (Cli.options && Cli.options[key] !== undefined) {
return Cli.options[key];
}
@@ -23,13 +31,14 @@ export class Cli {
}
public static InitCliMode() {
CliFunctionsRepository.PushCliFunctionSource(RemoteClient);
CliFunctionsRepository.PushCliFunctionSource(Caching);
CliFunctionsRepository.PushCliFunctionSource(LfsHashing);
const program = new Command();
program.version('0.0.1');
const properties = CloudRunnerOptionsReader.GetProperties();
const actionYamlReader: ActionYamlReader = new ActionYamlReader();
const properties = Object.getOwnPropertyNames(Input).filter(
(p) => p !== 'length' && p !== 'prototype' && p !== 'name',
);
for (const element of properties) {
program.option(`--${element} <${element}>`, actionYamlReader.GetActionYamlValue(element));
}
@@ -44,12 +53,6 @@ export class Cli {
program.option('--cachePushTo <cachePushTo>', 'cache push to caching folder');
program.option('--artifactName <artifactName>', 'caching artifact name');
program.option('--select <select>', 'select a particular resource');
program.option('--logFile <logFile>', 'output to log file (log stream only)');
program.option('--profilePath <profilePath>', 'path to submodule profile YAML');
program.option('--variantPath <variantPath>', 'path to submodule variant YAML');
program.option('--agentPath <agentPath>', 'path to custom LFS transfer agent');
program.option('--agentArgs <agentArgs>', 'arguments for custom LFS transfer agent');
program.option('--storagePaths <storagePaths>', 'semicolon-separated storage paths for LFS agent');
program.parse(process.argv);
Cli.options = program.opts();
@@ -57,14 +60,23 @@ export class Cli {
}
static async RunCli(): Promise<void> {
const results = CliFunctionsRepository.GetCliFunctions(Cli.options?.mode);
if (!results) {
throw new Error(
`Unknown CLI mode: ${Cli.options?.mode}. Orchestrator CLI features require @game-ci/orchestrator.`,
);
GitHub.githubInputEnabled = false;
if (Cli.options['populateOverride'] === `true`) {
await CloudRunnerQueryOverride.PopulateQueryOverrideInput();
}
core.info(`Entrypoint: ${results.key}`);
Cli.options!.versioning = 'None';
if (Cli.options['logInput']) {
Cli.logInput();
}
const results = CliFunctionsRepository.GetCliFunctions(Cli.options.mode);
CloudRunnerLogger.log(`Entrypoint: ${results.key}`);
Cli.options.versioning = 'None';
const buildParameter = TaskParameterSerializer.readBuildParameterFromEnvironment();
CloudRunnerLogger.log(`Build Params:
${JSON.stringify(buildParameter, undefined, 4)}
`);
CloudRunner.buildParameters = buildParameter;
CloudRunner.lockedWorkspace = process.env.LOCKED_WORKSPACE;
return await results.target[results.propertyKey](Cli.options);
}
@@ -73,22 +85,126 @@ export class Cli {
private static logInput() {
core.info(`\n`);
core.info(`INPUT:`);
const properties = Object.getOwnPropertyNames(Input).filter(
(p) => p !== 'length' && p !== 'prototype' && p !== 'name',
);
const properties = CloudRunnerOptionsReader.GetProperties();
for (const element of properties) {
if (
element in Input &&
Input[element as InputKey] !== undefined &&
Input[element as InputKey] !== '' &&
typeof Input[element as InputKey] !== `function` &&
Input[element] !== undefined &&
Input[element] !== '' &&
typeof Input[element] !== `function` &&
element !== 'length' &&
element !== 'cliOptions' &&
element !== 'prototype'
) {
core.info(`${element} ${Input[element as InputKey]}`);
core.info(`${element} ${Input[element]}`);
}
}
core.info(`\n`);
}
@CliFunction(`cli-build`, `runs a cloud runner build`)
public static async CLIBuild(): Promise<string> {
const buildParameter = await BuildParameters.create();
const baseImage = new ImageTag(buildParameter);
return await CloudRunner.run(buildParameter, baseImage.toString());
}
@CliFunction(`async-workflow`, `runs a cloud runner build`)
public static async asyncronousWorkflow(): Promise<string> {
const buildParameter = await BuildParameters.create();
const baseImage = new ImageTag(buildParameter);
return await CloudRunner.run(buildParameter, baseImage.toString());
}
@CliFunction(`checks-update`, `runs a cloud runner build`)
public static async checksUpdate() {
const input = JSON.parse(process.env.CHECKS_UPDATE || ``);
core.info(`Checks Update ${process.env.CHECKS_UPDATE}`);
if (input.mode === `create`) {
throw new Error(`Not supported: only use update`);
} else if (input.mode === `update`) {
await GitHub.updateGitHubCheckRequest(input.data);
}
}
@CliFunction(`garbage-collect`, `runs garbage collection`)
public static async GarbageCollect(): Promise<string> {
const buildParameter = await BuildParameters.create();
await CloudRunner.setup(buildParameter);
return await CloudRunner.Provider.garbageCollect(``, false, 0, false, false);
}
@CliFunction(`list-resources`, `lists active resources`)
public static async ListResources(): Promise<string[]> {
const buildParameter = await BuildParameters.create();
await CloudRunner.setup(buildParameter);
const result = await CloudRunner.Provider.listResources();
CloudRunnerLogger.log(JSON.stringify(result, undefined, 4));
return result.map((x) => x.Name);
}
@CliFunction(`list-worfklow`, `lists running workflows`)
public static async ListWorfklow(): Promise<string[]> {
const buildParameter = await BuildParameters.create();
await CloudRunner.setup(buildParameter);
return (await CloudRunner.Provider.listWorkflow()).map((x) => x.Name);
}
@CliFunction(`watch`, `follows logs of a running workflow`)
public static async Watch(): Promise<string> {
const buildParameter = await BuildParameters.create();
await CloudRunner.setup(buildParameter);
return await CloudRunner.Provider.watchWorkflow();
}
@CliFunction(`remote-cli-post-build`, `runs a cloud runner build`)
public static async PostCLIBuild(): Promise<string> {
core.info(`Running POST build tasks`);
await Caching.PushToCache(
CloudRunnerFolders.ToLinuxFolder(`${CloudRunnerFolders.cacheFolderForCacheKeyFull}/Library`),
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.libraryFolderAbsolute),
`lib-${CloudRunner.buildParameters.buildGuid}`,
);
await Caching.PushToCache(
CloudRunnerFolders.ToLinuxFolder(`${CloudRunnerFolders.cacheFolderForCacheKeyFull}/build`),
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.projectBuildFolderAbsolute),
`build-${CloudRunner.buildParameters.buildGuid}`,
);
if (!CloudRunner.buildParameters.retainWorkspace) {
await CloudRunnerSystem.Run(
`rm -r ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)}`,
);
}
await RemoteClient.runCustomHookFiles(`after-build`);
const parameters = await BuildParameters.create();
CloudRunner.setup(parameters);
if (parameters.constantGarbageCollection) {
await CloudRunnerSystem.Run(
`find /${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.buildVolumeFolder)}/ -name '*.*' -mmin +${
parameters.garbageCollectionMaxAge * 60
} -delete`,
);
await CloudRunnerSystem.Run(
`find ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.cacheFolderForAllFull)} -name '*.*' -mmin +${
parameters.garbageCollectionMaxAge * 60
} -delete`,
);
}
return new Promise((result) => result(``));
}
}

View File

@@ -0,0 +1,290 @@
import { Cli } from '../cli/cli';
import CloudRunnerQueryOverride from './services/cloud-runner-query-override';
import GitHub from '../github';
const core = require('@actions/core');
class CloudRunnerOptions {
// ### ### ###
// Input Handling
// ### ### ###
public static getInput(query) {
if (GitHub.githubInputEnabled) {
const coreInput = core.getInput(query);
if (coreInput && coreInput !== '') {
return coreInput;
}
}
const alternativeQuery = CloudRunnerOptions.ToEnvVarFormat(query);
// Query input sources
if (Cli.query(query, alternativeQuery)) {
return Cli.query(query, alternativeQuery);
}
if (CloudRunnerQueryOverride.query(query, alternativeQuery)) {
return CloudRunnerQueryOverride.query(query, alternativeQuery);
}
if (process.env[query] !== undefined) {
return process.env[query];
}
if (alternativeQuery !== query && process.env[alternativeQuery] !== undefined) {
return process.env[alternativeQuery];
}
return;
}
public static ToEnvVarFormat(input: string) {
if (input.toUpperCase() === input) {
return input;
}
return input
.replace(/([A-Z])/g, ' $1')
.trim()
.toUpperCase()
.replace(/ /g, '_');
}
// ### ### ###
// Provider parameters
// ### ### ###
static get region(): string {
return CloudRunnerOptions.getInput('region') || 'eu-west-2';
}
// ### ### ###
// GitHub parameters
// ### ### ###
static get githubChecks(): boolean {
return CloudRunnerOptions.getInput('githubChecks') || false;
}
static get githubOwner() {
return CloudRunnerOptions.getInput('githubOwner') || CloudRunnerOptions.githubRepo.split(`/`)[0] || false;
}
static get githubRepoName() {
return CloudRunnerOptions.getInput('githubRepoName') || CloudRunnerOptions.githubRepo.split(`/`)[1] || false;
}
// ### ### ###
// Git syncronization parameters
// ### ### ###
static get githubRepo() {
return CloudRunnerOptions.getInput('GITHUB_REPOSITORY') || CloudRunnerOptions.getInput('GITHUB_REPO') || undefined;
}
static get branch() {
if (CloudRunnerOptions.getInput(`GITHUB_REF`)) {
return CloudRunnerOptions.getInput(`GITHUB_REF`).replace('refs/', '').replace(`head/`, '').replace(`heads/`, '');
} else if (CloudRunnerOptions.getInput('branch')) {
return CloudRunnerOptions.getInput('branch');
} else {
return '';
}
}
static get gitSha() {
if (CloudRunnerOptions.getInput(`GITHUB_SHA`)) {
return CloudRunnerOptions.getInput(`GITHUB_SHA`);
} else if (CloudRunnerOptions.getInput(`GitSHA`)) {
return CloudRunnerOptions.getInput(`GitSHA`);
}
}
// ### ### ###
// Cloud Runner parameters
// ### ### ###
static get cloudRunnerBuilderPlatform() {
const input = CloudRunnerOptions.getInput('cloudRunnerBuilderPlatform');
if (input) {
return input;
}
if (CloudRunnerOptions.cloudRunnerCluster !== 'local') {
return 'linux';
}
return;
}
static get cloudRunnerBranch() {
return CloudRunnerOptions.getInput('cloudRunnerBranch') || 'main';
}
static get cloudRunnerCluster() {
if (Cli.isCliMode) {
return CloudRunnerOptions.getInput('cloudRunnerCluster') || 'aws';
}
return CloudRunnerOptions.getInput('cloudRunnerCluster') || 'local';
}
static get cloudRunnerCpu() {
return CloudRunnerOptions.getInput('cloudRunnerCpu');
}
static get cloudRunnerMemory() {
return CloudRunnerOptions.getInput('cloudRunnerMemory');
}
static get customJob() {
return CloudRunnerOptions.getInput('customJob') || '';
}
// ### ### ###
// Custom commands from files parameters
// ### ### ###
static get customStepFiles() {
return CloudRunnerOptions.getInput('customStepFiles')?.split(`,`) || [];
}
static get customHookFiles() {
return CloudRunnerOptions.getInput('customHookFiles')?.split(`,`) || [];
}
// ### ### ###
// Custom commands from yaml parameters
// ### ### ###
static customJobHooks() {
return CloudRunnerOptions.getInput('customJobHooks') || '';
}
static get postBuildSteps() {
return CloudRunnerOptions.getInput('postBuildSteps') || '';
}
static get preBuildSteps() {
return CloudRunnerOptions.getInput('preBuildSteps') || '';
}
// ### ### ###
// Input override handling
// ### ### ###
static readInputFromOverrideList() {
return CloudRunnerOptions.getInput('readInputFromOverrideList') || '';
}
static readInputOverrideCommand() {
const value = CloudRunnerOptions.getInput('readInputOverrideCommand');
if (value === 'gcp-secret-manager') {
return 'gcloud secrets versions access 1 --secret="{0}"';
} else if (value === 'aws-secret-manager') {
return 'aws secretsmanager get-secret-value --secret-id {0}';
}
return value || '';
}
// ### ### ###
// Aws
// ### ### ###
static get awsBaseStackName() {
return CloudRunnerOptions.getInput('awsBaseStackName') || 'game-ci';
}
// ### ### ###
// K8s
// ### ### ###
static get kubeConfig() {
return CloudRunnerOptions.getInput('kubeConfig') || '';
}
static get kubeVolume() {
return CloudRunnerOptions.getInput('kubeVolume') || '';
}
static get kubeVolumeSize() {
return CloudRunnerOptions.getInput('kubeVolumeSize') || '5Gi';
}
static get kubeStorageClass(): string {
return CloudRunnerOptions.getInput('kubeStorageClass') || '';
}
// ### ### ###
// Caching
// ### ### ###
static get cacheKey(): string {
return CloudRunnerOptions.getInput('cacheKey') || CloudRunnerOptions.branch;
}
// ### ### ###
// Utility Parameters
// ### ### ###
static get cloudRunnerDebug(): boolean {
return CloudRunnerOptions.getInput(`cloudRunnerTests`) || CloudRunnerOptions.getInput(`cloudRunnerDebug`) || false;
}
static get cloudRunnerDebugTree(): boolean {
return CloudRunnerOptions.getInput(`cloudRunnerDebugTree`) || false;
}
static get cloudRunnerDebugEnv(): boolean {
return CloudRunnerOptions.getInput(`cloudRunnerDebugEnv`) || false;
}
static get watchCloudRunnerToEnd(): boolean {
if (CloudRunnerOptions.asyncCloudRunner) {
return false;
}
return CloudRunnerOptions.getInput(`watchToEnd`) || true;
}
static get asyncCloudRunner(): boolean {
return (CloudRunnerOptions.getInput('asyncCloudRunner') || `false`) === `true` || false;
}
public static get useSharedLargePackages(): boolean {
return (CloudRunnerOptions.getInput(`useSharedLargePackages`) || 'false') === 'true';
}
public static get useSharedBuilder(): boolean {
return (CloudRunnerOptions.getInput(`useSharedBuilder`) || 'true') === 'true';
}
public static get useLz4Compression(): boolean {
return (CloudRunnerOptions.getInput(`useLz4Compression`) || 'false') === 'true';
}
public static get useCleanupCron(): boolean {
return (CloudRunnerOptions.getInput(`useCleanupCron`) || 'true') === 'true';
}
// ### ### ###
// Retained Workspace
// ### ### ###
public static get retainWorkspaces(): boolean {
return CloudRunnerOptions.getInput(`retainWorkspaces`) || false;
}
static get maxRetainedWorkspaces(): number {
return Number(CloudRunnerOptions.getInput(`maxRetainedWorkspaces`)) || 3;
}
// ### ### ###
// Garbage Collection
// ### ### ###
static get constantGarbageCollection(): boolean {
return CloudRunnerOptions.getInput(`constantGarbageCollection`) || true;
}
static get garbageCollectionMaxAge(): number {
return Number(CloudRunnerOptions.getInput(`garbageCollectionMaxAge`)) || 24;
}
}
export default CloudRunnerOptions;

View File

@@ -0,0 +1,3 @@
export class CloudRunnerStatics {
public static readonly logPrefix = `Cloud-Runner`;
}

View File

@@ -0,0 +1,13 @@
import CloudRunnerEnvironmentVariable from './services/cloud-runner-environment-variable';
import CloudRunnerSecret from './services/cloud-runner-secret';
export class CloudRunnerStepState {
public image: string;
public environment: CloudRunnerEnvironmentVariable[];
public secrets: CloudRunnerSecret[];
constructor(image: string, environmentVariables: CloudRunnerEnvironmentVariable[], secrets: CloudRunnerSecret[]) {
this.image = image;
this.environment = environmentVariables;
this.secrets = secrets;
}
}

View File

@@ -0,0 +1,146 @@
import AwsBuildPlatform from './providers/aws';
import { BuildParameters, Input } from '..';
import Kubernetes from './providers/k8s';
import CloudRunnerLogger from './services/cloud-runner-logger';
import { CloudRunnerStepState } from './cloud-runner-step-state';
import { WorkflowCompositionRoot } from './workflows/workflow-composition-root';
import { CloudRunnerError } from './error/cloud-runner-error';
import { TaskParameterSerializer } from './services/task-parameter-serializer';
import * as core from '@actions/core';
import CloudRunnerSecret from './services/cloud-runner-secret';
import { ProviderInterface } from './providers/provider-interface';
import CloudRunnerEnvironmentVariable from './services/cloud-runner-environment-variable';
import TestCloudRunner from './providers/test';
import LocalCloudRunner from './providers/local';
import LocalDockerCloudRunner from './providers/docker';
import GitHub from '../github';
import SharedWorkspaceLocking from './services/shared-workspace-locking';
class CloudRunner {
public static Provider: ProviderInterface;
public static buildParameters: BuildParameters;
private static defaultSecrets: CloudRunnerSecret[];
private static cloudRunnerEnvironmentVariables: CloudRunnerEnvironmentVariable[];
static lockedWorkspace: string | undefined;
public static readonly retainedWorkspacePrefix: string = `retained-workspace`;
public static githubCheckId;
public static setup(buildParameters: BuildParameters) {
CloudRunnerLogger.setup();
CloudRunnerLogger.log(`Setting up cloud runner`);
CloudRunner.buildParameters = buildParameters;
CloudRunner.setupSelectedBuildPlatform();
CloudRunner.defaultSecrets = TaskParameterSerializer.readDefaultSecrets();
CloudRunner.cloudRunnerEnvironmentVariables =
TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameters);
if (GitHub.githubInputEnabled) {
const buildParameterPropertyNames = Object.getOwnPropertyNames(buildParameters);
for (const element of CloudRunner.cloudRunnerEnvironmentVariables) {
// CloudRunnerLogger.log(`Cloud Runner output ${Input.ToEnvVarFormat(element.name)} = ${element.value}`);
core.setOutput(Input.ToEnvVarFormat(element.name), element.value);
}
for (const element of buildParameterPropertyNames) {
// CloudRunnerLogger.log(`Cloud Runner output ${Input.ToEnvVarFormat(element)} = ${buildParameters[element]}`);
core.setOutput(Input.ToEnvVarFormat(element), buildParameters[element]);
}
core.setOutput(
Input.ToEnvVarFormat(`buildArtifact`),
`build-${CloudRunner.buildParameters.buildGuid}.tar${
CloudRunner.buildParameters.useLz4Compression ? '.lz4' : ''
}`,
);
}
}
private static setupSelectedBuildPlatform() {
CloudRunnerLogger.log(`Cloud Runner platform selected ${CloudRunner.buildParameters.cloudRunnerCluster}`);
switch (CloudRunner.buildParameters.cloudRunnerCluster) {
case 'k8s':
CloudRunner.Provider = new Kubernetes(CloudRunner.buildParameters);
break;
case 'aws':
CloudRunner.Provider = new AwsBuildPlatform(CloudRunner.buildParameters);
break;
case 'test':
CloudRunner.Provider = new TestCloudRunner();
break;
case 'local-docker':
CloudRunner.Provider = new LocalDockerCloudRunner();
break;
case 'local-system':
CloudRunner.Provider = new LocalCloudRunner();
break;
}
}
static async run(buildParameters: BuildParameters, baseImage: string) {
CloudRunner.setup(buildParameters);
try {
CloudRunner.githubCheckId = await GitHub.createGitHubCheck(CloudRunner.buildParameters.buildGuid);
if (buildParameters.retainWorkspace) {
CloudRunner.lockedWorkspace = `${CloudRunner.retainedWorkspacePrefix}-${CloudRunner.buildParameters.buildGuid}`;
const result = await SharedWorkspaceLocking.GetOrCreateLockedWorkspace(
CloudRunner.lockedWorkspace,
CloudRunner.buildParameters.buildGuid,
CloudRunner.buildParameters,
);
if (result) {
CloudRunnerLogger.logLine(`Using retained workspace ${CloudRunner.lockedWorkspace}`);
CloudRunner.cloudRunnerEnvironmentVariables = [
...CloudRunner.cloudRunnerEnvironmentVariables,
{ name: `LOCKED_WORKSPACE`, value: CloudRunner.lockedWorkspace },
];
} else {
CloudRunnerLogger.log(`Max retained workspaces reached ${buildParameters.maxRetainedWorkspaces}`);
buildParameters.retainWorkspace = false;
CloudRunner.lockedWorkspace = undefined;
}
}
if (!CloudRunner.buildParameters.isCliMode) core.startGroup('Setup shared cloud runner resources');
await CloudRunner.Provider.setupWorkflow(
CloudRunner.buildParameters.buildGuid,
CloudRunner.buildParameters,
CloudRunner.buildParameters.branch,
CloudRunner.defaultSecrets,
);
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
await GitHub.updateGitHubCheck(CloudRunner.buildParameters.buildGuid, CloudRunner.buildParameters.buildGuid);
const output = await new WorkflowCompositionRoot().run(
new CloudRunnerStepState(baseImage, CloudRunner.cloudRunnerEnvironmentVariables, CloudRunner.defaultSecrets),
);
if (!CloudRunner.buildParameters.isCliMode) core.startGroup('Cleanup shared cloud runner resources');
await CloudRunner.Provider.cleanupWorkflow(
CloudRunner.buildParameters.buildGuid,
CloudRunner.buildParameters,
CloudRunner.buildParameters.branch,
CloudRunner.defaultSecrets,
);
CloudRunnerLogger.log(`Cleanup complete`);
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
await GitHub.updateGitHubCheck(CloudRunner.buildParameters.buildGuid, `success`, `success`, `completed`);
if (CloudRunner.buildParameters.retainWorkspace) {
await SharedWorkspaceLocking.ReleaseWorkspace(
CloudRunner.lockedWorkspace || ``,
CloudRunner.buildParameters.buildGuid,
CloudRunner.buildParameters,
);
CloudRunner.lockedWorkspace = undefined;
}
if (buildParameters.constantGarbageCollection) {
CloudRunner.Provider.garbageCollect(``, true, buildParameters.garbageCollectionMaxAge, true, true);
}
return output;
} catch (error) {
await GitHub.updateGitHubCheck(CloudRunner.buildParameters.buildGuid, error, `failure`, `completed`);
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
await CloudRunnerError.handleException(error, CloudRunner.buildParameters, CloudRunner.defaultSecrets);
throw error;
}
}
}
export default CloudRunner;

View File

@@ -0,0 +1,20 @@
import CloudRunnerLogger from '../services/cloud-runner-logger';
import * as core from '@actions/core';
import CloudRunner from '../cloud-runner';
import CloudRunnerSecret from '../services/cloud-runner-secret';
import BuildParameters from '../../build-parameters';
export class CloudRunnerError {
public static async handleException(error: unknown, buildParameters: BuildParameters, secrets: CloudRunnerSecret[]) {
CloudRunnerLogger.error(JSON.stringify(error, undefined, 4));
core.setFailed('Cloud Runner failed');
if (CloudRunner.Provider !== undefined) {
await CloudRunner.Provider.cleanupWorkflow(
buildParameters.buildGuid,
buildParameters,
buildParameters.branch,
secrets,
);
}
}
}

View File

@@ -0,0 +1,105 @@
import CloudRunnerLogger from '../../services/cloud-runner-logger';
import * as core from '@actions/core';
import * as SDK from 'aws-sdk';
import { BaseStackFormation } from './cloud-formations/base-stack-formation';
const crypto = require('crypto');
export class AWSBaseStack {
constructor(baseStackName: string) {
this.baseStackName = baseStackName;
}
private baseStackName: string;
async setupBaseStack(CF: SDK.CloudFormation) {
const baseStackName = this.baseStackName;
const baseStack = BaseStackFormation.formation;
// Cloud Formation Input
const describeStackInput: SDK.CloudFormation.DescribeStacksInput = {
StackName: baseStackName,
};
const parametersWithoutHash: SDK.CloudFormation.Parameter[] = [
{ ParameterKey: 'EnvironmentName', ParameterValue: baseStackName },
];
const parametersHash = crypto
.createHash('md5')
.update(baseStack + JSON.stringify(parametersWithoutHash))
.digest('hex');
const parameters: SDK.CloudFormation.Parameter[] = [
...parametersWithoutHash,
...[{ ParameterKey: 'Version', ParameterValue: parametersHash }],
];
const updateInput: SDK.CloudFormation.UpdateStackInput = {
StackName: baseStackName,
TemplateBody: baseStack,
Parameters: parameters,
Capabilities: ['CAPABILITY_IAM'],
};
const createStackInput: SDK.CloudFormation.CreateStackInput = {
StackName: baseStackName,
TemplateBody: baseStack,
Parameters: parameters,
Capabilities: ['CAPABILITY_IAM'],
};
const stacks = await CF.listStacks({
StackStatusFilter: ['UPDATE_COMPLETE', 'CREATE_COMPLETE', 'ROLLBACK_COMPLETE'],
}).promise();
const stackNames = stacks.StackSummaries?.map((x) => x.StackName) || [];
const stackExists: Boolean = stackNames.includes(baseStackName) || false;
const describeStack = async () => {
return await CF.describeStacks(describeStackInput).promise();
};
try {
if (!stackExists) {
CloudRunnerLogger.log(`${baseStackName} stack does not exist (${JSON.stringify(stackNames)})`);
await CF.createStack(createStackInput).promise();
CloudRunnerLogger.log(`created stack (version: ${parametersHash})`);
}
const CFState = await describeStack();
let stack = CFState.Stacks?.[0];
if (!stack) {
throw new Error(`Base stack doesn't exist, even after creation, stackExists check: ${stackExists}`);
}
const stackVersion = stack.Parameters?.find((x) => x.ParameterKey === 'Version')?.ParameterValue;
if (stack.StackStatus === 'CREATE_IN_PROGRESS') {
await CF.waitFor('stackCreateComplete', describeStackInput).promise();
}
if (stackExists) {
CloudRunnerLogger.log(`Base stack exists (version: ${stackVersion}, local version: ${parametersHash})`);
if (parametersHash !== stackVersion) {
CloudRunnerLogger.log(`Attempting update of base stack`);
try {
await CF.updateStack(updateInput).promise();
} catch (error: any) {
if (error['message'].includes('No updates are to be performed')) {
CloudRunnerLogger.log(`No updates are to be performed`);
} else {
CloudRunnerLogger.log(`Update Failed (Stack name: ${baseStackName})`);
CloudRunnerLogger.log(error['message']);
}
CloudRunnerLogger.log(`Continuing...`);
}
} else {
CloudRunnerLogger.log(`No update required`);
}
stack = (await describeStack()).Stacks?.[0];
if (!stack) {
throw new Error(
`Base stack doesn't exist, even after updating and creation, stackExists check: ${stackExists}`,
);
}
if (stack.StackStatus === 'UPDATE_IN_PROGRESS') {
await CF.waitFor('stackUpdateComplete', describeStackInput).promise();
}
}
CloudRunnerLogger.log('base stack is now ready');
} catch (error) {
core.error(JSON.stringify(await describeStack(), undefined, 4));
throw error;
}
}
}

View File

@@ -0,0 +1,39 @@
import { TaskDefinitionFormation } from './cloud-formations/task-definition-formation';
export class AWSCloudFormationTemplates {
public static getParameterTemplate(p1) {
return `
${p1}:
Type: String
Default: ''
`;
}
public static getSecretTemplate(p1) {
return `
${p1}Secret:
Type: AWS::SecretsManager::Secret
Properties:
Name: '${p1}'
SecretString: !Ref ${p1}
`;
}
public static getSecretDefinitionTemplate(p1, p2) {
return `
- Name: '${p1}'
ValueFrom: !Ref ${p2}Secret
`;
}
public static insertAtTemplate(template, insertionKey, insertion) {
const index = template.search(insertionKey) + insertionKey.length + '\n'.length;
template = [template.slice(0, index), insertion, template.slice(index)].join('');
return template;
}
public static readTaskCloudFormationTemplate(): string {
return TaskDefinitionFormation.formation;
}
}

View File

@@ -0,0 +1,16 @@
import CloudRunnerLogger from '../../services/cloud-runner-logger';
import * as SDK from 'aws-sdk';
import * as core from '@actions/core';
import CloudRunner from '../../cloud-runner';
export class AWSError {
static async handleStackCreationFailure(error: any, CF: SDK.CloudFormation, taskDefStackName: string) {
CloudRunnerLogger.log('aws error: ');
core.error(JSON.stringify(error, undefined, 4));
if (CloudRunner.buildParameters.cloudRunnerDebug) {
CloudRunnerLogger.log('Getting events and resources for task stack');
const events = (await CF.describeStackEvents({ StackName: taskDefStackName }).promise()).StackEvents;
CloudRunnerLogger.log(JSON.stringify(events, undefined, 4));
}
}
}

View File

@@ -0,0 +1,207 @@
import * as SDK from 'aws-sdk';
import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def';
import CloudRunnerSecret from '../../services/cloud-runner-secret';
import { AWSCloudFormationTemplates } from './aws-cloud-formation-templates';
import CloudRunnerLogger from '../../services/cloud-runner-logger';
import { AWSError } from './aws-error';
import CloudRunner from '../../cloud-runner';
import { CleanupCronFormation } from './cloud-formations/cleanup-cron-formation';
import CloudRunnerOptions from '../../cloud-runner-options';
import { TaskDefinitionFormation } from './cloud-formations/task-definition-formation';
export class AWSJobStack {
private baseStackName: string;
constructor(baseStackName: string) {
this.baseStackName = baseStackName;
}
public async setupCloudFormations(
CF: SDK.CloudFormation,
buildGuid: string,
image: string,
entrypoint: string[],
commands: string,
mountdir: string,
workingdir: string,
secrets: CloudRunnerSecret[],
): Promise<CloudRunnerAWSTaskDef> {
const taskDefStackName = `${this.baseStackName}-${buildGuid}`;
let taskDefCloudFormation = AWSCloudFormationTemplates.readTaskCloudFormationTemplate();
const cpu = CloudRunner.buildParameters.cloudRunnerCpu || '1024';
const memory = CloudRunner.buildParameters.cloudRunnerMemory || '3072';
taskDefCloudFormation = taskDefCloudFormation.replace(
`ContainerCpu:
Default: 1024`,
`ContainerCpu:
Default: ${Number.parseInt(cpu)}`,
);
taskDefCloudFormation = taskDefCloudFormation.replace(
`ContainerMemory:
Default: 2048`,
`ContainerMemory:
Default: ${Number.parseInt(memory)}`,
);
if (CloudRunnerOptions.watchCloudRunnerToEnd) {
taskDefCloudFormation = AWSCloudFormationTemplates.insertAtTemplate(
taskDefCloudFormation,
'# template resources logstream',
TaskDefinitionFormation.streamLogs,
);
}
for (const secret of secrets) {
secret.ParameterKey = `${buildGuid.replace(/[^\dA-Za-z]/g, '')}${secret.ParameterKey.replace(
/[^\dA-Za-z]/g,
'',
)}`;
if (typeof secret.ParameterValue == 'number') {
secret.ParameterValue = `${secret.ParameterValue}`;
}
if (!secret.ParameterValue || secret.ParameterValue === '') {
secrets = secrets.filter((x) => x !== secret);
continue;
}
taskDefCloudFormation = AWSCloudFormationTemplates.insertAtTemplate(
taskDefCloudFormation,
'p1 - input',
AWSCloudFormationTemplates.getParameterTemplate(secret.ParameterKey),
);
taskDefCloudFormation = AWSCloudFormationTemplates.insertAtTemplate(
taskDefCloudFormation,
'# template resources secrets',
AWSCloudFormationTemplates.getSecretTemplate(`${secret.ParameterKey}`),
);
taskDefCloudFormation = AWSCloudFormationTemplates.insertAtTemplate(
taskDefCloudFormation,
'p3 - container def',
AWSCloudFormationTemplates.getSecretDefinitionTemplate(secret.EnvironmentVariable, secret.ParameterKey),
);
}
const secretsMappedToCloudFormationParameters = secrets.map((x) => {
return { ParameterKey: x.ParameterKey.replace(/[^\dA-Za-z]/g, ''), ParameterValue: x.ParameterValue };
});
const logGroupName = `${this.baseStackName}/${taskDefStackName}`;
const parameters = [
{
ParameterKey: 'EnvironmentName',
ParameterValue: this.baseStackName,
},
{
ParameterKey: 'ImageUrl',
ParameterValue: image,
},
{
ParameterKey: 'ServiceName',
ParameterValue: taskDefStackName,
},
{
ParameterKey: 'LogGroupName',
ParameterValue: logGroupName,
},
{
ParameterKey: 'Command',
ParameterValue: 'echo "this template should be overwritten when running a task"',
},
{
ParameterKey: 'EntryPoint',
ParameterValue: entrypoint.join(','),
},
{
ParameterKey: 'WorkingDirectory',
ParameterValue: workingdir,
},
{
ParameterKey: 'EFSMountDirectory',
ParameterValue: mountdir,
},
...secretsMappedToCloudFormationParameters,
];
CloudRunnerLogger.log(
`Starting AWS job with memory: ${CloudRunner.buildParameters.cloudRunnerMemory} cpu: ${CloudRunner.buildParameters.cloudRunnerCpu}`,
);
let previousStackExists = true;
while (previousStackExists) {
previousStackExists = false;
const stacks = await CF.listStacks().promise();
if (!stacks.StackSummaries) {
throw new Error('Faild to get stacks');
}
for (let index = 0; index < stacks.StackSummaries.length; index++) {
const element = stacks.StackSummaries[index];
if (element.StackName === taskDefStackName && element.StackStatus !== 'DELETE_COMPLETE') {
previousStackExists = true;
CloudRunnerLogger.log(`Previous stack still exists: ${JSON.stringify(element)}`);
await new Promise((promise) => setTimeout(promise, 5000));
}
}
}
const createStackInput: SDK.CloudFormation.CreateStackInput = {
StackName: taskDefStackName,
TemplateBody: taskDefCloudFormation,
Capabilities: ['CAPABILITY_IAM'],
Parameters: parameters,
};
try {
CloudRunnerLogger.log(`Creating job aws formation ${taskDefStackName}`);
await CF.createStack(createStackInput).promise();
await CF.waitFor('stackCreateComplete', { StackName: taskDefStackName }).promise();
} catch (error) {
await AWSError.handleStackCreationFailure(error, CF, taskDefStackName);
throw error;
}
const createCleanupStackInput: SDK.CloudFormation.CreateStackInput = {
StackName: `${taskDefStackName}-cleanup`,
TemplateBody: CleanupCronFormation.formation,
Capabilities: ['CAPABILITY_IAM'],
Parameters: [
{
ParameterKey: 'StackName',
ParameterValue: taskDefStackName,
},
{
ParameterKey: 'DeleteStackName',
ParameterValue: `${taskDefStackName}-cleanup`,
},
{
ParameterKey: 'TTL',
ParameterValue: `1080`,
},
{
ParameterKey: 'BUILDGUID',
ParameterValue: CloudRunner.buildParameters.buildGuid,
},
{
ParameterKey: 'EnvironmentName',
ParameterValue: this.baseStackName,
},
],
};
if (CloudRunnerOptions.useCleanupCron) {
try {
CloudRunnerLogger.log(`Creating job cleanup formation`);
CF.createStack(createCleanupStackInput).promise();
// await CF.waitFor('stackCreateComplete', { StackName: createCleanupStackInput.StackName }).promise();
} catch (error) {
await AWSError.handleStackCreationFailure(error, CF, taskDefStackName);
throw error;
}
}
const taskDefResources = (
await CF.describeStackResources({
StackName: taskDefStackName,
}).promise()
).StackResources;
const baseResources = (await CF.describeStackResources({ StackName: this.baseStackName }).promise()).StackResources;
return {
taskDefStackName,
taskDefCloudFormation,
taskDefResources,
baseResources,
};
}
}

View File

@@ -0,0 +1,241 @@
import * as AWS from 'aws-sdk';
import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable';
import * as core from '@actions/core';
import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def';
import * as zlib from 'zlib';
import CloudRunnerLogger from '../../services/cloud-runner-logger';
import { Input } from '../../..';
import CloudRunner from '../../cloud-runner';
import { CloudRunnerCustomHooks } from '../../services/cloud-runner-custom-hooks';
import { FollowLogStreamService } from '../../services/follow-log-stream-service';
import CloudRunnerOptions from '../../cloud-runner-options';
import GitHub from '../../../github';
class AWSTaskRunner {
public static ECS: AWS.ECS;
public static Kinesis: AWS.Kinesis;
private static readonly encodedUnderscore = `$252F`;
static async runTask(
taskDef: CloudRunnerAWSTaskDef,
environment: CloudRunnerEnvironmentVariable[],
commands: string,
): Promise<{ output: string; shouldCleanup: boolean }> {
const cluster = taskDef.baseResources?.find((x) => x.LogicalResourceId === 'ECSCluster')?.PhysicalResourceId || '';
const taskDefinition =
taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'TaskDefinition')?.PhysicalResourceId || '';
const SubnetOne =
taskDef.baseResources?.find((x) => x.LogicalResourceId === 'PublicSubnetOne')?.PhysicalResourceId || '';
const SubnetTwo =
taskDef.baseResources?.find((x) => x.LogicalResourceId === 'PublicSubnetTwo')?.PhysicalResourceId || '';
const ContainerSecurityGroup =
taskDef.baseResources?.find((x) => x.LogicalResourceId === 'ContainerSecurityGroup')?.PhysicalResourceId || '';
const streamName =
taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'KinesisStream')?.PhysicalResourceId || '';
const task = await AWSTaskRunner.ECS.runTask({
cluster,
taskDefinition,
platformVersion: '1.4.0',
overrides: {
containerOverrides: [
{
name: taskDef.taskDefStackName,
environment,
command: ['-c', CloudRunnerCustomHooks.ApplyHooksToCommands(commands, CloudRunner.buildParameters)],
},
],
},
launchType: 'FARGATE',
networkConfiguration: {
awsvpcConfiguration: {
subnets: [SubnetOne, SubnetTwo],
assignPublicIp: 'ENABLED',
securityGroups: [ContainerSecurityGroup],
},
},
}).promise();
const taskArn = task.tasks?.[0].taskArn || '';
CloudRunnerLogger.log('Cloud runner job is starting');
await AWSTaskRunner.waitUntilTaskRunning(taskArn, cluster);
CloudRunnerLogger.log(
`Cloud runner job status is running ${(await AWSTaskRunner.describeTasks(cluster, taskArn))?.lastStatus} Watch:${
CloudRunnerOptions.watchCloudRunnerToEnd
} Async:${CloudRunnerOptions.asyncCloudRunner}`,
);
if (!CloudRunnerOptions.watchCloudRunnerToEnd) {
const shouldCleanup: boolean = false;
const output: string = '';
CloudRunnerLogger.log(`Watch Cloud Runner To End: false`);
return { output, shouldCleanup };
}
CloudRunnerLogger.log(`Streaming...`);
const { output, shouldCleanup } = await this.streamLogsUntilTaskStops(cluster, taskArn, streamName);
await new Promise((resolve) => resolve(5000));
const taskData = await AWSTaskRunner.describeTasks(cluster, taskArn);
const containerState = taskData.containers?.[0];
const exitCode = containerState?.exitCode || undefined;
CloudRunnerLogger.log(`Container State: ${JSON.stringify(containerState, undefined, 4)}`);
const wasSuccessful = exitCode === 0 || (exitCode === undefined && taskData.lastStatus === 'RUNNING');
if (wasSuccessful) {
CloudRunnerLogger.log(`Cloud runner job has finished successfully`);
return { output, shouldCleanup };
} else {
if (taskData.stoppedReason === 'Essential container in task exited' && exitCode === 1) {
throw new Error('Container exited with code 1');
}
const message = `Cloud runner job exit code ${exitCode}`;
taskData.overrides = undefined;
taskData.attachments = undefined;
CloudRunnerLogger.log(`${message} ${JSON.stringify(taskData, undefined, 4)}`);
throw new Error(message);
}
}
private static async waitUntilTaskRunning(taskArn: string, cluster: string) {
try {
await AWSTaskRunner.ECS.waitFor('tasksRunning', { tasks: [taskArn], cluster }).promise();
} catch (error_) {
const error = error_ as Error;
await new Promise((resolve) => setTimeout(resolve, 3000));
CloudRunnerLogger.log(
`Cloud runner job has ended ${
(await AWSTaskRunner.describeTasks(cluster, taskArn)).containers?.[0].lastStatus
}`,
);
core.setFailed(error);
core.error(error);
}
}
static async describeTasks(clusterName: string, taskArn: string) {
const tasks = await AWSTaskRunner.ECS.describeTasks({
cluster: clusterName,
tasks: [taskArn],
}).promise();
if (tasks.tasks?.[0]) {
return tasks.tasks?.[0];
} else {
throw new Error('No task found');
}
}
static async streamLogsUntilTaskStops(clusterName: string, taskArn: string, kinesisStreamName: string) {
await new Promise((resolve) => setTimeout(resolve, 3000));
CloudRunnerLogger.log(`Streaming...`);
const stream = await AWSTaskRunner.getLogStream(kinesisStreamName);
let iterator = await AWSTaskRunner.getLogIterator(stream);
const logBaseUrl = `https://${Input.region}.console.aws.amazon.com/cloudwatch/home?region=${Input.region}#logsV2:log-groups/log-group/${CloudRunner.buildParameters.awsBaseStackName}${AWSTaskRunner.encodedUnderscore}${CloudRunner.buildParameters.awsBaseStackName}-${CloudRunner.buildParameters.buildGuid}`;
CloudRunnerLogger.log(`You view the log stream on AWS Cloud Watch: ${logBaseUrl}`);
await GitHub.updateGitHubCheck(`You view the log stream on AWS Cloud Watch: ${logBaseUrl}`, ``);
let shouldReadLogs = true;
let shouldCleanup = true;
let timestamp: number = 0;
let output = '';
while (shouldReadLogs) {
await new Promise((resolve) => setTimeout(resolve, 1500));
const taskData = await AWSTaskRunner.describeTasks(clusterName, taskArn);
({ timestamp, shouldReadLogs } = AWSTaskRunner.checkStreamingShouldContinue(taskData, timestamp, shouldReadLogs));
({ iterator, shouldReadLogs, output, shouldCleanup } = await AWSTaskRunner.handleLogStreamIteration(
iterator,
shouldReadLogs,
output,
shouldCleanup,
));
}
return { output, shouldCleanup };
}
private static async handleLogStreamIteration(
iterator: string,
shouldReadLogs: boolean,
output: string,
shouldCleanup: boolean,
) {
const records = await AWSTaskRunner.Kinesis.getRecords({
ShardIterator: iterator,
}).promise();
iterator = records.NextShardIterator || '';
({ shouldReadLogs, output, shouldCleanup } = AWSTaskRunner.logRecords(
records,
iterator,
shouldReadLogs,
output,
shouldCleanup,
));
return { iterator, shouldReadLogs, output, shouldCleanup };
}
private static checkStreamingShouldContinue(taskData: AWS.ECS.Task, timestamp: number, shouldReadLogs: boolean) {
if (taskData?.lastStatus === 'UNKNOWN') {
CloudRunnerLogger.log('## Cloud runner job unknwon');
}
if (taskData?.lastStatus !== 'RUNNING') {
if (timestamp === 0) {
CloudRunnerLogger.log('## Cloud runner job stopped, streaming end of logs');
timestamp = Date.now();
}
if (timestamp !== 0 && Date.now() - timestamp > 30000) {
CloudRunnerLogger.log('## Cloud runner status is not RUNNING for 30 seconds, last query for logs');
shouldReadLogs = false;
}
CloudRunnerLogger.log(`## Status of job: ${taskData.lastStatus}`);
}
return { timestamp, shouldReadLogs };
}
private static logRecords(
records,
iterator: string,
shouldReadLogs: boolean,
output: string,
shouldCleanup: boolean,
) {
if (records.Records.length > 0 && iterator) {
for (let index = 0; index < records.Records.length; index++) {
const json = JSON.parse(
zlib.gunzipSync(Buffer.from(records.Records[index].Data as string, 'base64')).toString('utf8'),
);
if (json.messageType === 'DATA_MESSAGE') {
for (let logEventsIndex = 0; logEventsIndex < json.logEvents.length; logEventsIndex++) {
const message = json.logEvents[logEventsIndex].message;
({ shouldReadLogs, shouldCleanup, output } = FollowLogStreamService.handleIteration(
message,
shouldReadLogs,
shouldCleanup,
output,
));
}
}
}
}
return { shouldReadLogs, output, shouldCleanup };
}
private static async getLogStream(kinesisStreamName: string) {
return await AWSTaskRunner.Kinesis.describeStream({
StreamName: kinesisStreamName,
}).promise();
}
private static async getLogIterator(stream) {
return (
(
await AWSTaskRunner.Kinesis.getShardIterator({
ShardIteratorType: 'TRIM_HORIZON',
StreamName: stream.StreamDescription.StreamName,
ShardId: stream.StreamDescription.Shards[0].ShardId,
}).promise()
).ShardIterator || ''
);
}
}
export default AWSTaskRunner;

View File

@@ -0,0 +1,397 @@
export class BaseStackFormation {
public static readonly baseStackDecription = `Game-CI base stack`;
public static readonly formation: string = `AWSTemplateFormatVersion: '2010-09-09'
Description: ${BaseStackFormation.baseStackDecription}
Parameters:
EnvironmentName:
Type: String
Default: development
Description: 'Your deployment environment: DEV, QA , PROD'
Version:
Type: String
Description: 'hash of template'
# ContainerPort:
# Type: Number
# Default: 80
# Description: What port number the application inside the docker container is binding to
Mappings:
# Hard values for the subnet masks. These masks define
# the range of internal IP addresses that can be assigned.
# The VPC can have all IP's from 10.0.0.0 to 10.0.255.255
# There are four subnets which cover the ranges:
#
# 10.0.0.0 - 10.0.0.255
# 10.0.1.0 - 10.0.1.255
# 10.0.2.0 - 10.0.2.255
# 10.0.3.0 - 10.0.3.255
SubnetConfig:
VPC:
CIDR: '10.0.0.0/16'
PublicOne:
CIDR: '10.0.0.0/24'
PublicTwo:
CIDR: '10.0.1.0/24'
Resources:
# VPC in which containers will be networked.
# It has two public subnets, and two private subnets.
# We distribute the subnets across the first two available subnets
# for the region, for high availability.
VPC:
Type: AWS::EC2::VPC
Properties:
EnableDnsSupport: true
EnableDnsHostnames: true
CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR']
MainBucket:
Type: "AWS::S3::Bucket"
Properties:
BucketName: !Ref EnvironmentName
EFSServerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: 'efs-server-endpoints'
GroupDescription: Which client ip addrs are allowed to access EFS server
VpcId: !Ref 'VPC'
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 2049
ToPort: 2049
SourceSecurityGroupId: !Ref ContainerSecurityGroup
#CidrIp: !FindInMap ['SubnetConfig', 'VPC', 'CIDR']
# A security group for the containers we will run in Fargate.
# Rules are added to this security group based on what ingress you
# add for the cluster.
ContainerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: 'task security group'
GroupDescription: Access to the Fargate containers
VpcId: !Ref 'VPC'
# SecurityGroupIngress:
# - IpProtocol: tcp
# FromPort: !Ref ContainerPort
# ToPort: !Ref ContainerPort
# CidrIp: 0.0.0.0/0
SecurityGroupEgress:
- IpProtocol: -1
FromPort: 2049
ToPort: 2049
CidrIp: '0.0.0.0/0'
# Two public subnets, where containers can have public IP addresses
PublicSubnetOne:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select
- 0
- Fn::GetAZs: !Ref 'AWS::Region'
VpcId: !Ref 'VPC'
CidrBlock: !FindInMap ['SubnetConfig', 'PublicOne', 'CIDR']
# MapPublicIpOnLaunch: true
PublicSubnetTwo:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select
- 1
- Fn::GetAZs: !Ref 'AWS::Region'
VpcId: !Ref 'VPC'
CidrBlock: !FindInMap ['SubnetConfig', 'PublicTwo', 'CIDR']
# MapPublicIpOnLaunch: true
# Setup networking resources for the public subnets. Containers
# in the public subnets have public IP addresses and the routing table
# sends network traffic via the internet gateway.
InternetGateway:
Type: AWS::EC2::InternetGateway
GatewayAttachement:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref 'VPC'
InternetGatewayId: !Ref 'InternetGateway'
# Attaching a Internet Gateway to route table makes it public.
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref 'VPC'
PublicRoute:
Type: AWS::EC2::Route
DependsOn: GatewayAttachement
Properties:
RouteTableId: !Ref 'PublicRouteTable'
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref 'InternetGateway'
# Attaching a public route table makes a subnet public.
PublicSubnetOneRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetOne
RouteTableId: !Ref PublicRouteTable
PublicSubnetTwoRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetTwo
RouteTableId: !Ref PublicRouteTable
# ECS Resources
ECSCluster:
Type: AWS::ECS::Cluster
# A role used to allow AWS Autoscaling to inspect stats and adjust scaleable targets
# on your AWS account
AutoscalingRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [application-autoscaling.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies:
- PolicyName: service-autoscaling
PolicyDocument:
Statement:
- Effect: Allow
Action:
- 'application-autoscaling:*'
- 'cloudwatch:DescribeAlarms'
- 'cloudwatch:PutMetricAlarm'
- 'ecs:DescribeServices'
- 'ecs:UpdateService'
Resource: '*'
# This is an IAM role which authorizes ECS to manage resources on your
# account on your behalf, such as updating your load balancer with the
# details of where your containers are, so that traffic can reach your
# containers.
ECSRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ecs.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies:
- PolicyName: ecs-service
PolicyDocument:
Statement:
- Effect: Allow
Action:
# Rules which allow ECS to attach network interfaces to instances
# on your behalf in order for awsvpc networking mode to work right
- 'ec2:AttachNetworkInterface'
- 'ec2:CreateNetworkInterface'
- 'ec2:CreateNetworkInterfacePermission'
- 'ec2:DeleteNetworkInterface'
- 'ec2:DeleteNetworkInterfacePermission'
- 'ec2:Describe*'
- 'ec2:DetachNetworkInterface'
# Rules which allow ECS to update load balancers on your behalf
# with the information sabout how to send traffic to your containers
- 'elasticloadbalancing:DeregisterInstancesFromLoadBalancer'
- 'elasticloadbalancing:DeregisterTargets'
- 'elasticloadbalancing:Describe*'
- 'elasticloadbalancing:RegisterInstancesWithLoadBalancer'
- 'elasticloadbalancing:RegisterTargets'
Resource: '*'
# This is a role which is used by the ECS tasks themselves.
ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ecs-tasks.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies:
- PolicyName: AmazonECSTaskExecutionRolePolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
# Allow the use of secret manager
- 'secretsmanager:GetSecretValue'
- 'kms:Decrypt'
# Allow the ECS Tasks to download images from ECR
- 'ecr:GetAuthorizationToken'
- 'ecr:BatchCheckLayerAvailability'
- 'ecr:GetDownloadUrlForLayer'
- 'ecr:BatchGetImage'
# Allow the ECS tasks to upload logs to CloudWatch
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: '*'
DeleteCFNLambdaExecutionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Principal:
Service: ['lambda.amazonaws.com']
Action: 'sts:AssumeRole'
Path: '/'
Policies:
- PolicyName: DeleteCFNLambdaExecutionRole
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Action:
- 'logs:CreateLogGroup'
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: 'arn:aws:logs:*:*:*'
- Effect: 'Allow'
Action:
- 'cloudformation:DeleteStack'
- 'kinesis:DeleteStream'
- 'secretsmanager:DeleteSecret'
- 'kinesis:DescribeStreamSummary'
- 'logs:DeleteLogGroup'
- 'logs:DeleteSubscriptionFilter'
- 'ecs:DeregisterTaskDefinition'
- 'lambda:DeleteFunction'
- 'lambda:InvokeFunction'
- 'events:RemoveTargets'
- 'events:DeleteRule'
- 'lambda:RemovePermission'
Resource: '*'
### cloud watch to kinesis role
CloudWatchIAMRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [logs.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies:
- PolicyName: service-autoscaling
PolicyDocument:
Statement:
- Effect: Allow
Action:
- 'kinesis:PutRecord'
Resource: '*'
#####################EFS#####################
EfsFileStorage:
Type: 'AWS::EFS::FileSystem'
Properties:
BackupPolicy:
Status: ENABLED
PerformanceMode: maxIO
Encrypted: false
FileSystemPolicy:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Action:
- 'elasticfilesystem:ClientMount'
- 'elasticfilesystem:ClientWrite'
- 'elasticfilesystem:ClientRootAccess'
Principal:
AWS: '*'
MountTargetResource1:
Type: AWS::EFS::MountTarget
Properties:
FileSystemId: !Ref EfsFileStorage
SubnetId: !Ref PublicSubnetOne
SecurityGroups:
- !Ref EFSServerSecurityGroup
MountTargetResource2:
Type: AWS::EFS::MountTarget
Properties:
FileSystemId: !Ref EfsFileStorage
SubnetId: !Ref PublicSubnetTwo
SecurityGroups:
- !Ref EFSServerSecurityGroup
Outputs:
EfsFileStorageId:
Description: 'The connection endpoint for the database.'
Value: !Ref EfsFileStorage
Export:
Name: !Sub ${'${EnvironmentName}'}:EfsFileStorageId
ClusterName:
Description: The name of the ECS cluster
Value: !Ref 'ECSCluster'
Export:
Name: !Sub${' ${EnvironmentName}'}:ClusterName
AutoscalingRole:
Description: The ARN of the role used for autoscaling
Value: !GetAtt 'AutoscalingRole.Arn'
Export:
Name: !Sub ${'${EnvironmentName}'}:AutoscalingRole
ECSRole:
Description: The ARN of the ECS role
Value: !GetAtt 'ECSRole.Arn'
Export:
Name: !Sub ${'${EnvironmentName}'}:ECSRole
ECSTaskExecutionRole:
Description: The ARN of the ECS role tsk execution role
Value: !GetAtt 'ECSTaskExecutionRole.Arn'
Export:
Name: !Sub ${'${EnvironmentName}'}:ECSTaskExecutionRole
DeleteCFNLambdaExecutionRole:
Description: Lambda execution role for cleaning up cloud formations
Value: !GetAtt 'DeleteCFNLambdaExecutionRole.Arn'
Export:
Name: !Sub ${'${EnvironmentName}'}:DeleteCFNLambdaExecutionRole
CloudWatchIAMRole:
Description: The ARN of the CloudWatch role for subscription filter
Value: !GetAtt 'CloudWatchIAMRole.Arn'
Export:
Name: !Sub ${'${EnvironmentName}'}:CloudWatchIAMRole
VpcId:
Description: The ID of the VPC that this stack is deployed in
Value: !Ref 'VPC'
Export:
Name: !Sub ${'${EnvironmentName}'}:VpcId
PublicSubnetOne:
Description: Public subnet one
Value: !Ref 'PublicSubnetOne'
Export:
Name: !Sub ${'${EnvironmentName}'}:PublicSubnetOne
PublicSubnetTwo:
Description: Public subnet two
Value: !Ref 'PublicSubnetTwo'
Export:
Name: !Sub ${'${EnvironmentName}'}:PublicSubnetTwo
ContainerSecurityGroup:
Description: A security group used to allow Fargate containers to receive traffic
Value: !Ref 'ContainerSecurityGroup'
Export:
Name: !Sub ${'${EnvironmentName}'}:ContainerSecurityGroup
`;
}

View File

@@ -0,0 +1,146 @@
export class CleanupCronFormation {
public static readonly formation: string = `AWSTemplateFormatVersion: '2010-09-09'
Description: Schedule automatic deletion of CloudFormation stacks
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Input configuration
Parameters:
- StackName
- TTL
ParameterLabels:
StackName:
default: Stack name
TTL:
default: Time-to-live
Parameters:
EnvironmentName:
Type: String
Default: development
Description: 'Your deployment environment: DEV, QA , PROD'
BUILDGUID:
Type: String
Default: ''
StackName:
Type: String
Description: Stack name that will be deleted.
DeleteStackName:
Type: String
Description: Stack name that will be deleted.
TTL:
Type: Number
Description: Time-to-live in minutes for the stack.
Resources:
DeleteCFNLambda:
Type: "AWS::Lambda::Function"
Properties:
FunctionName: !Join [ "", [ 'DeleteCFNLambda', !Ref BUILDGUID ] ]
Code:
ZipFile: |
import boto3
import os
import json
stack_name = os.environ['stackName']
delete_stack_name = os.environ['deleteStackName']
def delete_cfn(stack_name):
try:
cfn = boto3.resource('cloudformation')
stack = cfn.Stack(stack_name)
stack.delete()
return "SUCCESS"
except:
return "ERROR"
def handler(event, context):
print("Received event:")
print(json.dumps(event))
result = delete_cfn(stack_name)
delete_cfn(delete_stack_name)
return result
Environment:
Variables:
stackName: !Ref 'StackName'
deleteStackName: !Ref 'DeleteStackName'
Handler: "index.handler"
Runtime: "python3.9"
Timeout: "5"
Role:
'Fn::ImportValue': !Sub '\${EnvironmentName}:DeleteCFNLambdaExecutionRole'
DeleteStackEventRule:
DependsOn:
- DeleteCFNLambda
- GenerateCronExpression
Type: "AWS::Events::Rule"
Properties:
Name: !Join [ "", [ 'DeleteStackEventRule', !Ref BUILDGUID ] ]
Description: Delete stack event
ScheduleExpression: !GetAtt GenerateCronExpression.cron_exp
State: "ENABLED"
Targets:
-
Arn: !GetAtt DeleteCFNLambda.Arn
Id: 'DeleteCFNLambda'
PermissionForDeleteCFNLambda:
Type: "AWS::Lambda::Permission"
DependsOn:
- DeleteStackEventRule
Properties:
FunctionName: !Join [ "", [ 'DeleteCFNLambda', !Ref BUILDGUID ] ]
Action: "lambda:InvokeFunction"
Principal: "events.amazonaws.com"
SourceArn: !GetAtt DeleteStackEventRule.Arn
GenerateCronExpLambda:
Type: "AWS::Lambda::Function"
Properties:
FunctionName: !Join [ "", [ 'GenerateCronExpressionLambda', !Ref BUILDGUID ] ]
Code:
ZipFile: |
from datetime import datetime, timedelta
import os
import logging
import json
import cfnresponse
def deletion_time(ttl):
delete_at_time = datetime.now() + timedelta(minutes=int(ttl))
hh = delete_at_time.hour
mm = delete_at_time.minute
yyyy = delete_at_time.year
month = delete_at_time.month
dd = delete_at_time.day
# minutes hours day month day-of-week year
cron_exp = "cron({} {} {} {} ? {})".format(mm, hh, dd, month, yyyy)
return cron_exp
def handler(event, context):
print('Received event: %s' % json.dumps(event))
status = cfnresponse.SUCCESS
try:
if event['RequestType'] == 'Delete':
cfnresponse.send(event, context, status, {})
else:
ttl = event['ResourceProperties']['ttl']
responseData = {}
responseData['cron_exp'] = deletion_time(ttl)
cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)
except Exception as e:
logging.error('Exception: %s' % e, exc_info=True)
status = cfnresponse.FAILED
cfnresponse.send(event, context, status, {}, None)
Handler: "index.handler"
Runtime: "python3.9"
Timeout: "5"
Role:
'Fn::ImportValue': !Sub '\${EnvironmentName}:DeleteCFNLambdaExecutionRole'
GenerateCronExpression:
Type: "Custom::GenerateCronExpression"
Version: "1.0"
Properties:
Name: !Join [ "", [ 'GenerateCronExpression', !Ref BUILDGUID ] ]
ServiceToken: !GetAtt GenerateCronExpLambda.Arn
ttl: !Ref 'TTL'
`;
}

View File

@@ -0,0 +1,165 @@
export class TaskDefinitionFormation {
public static readonly description: string = `Game CI Cloud Runner Task Stack`;
public static readonly formation: string = `AWSTemplateFormatVersion: 2010-09-09
Description: ${TaskDefinitionFormation.description}
Parameters:
EnvironmentName:
Type: String
Default: development
Description: 'Your deployment environment: DEV, QA , PROD'
ServiceName:
Type: String
Default: example
Description: A name for the service
LogGroupName:
Type: String
Default: example
Description: Name to use for the log group created for this task
ImageUrl:
Type: String
Default: nginx
Description: >-
The url of a docker image that contains the application process that will
handle the traffic for this service
ContainerPort:
Type: Number
Default: 80
Description: What port number the application inside the docker container is binding to
ContainerCpu:
Default: 1024
Type: Number
Description: How much CPU to give the container. 1024 is 1 CPU
ContainerMemory:
Default: 2048
Type: Number
Description: How much memory in megabytes to give the container
BUILDGUID:
Type: String
Default: ''
Command:
Type: String
Default: 'ls'
EntryPoint:
Type: String
Default: '/bin/sh'
WorkingDirectory:
Type: String
Default: '/efsdata/'
Role:
Type: String
Default: ''
Description: >-
(Optional) An IAM role to give the service's containers if the code within
needs to access other AWS resources like S3 buckets, DynamoDB tables, etc
EFSMountDirectory:
Type: String
Default: '/efsdata'
# template secrets p1 - input
Mappings:
SubnetConfig:
VPC:
CIDR: 10.0.0.0/16
PublicOne:
CIDR: 10.0.0.0/24
PublicTwo:
CIDR: 10.0.1.0/24
Conditions:
HasCustomRole: !Not
- !Equals
- Ref: Role
- ''
Resources:
LogGroup:
Type: 'AWS::Logs::LogGroup'
Properties:
LogGroupName: !Ref LogGroupName
Metadata:
'AWS::CloudFormation::Designer':
id: aece53ae-b82d-4267-bc16-ed964b05db27
# template resources secrets
# template resources logstream
TaskDefinition:
Type: 'AWS::ECS::TaskDefinition'
Properties:
Family: !Ref ServiceName
Cpu: !Ref ContainerCpu
Memory: !Ref ContainerMemory
NetworkMode: awsvpc
Volumes:
- Name: efs-data
EFSVolumeConfiguration:
FilesystemId:
'Fn::ImportValue': !Sub '${'${EnvironmentName}'}:EfsFileStorageId'
TransitEncryption: ENABLED
RequiresCompatibilities:
- FARGATE
ExecutionRoleArn:
'Fn::ImportValue': !Sub '${'${EnvironmentName}'}:ECSTaskExecutionRole'
TaskRoleArn:
'Fn::If':
- HasCustomRole
- !Ref Role
- !Ref 'AWS::NoValue'
ContainerDefinitions:
- Name: !Ref ServiceName
Cpu: !Ref ContainerCpu
Memory: !Ref ContainerMemory
Image: !Ref ImageUrl
EntryPoint:
Fn::Split:
- ','
- !Ref EntryPoint
Command:
Fn::Split:
- ','
- !Ref Command
WorkingDirectory: !Ref WorkingDirectory
Environment:
- Name: ALLOW_EMPTY_PASSWORD
Value: 'yes'
# template - env vars
MountPoints:
- SourceVolume: efs-data
ContainerPath: !Ref EFSMountDirectory
ReadOnly: false
Secrets:
# template secrets p3 - container def
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroupName
awslogs-region: !Ref 'AWS::Region'
awslogs-stream-prefix: !Ref ServiceName
DependsOn:
- LogGroup
`;
public static streamLogs = `
SubscriptionFilter:
Type: 'AWS::Logs::SubscriptionFilter'
Properties:
FilterPattern: ''
RoleArn:
'Fn::ImportValue': !Sub '${'${EnvironmentName}'}:CloudWatchIAMRole'
LogGroupName: !Ref LogGroupName
DestinationArn:
'Fn::GetAtt':
- KinesisStream
- Arn
Metadata:
'AWS::CloudFormation::Designer':
id: 7f809e91-9e5d-4678-98c1-c5085956c480
DependsOn:
- LogGroup
- KinesisStream
KinesisStream:
Type: 'AWS::Kinesis::Stream'
Properties:
Name: !Ref ServiceName
ShardCount: 1
Metadata:
'AWS::CloudFormation::Designer':
id: c6f18447-b879-4696-8873-f981b2cedd2b
`;
}

View File

@@ -0,0 +1,9 @@
import * as AWS from 'aws-sdk';
class CloudRunnerAWSTaskDef {
public taskDefStackName!: string;
public taskDefCloudFormation!: string;
public taskDefResources: AWS.CloudFormation.StackResources | undefined;
public baseResources: AWS.CloudFormation.StackResources | undefined;
}
export default CloudRunnerAWSTaskDef;

View File

@@ -0,0 +1,150 @@
import * as SDK from 'aws-sdk';
import CloudRunnerSecret from '../../services/cloud-runner-secret';
import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable';
import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def';
import AwsTaskRunner from './aws-task-runner';
import { ProviderInterface } from '../provider-interface';
import BuildParameters from '../../../build-parameters';
import CloudRunnerLogger from '../../services/cloud-runner-logger';
import { AWSJobStack as AwsJobStack } from './aws-job-stack';
import { AWSBaseStack as AwsBaseStack } from './aws-base-stack';
import { Input } from '../../..';
import { GarbageCollectionService } from './services/garbage-collection-service';
import { ProviderResource } from '../provider-resource';
import { ProviderWorkflow } from '../provider-workflow';
import { TaskService } from './services/task-service';
import CloudRunnerOptions from '../../cloud-runner-options';
class AWSBuildEnvironment implements ProviderInterface {
private baseStackName: string;
constructor(buildParameters: BuildParameters) {
this.baseStackName = buildParameters.awsBaseStackName;
}
async listResources(): Promise<ProviderResource[]> {
await TaskService.getCloudFormationJobStacks();
await TaskService.getLogGroups();
await TaskService.getTasks();
return [];
}
listWorkflow(): Promise<ProviderWorkflow[]> {
throw new Error('Method not implemented.');
}
async watchWorkflow(): Promise<string> {
return await TaskService.watch();
}
async listOtherResources(): Promise<string> {
await TaskService.getLogGroups();
return '';
}
async garbageCollect(
filter: string,
previewOnly: boolean,
// eslint-disable-next-line no-unused-vars
olderThan: Number,
// eslint-disable-next-line no-unused-vars
fullCache: boolean,
// eslint-disable-next-line no-unused-vars
baseDependencies: boolean,
): Promise<string> {
await GarbageCollectionService.cleanup(!previewOnly);
return ``;
}
async cleanupWorkflow(
// eslint-disable-next-line no-unused-vars
buildGuid: string,
// eslint-disable-next-line no-unused-vars
buildParameters: BuildParameters,
// eslint-disable-next-line no-unused-vars
branchName: string,
// eslint-disable-next-line no-unused-vars
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
) {}
async setupWorkflow(
// eslint-disable-next-line no-unused-vars
buildGuid: string,
// eslint-disable-next-line no-unused-vars
buildParameters: BuildParameters,
// eslint-disable-next-line no-unused-vars
branchName: string,
// eslint-disable-next-line no-unused-vars
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
) {}
async runTaskInWorkflow(
buildGuid: string,
image: string,
commands: string,
mountdir: string,
workingdir: string,
environment: CloudRunnerEnvironmentVariable[],
secrets: CloudRunnerSecret[],
): Promise<string> {
process.env.AWS_REGION = Input.region;
const ECS = new SDK.ECS();
const CF = new SDK.CloudFormation();
AwsTaskRunner.ECS = ECS;
AwsTaskRunner.Kinesis = new SDK.Kinesis();
CloudRunnerLogger.log(`AWS Region: ${CF.config.region}`);
const entrypoint = ['/bin/sh'];
const startTimeMs = Date.now();
await new AwsBaseStack(this.baseStackName).setupBaseStack(CF);
const taskDef = await new AwsJobStack(this.baseStackName).setupCloudFormations(
CF,
buildGuid,
image,
entrypoint,
commands,
mountdir,
workingdir,
secrets,
);
let postRunTaskTimeMs;
try {
const postSetupStacksTimeMs = Date.now();
CloudRunnerLogger.log(`Setup job time: ${Math.floor((postSetupStacksTimeMs - startTimeMs) / 1000)}s`);
const { output, shouldCleanup } = await AwsTaskRunner.runTask(taskDef, environment, commands);
postRunTaskTimeMs = Date.now();
CloudRunnerLogger.log(`Run job time: ${Math.floor((postRunTaskTimeMs - postSetupStacksTimeMs) / 1000)}s`);
if (shouldCleanup) {
await this.cleanupResources(CF, taskDef);
}
const postCleanupTimeMs = Date.now();
if (postRunTaskTimeMs !== undefined)
CloudRunnerLogger.log(`Cleanup job time: ${Math.floor((postCleanupTimeMs - postRunTaskTimeMs) / 1000)}s`);
return output;
} catch (error) {
CloudRunnerLogger.log(`error running task ${error}`);
await this.cleanupResources(CF, taskDef);
throw error;
}
}
async cleanupResources(CF: SDK.CloudFormation, taskDef: CloudRunnerAWSTaskDef) {
CloudRunnerLogger.log('Cleanup starting');
await CF.deleteStack({
StackName: taskDef.taskDefStackName,
}).promise();
if (CloudRunnerOptions.useCleanupCron) {
await CF.deleteStack({
StackName: `${taskDef.taskDefStackName}-cleanup`,
}).promise();
}
await CF.waitFor('stackDeleteComplete', {
StackName: taskDef.taskDefStackName,
}).promise();
CloudRunnerLogger.log(`Deleted Stack: ${taskDef.taskDefStackName}`);
CloudRunnerLogger.log('Cleanup complete');
}
}
export default AWSBuildEnvironment;

View File

@@ -0,0 +1,66 @@
import AWS from 'aws-sdk';
import Input from '../../../../input';
import CloudRunnerLogger from '../../../services/cloud-runner-logger';
import { TaskService } from './task-service';
export class GarbageCollectionService {
static isOlderThan1day(date: any) {
const ageDate = new Date(date.getTime() - Date.now());
return ageDate.getDay() > 0;
}
public static async cleanup(deleteResources = false, OneDayOlderOnly: boolean = false) {
process.env.AWS_REGION = Input.region;
const CF = new AWS.CloudFormation();
const ecs = new AWS.ECS();
const cwl = new AWS.CloudWatchLogs();
const taskDefinitionsInUse = new Array();
const tasks = await TaskService.getTasks();
for (const task of tasks) {
const { taskElement, element } = task;
taskDefinitionsInUse.push(taskElement.taskDefinitionArn);
if (deleteResources && (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(taskElement.CreatedAt))) {
CloudRunnerLogger.log(`Stopping task ${taskElement.containers?.[0].name}`);
await ecs.stopTask({ task: taskElement.taskArn || '', cluster: element }).promise();
}
}
const jobStacks = await TaskService.getCloudFormationJobStacks();
for (const element of jobStacks) {
if (
(await CF.describeStackResources({ StackName: element.StackName }).promise()).StackResources?.some(
(x) => x.ResourceType === 'AWS::ECS::TaskDefinition' && taskDefinitionsInUse.includes(x.PhysicalResourceId),
)
) {
CloudRunnerLogger.log(`Skipping ${element.StackName} - active task was running not deleting`);
return;
}
if (deleteResources && (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(element.CreationTime))) {
if (element.StackName === 'game-ci' || element.TemplateDescription === 'Game-CI base stack') {
CloudRunnerLogger.log(`Skipping ${element.StackName} ignore list`);
return;
}
CloudRunnerLogger.log(`Deleting ${element.logGroupName}`);
const deleteStackInput: AWS.CloudFormation.DeleteStackInput = { StackName: element.StackName };
await CF.deleteStack(deleteStackInput).promise();
}
}
const logGroups = await TaskService.getLogGroups();
for (const element of logGroups) {
if (
deleteResources &&
(!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(new Date(element.createdAt)))
) {
CloudRunnerLogger.log(`Deleting ${element.logGroupName}`);
await cwl.deleteLogGroup({ logGroupName: element.logGroupName || '' }).promise();
}
}
const locks = await TaskService.getLocks();
for (const element of locks) {
CloudRunnerLogger.log(`Lock: ${element.Key}`);
}
}
}

View File

@@ -0,0 +1,168 @@
import AWS from 'aws-sdk';
import Input from '../../../../input';
import CloudRunnerLogger from '../../../services/cloud-runner-logger';
import { BaseStackFormation } from '../cloud-formations/base-stack-formation';
import AwsTaskRunner from '../aws-task-runner';
import { ListObjectsRequest } from 'aws-sdk/clients/s3';
import CloudRunner from '../../../cloud-runner';
export class TaskService {
static async watch() {
// eslint-disable-next-line no-unused-vars
const { output, shouldCleanup } = await AwsTaskRunner.streamLogsUntilTaskStops(
process.env.cluster || ``,
process.env.taskArn || ``,
process.env.streamName || ``,
);
return output;
}
public static async getCloudFormationJobStacks() {
const result: any[] = [];
CloudRunnerLogger.log(``);
CloudRunnerLogger.log(`List Cloud Formation Stacks`);
process.env.AWS_REGION = Input.region;
const CF = new AWS.CloudFormation();
const stacks =
(await CF.listStacks().promise()).StackSummaries?.filter(
(_x) =>
_x.StackStatus !== 'DELETE_COMPLETE' && _x.TemplateDescription !== BaseStackFormation.baseStackDecription,
) || [];
CloudRunnerLogger.log(``);
CloudRunnerLogger.log(`Cloud Formation Stacks ${stacks.length}`);
for (const element of stacks) {
const ageDate: Date = new Date(Date.now() - element.CreationTime.getTime());
CloudRunnerLogger.log(
`Task Stack ${element.StackName} - Age D${Math.floor(
ageDate.getHours() / 24,
)} H${ageDate.getHours()} M${ageDate.getMinutes()}`,
);
result.push(element);
}
const baseStacks =
(await CF.listStacks().promise()).StackSummaries?.filter(
(_x) =>
_x.StackStatus !== 'DELETE_COMPLETE' && _x.TemplateDescription === BaseStackFormation.baseStackDecription,
) || [];
CloudRunnerLogger.log(``);
CloudRunnerLogger.log(`Base Stacks ${baseStacks.length}`);
for (const element of baseStacks) {
const ageDate: Date = new Date(Date.now() - element.CreationTime.getTime());
CloudRunnerLogger.log(
`Task Stack ${element.StackName} - Age D${Math.floor(
ageDate.getHours() / 24,
)} H${ageDate.getHours()} M${ageDate.getMinutes()}`,
);
result.push(element);
}
CloudRunnerLogger.log(``);
return result;
}
public static async getTasks() {
const result: any[] = [];
CloudRunnerLogger.log(``);
CloudRunnerLogger.log(`List Tasks`);
process.env.AWS_REGION = Input.region;
const ecs = new AWS.ECS();
const clusters = (await ecs.listClusters().promise()).clusterArns || [];
CloudRunnerLogger.log(`Task Clusters ${clusters.length}`);
for (const element of clusters) {
const input: AWS.ECS.ListTasksRequest = {
cluster: element,
};
const list = (await ecs.listTasks(input).promise()).taskArns || [];
if (list.length > 0) {
const describeInput: AWS.ECS.DescribeTasksRequest = { tasks: list, cluster: element };
const describeList = (await ecs.describeTasks(describeInput).promise()).tasks || [];
if (describeList.length === 0) {
CloudRunnerLogger.log(`No Tasks`);
continue;
}
CloudRunnerLogger.log(`Tasks ${describeList.length}`);
for (const taskElement of describeList) {
if (taskElement === undefined) {
continue;
}
taskElement.overrides = {};
taskElement.attachments = [];
if (taskElement.createdAt === undefined) {
CloudRunnerLogger.log(`Skipping ${taskElement.taskDefinitionArn} no createdAt date`);
continue;
}
result.push({ taskElement, element });
}
}
}
CloudRunnerLogger.log(``);
return result;
}
public static async awsDescribeJob(job: string) {
process.env.AWS_REGION = Input.region;
const CF = new AWS.CloudFormation();
const stack = (await CF.listStacks().promise()).StackSummaries?.find((_x) => _x.StackName === job) || undefined;
const stackInfo = (await CF.describeStackResources({ StackName: job }).promise()) || undefined;
const stackInfo2 = (await CF.describeStacks({ StackName: job }).promise()) || undefined;
if (stack === undefined) {
throw new Error('stack not defined');
}
const ageDate: Date = new Date(Date.now() - stack.CreationTime.getTime());
const message = `
Task Stack ${stack.StackName}
Age D${Math.floor(ageDate.getHours() / 24)} H${ageDate.getHours()} M${ageDate.getMinutes()}
${JSON.stringify(stack, undefined, 4)}
${JSON.stringify(stackInfo, undefined, 4)}
${JSON.stringify(stackInfo2, undefined, 4)}
`;
CloudRunnerLogger.log(message);
return message;
}
public static async getLogGroups() {
const result: any[] = [];
process.env.AWS_REGION = Input.region;
const ecs = new AWS.CloudWatchLogs();
let logStreamInput: AWS.CloudWatchLogs.DescribeLogGroupsRequest = {
/* logGroupNamePrefix: 'game-ci' */
};
let logGroupsDescribe = await ecs.describeLogGroups(logStreamInput).promise();
const logGroups = logGroupsDescribe.logGroups || [];
while (logGroupsDescribe.nextToken) {
logStreamInput = { /* logGroupNamePrefix: 'game-ci',*/ nextToken: logGroupsDescribe.nextToken };
logGroupsDescribe = await ecs.describeLogGroups(logStreamInput).promise();
logGroups.push(...(logGroupsDescribe?.logGroups || []));
}
CloudRunnerLogger.log(`Log Groups ${logGroups.length}`);
for (const element of logGroups) {
if (element.creationTime === undefined) {
CloudRunnerLogger.log(`Skipping ${element.logGroupName} no createdAt date`);
continue;
}
const ageDate: Date = new Date(Date.now() - element.creationTime);
CloudRunnerLogger.log(
`Task Stack ${element.logGroupName} - Age D${Math.floor(
ageDate.getHours() / 24,
)} H${ageDate.getHours()} M${ageDate.getMinutes()}`,
);
result.push(element);
}
return result;
}
public static async getLocks() {
process.env.AWS_REGION = Input.region;
const s3 = new AWS.S3();
const listRequest: ListObjectsRequest = {
Bucket: CloudRunner.buildParameters.awsBaseStackName,
};
const results = await s3.listObjects(listRequest).promise();
return results.Contents || [];
}
}

Some files were not shown because too many files have changed in this diff Show More