Compare commits

..

1 Commits

Author SHA1 Message Date
David Finol
420863deca Fix Android cmdline-tools path 2023-09-25 21:20:29 -05:00
239 changed files with 78512 additions and 140632 deletions

View File

@@ -1,11 +1,22 @@
{
"plugins": ["jest", "@typescript-eslint", "prettier", "unicorn"],
"extends": ["plugin:unicorn/recommended", "plugin:github/recommended", "plugin:prettier/recommended"],
"plugins": [
"jest",
"@typescript-eslint",
"prettier",
"unicorn"
],
"extends": [
"plugin:unicorn/recommended",
"plugin:github/recommended",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"extraFileExtensions": [".mjs"],
"extraFileExtensions": [
".mjs"
],
"ecmaFeatures": {
"impliedStrict": true
},
@@ -14,8 +25,7 @@
"env": {
"node": true,
"es6": true,
"jest/globals": true,
"es2020": true
"jest/globals": true
},
"rules": {
// Error out for code formatting errors
@@ -23,7 +33,10 @@
// Namespaces or sometimes needed
"import/no-namespace": "off",
// Properly format comments
"spaced-comment": ["error", "always"],
"spaced-comment": [
"error",
"always"
],
"lines-around-comment": [
"error",
{
@@ -58,7 +71,12 @@
// Enforce camelCase
"camelcase": "error",
// Allow forOfStatements
"no-restricted-syntax": ["error", "ForInStatement", "LabeledStatement", "WithStatement"],
"no-restricted-syntax": [
"error",
"ForInStatement",
"LabeledStatement",
"WithStatement"
],
// Continue is viable in forOf loops in generators
"no-continue": "off",
// From experience, named exports are almost always desired. I got tired of this rule
@@ -78,13 +96,5 @@
"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"
}
}
]
}
}

View File

@@ -2,28 +2,13 @@
- ...
#### 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))
- [ ] 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))
- [ ] 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

@@ -3,48 +3,43 @@ name: Builds - MacOS
on:
workflow_dispatch:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
buildForAllPlatformsMacOS:
buildForAllPlatformsWindows:
name: ${{ matrix.targetPlatform }} on ${{ matrix.unityVersion }}
runs-on: macos-latest
continue-on-error: true
strategy:
fail-fast: false
matrix:
projectPath:
- test-project
unityVersion:
- 2021.3.45f1
- 2022.3.13f1
- 2023.2.2f1
- 2021.3.29f1
- 2022.1.24f1
- 2022.2.21f1
- 2022.3.7f1
- 2023.1.8f1
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
- uses: actions/checkout@v3
with:
lfs: true
###########################
# Cache #
###########################
- uses: actions/cache@v4
- uses: actions/cache@v3
with:
path: ${{ matrix.projectPath }}/Library
key: Library-${{ matrix.projectPath }}-macos-${{ matrix.targetPlatform }}
@@ -67,13 +62,10 @@ jobs:
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
@@ -81,8 +73,8 @@ jobs:
###########################
# Upload #
###########################
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
with:
name: Build ${{ matrix.targetPlatform }} on MacOS (${{ matrix.unityVersion }})${{ matrix.buildProfile && ' With Build Profile' || '' }}
name: Build MacOS (${{ matrix.unityVersion }})
path: build
retention-days: 14

View File

@@ -3,6 +3,11 @@ name: Builds - Ubuntu
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
paths-ignore:
- '.github/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -36,8 +41,7 @@ env:
jobs:
buildForAllPlatformsUbuntu:
name:
"${{ matrix.targetPlatform }} on ${{ matrix.unityVersion}}${{startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }}"
name: ${{ matrix.targetPlatform }} on ${{ matrix.unityVersion }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -48,73 +52,36 @@ jobs:
projectPath:
- test-project
unityVersion:
- 2021.3.32f1
- 2022.3.13f1
- 2023.2.2f1
- 2021.3.29f1
- 2022.1.24f1
- 2022.2.21f1
- 2022.3.7f1
- 2023.1.8f1
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.
- StandaloneLinux64 # Build a Linux 64-bit standalone with mono backend.
- iOS # Build an iOS player.
- 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'
# - 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:
- name: Clear Space for Android Build
if: matrix.targetPlatform == 'Android'
uses: jlumbroso/free-disk-space@v1.3.1
###########################
# Checkout #
###########################
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
lfs: true
###########################
# Cache #
###########################
- uses: actions/cache@v4
- uses: actions/cache@v3
with:
path: ${{ matrix.projectPath }}/Library
key: Library-${{ matrix.projectPath }}-ubuntu-${{ matrix.targetPlatform }}
@@ -122,85 +89,22 @@ jobs:
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 }}
- uses: ./
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 }}
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue
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
- uses: actions/upload-artifact@v3
with:
name:
"Build ${{ matrix.targetPlatform }}${{ startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }} on Ubuntu (${{ matrix.unityVersion }}_il2cpp_${{ matrix.buildWithIl2cpp }}_params_${{ matrix.additionalParameters }})"
name: Build Ubuntu (${{ matrix.unityVersion }})
path: build
retention-days: 14

View File

@@ -3,6 +3,8 @@ name: Builds - Windows
on:
workflow_dispatch:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -11,47 +13,36 @@ concurrency:
jobs:
buildForAllPlatformsWindows:
name: ${{ matrix.targetPlatform }} on ${{ matrix.unityVersion }}
runs-on: windows-2022
runs-on: windows-2019
strategy:
fail-fast: false
matrix:
projectPath:
- test-project
unityVersion:
- 2021.3.32f1
- 2022.3.13f1
- 2023.2.2f1
- 2021.3.29f1
- 2022.1.24f1
- 2022.2.21f1
- 2022.3.7f1
- 2023.1.8f1
targetPlatform:
- Android # Build an Android apk.
- 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
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
- uses: actions/checkout@v3
with:
lfs: true
###########################
# Cache #
###########################
- uses: actions/cache@v4
- uses: actions/cache@v3
with:
path: ${{ matrix.projectPath }}/Library
key: Library-${{ matrix.projectPath }}-windows-${{ matrix.targetPlatform }}
@@ -78,14 +69,10 @@ jobs:
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
@@ -105,13 +92,10 @@ jobs:
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
@@ -130,13 +114,10 @@ jobs:
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
@@ -144,8 +125,8 @@ jobs:
###########################
# Upload #
###########################
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
with:
name: Build ${{ matrix.targetPlatform }} on Windows (${{ matrix.unityVersion }})${{ matrix.enableGpu && ' With GPU' || '' }}${{ matrix.buildProfile && ' With Build Profile' || '' }}
name: Build Windows (${{ 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: '18'
- 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

@@ -18,16 +18,15 @@ env:
GKE_CLUSTER: 'game-ci-github-pipelines'
GCP_LOGGING: true
GCP_PROJECT: unitykubernetesbuilder
GCP_LOG_FILE: ${{ github.workspace }}/orchestrator-logs.txt
# Commented out: Using LocalStack tests instead of real AWS
# 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_STACK_NAME: game-ci-github-pipelines
ORCHESTRATOR_BRANCH: ${{ github.ref }}
ORCHESTRATOR_DEBUG: true
ORCHESTRATOR_DEBUG_TREE: true
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_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
@@ -47,14 +46,13 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_PRIVATE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TARGET_PLATFORM: StandaloneWindows64
orchestratorTests: true
cloudRunnerTests: true
versioning: None
ORCHESTRATOR_CLUSTER: local-docker
# Commented out: Using LocalStack tests instead of real AWS
# AWS_STACK_NAME: game-ci-github-pipelines
CLOUD_RUNNER_CLUSTER: local-docker
AWS_STACK_NAME: game-ci-github-pipelines
CHECKS_UPDATE: ${{ github.event.inputs.checksObject }}
run: |
git clone -b main https://github.com/game-ci/unity-builder
git clone -b cloud-runner-develop https://github.com/game-ci/unity-builder
cd unity-builder
yarn
ls

View File

@@ -0,0 +1,190 @@
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_STACK_NAME: game-ci-team-pipelines
CLOUD_RUNNER_BRANCH: ${{ github.ref }}
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
GIT_PRIVATE_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
smokeTests:
name: Smoke Tests
if: github.event.event_type != 'pull_request_target'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
test:
#- 'cloud-runner-async-workflow'
- 'cloud-runner-caching'
# - 'cloud-runner-end2end-caching'
# - 'cloud-runner-end2end-retaining'
- 'cloud-runner-environment'
- 'cloud-runner-hooks'
- 'cloud-runner-local-persistence'
- 'cloud-runner-locking-core'
- 'cloud-runner-locking-get-locked'
providerStrategy:
#- aws
- local-docker
#- k8s
steps:
- name: Checkout (default)
uses: actions/checkout@v3
with:
lfs: false
- 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
- uses: google-github-actions/auth@v1
if: matrix.providerStrategy == 'k8s'
with:
credentials_json: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
- name: 'Set up Cloud SDK'
if: matrix.providerStrategy == 'k8s'
uses: 'google-github-actions/setup-gcloud@v1.1.0'
- name: Get GKE cluster credentials
if: matrix.providerStrategy == 'k8s'
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
- run: yarn
- run: yarn run test "${{ matrix.test }}" --detectOpenHandles --forceExit --runInBand
timeout-minutes: 35
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
PROJECT_PATH: test-project
TARGET_PLATFORM: StandaloneWindows64
cloudRunnerTests: true
versioning: None
CLOUD_RUNNER_CLUSTER: ${{ matrix.providerStrategy }}
tests:
# needs:
# - smokeTests
# - buildTargetTests
name: Integration Tests
if: github.event.event_type != 'pull_request_target'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
providerStrategy:
- aws
- local-docker
- k8s
test:
- 'cloud-runner-async-workflow'
#- 'cloud-runner-caching'
- 'cloud-runner-end2end-locking'
- 'cloud-runner-end2end-caching'
- 'cloud-runner-end2end-retaining'
- 'cloud-runner-environment'
#- 'cloud-runner-hooks'
- 'cloud-runner-s3-steps'
#- 'cloud-runner-local-persistence'
#- 'cloud-runner-locking-core'
#- 'cloud-runner-locking-get-locked'
steps:
- name: Checkout (default)
uses: actions/checkout@v2
with:
lfs: false
- 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
- uses: google-github-actions/auth@v1
if: matrix.providerStrategy == 'k8s'
with:
credentials_json: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
- name: 'Set up Cloud SDK'
if: matrix.providerStrategy == 'k8s'
uses: 'google-github-actions/setup-gcloud@v1.1.0'
- name: Get GKE cluster credentials
if: matrix.providerStrategy == 'k8s'
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
- run: yarn
- run: yarn run test "${{ matrix.test }}" --detectOpenHandles --forceExit --runInBand
timeout-minutes: 60
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
PROJECT_PATH: test-project
TARGET_PLATFORM: StandaloneWindows64
cloudRunnerTests: true
versioning: None
PROVIDER_STRATEGY: ${{ matrix.providerStrategy }}
buildTargetTests:
name: Local Build Target Tests
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
providerStrategy:
#- 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: 30
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
with:
cloudRunnerTests: true
versioning: None
targetPlatform: ${{ matrix.targetPlatform }}
providerStrategy: ${{ matrix.providerStrategy }}
- 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.providerStrategy }} Build (${{ matrix.targetPlatform }})
path: ${{ steps.unity-build.outputs.BUILD_ARTIFACT }}
retention-days: 14

View File

@@ -4,11 +4,6 @@ on:
push: { branches: [main] }
pull_request: {}
permissions:
contents: read
checks: write
statuses: write
env:
CODECOV_TOKEN: '2f2eb890-30e2-4724-83eb-7633832cf0de'
@@ -21,18 +16,13 @@ jobs:
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'
- 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:
name: Orchestrator Integrity
uses: ./.github/workflows/orchestrator-integrity.yml
secrets: inherit
# - run: yarn build --quiet && git diff --quiet dist || { echo "dist should be auto generated" ; git diff dist ; exit 62; }

File diff suppressed because it is too large Load Diff

2
.gitignore vendored
View File

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

7
.vscode/launch.json vendored
View File

@@ -1,12 +1,5 @@
{
"configurations": [
{
"name": "PowerShell Launch Current File",
"type": "PowerShell",
"request": "launch",
"script": "${file}",
"cwd": "${cwd}"
},
{
"type": "node",
"request": "launch",

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

@@ -18,11 +18,7 @@ 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: ''
@@ -39,10 +35,6 @@ inputs:
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: ''
@@ -104,182 +96,110 @@ inputs:
gitPrivateToken:
required: false
default: ''
description: '[Orchestrator] Github private token to pull from github'
description: '[CloudRunner] Github private token to pull from github'
githubOwner:
required: false
default: ''
description: '[Orchestrator] GitHub owner name or organization/team name'
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.'
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: '[Orchestrator] Allows the branch of the build to be dirty, and still generate the build.'
description: '[CloudRunner] Allows the branch of the build to be dirty, and still generate the build.'
postBuildSteps:
required: false
default: ''
description:
'[Orchestrator] run a post build job in yaml format with the keys image, secrets (name, value object array),
'[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:
'[Orchestrator] Run a pre build job after the repository setup but before the build job (in yaml format with the
'[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)'
containerHookFiles:
required: false
default: ''
description:
'[Orchestrator] Specify the names (by file name) of custom steps to run before or after orchestrator jobs, must
'[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:
'[Orchestrator] Specify the names (by file name) of custom hooks to run before or after orchestrator jobs, must
'[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/'
customCommandHooks:
required: false
default: ''
description: '[Orchestrator] Specify custom commands and trigger hooks (injects commands into jobs)'
description: '[CloudRunner] Specify custom commands and trigger hooks (injects commands into jobs)'
customJob:
required: false
default: ''
description:
'[Orchestrator] Run a custom job instead of the standard build automation for orchestrator (in yaml format with the
'[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)'
awsStackName:
default: 'game-ci'
required: false
description: '[Orchestrator] The Cloud Formation stack name that must be setup before using this option.'
description: '[CloudRunner] The Cloud Formation stack name that must be setup before using this option.'
providerStrategy:
default: 'local'
required: false
description:
'[Orchestrator] Either local, k8s or aws can be used to run builds on a remote cluster. Additional parameters must
'[CloudRunner] Either local, k8s or aws can be used to run builds on a remote cluster. Additional parameters must
be configured.'
fallbackProviderStrategy:
cloudRunnerCpu:
default: ''
required: false
description:
'[Orchestrator] Fallback provider when the primary is unavailable. Used with runnerCheckEnabled for automatic
failover, or as a catch-all if the primary provider fails to initialize.'
runnerCheckEnabled:
default: 'false'
required: false
description:
'[Orchestrator] Check GitHub Actions runner availability before starting a build. When no suitable runners are
available and fallbackProviderStrategy is set, automatically routes to the fallback provider.'
runnerCheckLabels:
description: '[CloudRunner] Amount of CPU time to assign the remote build container'
cloudRunnerMemory:
default: ''
required: false
description:
'[Orchestrator] Comma-separated runner labels to filter when checking availability (e.g. self-hosted,linux).
When empty, checks all runners in the repository.'
runnerCheckMinAvailable:
default: '1'
required: false
description:
'[Orchestrator] Minimum number of idle runners required for the primary provider. If fewer are available,
routes to fallbackProviderStrategy.'
retryOnFallback:
default: 'false'
required: false
description:
'[Orchestrator] When true and fallbackProviderStrategy is set, automatically retry the build on the fallback
provider if the primary provider fails. Useful for long builds where transient cloud failures are common.'
providerInitTimeout:
default: '0'
required: false
description:
'[Orchestrator] Maximum seconds to wait for the primary provider to initialize (setupWorkflow). If exceeded
and fallbackProviderStrategy is set, switches to the fallback. Set to 0 to disable (default).'
resourceTracking:
default: 'false'
required: false
description: '[Orchestrator] Enable resource tracking logs for disk usage and allocation summaries.'
containerCpu:
default: ''
required: false
description: '[Orchestrator] Amount of CPU time to assign the remote build container'
containerMemory:
default: ''
required: false
description: '[Orchestrator] Amount of memory to assign the remote build container'
description: '[CloudRunner] Amount of memory to assign the remote build container'
readInputFromOverrideList:
default: ''
required: false
description: '[Orchestrator] Comma separated list of input value names to read from "input override command"'
description: '[CloudRunner] Comma separated list of input value names to read from "input override command"'
readInputOverrideCommand:
default: ''
required: false
description:
'[Orchestrator] Extend game ci by specifying a command to execute to pull input from external source e.g cloud
'[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:
'[Orchestrator] Supply a base64 encoded kubernetes config to run builds on kubernetes and stream logs until
'[CloudRunner] Supply a base64 encoded kubernetes config to run builds on kubernetes and stream logs until
completion.'
kubeVolume:
default: ''
required: false
description: '[Orchestrator] Supply a Persistent Volume Claim name to use for the Unity build.'
description: '[CloudRunner] Supply a Persistent Volume Claim name to use for the Unity build.'
kubeStorageClass:
default: ''
required: false
description:
'[Orchestrator] Kubernetes storage class to use for orchestrator jobs, leave empty to install rook cluster.'
'[CloudRunner] Kubernetes storage class to use for cloud runner jobs, leave empty to install rook cluster.'
kubeVolumeSize:
default: '5Gi'
required: false
description: '[Orchestrator] Amount of disc space to assign the Kubernetes Persistent Volume'
description: '[CloudRunner] Amount of disc space to assign the Kubernetes Persistent Volume'
cacheKey:
default: ''
required: false
description: '[Orchestrator] Cache key to indicate bucket for cache'
description: '[CloudRunner] Cache key to indicate bucket for cache'
watchToEnd:
default: 'true'
required: false
description:
'[Orchestrator] Whether or not to watch the build to the end. Can be used for especially long running jobs e.g
'[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.'
cacheUnityInstallationOnMac:
default: 'false'
@@ -301,20 +221,6 @@ inputs:
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'
required: false
description: 'Skip the activation/deactivation of Unity. This assumes Unity is already activated.'
cloneDepth:
default: '50'
required: false
description: '[Orchestrator] Specifies the depth of the git clone for the repository. Use 0 for full clone.'
orchestratorRepoName:
default: 'game-ci/unity-builder'
required: false
description:
'[Orchestrator] Specifies the repo for the unity builder. Useful if you forked the repo for testing, features, or
fixes.'
outputs:
volume:
@@ -323,14 +229,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'

BIN
dist/BlankProject/Packages/.DS_Store vendored Normal file

Binary file not shown.

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,47 @@ 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;
}
}
#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
// Define BuildPlayer Options
var buildPlayerOptions = new BuildPlayerOptions {
scenes = scenes,
locationPathName = options["customBuildPath"],
target = (BuildTarget) Enum.Parse(typeof(BuildTarget), options["buildTarget"]),
options = buildOptions,
#if UNITY_2021_2_OR_NEWER
subtarget = (int) buildSubtarget
#endif
};
// 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 +74,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

@@ -56,17 +56,17 @@ namespace UnityBuilderAction.Input
case "androidStudioProject":
EditorUserBuildSettings.exportAsGoogleAndroidProject = true;
if (buildAppBundle != null)
buildAppBundle.SetValue(null, false, null);
buildAppBundle.SetValue(null, false);
break;
case "androidAppBundle":
EditorUserBuildSettings.exportAsGoogleAndroidProject = false;
if (buildAppBundle != null)
buildAppBundle.SetValue(null, true, null);
buildAppBundle.SetValue(null, true);
break;
case "androidPackage":
EditorUserBuildSettings.exportAsGoogleAndroidProject = false;
if (buildAppBundle != null)
buildAppBundle.SetValue(null, false, null);
buildAppBundle.SetValue(null, false);
break;
}
}
@@ -74,20 +74,7 @@ namespace UnityBuilderAction.Input
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
#if UNITY_2021_1_OR_NEWER
switch (symbolType)
{
case "public":
@@ -114,37 +101,5 @@ namespace UnityBuilderAction.Input
#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

@@ -21,19 +21,6 @@ namespace UnityBuilderAction.Input
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)) {
Console.WriteLine("Missing argument -buildTarget");
@@ -41,10 +28,9 @@ namespace UnityBuilderAction.Input
}
if (!Enum.IsDefined(typeof(BuildTarget), buildTarget)) {
Console.WriteLine(buildTarget + " is not a defined " + typeof(BuildTarget).Name);
Console.WriteLine($"{buildTarget} is not a defined {nameof(BuildTarget)}");
EditorApplication.Exit(121);
}
}
string customBuildPath;
if (!validatedOptions.TryGetValue("customBuildPath", out customBuildPath)) {
@@ -55,10 +41,10 @@ namespace UnityBuilderAction.Input
const string defaultCustomBuildName = "TestBuild";
string customBuildName;
if (!validatedOptions.TryGetValue("customBuildName", out customBuildName)) {
Console.WriteLine("Missing argument -customBuildName, defaulting to" + defaultCustomBuildName);
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 +57,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 +78,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;
}

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>

180919
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

15815
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 - \
-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
#
@@ -146,16 +129,16 @@ echo ""
/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" \

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 - \
-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,46 @@
#!/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_extra_git_configs.sh
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/*)/cmdline-tools/6.0/bin/sdkmanager" "$ANDROID_SDK_MANAGER_PARAMETERS"
echo "Updated Android SDK."
else
echo "Not updating Android SDK."
fi
#
# Pre-build debug information
#
@@ -125,11 +122,9 @@ unity-editor \
$( [ "${MANUAL_EXIT}" == "true" ] || echo "-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" \

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,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,51 @@ 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 `
-androidExportType $Env:ANDROID_EXPORT_TYPE `
-androidSymbolType $Env:ANDROID_SYMBOL_TYPE `
$customParametersArray `
-logfile | Out-Host
# Catch exit code
$Env:BUILD_EXIT_CODE=$LastExitCode
# 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,18 @@
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"
}
# 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"
}
& "c:\steps\set_gitcredential.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"
}
& "c:\steps\activate.ps1"
# 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,16 +1,16 @@
if ($null -eq ${env:GIT_PRIVATE_TOKEN}) {
if ([string]::IsNullOrEmpty($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:"
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 -------------"

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

@@ -25,6 +25,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

@@ -7,56 +7,48 @@
"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 providerStrategy=aws orchestratorTests=true inputPullCommand=\"gcp-secret-manager\" populateOverride=true pullInputList=UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD yarn test -i -t \"orchestrator\"",
"gcp-secrets-cli": "cross-env orchestratorTests=true USE_IL2CPP=false inputPullCommand=\"gcp-secret-manager\" yarn ts-node src/index.ts -m cli --populateOverride true --pullInputList UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD",
"aws-secrets-cli": "cross-env orchestratorTests=true inputPullCommand=\"aws-secret-manager\" yarn ts-node src/index.ts -m cli --populateOverride true --pullInputList UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD",
"gcp-secrets-tests": "cross-env providerStrategy=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 providerStrategy=aws yarn run test-cli",
"cli-k8s": "cross-env providerStrategy=k8s yarn run test-cli",
"test-cli": "cross-env orchestratorTests=true yarn ts-node src/index.ts -m cli --projectPath test-project",
"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",
"test-i": "cross-env orchestratorTests=true yarn test -i -t \"orchestrator\"",
"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 orchestratorTests=true providerStrategy=aws yarn test -i -t \"orchestrator\"",
"test-i-k8s": "cross-env orchestratorTests=true providerStrategy=k8s yarn test -i -t \"orchestrator\""
"test-i-aws": "cross-env cloudRunnerTests=true providerStrategy=aws yarn test -i -t \"cloud runner\"",
"test-i-k8s": "cross-env cloudRunnerTests=true providerStrategy=k8s yarn test -i -t \"cloud runner\""
},
"engines": {
"node": ">=18.x"
},
"dependencies": {
"@actions/cache": "^4.0.0",
"@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
"@aws-sdk/client-cloudformation": "^3.777.0",
"@aws-sdk/client-cloudwatch-logs": "^3.777.0",
"@aws-sdk/client-ecs": "^3.778.0",
"@aws-sdk/client-kinesis": "^3.777.0",
"@aws-sdk/client-s3": "^3.779.0",
"@actions/cache": "^3.1.3",
"@actions/core": "^1.10.0",
"@actions/exec": "^1.1.0",
"@actions/github": "^5.0.0",
"@kubernetes/client-node": "^0.16.3",
"@octokit/core": "^5.1.0",
"@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",
"kubernetes-client": "^9.0.0",
"md5": "^2.3.0",
"nanoid": "^3.3.1",
"reflect-metadata": "^0.1.13",
"semver": "^7.5.2",
"shell-quote": "^1.8.3",
"ts-md5": "^1.3.1",
"unity-changeset": "^3.1.0",
"unity-changeset": "^2.0.0",
"uuid": "^9.0.0",
"yaml": "^2.2.2"
},
"devDependencies": {
"@evilmartians/lefthook": "^1.2.9",
"@types/base-64": "^1.0.0",
"@types/jest": "^27.4.1",
"@types/node": "^17.0.23",
@@ -75,11 +67,9 @@
"jest-circus": "^27.5.1",
"jest-fail-on-console": "^3.0.2",
"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",
"ts-node": "10.4.0",
"typescript": "4.7.4",
"yarn-audit-fix": "^9.3.8"
},

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,5 +1,5 @@
import * as core from '@actions/core';
import { Action, BuildParameters, Cache, Orchestrator, 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';
@@ -19,35 +19,23 @@ async function runMain() {
const buildParameters = await BuildParameters.create();
const baseImage = new ImageTag(buildParameters);
let exitCode = -1;
if (buildParameters.providerStrategy === 'local') {
core.info('Building locally');
await PlatformSetup.setup(buildParameters, actionFolder);
exitCode =
process.platform === 'darwin'
? await MacBuilder.run(actionFolder)
: await Docker.run(baseImage.toString(), {
workspace,
actionFolder,
...buildParameters,
});
if (process.platform === 'darwin') {
MacBuilder.run(actionFolder);
} else {
await Docker.run(baseImage.toString(), { workspace, actionFolder, ...buildParameters });
}
} else {
await Orchestrator.run(buildParameters, baseImage.toString());
exitCode = 0;
await CloudRunner.run(buildParameters, baseImage.toString());
}
// Set output
await Output.setBuildVersion(buildParameters.buildVersion);
await Output.setAndroidVersionCode(buildParameters.androidVersionCode);
await Output.setEngineExitCode(exitCode);
if (exitCode !== 0) {
core.setFailed(`Build failed with exit code ${exitCode}`);
}
} catch (error) {
core.setFailed((error as Error).message);
}
}
runMain();

View File

@@ -1,29 +0,0 @@
// Integration test for exercising real GitHub check creation and updates.
import Orchestrator from '../model/orchestrator/orchestrator';
import UnityVersioning from '../model/unity-versioning';
import GitHub from '../model/github';
import { TIMEOUT_INFINITE, createParameters } from '../test-utils/orchestrator-test-helpers';
const runIntegration = process.env.RUN_GITHUB_INTEGRATION_TESTS === 'true';
const describeOrSkip = runIntegration ? describe : describe.skip;
describeOrSkip('Orchestrator Github Checks Integration', () => {
it(
'creates and updates a real GitHub check',
async () => {
const buildParameter = await createParameters({
versioning: 'None',
projectPath: 'test-project',
unityVersion: UnityVersioning.read('test-project'),
asyncOrchestrator: `true`,
githubChecks: `true`,
});
await Orchestrator.setup(buildParameter);
const checkId = await GitHub.createGitHubCheck(`integration create`);
expect(checkId).not.toEqual('');
await GitHub.updateGitHubCheck(`1 ${new Date().toISOString()}`, `integration`);
await GitHub.updateGitHubCheck(`2 ${new Date().toISOString()}`, `integration`, `success`, `completed`);
},
TIMEOUT_INFINITE,
);
});

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

@@ -71,12 +71,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);

View File

@@ -1,7 +1,7 @@
import { customAlphabet } from 'nanoid';
import AndroidVersioning from './android-versioning';
import OrchestratorConstants from './orchestrator/options/orchestrator-constants';
import OrchestratorBuildGuid from './orchestrator/options/orchestrator-guid';
import CloudRunnerConstants from './cloud-runner/options/cloud-runner-constants';
import CloudRunnerBuildGuid from './cloud-runner/options/cloud-runner-guid';
import Input from './input';
import Platform from './platform';
import UnityVersioning from './unity-versioning';
@@ -10,9 +10,8 @@ import { GitRepoReader } from './input-readers/git-repo';
import { GithubCliReader } from './input-readers/github-cli';
import { Cli } from './cli/cli';
import GitHub from './github';
import OrchestratorOptions from './orchestrator/options/orchestrator-options';
import Orchestrator from './orchestrator/orchestrator';
import * as core from '@actions/core';
import CloudRunnerOptions from './cloud-runner/options/cloud-runner-options';
import CloudRunner from './cloud-runner/cloud-runner';
class BuildParameters {
// eslint-disable-next-line no-undef
@@ -22,18 +21,15 @@ class BuildParameters {
public customImage!: string;
public unitySerial!: string;
public unityLicensingServer!: string;
public skipActivation!: string;
public runnerTempPath!: string;
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;
@@ -44,40 +40,19 @@ class BuildParameters {
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 sshAgent!: string;
public sshPublicKeysDirectoryPath!: string;
public providerStrategy!: string;
public fallbackProviderStrategy!: string;
public runnerCheckEnabled!: boolean;
public runnerCheckLabels!: string[];
public runnerCheckMinAvailable!: number;
public retryOnFallback!: boolean;
public providerInitTimeout!: number;
public gitPrivateToken!: string;
public awsStackName!: string;
public awsEndpoint?: string;
public awsCloudFormationEndpoint?: string;
public awsEcsEndpoint?: string;
public awsKinesisEndpoint?: string;
public awsCloudWatchLogsEndpoint?: string;
public awsS3Endpoint?: string;
public storageProvider!: string;
public rcloneRemote!: string;
public kubeConfig!: string;
public containerMemory!: string;
public containerCpu!: string;
public containerNamespace!: string;
public kubeVolumeSize!: string;
public kubeVolume!: string;
public kubeStorageClass!: string;
public runAsHostUser!: string;
public chownFilesTo!: string;
public commandHooks!: string;
public pullInputList!: string[];
@@ -90,13 +65,11 @@ class BuildParameters {
public runNumber!: string;
public branch!: string;
public githubRepo!: string;
public orchestratorRepoName!: string;
public cloneDepth!: number;
public gitSha!: string;
public logId!: string;
public buildGuid!: string;
public orchestratorBranch!: string;
public orchestratorDebug!: boolean | undefined;
public cloudRunnerBranch!: string;
public cloudRunnerDebug!: boolean | undefined;
public buildPlatform!: string | undefined;
public isCliMode!: boolean;
public maxRetainedWorkspaces!: number;
@@ -114,7 +87,7 @@ class BuildParameters {
public dockerWorkspacePath!: string;
public static shouldUseRetainedWorkspaceMode(buildParameters: BuildParameters) {
return buildParameters.maxRetainedWorkspaces > 0 && Orchestrator.lockedWorkspace !== ``;
return buildParameters.maxRetainedWorkspaces > 0 && CloudRunner.lockedWorkspace !== ``;
}
static async create(): Promise<BuildParameters> {
@@ -143,12 +116,10 @@ class BuildParameters {
if (!Input.unitySerial && 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
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);
} else {
@@ -156,28 +127,20 @@ class BuildParameters {
}
}
if (unitySerial !== undefined && unitySerial.length === 27) {
core.setSecret(unitySerial);
core.setSecret(`${unitySerial.slice(0, -4)}XXXX`);
}
return {
editorVersion,
customImage: Input.customImage,
unitySerial,
unityLicensingServer: Input.unityLicensingServer,
skipActivation: Input.skipActivation,
runnerTempPath: Input.runnerTempPath,
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,
@@ -191,66 +154,43 @@ class BuildParameters {
customParameters: Input.customParameters,
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: OrchestratorOptions.providerStrategy,
fallbackProviderStrategy: OrchestratorOptions.fallbackProviderStrategy,
runnerCheckEnabled: OrchestratorOptions.runnerCheckEnabled,
runnerCheckLabels: OrchestratorOptions.runnerCheckLabels,
runnerCheckMinAvailable: OrchestratorOptions.runnerCheckMinAvailable,
retryOnFallback: OrchestratorOptions.retryOnFallback,
providerInitTimeout: OrchestratorOptions.providerInitTimeout,
buildPlatform: OrchestratorOptions.buildPlatform,
kubeConfig: OrchestratorOptions.kubeConfig,
containerMemory: OrchestratorOptions.containerMemory,
containerCpu: OrchestratorOptions.containerCpu,
containerNamespace: OrchestratorOptions.containerNamespace,
kubeVolumeSize: OrchestratorOptions.kubeVolumeSize,
kubeVolume: OrchestratorOptions.kubeVolume,
postBuildContainerHooks: OrchestratorOptions.postBuildContainerHooks,
preBuildContainerHooks: OrchestratorOptions.preBuildContainerHooks,
customJob: OrchestratorOptions.customJob,
providerStrategy: CloudRunnerOptions.providerStrategy,
buildPlatform: CloudRunnerOptions.buildPlatform,
kubeConfig: CloudRunnerOptions.kubeConfig,
containerMemory: CloudRunnerOptions.containerMemory,
containerCpu: CloudRunnerOptions.containerCpu,
kubeVolumeSize: CloudRunnerOptions.kubeVolumeSize,
kubeVolume: CloudRunnerOptions.kubeVolume,
postBuildContainerHooks: CloudRunnerOptions.postBuildContainerHooks,
preBuildContainerHooks: CloudRunnerOptions.preBuildContainerHooks,
customJob: CloudRunnerOptions.customJob,
runNumber: Input.runNumber,
branch: Input.branch.replace('/head', '') || (await GitRepoReader.GetBranch()),
orchestratorBranch: OrchestratorOptions.orchestratorBranch.split('/').reverse()[0],
orchestratorDebug: OrchestratorOptions.orchestratorDebug,
githubRepo: (Input.githubRepo ?? (await GitRepoReader.GetRemote())) || OrchestratorOptions.orchestratorRepoName,
orchestratorRepoName: OrchestratorOptions.orchestratorRepoName,
cloneDepth: Number.parseInt(OrchestratorOptions.cloneDepth),
cloudRunnerBranch: CloudRunnerOptions.cloudRunnerBranch.split('/').reverse()[0],
cloudRunnerDebug: CloudRunnerOptions.cloudRunnerDebug,
githubRepo: Input.githubRepo || (await GitRepoReader.GetRemote()) || 'game-ci/unity-builder',
isCliMode: Cli.isCliMode,
awsStackName: OrchestratorOptions.awsStackName,
awsEndpoint: OrchestratorOptions.awsEndpoint,
awsCloudFormationEndpoint: OrchestratorOptions.awsCloudFormationEndpoint,
awsEcsEndpoint: OrchestratorOptions.awsEcsEndpoint,
awsKinesisEndpoint: OrchestratorOptions.awsKinesisEndpoint,
awsCloudWatchLogsEndpoint: OrchestratorOptions.awsCloudWatchLogsEndpoint,
awsS3Endpoint: OrchestratorOptions.awsS3Endpoint,
storageProvider: OrchestratorOptions.storageProvider,
rcloneRemote: OrchestratorOptions.rcloneRemote,
awsStackName: CloudRunnerOptions.awsStackName,
gitSha: Input.gitSha,
logId: customAlphabet(OrchestratorConstants.alphabet, 9)(),
buildGuid: OrchestratorBuildGuid.generateGuid(Input.runNumber, Input.targetPlatform),
commandHooks: OrchestratorOptions.commandHooks,
inputPullCommand: OrchestratorOptions.inputPullCommand,
pullInputList: OrchestratorOptions.pullInputList,
kubeStorageClass: OrchestratorOptions.kubeStorageClass,
cacheKey: OrchestratorOptions.cacheKey,
maxRetainedWorkspaces: Number.parseInt(OrchestratorOptions.maxRetainedWorkspaces),
useLargePackages: OrchestratorOptions.useLargePackages,
useCompressionStrategy: OrchestratorOptions.useCompressionStrategy,
garbageMaxAge: OrchestratorOptions.garbageMaxAge,
githubChecks: OrchestratorOptions.githubChecks,
asyncWorkflow: OrchestratorOptions.asyncOrchestrator,
githubCheckId: OrchestratorOptions.githubCheckId,
finalHooks: OrchestratorOptions.finalHooks,
skipLfs: OrchestratorOptions.skipLfs,
skipCache: OrchestratorOptions.skipCache,
logId: customAlphabet(CloudRunnerConstants.alphabet, 9)(),
buildGuid: CloudRunnerBuildGuid.generateGuid(Input.runNumber, Input.targetPlatform),
commandHooks: CloudRunnerOptions.commandHooks,
inputPullCommand: CloudRunnerOptions.inputPullCommand,
pullInputList: CloudRunnerOptions.pullInputList,
kubeStorageClass: CloudRunnerOptions.kubeStorageClass,
cacheKey: CloudRunnerOptions.cacheKey,
maxRetainedWorkspaces: Number.parseInt(CloudRunnerOptions.maxRetainedWorkspaces),
useLargePackages: CloudRunnerOptions.useLargePackages,
useCompressionStrategy: CloudRunnerOptions.useCompressionStrategy,
garbageMaxAge: CloudRunnerOptions.garbageMaxAge,
githubChecks: CloudRunnerOptions.githubChecks,
asyncWorkflow: CloudRunnerOptions.asyncCloudRunner,
githubCheckId: CloudRunnerOptions.githubCheckId,
finalHooks: CloudRunnerOptions.finalHooks,
skipLfs: CloudRunnerOptions.skipLfs,
skipCache: CloudRunnerOptions.skipCache,
cacheUnityInstallationOnMac: Input.cacheUnityInstallationOnMac,
unityHubVersionOnMac: Input.unityHubVersionOnMac,
dockerWorkspacePath: Input.dockerWorkspacePath,

View File

@@ -1,15 +1,17 @@
import { Command } from 'commander-ts';
import { BuildParameters, Orchestrator, ImageTag, Input } from '..';
import { BuildParameters, CloudRunner, ImageTag, Input } from '..';
import * as core from '@actions/core';
import { ActionYamlReader } from '../input-readers/action-yaml';
import OrchestratorLogger from '../orchestrator/services/core/orchestrator-logger';
import OrchestratorQueryOverride from '../orchestrator/options/orchestrator-query-override';
import CloudRunnerLogger from '../cloud-runner/services/core/cloud-runner-logger';
import CloudRunnerQueryOverride from '../cloud-runner/options/cloud-runner-query-override';
import { CliFunction, CliFunctionsRepository } from './cli-functions-repository';
import { Caching } from '../orchestrator/remote-client/caching';
import { LfsHashing } from '../orchestrator/services/utility/lfs-hashing';
import { RemoteClient } from '../orchestrator/remote-client';
import OrchestratorOptionsReader from '../orchestrator/options/orchestrator-options-reader';
import { Caching } from '../cloud-runner/remote-client/caching';
import { LfsHashing } from '../cloud-runner/services/utility/lfs-hashing';
import { RemoteClient } from '../cloud-runner/remote-client';
import CloudRunnerOptionsReader from '../cloud-runner/options/cloud-runner-options-reader';
import GitHub from '../github';
import { CloudRunnerFolders } from '../cloud-runner/options/cloud-runner-folders';
import { CloudRunnerSystem } from '../cloud-runner/services/core/cloud-runner-system';
import { OptionValues } from 'commander';
import { InputKey } from '../input';
@@ -36,7 +38,7 @@ export class Cli {
const program = new Command();
program.version('0.0.1');
const properties = OrchestratorOptionsReader.GetProperties();
const properties = CloudRunnerOptionsReader.GetProperties();
const actionYamlReader: ActionYamlReader = new ActionYamlReader();
for (const element of properties) {
program.option(`--${element} <${element}>`, actionYamlReader.GetActionYamlValue(element));
@@ -52,7 +54,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.parse(process.argv);
Cli.options = program.opts();
@@ -62,23 +63,23 @@ export class Cli {
static async RunCli(): Promise<void> {
GitHub.githubInputEnabled = false;
if (Cli.options!['populateOverride'] === `true`) {
await OrchestratorQueryOverride.PopulateQueryOverrideInput();
await CloudRunnerQueryOverride.PopulateQueryOverrideInput();
}
if (Cli.options!['logInput']) {
Cli.logInput();
}
const results = CliFunctionsRepository.GetCliFunctions(Cli.options?.mode);
OrchestratorLogger.log(`Entrypoint: ${results.key}`);
CloudRunnerLogger.log(`Entrypoint: ${results.key}`);
Cli.options!.versioning = 'None';
Orchestrator.buildParameters = await BuildParameters.create();
Orchestrator.buildParameters.buildGuid = process.env.BUILD_GUID || ``;
OrchestratorLogger.log(`Build Params:
${JSON.stringify(Orchestrator.buildParameters, undefined, 4)}
CloudRunner.buildParameters = await BuildParameters.create();
CloudRunner.buildParameters.buildGuid = process.env.BUILD_GUID || ``;
CloudRunnerLogger.log(`Build Params:
${JSON.stringify(CloudRunner.buildParameters, undefined, 4)}
`);
Orchestrator.lockedWorkspace = process.env.LOCKED_WORKSPACE || ``;
OrchestratorLogger.log(`Locked Workspace: ${Orchestrator.lockedWorkspace}`);
await Orchestrator.setup(Orchestrator.buildParameters);
CloudRunner.lockedWorkspace = process.env.LOCKED_WORKSPACE || ``;
CloudRunnerLogger.log(`Locked Workspace: ${CloudRunner.lockedWorkspace}`);
await CloudRunner.setup(CloudRunner.buildParameters);
return await results.target[results.propertyKey](Cli.options);
}
@@ -87,7 +88,7 @@ export class Cli {
private static logInput() {
core.info(`\n`);
core.info(`INPUT:`);
const properties = OrchestratorOptionsReader.GetProperties();
const properties = CloudRunnerOptionsReader.GetProperties();
for (const element of properties) {
if (
element in Input &&
@@ -104,28 +105,28 @@ export class Cli {
core.info(`\n`);
}
@CliFunction(`cli-build`, `runs a orchestrator build`)
@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 Orchestrator.run(buildParameter, baseImage.toString())).BuildResults;
return await CloudRunner.run(buildParameter, baseImage.toString());
}
@CliFunction(`async-workflow`, `runs a orchestrator build`)
@CliFunction(`async-workflow`, `runs a cloud runner build`)
public static async asyncronousWorkflow(): Promise<string> {
const buildParameter = await BuildParameters.create();
const baseImage = new ImageTag(buildParameter);
await Orchestrator.setup(buildParameter);
await CloudRunner.setup(buildParameter);
return (await Orchestrator.run(buildParameter, baseImage.toString())).BuildResults;
return await CloudRunner.run(buildParameter, baseImage.toString());
}
@CliFunction(`checks-update`, `runs a orchestrator build`)
@CliFunction(`checks-update`, `runs a cloud runner build`)
public static async checksUpdate() {
const buildParameter = await BuildParameters.create();
await Orchestrator.setup(buildParameter);
await CloudRunner.setup(buildParameter);
const input = JSON.parse(process.env.CHECKS_UPDATE || ``);
core.info(`Checks Update ${process.env.CHECKS_UPDATE}`);
if (input.mode === `create`) {
@@ -139,18 +140,18 @@ export class Cli {
public static async GarbageCollect(): Promise<string> {
const buildParameter = await BuildParameters.create();
await Orchestrator.setup(buildParameter);
await CloudRunner.setup(buildParameter);
return await Orchestrator.Provider.garbageCollect(``, false, 0, false, false);
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 Orchestrator.setup(buildParameter);
const result = await Orchestrator.Provider.listResources();
OrchestratorLogger.log(JSON.stringify(result, undefined, 4));
await CloudRunner.setup(buildParameter);
const result = await CloudRunner.Provider.listResources();
CloudRunnerLogger.log(JSON.stringify(result, undefined, 4));
return result.map((x) => x.Name);
}
@@ -159,17 +160,44 @@ export class Cli {
public static async ListWorfklow(): Promise<string[]> {
const buildParameter = await BuildParameters.create();
await Orchestrator.setup(buildParameter);
await CloudRunner.setup(buildParameter);
return (await Orchestrator.Provider.listWorkflow()).map((x) => x.Name);
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 Orchestrator.setup(buildParameter);
await CloudRunner.setup(buildParameter);
return await Orchestrator.Provider.watchWorkflow();
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 (!BuildParameters.shouldUseRetainedWorkspaceMode(CloudRunner.buildParameters)) {
await CloudRunnerSystem.Run(
`rm -r ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)}`,
);
}
await RemoteClient.runCustomHookFiles(`after-build`);
return new Promise((result) => result(``));
}
}

View File

@@ -0,0 +1,180 @@
import AwsBuildPlatform from './providers/aws';
import { BuildParameters, Input } from '..';
import Kubernetes from './providers/k8s';
import CloudRunnerLogger from './services/core/cloud-runner-logger';
import { CloudRunnerStepParameters } from './options/cloud-runner-step-parameters';
import { WorkflowCompositionRoot } from './workflows/workflow-composition-root';
import { CloudRunnerError } from './error/cloud-runner-error';
import { TaskParameterSerializer } from './services/core/task-parameter-serializer';
import * as core from '@actions/core';
import CloudRunnerSecret from './options/cloud-runner-secret';
import { ProviderInterface } from './providers/provider-interface';
import CloudRunnerEnvironmentVariable from './options/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/core/shared-workspace-locking';
import { FollowLogStreamService } from './services/core/follow-log-stream-service';
class CloudRunner {
public static Provider: ProviderInterface;
public static buildParameters: BuildParameters;
private static defaultSecrets: CloudRunnerSecret[];
private static cloudRunnerEnvironmentVariables: CloudRunnerEnvironmentVariable[];
static lockedWorkspace: string = ``;
public static readonly retainedWorkspacePrefix: string = `retained-workspace`;
public static get isCloudRunnerEnvironment() {
return process.env[`GITHUB_ACTIONS`] !== `true`;
}
public static get isCloudRunnerAsyncEnvironment() {
return process.env[`ASYNC_WORKFLOW`] === `true`;
}
public static async setup(buildParameters: BuildParameters) {
CloudRunnerLogger.setup();
CloudRunnerLogger.log(`Setting up cloud runner`);
CloudRunner.buildParameters = buildParameters;
if (CloudRunner.buildParameters.githubCheckId === ``) {
CloudRunner.buildParameters.githubCheckId = await GitHub.createGitHubCheck(CloudRunner.buildParameters.buildGuid);
}
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.useCompressionStrategy ? '.lz4' : ''
}`,
);
}
FollowLogStreamService.Reset();
}
private static setupSelectedBuildPlatform() {
CloudRunnerLogger.log(`Cloud Runner platform selected ${CloudRunner.buildParameters.providerStrategy}`);
switch (CloudRunner.buildParameters.providerStrategy) {
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) {
await CloudRunner.setup(buildParameters);
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();
try {
if (buildParameters.maxRetainedWorkspaces > 0) {
CloudRunner.lockedWorkspace = SharedWorkspaceLocking.NewWorkspaceName();
const result = await SharedWorkspaceLocking.GetLockedWorkspace(
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.maxRetainedWorkspaces = 0;
CloudRunner.lockedWorkspace = ``;
}
}
const content = { ...CloudRunner.buildParameters };
content.gitPrivateToken = ``;
content.unitySerial = ``;
const jsonContent = JSON.stringify(content, undefined, 4);
await GitHub.updateGitHubCheck(jsonContent, CloudRunner.buildParameters.buildGuid);
const output = await new WorkflowCompositionRoot().run(
new CloudRunnerStepParameters(
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 (BuildParameters.shouldUseRetainedWorkspaceMode(buildParameters)) {
const workspace = CloudRunner.lockedWorkspace || ``;
await SharedWorkspaceLocking.ReleaseWorkspace(
workspace,
CloudRunner.buildParameters.buildGuid,
CloudRunner.buildParameters,
);
const isLocked = await SharedWorkspaceLocking.IsWorkspaceLocked(workspace, CloudRunner.buildParameters);
if (isLocked) {
throw new Error(
`still locked after releasing ${await SharedWorkspaceLocking.GetAllLocksForWorkspace(
workspace,
buildParameters,
)}`,
);
}
CloudRunner.lockedWorkspace = ``;
}
await GitHub.triggerWorkflowOnComplete(CloudRunner.buildParameters.finalHooks);
if (buildParameters.constantGarbageCollection) {
CloudRunner.Provider.garbageCollect(``, true, buildParameters.garbageMaxAge, true, true);
}
return output;
} catch (error: any) {
CloudRunnerLogger.log(JSON.stringify(error, undefined, 4));
await GitHub.updateGitHubCheck(
CloudRunner.buildParameters.buildGuid,
`Failed - Error ${error?.message || 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/core/cloud-runner-logger';
import * as core from '@actions/core';
import CloudRunner from '../cloud-runner';
import CloudRunnerSecret from '../options/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,4 @@
class CloudRunnerConstants {
static alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
}
export default CloudRunnerConstants;

View File

@@ -0,0 +1,5 @@
class CloudRunnerEnvironmentVariable {
public name!: string;
public value!: string;
}
export default CloudRunnerEnvironmentVariable;

View File

@@ -0,0 +1,90 @@
import path from 'node:path';
import CloudRunnerOptions from './cloud-runner-options';
import CloudRunner from '../cloud-runner';
import BuildParameters from '../../build-parameters';
export class CloudRunnerFolders {
public static readonly repositoryFolder = 'repo';
public static ToLinuxFolder(folder: string) {
return folder.replace(/\\/g, `/`);
}
// Only the following paths that do not start a path.join with another "Full" suffixed property need to start with an absolute /
public static get uniqueCloudRunnerJobFolderAbsolute(): string {
return CloudRunner.buildParameters && BuildParameters.shouldUseRetainedWorkspaceMode(CloudRunner.buildParameters)
? path.join(`/`, CloudRunnerFolders.buildVolumeFolder, CloudRunner.lockedWorkspace)
: path.join(`/`, CloudRunnerFolders.buildVolumeFolder, CloudRunner.buildParameters.buildGuid);
}
public static get cacheFolderForAllFull(): string {
return path.join('/', CloudRunnerFolders.buildVolumeFolder, CloudRunnerFolders.cacheFolder);
}
public static get cacheFolderForCacheKeyFull(): string {
return path.join(
'/',
CloudRunnerFolders.buildVolumeFolder,
CloudRunnerFolders.cacheFolder,
CloudRunner.buildParameters.cacheKey,
);
}
public static get builderPathAbsolute(): string {
return path.join(
CloudRunnerOptions.useSharedBuilder
? `/${CloudRunnerFolders.buildVolumeFolder}`
: CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute,
`builder`,
);
}
public static get repoPathAbsolute(): string {
return path.join(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute, CloudRunnerFolders.repositoryFolder);
}
public static get projectPathAbsolute(): string {
return path.join(CloudRunnerFolders.repoPathAbsolute, CloudRunner.buildParameters.projectPath);
}
public static get libraryFolderAbsolute(): string {
return path.join(CloudRunnerFolders.projectPathAbsolute, `Library`);
}
public static get projectBuildFolderAbsolute(): string {
return path.join(CloudRunnerFolders.repoPathAbsolute, CloudRunner.buildParameters.buildPath);
}
public static get lfsFolderAbsolute(): string {
return path.join(CloudRunnerFolders.repoPathAbsolute, `.git`, `lfs`);
}
public static get purgeRemoteCaching(): boolean {
return process.env.PURGE_REMOTE_BUILDER_CACHE !== undefined;
}
public static get lfsCacheFolderFull() {
return path.join(CloudRunnerFolders.cacheFolderForCacheKeyFull, `lfs`);
}
public static get libraryCacheFolderFull() {
return path.join(CloudRunnerFolders.cacheFolderForCacheKeyFull, `Library`);
}
public static get unityBuilderRepoUrl(): string {
return `https://${CloudRunner.buildParameters.gitPrivateToken}@github.com/game-ci/unity-builder.git`;
}
public static get targetBuildRepoUrl(): string {
return `https://${CloudRunner.buildParameters.gitPrivateToken}@github.com/${CloudRunner.buildParameters.githubRepo}.git`;
}
public static get buildVolumeFolder() {
return 'data';
}
public static get cacheFolder() {
return 'cache';
}
}

View File

@@ -1,11 +1,11 @@
import { customAlphabet } from 'nanoid';
import OrchestratorConstants from './orchestrator-constants';
import CloudRunnerConstants from './cloud-runner-constants';
class OrchestratorNamespace {
class CloudRunnerNamespace {
static generateGuid(runNumber: string | number, platform: string) {
const nanoid = customAlphabet(OrchestratorConstants.alphabet, 4);
const nanoid = customAlphabet(CloudRunnerConstants.alphabet, 4);
return `${runNumber}-${platform.toLowerCase().replace('standalone', '')}-${nanoid()}`;
}
}
export default OrchestratorNamespace;
export default CloudRunnerNamespace;

View File

@@ -0,0 +1,10 @@
import Input from '../../input';
import CloudRunnerOptions from './cloud-runner-options';
class CloudRunnerOptionsReader {
static GetProperties() {
return [...Object.getOwnPropertyNames(Input), ...Object.getOwnPropertyNames(CloudRunnerOptions)];
}
}
export default CloudRunnerOptionsReader;

View File

@@ -0,0 +1,283 @@
import { Cli } from '../../cli/cli';
import CloudRunnerQueryOverride from './cloud-runner-query-override';
import GitHub from '../../github';
import * as core from '@actions/core';
class CloudRunnerOptions {
// ### ### ###
// Input Handling
// ### ### ###
public static getInput(query: string): string | undefined {
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];
}
}
public static ToEnvVarFormat(input: string): 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 {
const value = CloudRunnerOptions.getInput('githubChecks');
return value === `true` || false;
}
static get githubCheckId(): string {
return CloudRunnerOptions.getInput('githubCheckId') || ``;
}
static get githubOwner(): string {
return CloudRunnerOptions.getInput('githubOwner') || CloudRunnerOptions.githubRepo?.split(`/`)[0] || '';
}
static get githubRepoName(): string {
return CloudRunnerOptions.getInput('githubRepoName') || CloudRunnerOptions.githubRepo?.split(`/`)[1] || '';
}
static get finalHooks(): string[] {
return CloudRunnerOptions.getInput('finalHooks')?.split(',') || [];
}
// ### ### ###
// Git syncronization parameters
// ### ### ###
static get githubRepo(): string | undefined {
return CloudRunnerOptions.getInput('GITHUB_REPOSITORY') || CloudRunnerOptions.getInput('GITHUB_REPO') || undefined;
}
static get branch(): string {
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 '';
}
}
// ### ### ###
// Cloud Runner parameters
// ### ### ###
static get buildPlatform(): string {
const input = CloudRunnerOptions.getInput('buildPlatform');
if (input) {
return input;
}
if (CloudRunnerOptions.providerStrategy !== 'local') {
return 'linux';
}
return ``;
}
static get cloudRunnerBranch(): string {
return CloudRunnerOptions.getInput('cloudRunnerBranch') || 'main';
}
static get providerStrategy(): string {
const provider =
CloudRunnerOptions.getInput('cloudRunnerCluster') || CloudRunnerOptions.getInput('providerStrategy');
if (Cli.isCliMode) {
return provider || 'aws';
}
return provider || 'local';
}
static get containerCpu(): string {
return CloudRunnerOptions.getInput('containerCpu') || `1024`;
}
static get containerMemory(): string {
return CloudRunnerOptions.getInput('containerMemory') || `3072`;
}
static get customJob(): string {
return CloudRunnerOptions.getInput('customJob') || '';
}
// ### ### ###
// Custom commands from files parameters
// ### ### ###
static get containerHookFiles(): string[] {
return CloudRunnerOptions.getInput('containerHookFiles')?.split(`,`) || [];
}
static get commandHookFiles(): string[] {
return CloudRunnerOptions.getInput('commandHookFiles')?.split(`,`) || [];
}
// ### ### ###
// Custom commands from yaml parameters
// ### ### ###
static get commandHooks(): string {
return CloudRunnerOptions.getInput('commandHooks') || '';
}
static get postBuildContainerHooks(): string {
return CloudRunnerOptions.getInput('postBuildContainerHooks') || '';
}
static get preBuildContainerHooks(): string {
return CloudRunnerOptions.getInput('preBuildContainerHooks') || '';
}
// ### ### ###
// Input override handling
// ### ### ###
static get pullInputList(): string[] {
return CloudRunnerOptions.getInput('pullInputList')?.split(`,`) || [];
}
static get inputPullCommand(): string {
const value = CloudRunnerOptions.getInput('inputPullCommand');
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 awsStackName() {
return CloudRunnerOptions.getInput('awsStackName') || 'game-ci';
}
// ### ### ###
// K8s
// ### ### ###
static get kubeConfig(): string {
return CloudRunnerOptions.getInput('kubeConfig') || '';
}
static get kubeVolume(): string {
return CloudRunnerOptions.getInput('kubeVolume') || '';
}
static get kubeVolumeSize(): string {
return CloudRunnerOptions.getInput('kubeVolumeSize') || '25Gi';
}
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`) === `true` ||
CloudRunnerOptions.getInput(`cloudRunnerDebug`) === `true` ||
CloudRunnerOptions.getInput(`cloudRunnerDebugTree`) === `true` ||
CloudRunnerOptions.getInput(`cloudRunnerDebugEnv`) === `true` ||
false
);
}
static get skipLfs(): boolean {
return CloudRunnerOptions.getInput(`skipLfs`) === `true`;
}
static get skipCache(): boolean {
return CloudRunnerOptions.getInput(`skipCache`) === `true`;
}
public static get asyncCloudRunner(): boolean {
return CloudRunnerOptions.getInput('asyncCloudRunner') === 'true';
}
public static get useLargePackages(): boolean {
return CloudRunnerOptions.getInput(`useLargePackages`) === `true`;
}
public static get useSharedBuilder(): boolean {
return CloudRunnerOptions.getInput(`useSharedBuilder`) === `true`;
}
public static get useCompressionStrategy(): boolean {
return CloudRunnerOptions.getInput(`useCompressionStrategy`) === `true`;
}
public static get useCleanupCron(): boolean {
return (CloudRunnerOptions.getInput(`useCleanupCron`) || 'true') === 'true';
}
// ### ### ###
// Retained Workspace
// ### ### ###
public static get maxRetainedWorkspaces(): string {
return CloudRunnerOptions.getInput(`maxRetainedWorkspaces`) || `0`;
}
// ### ### ###
// Garbage Collection
// ### ### ###
static get garbageMaxAge(): number {
return Number(CloudRunnerOptions.getInput(`garbageMaxAge`)) || 24;
}
}
export default CloudRunnerOptions;

View File

@@ -0,0 +1,67 @@
import Input from '../../input';
import { GenericInputReader } from '../../input-readers/generic-input-reader';
import CloudRunnerOptions from './cloud-runner-options';
const formatFunction = (value: string, arguments_: any[]) => {
for (const element of arguments_) {
value = value.replace(`{${element.key}}`, element.value);
}
return value;
};
class CloudRunnerQueryOverride {
static queryOverrides: { [key: string]: string } | undefined;
// TODO accept premade secret sources or custom secret source definition yamls
public static query(key: string, alternativeKey: string) {
if (CloudRunnerQueryOverride.queryOverrides && CloudRunnerQueryOverride.queryOverrides[key] !== undefined) {
return CloudRunnerQueryOverride.queryOverrides[key];
}
if (
CloudRunnerQueryOverride.queryOverrides &&
alternativeKey &&
CloudRunnerQueryOverride.queryOverrides[alternativeKey] !== undefined
) {
return CloudRunnerQueryOverride.queryOverrides[alternativeKey];
}
return;
}
private static shouldUseOverride(query: string) {
if (CloudRunnerOptions.inputPullCommand !== '') {
if (CloudRunnerOptions.pullInputList.length > 0) {
const doesInclude =
CloudRunnerOptions.pullInputList.includes(query) ||
CloudRunnerOptions.pullInputList.includes(Input.ToEnvVarFormat(query));
return doesInclude ? true : false;
} else {
return true;
}
}
}
private static async queryOverride(query: string) {
if (!this.shouldUseOverride(query)) {
throw new Error(`Should not be trying to run override query on ${query}`);
}
return await GenericInputReader.Run(
formatFunction(CloudRunnerOptions.inputPullCommand, [{ key: 0, value: query }]),
);
}
public static async PopulateQueryOverrideInput() {
const queries = CloudRunnerOptions.pullInputList;
CloudRunnerQueryOverride.queryOverrides = {};
for (const element of queries) {
if (CloudRunnerQueryOverride.shouldUseOverride(element)) {
CloudRunnerQueryOverride.queryOverrides[element] = await CloudRunnerQueryOverride.queryOverride(element);
}
}
}
}
export default CloudRunnerQueryOverride;

View File

@@ -1,6 +1,6 @@
class OrchestratorSecret {
class CloudRunnerSecret {
public ParameterKey!: string;
public EnvironmentVariable!: string;
public ParameterValue!: string;
}
export default OrchestratorSecret;
export default CloudRunnerSecret;

View File

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

View File

@@ -0,0 +1,13 @@
import CloudRunnerEnvironmentVariable from './cloud-runner-environment-variable';
import CloudRunnerSecret from './cloud-runner-secret';
export class CloudRunnerStepParameters {
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,105 @@
import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
import * as core from '@actions/core';
import * as SDK from 'aws-sdk';
import { BaseStackFormation } from './cloud-formations/base-stack-formation';
import crypto from 'node: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

@@ -21,7 +21,6 @@ export class AWSCloudFormationTemplates {
public static getSecretDefinitionTemplate(p1: string, p2: string) {
return `
Secrets:
- Name: '${p1}'
ValueFrom: !Ref ${p2}Secret
`;

View File

@@ -0,0 +1,16 @@
import CloudRunnerLogger from '../../services/core/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

@@ -1,34 +1,14 @@
import {
CloudFormation,
CreateStackCommand,
// eslint-disable-next-line import/named
CreateStackCommandInput,
DescribeStackResourcesCommand,
DescribeStacksCommand,
ListStacksCommand,
waitUntilStackCreateComplete,
} from '@aws-sdk/client-cloudformation';
import OrchestratorAWSTaskDef from './orchestrator-aws-task-def';
import OrchestratorSecret from '../../options/orchestrator-secret';
import * as SDK from 'aws-sdk';
import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def';
import CloudRunnerSecret from '../../options/cloud-runner-secret';
import { AWSCloudFormationTemplates } from './aws-cloud-formation-templates';
import OrchestratorLogger from '../../services/core/orchestrator-logger';
import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
import { AWSError } from './aws-error';
import Orchestrator from '../../orchestrator';
import CloudRunner from '../../cloud-runner';
import { CleanupCronFormation } from './cloud-formations/cleanup-cron-formation';
import OrchestratorOptions from '../../options/orchestrator-options';
import CloudRunnerOptions from '../../options/cloud-runner-options';
import { TaskDefinitionFormation } from './cloud-formations/task-definition-formation';
const DEFAULT_STACK_WAIT_TIME_SECONDS = 600;
function getStackWaitTime(): number {
const overrideValue = Number(process.env.ORCHESTRATOR_AWS_STACK_WAIT_TIME ?? '');
if (!Number.isNaN(overrideValue) && overrideValue > 0) {
return overrideValue;
}
return DEFAULT_STACK_WAIT_TIME_SECONDS;
}
export class AWSJobStack {
private baseStackName: string;
constructor(baseStackName: string) {
@@ -36,30 +16,30 @@ export class AWSJobStack {
}
public async setupCloudFormations(
CF: CloudFormation,
CF: SDK.CloudFormation,
buildGuid: string,
image: string,
entrypoint: string[],
commands: string,
mountdir: string,
workingdir: string,
secrets: OrchestratorSecret[],
): Promise<OrchestratorAWSTaskDef> {
secrets: CloudRunnerSecret[],
): Promise<CloudRunnerAWSTaskDef> {
const taskDefStackName = `${this.baseStackName}-${buildGuid}`;
let taskDefCloudFormation = AWSCloudFormationTemplates.readTaskCloudFormationTemplate();
taskDefCloudFormation = taskDefCloudFormation.replace(
`ContainerCpu:
Default: 1024`,
`ContainerCpu:
Default: ${Number.parseInt(Orchestrator.buildParameters.containerCpu)}`,
Default: ${Number.parseInt(CloudRunner.buildParameters.containerCpu)}`,
);
taskDefCloudFormation = taskDefCloudFormation.replace(
`ContainerMemory:
Default: 2048`,
`ContainerMemory:
Default: ${Number.parseInt(Orchestrator.buildParameters.containerMemory)}`,
Default: ${Number.parseInt(CloudRunner.buildParameters.containerMemory)}`,
);
if (!OrchestratorOptions.asyncOrchestrator) {
if (!CloudRunnerOptions.asyncCloudRunner) {
taskDefCloudFormation = AWSCloudFormationTemplates.insertAtTemplate(
taskDefCloudFormation,
'# template resources logstream',
@@ -133,13 +113,13 @@ export class AWSJobStack {
},
...secretsMappedToCloudFormationParameters,
];
OrchestratorLogger.log(
`Starting AWS job with memory: ${Orchestrator.buildParameters.containerMemory} cpu: ${Orchestrator.buildParameters.containerCpu}`,
CloudRunnerLogger.log(
`Starting AWS job with memory: ${CloudRunner.buildParameters.containerMemory} cpu: ${CloudRunner.buildParameters.containerCpu}`,
);
let previousStackExists = true;
while (previousStackExists) {
previousStackExists = false;
const stacks = await CF.send(new ListStacksCommand({}));
const stacks = await CF.listStacks().promise();
if (!stacks.StackSummaries) {
throw new Error('Faild to get stacks');
}
@@ -147,31 +127,22 @@ export class AWSJobStack {
const element = stacks.StackSummaries[index];
if (element.StackName === taskDefStackName && element.StackStatus !== 'DELETE_COMPLETE') {
previousStackExists = true;
OrchestratorLogger.log(`Previous stack still exists: ${JSON.stringify(element)}`);
CloudRunnerLogger.log(`Previous stack still exists: ${JSON.stringify(element)}`);
await new Promise((promise) => setTimeout(promise, 5000));
}
}
}
const createStackInput: CreateStackCommandInput = {
const createStackInput: SDK.CloudFormation.CreateStackInput = {
StackName: taskDefStackName,
TemplateBody: taskDefCloudFormation,
Capabilities: ['CAPABILITY_IAM'],
Parameters: parameters,
};
try {
const stackWaitTimeSeconds = getStackWaitTime();
OrchestratorLogger.log(
`Creating job aws formation ${taskDefStackName} (waiting up to ${stackWaitTimeSeconds}s for completion)`,
);
await CF.send(new CreateStackCommand(createStackInput));
await waitUntilStackCreateComplete(
{
client: CF,
maxWaitTime: stackWaitTimeSeconds,
},
{ StackName: taskDefStackName },
);
const describeStack = await CF.send(new DescribeStacksCommand({ StackName: taskDefStackName }));
CloudRunnerLogger.log(`Creating job aws formation ${taskDefStackName}`);
await CF.createStack(createStackInput).promise();
await CF.waitFor('stackCreateComplete', { StackName: taskDefStackName }).promise();
const describeStack = await CF.describeStacks({ StackName: taskDefStackName }).promise();
for (const parameter of parameters) {
if (!describeStack.Stacks?.[0].Parameters?.some((x) => x.ParameterKey === parameter.ParameterKey)) {
throw new Error(`Parameter ${parameter.ParameterKey} not found in stack`);
@@ -182,7 +153,7 @@ export class AWSJobStack {
throw error;
}
const createCleanupStackInput: CreateStackCommandInput = {
const createCleanupStackInput: SDK.CloudFormation.CreateStackInput = {
StackName: `${taskDefStackName}-cleanup`,
TemplateBody: CleanupCronFormation.formation,
Capabilities: ['CAPABILITY_IAM'],
@@ -201,7 +172,7 @@ export class AWSJobStack {
},
{
ParameterKey: 'BUILDGUID',
ParameterValue: Orchestrator.buildParameters.buildGuid,
ParameterValue: CloudRunner.buildParameters.buildGuid,
},
{
ParameterKey: 'EnvironmentName',
@@ -209,10 +180,10 @@ export class AWSJobStack {
},
],
};
if (OrchestratorOptions.useCleanupCron) {
if (CloudRunnerOptions.useCleanupCron) {
try {
OrchestratorLogger.log(`Creating job cleanup formation`);
await CF.send(new CreateStackCommand(createCleanupStackInput));
CloudRunnerLogger.log(`Creating job cleanup formation`);
await CF.createStack(createCleanupStackInput).promise();
// await CF.waitFor('stackCreateComplete', { StackName: createCleanupStackInput.StackName }).promise();
} catch (error) {
@@ -222,15 +193,12 @@ export class AWSJobStack {
}
const taskDefResources = (
await CF.send(
new DescribeStackResourcesCommand({
StackName: taskDefStackName,
}),
)
await CF.describeStackResources({
StackName: taskDefStackName,
}).promise()
).StackResources;
const baseResources = (await CF.send(new DescribeStackResourcesCommand({ StackName: this.baseStackName })))
.StackResources;
const baseResources = (await CF.describeStackResources({ StackName: this.baseStackName }).promise()).StackResources;
return {
taskDefStackName,

View File

@@ -0,0 +1,250 @@
import * as AWS from 'aws-sdk';
import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable';
import * as core from '@actions/core';
import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def';
import * as zlib from 'node:zlib';
import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
import { Input } from '../../..';
import CloudRunner from '../../cloud-runner';
import { CommandHookService } from '../../services/hooks/command-hook-service';
import { FollowLogStreamService } from '../../services/core/follow-log-stream-service';
import CloudRunnerOptions from '../../options/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 runParameters = {
cluster,
taskDefinition,
platformVersion: '1.4.0',
overrides: {
containerOverrides: [
{
name: taskDef.taskDefStackName,
environment,
command: ['-c', CommandHookService.ApplyHooksToCommands(commands, CloudRunner.buildParameters)],
},
],
},
launchType: 'FARGATE',
networkConfiguration: {
awsvpcConfiguration: {
subnets: [SubnetOne, SubnetTwo],
assignPublicIp: 'ENABLED',
securityGroups: [ContainerSecurityGroup],
},
},
};
if (JSON.stringify(runParameters.overrides.containerOverrides).length > 8192) {
CloudRunnerLogger.log(JSON.stringify(runParameters.overrides.containerOverrides, undefined, 4));
throw new Error(`Container Overrides length must be at most 8192`);
}
const task = await AWSTaskRunner.ECS.runTask(runParameters).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} Async:${
CloudRunnerOptions.asyncCloudRunner
}`,
);
if (CloudRunnerOptions.asyncCloudRunner) {
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);
let exitCode;
let containerState;
let taskData;
while (exitCode === undefined) {
await new Promise((resolve) => resolve(10000));
taskData = await AWSTaskRunner.describeTasks(cluster, taskArn);
containerState = taskData.containers?.[0];
exitCode = containerState?.exitCode;
}
CloudRunnerLogger.log(`Container State: ${JSON.stringify(containerState, undefined, 4)}`);
if (exitCode === undefined) {
CloudRunnerLogger.logWarning(`Undefined exitcode for container`);
}
const wasSuccessful = exitCode === 0;
if (wasSuccessful) {
CloudRunnerLogger.log(`Cloud runner job has finished successfully`);
return { output, shouldCleanup };
}
if (taskData?.stoppedReason === 'Essential container in task exited' && exitCode === 1) {
throw new Error('Container exited with code 1');
}
throw new Error(`Task failed`);
}
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.awsStackName}${AWSTaskRunner.encodedUnderscore}${CloudRunner.buildParameters.awsStackName}-${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: AWS.Kinesis.GetRecordsOutput,
iterator: string,
shouldReadLogs: boolean,
output: string,
shouldCleanup: boolean,
) {
if (records.Records.length > 0 && iterator) {
for (const record of records.Records) {
const json = JSON.parse(zlib.gunzipSync(Buffer.from(record.Data as string, 'base64')).toString('utf8'));
if (json.messageType === 'DATA_MESSAGE') {
for (const logEvent of json.logEvents) {
({ shouldReadLogs, shouldCleanup, output } = FollowLogStreamService.handleIteration(
logEvent.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: AWS.Kinesis.DescribeStreamOutput) {
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

@@ -1,9 +1,6 @@
import Orchestrator from '../../../orchestrator';
export class TaskDefinitionFormation {
public static readonly description: string = `Game CI Orchestrator Task Stack`;
public static get formation(): string {
return `AWSTemplateFormatVersion: 2010-09-09
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:
@@ -29,11 +26,11 @@ Parameters:
Default: 80
Description: What port number the application inside the docker container is binding to
ContainerCpu:
Default: ${Orchestrator.buildParameters.containerCpu}
Default: 1024
Type: Number
Description: How much CPU to give the container. 1024 is 1 CPU
ContainerMemory:
Default: ${Orchestrator.buildParameters.containerMemory}
Default: 4096
Type: Number
Description: How much memory in megabytes to give the container
BUILDGUID:
@@ -95,7 +92,7 @@ Resources:
EFSVolumeConfiguration:
FilesystemId:
'Fn::ImportValue': !Sub '${'${EnvironmentName}'}:EfsFileStorageId'
TransitEncryption: DISABLED
TransitEncryption: ENABLED
RequiresCompatibilities:
- FARGATE
ExecutionRoleArn:
@@ -127,7 +124,8 @@ Resources:
- SourceVolume: efs-data
ContainerPath: !Ref EFSMountDirectory
ReadOnly: false
# template secrets p3 - container def
Secrets:
# template secrets p3 - container def
LogConfiguration:
LogDriver: awslogs
Options:
@@ -137,7 +135,6 @@ Resources:
DependsOn:
- LogGroup
`;
}
public static streamLogs = `
SubscriptionFilter:
Type: 'AWS::Logs::SubscriptionFilter'

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

@@ -1,11 +1,11 @@
import { CloudFormation, DeleteStackCommand, waitUntilStackDeleteComplete } from '@aws-sdk/client-cloudformation';
import OrchestratorSecret from '../../options/orchestrator-secret';
import OrchestratorEnvironmentVariable from '../../options/orchestrator-environment-variable';
import OrchestratorAWSTaskDef from './orchestrator-aws-task-def';
import * as SDK from 'aws-sdk';
import CloudRunnerSecret from '../../options/cloud-runner-secret';
import CloudRunnerEnvironmentVariable from '../../options/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 OrchestratorLogger from '../../services/core/orchestrator-logger';
import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
import { AWSJobStack as AwsJobStack } from './aws-job-stack';
import { AWSBaseStack as AwsBaseStack } from './aws-base-stack';
import { Input } from '../../..';
@@ -13,20 +13,7 @@ import { GarbageCollectionService } from './services/garbage-collection-service'
import { ProviderResource } from '../provider-resource';
import { ProviderWorkflow } from '../provider-workflow';
import { TaskService } from './services/task-service';
import OrchestratorOptions from '../../options/orchestrator-options';
import { AwsClientFactory } from './aws-client-factory';
import ResourceTracking from '../../services/core/resource-tracking';
const DEFAULT_STACK_WAIT_TIME_SECONDS = 600;
function getStackWaitTime(): number {
const overrideValue = Number(process.env.ORCHESTRATOR_AWS_STACK_WAIT_TIME ?? '');
if (!Number.isNaN(overrideValue) && overrideValue > 0) {
return overrideValue;
}
return DEFAULT_STACK_WAIT_TIME_SECONDS;
}
import CloudRunnerOptions from '../../options/cloud-runner-options';
class AWSBuildEnvironment implements ProviderInterface {
private baseStackName: string;
@@ -70,6 +57,8 @@ class AWSBuildEnvironment implements ProviderInterface {
}
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
@@ -88,7 +77,7 @@ class AWSBuildEnvironment implements ProviderInterface {
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
) {
process.env.AWS_REGION = Input.region;
const CF = AwsClientFactory.getCloudFormation();
const CF = new SDK.CloudFormation();
await new AwsBaseStack(this.baseStackName).setupBaseStack(CF);
}
@@ -98,16 +87,15 @@ class AWSBuildEnvironment implements ProviderInterface {
commands: string,
mountdir: string,
workingdir: string,
environment: OrchestratorEnvironmentVariable[],
secrets: OrchestratorSecret[],
environment: CloudRunnerEnvironmentVariable[],
secrets: CloudRunnerSecret[],
): Promise<string> {
process.env.AWS_REGION = Input.region;
ResourceTracking.logAllocationSummary('aws workflow');
await ResourceTracking.logDiskUsageSnapshot('aws workflow (host)');
AwsClientFactory.getECS();
const CF = AwsClientFactory.getCloudFormation();
AwsClientFactory.getKinesis();
OrchestratorLogger.log(`AWS Region: ${CF.config.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();
const taskDef = await new AwsJobStack(this.baseStackName).setupCloudFormations(
@@ -124,53 +112,44 @@ class AWSBuildEnvironment implements ProviderInterface {
let postRunTaskTimeMs;
try {
const postSetupStacksTimeMs = Date.now();
OrchestratorLogger.log(`Setup job time: ${Math.floor((postSetupStacksTimeMs - startTimeMs) / 1000)}s`);
CloudRunnerLogger.log(`Setup job time: ${Math.floor((postSetupStacksTimeMs - startTimeMs) / 1000)}s`);
const { output, shouldCleanup } = await AwsTaskRunner.runTask(taskDef, environment, commands);
postRunTaskTimeMs = Date.now();
OrchestratorLogger.log(`Run job time: ${Math.floor((postRunTaskTimeMs - postSetupStacksTimeMs) / 1000)}s`);
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)
OrchestratorLogger.log(`Cleanup job time: ${Math.floor((postCleanupTimeMs - postRunTaskTimeMs) / 1000)}s`);
CloudRunnerLogger.log(`Cleanup job time: ${Math.floor((postCleanupTimeMs - postRunTaskTimeMs) / 1000)}s`);
return output;
} catch (error) {
OrchestratorLogger.log(`error running task ${error}`);
CloudRunnerLogger.log(`error running task ${error}`);
await this.cleanupResources(CF, taskDef);
throw error;
}
}
async cleanupResources(CF: CloudFormation, taskDef: OrchestratorAWSTaskDef) {
const stackWaitTimeSeconds = getStackWaitTime();
OrchestratorLogger.log(`Cleanup starting (waiting up to ${stackWaitTimeSeconds}s for stack deletion)`);
await CF.send(new DeleteStackCommand({ StackName: taskDef.taskDefStackName }));
if (OrchestratorOptions.useCleanupCron) {
await CF.send(new DeleteStackCommand({ StackName: `${taskDef.taskDefStackName}-cleanup` }));
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 waitUntilStackDeleteComplete(
{
client: CF,
maxWaitTime: stackWaitTimeSeconds,
},
{
StackName: taskDef.taskDefStackName,
},
);
await waitUntilStackDeleteComplete(
{
client: CF,
maxWaitTime: stackWaitTimeSeconds,
},
{
StackName: `${taskDef.taskDefStackName}-cleanup`,
},
);
OrchestratorLogger.log(`Deleted Stack: ${taskDef.taskDefStackName}`);
OrchestratorLogger.log('Cleanup complete');
await CF.waitFor('stackDeleteComplete', {
StackName: taskDef.taskDefStackName,
}).promise();
await CF.waitFor('stackDeleteComplete', {
StackName: `${taskDef.taskDefStackName}-cleanup`,
}).promise();
CloudRunnerLogger.log(`Deleted Stack: ${taskDef.taskDefStackName}`);
CloudRunnerLogger.log('Cleanup complete');
}
}
export default AWSBuildEnvironment;

View File

@@ -1,10 +1,7 @@
import { DeleteStackCommand, DescribeStackResourcesCommand } from '@aws-sdk/client-cloudformation';
import { DeleteLogGroupCommand } from '@aws-sdk/client-cloudwatch-logs';
import { StopTaskCommand } from '@aws-sdk/client-ecs';
import AWS from 'aws-sdk';
import Input from '../../../../input';
import OrchestratorLogger from '../../../services/core/orchestrator-logger';
import CloudRunnerLogger from '../../../services/core/cloud-runner-logger';
import { TaskService } from './task-service';
import { AwsClientFactory } from '../aws-client-factory';
export class GarbageCollectionService {
static isOlderThan1day(date: Date) {
@@ -15,9 +12,9 @@ export class GarbageCollectionService {
public static async cleanup(deleteResources = false, OneDayOlderOnly: boolean = false) {
process.env.AWS_REGION = Input.region;
const CF = AwsClientFactory.getCloudFormation();
const ecs = AwsClientFactory.getECS();
const cwl = AwsClientFactory.getCloudWatchLogs();
const CF = new AWS.CloudFormation();
const ecs = new AWS.ECS();
const cwl = new AWS.CloudWatchLogs();
const taskDefinitionsInUse = new Array();
const tasks = await TaskService.getTasks();
@@ -25,35 +22,33 @@ export class GarbageCollectionService {
const { taskElement, element } = task;
taskDefinitionsInUse.push(taskElement.taskDefinitionArn);
if (deleteResources && (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(taskElement.createdAt!))) {
OrchestratorLogger.log(`Stopping task ${taskElement.containers?.[0].name}`);
await ecs.send(new StopTaskCommand({ task: taskElement.taskArn || '', cluster: element }));
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.send(new DescribeStackResourcesCommand({ StackName: element.StackName }))).StackResources?.some(
(await CF.describeStackResources({ StackName: element.StackName }).promise()).StackResources?.some(
(x) => x.ResourceType === 'AWS::ECS::TaskDefinition' && taskDefinitionsInUse.includes(x.PhysicalResourceId),
)
) {
OrchestratorLogger.log(`Skipping ${element.StackName} - active task was running not deleting`);
CloudRunnerLogger.log(`Skipping ${element.StackName} - active task was running not deleting`);
return;
}
if (
deleteResources &&
(!OneDayOlderOnly || (element.CreationTime && GarbageCollectionService.isOlderThan1day(element.CreationTime)))
) {
if (deleteResources && (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(element.CreationTime))) {
if (element.StackName === 'game-ci' || element.TemplateDescription === 'Game-CI base stack') {
OrchestratorLogger.log(`Skipping ${element.StackName} ignore list`);
CloudRunnerLogger.log(`Skipping ${element.StackName} ignore list`);
return;
}
OrchestratorLogger.log(`Deleting ${element.StackName}`);
await CF.send(new DeleteStackCommand({ StackName: element.StackName }));
CloudRunnerLogger.log(`Deleting ${element.StackName}`);
const deleteStackInput: AWS.CloudFormation.DeleteStackInput = { StackName: element.StackName };
await CF.deleteStack(deleteStackInput).promise();
}
}
const logGroups = await TaskService.getLogGroups();
@@ -62,14 +57,14 @@ export class GarbageCollectionService {
deleteResources &&
(!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(new Date(element.creationTime!)))
) {
OrchestratorLogger.log(`Deleting ${element.logGroupName}`);
await cwl.send(new DeleteLogGroupCommand({ logGroupName: element.logGroupName || '' }));
CloudRunnerLogger.log(`Deleting ${element.logGroupName}`);
await cwl.deleteLogGroup({ logGroupName: element.logGroupName || '' }).promise();
}
}
const locks = await TaskService.getLocks();
for (const element of locks) {
OrchestratorLogger.log(`Lock: ${element.Key}`);
CloudRunnerLogger.log(`Lock: ${element.Key}`);
}
}
}

View File

@@ -0,0 +1,170 @@
import AWS from 'aws-sdk';
import Input from '../../../../input';
import CloudRunnerLogger from '../../../services/core/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';
import { StackSummaries } from 'aws-sdk/clients/cloudformation';
import { LogGroups } from 'aws-sdk/clients/cloudwatchlogs';
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: StackSummaries = [];
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: { taskElement: AWS.ECS.Task; element: string }[] = [];
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: LogGroups = [];
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.awsStackName,
};
const results = await s3.listObjects(listRequest).promise();
return results.Contents || [];
}
}

View File

@@ -1,20 +1,20 @@
import BuildParameters from '../../../build-parameters';
import OrchestratorEnvironmentVariable from '../../options/orchestrator-environment-variable';
import OrchestratorLogger from '../../services/core/orchestrator-logger';
import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable';
import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
import { ProviderInterface } from '../provider-interface';
import OrchestratorSecret from '../../options/orchestrator-secret';
import CloudRunnerSecret from '../../options/cloud-runner-secret';
import Docker from '../../../docker';
import { Action } from '../../..';
import { writeFileSync } from 'node:fs';
import Orchestrator from '../../orchestrator';
import CloudRunner from '../../cloud-runner';
import { ProviderResource } from '../provider-resource';
import { ProviderWorkflow } from '../provider-workflow';
import { OrchestratorSystem } from '../../services/core/orchestrator-system';
import { CloudRunnerSystem } from '../../services/core/cloud-runner-system';
import * as fs from 'node:fs';
import { CommandHookService } from '../../services/hooks/command-hook-service';
import { StringKeyValuePair } from '../../../shared-types';
class LocalDockerOrchestrator implements ProviderInterface {
class LocalDockerCloudRunner implements ProviderInterface {
public buildParameters!: BuildParameters;
listResources(): Promise<ProviderResource[]> {
@@ -41,6 +41,7 @@ class LocalDockerOrchestrator implements ProviderInterface {
return new Promise((result) => result(``));
}
async cleanupWorkflow(
buildGuid: string,
buildParameters: BuildParameters,
// eslint-disable-next-line no-unused-vars
branchName: string,
@@ -50,15 +51,15 @@ class LocalDockerOrchestrator implements ProviderInterface {
const { workspace } = Action;
if (
fs.existsSync(
`${workspace}/orchestrator-cache/cache/build/build-${buildParameters.buildGuid}.tar${
Orchestrator.buildParameters.useCompressionStrategy ? '.lz4' : ''
`${workspace}/cloud-runner-cache/cache/build/build-${buildParameters.buildGuid}.tar${
CloudRunner.buildParameters.useCompressionStrategy ? '.lz4' : ''
}`,
)
) {
await OrchestratorSystem.Run(`ls ${workspace}/orchestrator-cache/cache/build/`);
await OrchestratorSystem.Run(
`rm -r ${workspace}/orchestrator-cache/cache/build/build-${buildParameters.buildGuid}.tar${
Orchestrator.buildParameters.useCompressionStrategy ? '.lz4' : ''
await CloudRunnerSystem.Run(`ls ${workspace}/cloud-runner-cache/cache/build/`);
await CloudRunnerSystem.Run(
`rm -r ${workspace}/cloud-runner-cache/cache/build/build-${buildParameters.buildGuid}.tar${
CloudRunner.buildParameters.useCompressionStrategy ? '.lz4' : ''
}`,
);
}
@@ -80,47 +81,22 @@ class LocalDockerOrchestrator implements ProviderInterface {
commands: string,
mountdir: string,
workingdir: string,
environment: OrchestratorEnvironmentVariable[],
secrets: OrchestratorSecret[],
environment: CloudRunnerEnvironmentVariable[],
secrets: CloudRunnerSecret[],
): Promise<string> {
OrchestratorLogger.log(buildGuid);
OrchestratorLogger.log(commands);
CloudRunnerLogger.log(buildGuid);
CloudRunnerLogger.log(commands);
const { workspace, actionFolder } = Action;
const content: StringKeyValuePair[] = [];
for (const x of secrets) {
content.push({ name: x.EnvironmentVariable, value: x.ParameterValue });
}
// Replace localhost with host.docker.internal for LocalStack endpoints (similar to K8s)
// This allows Docker containers to access LocalStack running on the host
const endpointEnvironmentNames = new Set([
'AWS_S3_ENDPOINT',
'AWS_ENDPOINT',
'AWS_CLOUD_FORMATION_ENDPOINT',
'AWS_ECS_ENDPOINT',
'AWS_KINESIS_ENDPOINT',
'AWS_CLOUD_WATCH_LOGS_ENDPOINT',
'INPUT_AWSS3ENDPOINT',
'INPUT_AWSENDPOINT',
]);
for (const x of environment) {
let value = x.value;
if (
typeof value === 'string' &&
endpointEnvironmentNames.has(x.name) &&
(value.startsWith('http://localhost') || value.startsWith('http://127.0.0.1'))
) {
// Replace localhost with host.docker.internal so containers can access host services
value = value
.replace('http://localhost', 'http://host.docker.internal')
.replace('http://127.0.0.1', 'http://host.docker.internal');
OrchestratorLogger.log(`Replaced localhost with host.docker.internal for ${x.name}: ${value}`);
}
content.push({ name: x.name, value });
content.push({ name: x.name, value: x.value });
}
// if (this.buildParameters?.orchestratorIntegrationTests) {
// if (this.buildParameters?.cloudRunnerIntegrationTests) {
// core.info(JSON.stringify(content, undefined, 4));
// core.info(JSON.stringify(secrets, undefined, 4));
// core.info(JSON.stringify(environment, undefined, 4));
@@ -137,35 +113,27 @@ class LocalDockerOrchestrator implements ProviderInterface {
// core.info(JSON.stringify({ workspace, actionFolder, ...this.buildParameters, ...content }, undefined, 4));
const entrypointFilePath = `start.sh`;
// Use #!/bin/sh for POSIX compatibility (Alpine-based images like rclone/rclone don't have bash)
const fileContents = `#!/bin/sh
const fileContents = `#!/bin/bash
set -e
mkdir -p /github/workspace/orchestrator-cache
mkdir -p /github/workspace/cloud-runner-cache
mkdir -p /data/cache
cp -a /github/workspace/orchestrator-cache/. ${sharedFolder}
cp -a /github/workspace/cloud-runner-cache/. ${sharedFolder}
${CommandHookService.ApplyHooksToCommands(commands, this.buildParameters)}
# Only copy cache directory, exclude retained workspaces to avoid running out of disk space
if [ -d "${sharedFolder}cache" ]; then
cp -a ${sharedFolder}cache/. /github/workspace/orchestrator-cache/cache/ || true
fi
# Copy test files from /data/ root to workspace for test assertions
# This allows tests to write files to /data/ and have them available in the workspace
find ${sharedFolder} -maxdepth 1 -type f -name "test-*" -exec cp -a {} /github/workspace/orchestrator-cache/ \\; || true
cp -a ${sharedFolder}. /github/workspace/cloud-runner-cache/
`;
writeFileSync(`${workspace}/${entrypointFilePath}`, fileContents, {
flag: 'w',
});
if (Orchestrator.buildParameters.orchestratorDebug) {
OrchestratorLogger.log(`Running local-docker: \n ${fileContents}`);
if (CloudRunner.buildParameters.cloudRunnerDebug) {
CloudRunnerLogger.log(`Running local-docker: \n ${fileContents}`);
}
if (fs.existsSync(`${workspace}/orchestrator-cache`)) {
await OrchestratorSystem.Run(`ls ${workspace}/orchestrator-cache && du -sh ${workspace}/orchestrator-cache`);
if (fs.existsSync(`${workspace}/cloud-runner-cache`)) {
await CloudRunnerSystem.Run(`ls ${workspace}/cloud-runner-cache && du -sh ${workspace}/cloud-runner-cache`);
}
const exitCode = await Docker.run(
await Docker.run(
image,
{ workspace, actionFolder, ...this.buildParameters },
false,
@@ -182,15 +150,10 @@ find ${sharedFolder} -maxdepth 1 -type f -name "test-*" -exec cp -a {} /github/w
},
},
true,
false,
);
// Docker doesn't exit on fail now so adding this to ensure behavior is unchanged
// TODO: Is there a helpful way to consume the exit code or is it best to except
if (exitCode !== 0) {
throw new Error(`Build failed with exit code ${exitCode}`);
}
return myOutput;
}
}
export default LocalDockerOrchestrator;
export default LocalDockerCloudRunner;

View File

@@ -0,0 +1,310 @@
import * as k8s from '@kubernetes/client-node';
import { BuildParameters } from '../../..';
import * as core from '@actions/core';
import { ProviderInterface } from '../provider-interface';
import CloudRunnerSecret from '../../options/cloud-runner-secret';
import KubernetesStorage from './kubernetes-storage';
import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable';
import KubernetesTaskRunner from './kubernetes-task-runner';
import KubernetesSecret from './kubernetes-secret';
import KubernetesJobSpecFactory from './kubernetes-job-spec-factory';
import KubernetesServiceAccount from './kubernetes-service-account';
import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
import { CoreV1Api } from '@kubernetes/client-node';
import CloudRunner from '../../cloud-runner';
import { ProviderResource } from '../provider-resource';
import { ProviderWorkflow } from '../provider-workflow';
class Kubernetes implements ProviderInterface {
public static Instance: Kubernetes;
public kubeConfig!: k8s.KubeConfig;
public kubeClient!: k8s.CoreV1Api;
public kubeClientBatch!: k8s.BatchV1Api;
public buildGuid: string = '';
public buildParameters!: BuildParameters;
public pvcName: string = '';
public secretName: string = '';
public jobName: string = '';
public namespace!: string;
public podName: string = '';
public containerName: string = '';
public cleanupCronJobName: string = '';
public serviceAccountName: string = '';
// eslint-disable-next-line no-unused-vars
constructor(buildParameters: BuildParameters) {
Kubernetes.Instance = this;
this.kubeConfig = new k8s.KubeConfig();
this.kubeConfig.loadFromDefault();
this.kubeClient = this.kubeConfig.makeApiClient(k8s.CoreV1Api);
this.kubeClientBatch = this.kubeConfig.makeApiClient(k8s.BatchV1Api);
this.namespace = 'default';
CloudRunnerLogger.log('Loaded default Kubernetes configuration for this environment');
}
async listResources(): Promise<ProviderResource[]> {
const pods = await this.kubeClient.listNamespacedPod(this.namespace);
const serviceAccounts = await this.kubeClient.listNamespacedServiceAccount(this.namespace);
const secrets = await this.kubeClient.listNamespacedSecret(this.namespace);
const jobs = await this.kubeClientBatch.listNamespacedJob(this.namespace);
return [
...pods.body.items.map((x) => {
return { Name: x.metadata?.name || `` };
}),
...serviceAccounts.body.items.map((x) => {
return { Name: x.metadata?.name || `` };
}),
...secrets.body.items.map((x) => {
return { Name: x.metadata?.name || `` };
}),
...jobs.body.items.map((x) => {
return { Name: x.metadata?.name || `` };
}),
];
}
listWorkflow(): Promise<ProviderWorkflow[]> {
throw new Error('Method not implemented.');
}
watchWorkflow(): Promise<string> {
throw new Error('Method not implemented.');
}
garbageCollect(
// eslint-disable-next-line no-unused-vars
filter: string,
// eslint-disable-next-line no-unused-vars
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> {
return new Promise((result) => result(``));
}
public async setupWorkflow(
buildGuid: string,
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 }[],
) {
try {
this.buildParameters = buildParameters;
this.cleanupCronJobName = `unity-builder-cronjob-${buildParameters.buildGuid}`;
this.serviceAccountName = `service-account-${buildParameters.buildGuid}`;
await KubernetesServiceAccount.createServiceAccount(this.serviceAccountName, this.namespace, this.kubeClient);
} catch (error) {
throw error;
}
}
async runTaskInWorkflow(
buildGuid: string,
image: string,
commands: string,
mountdir: string,
workingdir: string,
environment: CloudRunnerEnvironmentVariable[],
secrets: CloudRunnerSecret[],
): Promise<string> {
try {
CloudRunnerLogger.log('Cloud Runner K8s workflow!');
// Setup
const id = BuildParameters.shouldUseRetainedWorkspaceMode(this.buildParameters)
? CloudRunner.lockedWorkspace
: this.buildParameters.buildGuid;
this.pvcName = `unity-builder-pvc-${id}`;
await KubernetesStorage.createPersistentVolumeClaim(
this.buildParameters,
this.pvcName,
this.kubeClient,
this.namespace,
);
this.buildGuid = buildGuid;
this.secretName = `build-credentials-${this.buildGuid}`;
this.jobName = `unity-builder-job-${this.buildGuid}`;
this.containerName = `main`;
await KubernetesSecret.createSecret(secrets, this.secretName, this.namespace, this.kubeClient);
let output = '';
try {
CloudRunnerLogger.log('Job does not exist');
await this.createJob(commands, image, mountdir, workingdir, environment, secrets);
CloudRunnerLogger.log('Watching pod until running');
await KubernetesTaskRunner.watchUntilPodRunning(this.kubeClient, this.podName, this.namespace);
CloudRunnerLogger.log('Pod running, streaming logs');
CloudRunnerLogger.log(
`Starting logs follow for pod: ${this.podName} container: ${this.containerName} namespace: ${this.namespace} pvc: ${this.pvcName} ${CloudRunner.buildParameters.kubeVolumeSize}/${CloudRunner.buildParameters.containerCpu}/${CloudRunner.buildParameters.containerMemory}`,
);
output += await KubernetesTaskRunner.runTask(
this.kubeConfig,
this.kubeClient,
this.jobName,
this.podName,
this.containerName,
this.namespace,
);
} catch (error: any) {
CloudRunnerLogger.log(`error running k8s workflow ${error}`);
await new Promise((resolve) => setTimeout(resolve, 3000));
CloudRunnerLogger.log(
JSON.stringify(
(await this.kubeClient.listNamespacedEvent(this.namespace)).body.items
.map((x) => {
return {
message: x.message || ``,
name: x.metadata.name || ``,
reason: x.reason || ``,
};
})
.filter((x) => x.name.includes(this.podName)),
undefined,
4,
),
);
await this.cleanupTaskResources();
throw error;
}
await this.cleanupTaskResources();
return output;
} catch (error) {
CloudRunnerLogger.log('Running job failed');
core.error(JSON.stringify(error, undefined, 4));
// await this.cleanupTaskResources();
throw error;
}
}
private async createJob(
commands: string,
image: string,
mountdir: string,
workingdir: string,
environment: CloudRunnerEnvironmentVariable[],
secrets: CloudRunnerSecret[],
) {
await this.createNamespacedJob(commands, image, mountdir, workingdir, environment, secrets);
const find = await Kubernetes.findPodFromJob(this.kubeClient, this.jobName, this.namespace);
this.setPodNameAndContainerName(find);
}
private async doesJobExist(name: string) {
const jobs = await this.kubeClientBatch.listNamespacedJob(this.namespace);
return jobs.body.items.some((x) => x.metadata?.name === name);
}
private async doesFailedJobExist() {
const podStatus = await this.kubeClient.readNamespacedPodStatus(this.podName, this.namespace);
return podStatus.body.status?.phase === `Failed`;
}
private async createNamespacedJob(
commands: string,
image: string,
mountdir: string,
workingdir: string,
environment: CloudRunnerEnvironmentVariable[],
secrets: CloudRunnerSecret[],
) {
for (let index = 0; index < 3; index++) {
try {
const jobSpec = KubernetesJobSpecFactory.getJobSpec(
commands,
image,
mountdir,
workingdir,
environment,
secrets,
this.buildGuid,
this.buildParameters,
this.secretName,
this.pvcName,
this.jobName,
k8s,
this.containerName,
);
await new Promise((promise) => setTimeout(promise, 15000));
const result = await this.kubeClientBatch.createNamespacedJob(this.namespace, jobSpec);
CloudRunnerLogger.log(`Build job created`);
await new Promise((promise) => setTimeout(promise, 5000));
CloudRunnerLogger.log('Job created');
return result.body.metadata?.name;
} catch (error) {
CloudRunnerLogger.log(`Error occured creating job: ${error}`);
throw error;
}
}
}
setPodNameAndContainerName(pod: k8s.V1Pod) {
this.podName = pod.metadata?.name || '';
this.containerName = pod.status?.containerStatuses?.[0].name || this.containerName;
}
async cleanupTaskResources() {
CloudRunnerLogger.log('cleaning up');
try {
await this.kubeClientBatch.deleteNamespacedJob(this.jobName, this.namespace);
await this.kubeClient.deleteNamespacedPod(this.podName, this.namespace);
} catch (error: any) {
CloudRunnerLogger.log(`Failed to cleanup`);
if (error.response.body.reason !== `NotFound`) {
CloudRunnerLogger.log(`Wasn't a not found error: ${error.response.body.reason}`);
throw error;
}
}
try {
await this.kubeClient.deleteNamespacedSecret(this.secretName, this.namespace);
} catch (error: any) {
CloudRunnerLogger.log(`Failed to cleanup secret`);
CloudRunnerLogger.log(error.response.body.reason);
}
CloudRunnerLogger.log('cleaned up Secret, Job and Pod');
CloudRunnerLogger.log('cleaning up finished');
}
async cleanupWorkflow(
buildGuid: string,
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 }[],
) {
if (BuildParameters.shouldUseRetainedWorkspaceMode(buildParameters)) {
return;
}
CloudRunnerLogger.log(`deleting PVC`);
try {
await this.kubeClient.deleteNamespacedPersistentVolumeClaim(this.pvcName, this.namespace);
await this.kubeClient.deleteNamespacedServiceAccount(this.serviceAccountName, this.namespace);
CloudRunnerLogger.log('cleaned up PVC and Service Account');
} catch (error: any) {
CloudRunnerLogger.log(`Cleanup failed ${JSON.stringify(error, undefined, 4)}`);
throw error;
}
}
static async findPodFromJob(kubeClient: CoreV1Api, jobName: string, namespace: string) {
const namespacedPods = await kubeClient.listNamespacedPod(namespace);
const pod = namespacedPods.body.items.find((x) => x.metadata?.labels?.['job-name'] === jobName);
if (pod === undefined) {
throw new Error("pod with job-name label doesn't exist");
}
return pod;
}
}
export default Kubernetes;

View File

@@ -0,0 +1,116 @@
import { V1EnvVar, V1EnvVarSource, V1SecretKeySelector } from '@kubernetes/client-node';
import BuildParameters from '../../../build-parameters';
import { CommandHookService } from '../../services/hooks/command-hook-service';
import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable';
import CloudRunnerSecret from '../../options/cloud-runner-secret';
import CloudRunner from '../../cloud-runner';
class KubernetesJobSpecFactory {
static getJobSpec(
command: string,
image: string,
mountdir: string,
workingDirectory: string,
environment: CloudRunnerEnvironmentVariable[],
secrets: CloudRunnerSecret[],
buildGuid: string,
buildParameters: BuildParameters,
secretName: string,
pvcName: string,
jobName: string,
k8s: any,
containerName: string,
) {
const job = new k8s.V1Job();
job.apiVersion = 'batch/v1';
job.kind = 'Job';
job.metadata = {
name: jobName,
labels: {
app: 'unity-builder',
buildGuid,
},
};
job.spec = {
ttlSecondsAfterFinished: 9999,
backoffLimit: 0,
template: {
spec: {
volumes: [
{
name: 'build-mount',
persistentVolumeClaim: {
claimName: pvcName,
},
},
],
containers: [
{
ttlSecondsAfterFinished: 9999,
name: containerName,
image,
command: ['/bin/sh'],
args: [
'-c',
`${CommandHookService.ApplyHooksToCommands(`${command}\nsleep 2m`, CloudRunner.buildParameters)}`,
],
workingDir: `${workingDirectory}`,
resources: {
requests: {
memory: `${Number.parseInt(buildParameters.containerMemory) / 1024}G` || '750M',
cpu: Number.parseInt(buildParameters.containerCpu) / 1024 || '1',
},
},
env: [
...environment.map((x) => {
const environmentVariable = new V1EnvVar();
environmentVariable.name = x.name;
environmentVariable.value = x.value;
return environmentVariable;
}),
...secrets.map((x) => {
const secret = new V1EnvVarSource();
secret.secretKeyRef = new V1SecretKeySelector();
secret.secretKeyRef.key = x.ParameterKey;
secret.secretKeyRef.name = secretName;
const environmentVariable = new V1EnvVar();
environmentVariable.name = x.EnvironmentVariable;
environmentVariable.valueFrom = secret;
return environmentVariable;
}),
],
volumeMounts: [
{
name: 'build-mount',
mountPath: `${mountdir}`,
},
],
lifecycle: {
preStop: {
exec: {
command: [
'bin/bash',
'-c',
`cd /data/builder/action/steps;
chmod +x /return_license.sh;
/return_license.sh;`,
],
},
},
},
},
],
restartPolicy: 'Never',
},
},
};
job.spec.template.spec.containers[0].resources.requests[`ephemeral-storage`] = '10Gi';
return job;
}
}
export default KubernetesJobSpecFactory;

View File

@@ -0,0 +1,23 @@
import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
import { CoreV1Api } from '@kubernetes/client-node';
class KubernetesPods {
public static async IsPodRunning(podName: string, namespace: string, kubeClient: CoreV1Api) {
const pods = (await kubeClient.listNamespacedPod(namespace)).body.items.filter((x) => podName === x.metadata?.name);
const running = pods.length > 0 && (pods[0].status?.phase === `Running` || pods[0].status?.phase === `Pending`);
const phase = pods[0]?.status?.phase || 'undefined status';
CloudRunnerLogger.log(`Getting pod status: ${phase}`);
if (phase === `Failed`) {
throw new Error(`K8s pod failed`);
}
return running;
}
public static async GetPodStatus(podName: string, namespace: string, kubeClient: CoreV1Api) {
const pods = (await kubeClient.listNamespacedPod(namespace)).body.items.find((x) => podName === x.metadata?.name);
const phase = pods?.status?.phase || 'undefined status';
return phase;
}
}
export default KubernetesPods;

View File

@@ -1,12 +1,12 @@
import { CoreV1Api } from '@kubernetes/client-node';
import OrchestratorSecret from '../../options/orchestrator-secret';
import CloudRunnerSecret from '../../options/cloud-runner-secret';
import * as k8s from '@kubernetes/client-node';
import OrchestratorLogger from '../../services/core/orchestrator-logger';
import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
import * as base64 from 'base-64';
class KubernetesSecret {
static async createSecret(
secrets: OrchestratorSecret[],
secrets: CloudRunnerSecret[],
secretName: string,
namespace: string,
kubeClient: CoreV1Api,
@@ -23,20 +23,20 @@ class KubernetesSecret {
for (const buildSecret of secrets) {
secret.data[buildSecret.ParameterKey] = base64.encode(buildSecret.ParameterValue);
}
OrchestratorLogger.log(`Creating secret: ${secretName}`);
CloudRunnerLogger.log(`Creating secret: ${secretName}`);
const existingSecrets = await kubeClient.listNamespacedSecret(namespace);
const mappedSecrets = existingSecrets.body.items.map((x) => {
return x.metadata?.name || `no name`;
});
OrchestratorLogger.log(
CloudRunnerLogger.log(
`ExistsAlready: ${mappedSecrets.includes(secretName)} SecretsCount: ${mappedSecrets.length}`,
);
await new Promise((promise) => setTimeout(promise, 15000));
await kubeClient.createNamespacedSecret(namespace, secret);
OrchestratorLogger.log('Created secret');
CloudRunnerLogger.log('Created secret');
} catch (error) {
OrchestratorLogger.log(`Created secret failed ${error}`);
CloudRunnerLogger.log(`Created secret failed ${error}`);
throw new Error(`Failed to create kubernetes secret`);
}
}

View File

@@ -9,7 +9,7 @@ class KubernetesServiceAccount {
serviceAccount.metadata = {
name: serviceAccountName,
};
serviceAccount.automountServiceAccountToken = true;
serviceAccount.automountServiceAccountToken = false;
return kubeClient.createNamespacedServiceAccount(namespace, serviceAccount);
}

View File

@@ -0,0 +1,116 @@
import { waitUntil } from 'async-wait-until';
import * as core from '@actions/core';
import * as k8s from '@kubernetes/client-node';
import BuildParameters from '../../../build-parameters';
import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
import { IncomingMessage } from 'node:http';
import GitHub from '../../../github';
class KubernetesStorage {
public static async createPersistentVolumeClaim(
buildParameters: BuildParameters,
pvcName: string,
kubeClient: k8s.CoreV1Api,
namespace: string,
) {
if (buildParameters.kubeVolume !== ``) {
CloudRunnerLogger.log(`Kube Volume was input was set ${buildParameters.kubeVolume} overriding ${pvcName}`);
pvcName = buildParameters.kubeVolume;
return;
}
const allPvc = (await kubeClient.listNamespacedPersistentVolumeClaim(namespace)).body.items;
const pvcList = allPvc.map((x) => x.metadata?.name);
CloudRunnerLogger.log(`Current PVCs in namespace ${namespace}`);
CloudRunnerLogger.log(JSON.stringify(pvcList, undefined, 4));
if (pvcList.includes(pvcName)) {
CloudRunnerLogger.log(`pvc ${pvcName} already exists`);
if (GitHub.githubInputEnabled) {
core.setOutput('volume', pvcName);
}
return;
}
CloudRunnerLogger.log(`Creating PVC ${pvcName} (does not exist)`);
const result = await KubernetesStorage.createPVC(pvcName, buildParameters, kubeClient, namespace);
await KubernetesStorage.handleResult(result, kubeClient, namespace, pvcName);
}
public static async getPVCPhase(kubeClient: k8s.CoreV1Api, name: string, namespace: string) {
try {
return (await kubeClient.readNamespacedPersistentVolumeClaim(name, namespace)).body.status?.phase;
} catch (error) {
core.error('Failed to get PVC phase');
core.error(JSON.stringify(error, undefined, 4));
throw error;
}
}
public static async watchUntilPVCNotPending(kubeClient: k8s.CoreV1Api, name: string, namespace: string) {
try {
CloudRunnerLogger.log(`watch Until PVC Not Pending ${name} ${namespace}`);
CloudRunnerLogger.log(`${await this.getPVCPhase(kubeClient, name, namespace)}`);
await waitUntil(
async () => {
return (await this.getPVCPhase(kubeClient, name, namespace)) === 'Pending';
},
{
timeout: 750000,
intervalBetweenAttempts: 15000,
},
);
} catch (error: any) {
core.error('Failed to watch PVC');
core.error(error.toString());
core.error(
`PVC Body: ${JSON.stringify(
(await kubeClient.readNamespacedPersistentVolumeClaim(name, namespace)).body,
undefined,
4,
)}`,
);
throw error;
}
}
private static async createPVC(
pvcName: string,
buildParameters: BuildParameters,
kubeClient: k8s.CoreV1Api,
namespace: string,
) {
const pvc = new k8s.V1PersistentVolumeClaim();
pvc.apiVersion = 'v1';
pvc.kind = 'PersistentVolumeClaim';
pvc.metadata = {
name: pvcName,
};
pvc.spec = {
accessModes: ['ReadWriteOnce'],
storageClassName: buildParameters.kubeStorageClass === '' ? 'standard' : buildParameters.kubeStorageClass,
resources: {
requests: {
storage: buildParameters.kubeVolumeSize,
},
},
};
const result = await kubeClient.createNamespacedPersistentVolumeClaim(namespace, pvc);
return result;
}
private static async handleResult(
result: { response: IncomingMessage; body: k8s.V1PersistentVolumeClaim },
kubeClient: k8s.CoreV1Api,
namespace: string,
pvcName: string,
) {
const name = result.body.metadata?.name || '';
CloudRunnerLogger.log(`PVC ${name} created`);
await this.watchUntilPVCNotPending(kubeClient, name, namespace);
CloudRunnerLogger.log(`PVC ${name} is ready and not pending`);
core.setOutput('volume', pvcName);
}
}
export default KubernetesStorage;

View File

@@ -0,0 +1,153 @@
import { CoreV1Api, KubeConfig } from '@kubernetes/client-node';
import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
import { waitUntil } from 'async-wait-until';
import { CloudRunnerSystem } from '../../services/core/cloud-runner-system';
import CloudRunner from '../../cloud-runner';
import KubernetesPods from './kubernetes-pods';
import { FollowLogStreamService } from '../../services/core/follow-log-stream-service';
class KubernetesTaskRunner {
static lastReceivedTimestamp: number = 0;
static readonly maxRetry: number = 3;
static lastReceivedMessage: string = ``;
static async runTask(
kubeConfig: KubeConfig,
kubeClient: CoreV1Api,
jobName: string,
podName: string,
containerName: string,
namespace: string,
) {
let output = '';
let shouldReadLogs = true;
let shouldCleanup = true;
let sinceTime = ``;
let retriesAfterFinish = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
await new Promise((resolve) => setTimeout(resolve, 3000));
const lastReceivedMessage =
KubernetesTaskRunner.lastReceivedTimestamp > 0
? `\nLast Log Message "${this.lastReceivedMessage}" ${this.lastReceivedTimestamp}`
: ``;
CloudRunnerLogger.log(
`Streaming logs from pod: ${podName} container: ${containerName} namespace: ${namespace} ${CloudRunner.buildParameters.kubeVolumeSize}/${CloudRunner.buildParameters.containerCpu}/${CloudRunner.buildParameters.containerMemory}\n${lastReceivedMessage}`,
);
if (KubernetesTaskRunner.lastReceivedTimestamp > 0) {
const currentDate = new Date(KubernetesTaskRunner.lastReceivedTimestamp);
const dateTimeIsoString = currentDate.toISOString();
sinceTime = ` --since-time="${dateTimeIsoString}"`;
}
let extraFlags = ``;
extraFlags += (await KubernetesPods.IsPodRunning(podName, namespace, kubeClient))
? ` -f -c ${containerName}`
: ` --previous`;
let lastMessageSeenIncludedInChunk = false;
let lastMessageSeen = false;
let logs;
try {
logs = await CloudRunnerSystem.Run(
`kubectl logs ${podName}${extraFlags} --timestamps${sinceTime}`,
false,
true,
);
} catch (error: any) {
await new Promise((resolve) => setTimeout(resolve, 3000));
const continueStreaming = await KubernetesPods.IsPodRunning(podName, namespace, kubeClient);
CloudRunnerLogger.log(`K8s logging error ${error} ${continueStreaming}`);
if (continueStreaming) {
continue;
}
if (retriesAfterFinish < KubernetesTaskRunner.maxRetry) {
retriesAfterFinish++;
continue;
}
throw error;
}
const splitLogs = logs.split(`\n`);
for (const chunk of splitLogs) {
if (
chunk.replace(/\s/g, ``) === KubernetesTaskRunner.lastReceivedMessage.replace(/\s/g, ``) &&
KubernetesTaskRunner.lastReceivedMessage.replace(/\s/g, ``) !== ``
) {
CloudRunnerLogger.log(`Previous log message found ${chunk}`);
lastMessageSeenIncludedInChunk = true;
}
}
for (const chunk of splitLogs) {
const newDate = Date.parse(`${chunk.toString().split(`Z `)[0]}Z`);
if (chunk.replace(/\s/g, ``) === KubernetesTaskRunner.lastReceivedMessage.replace(/\s/g, ``)) {
lastMessageSeen = true;
}
if (lastMessageSeenIncludedInChunk && !lastMessageSeen) {
continue;
}
const message = CloudRunner.buildParameters.cloudRunnerDebug ? chunk : chunk.split(`Z `)[1];
KubernetesTaskRunner.lastReceivedMessage = chunk;
KubernetesTaskRunner.lastReceivedTimestamp = newDate;
({ shouldReadLogs, shouldCleanup, output } = FollowLogStreamService.handleIteration(
message,
shouldReadLogs,
shouldCleanup,
output,
));
}
if (FollowLogStreamService.DidReceiveEndOfTransmission) {
CloudRunnerLogger.log('end of log stream');
break;
}
}
return output;
}
static async watchUntilPodRunning(kubeClient: CoreV1Api, podName: string, namespace: string) {
let success: boolean = false;
let message = ``;
CloudRunnerLogger.log(`Watching ${podName} ${namespace}`);
await waitUntil(
async () => {
const status = await kubeClient.readNamespacedPodStatus(podName, namespace);
const phase = status?.body.status?.phase;
success = phase === 'Running';
message = `Phase:${status.body.status?.phase} \n Reason:${
status.body.status?.conditions?.[0].reason || ''
} \n Message:${status.body.status?.conditions?.[0].message || ''}`;
// CloudRunnerLogger.log(
// JSON.stringify(
// (await kubeClient.listNamespacedEvent(namespace)).body.items
// .map((x) => {
// return {
// message: x.message || ``,
// name: x.metadata.name || ``,
// reason: x.reason || ``,
// };
// })
// .filter((x) => x.name.includes(podName)),
// undefined,
// 4,
// ),
// );
if (success || phase !== 'Pending') return true;
return false;
},
{
timeout: 2000000,
intervalBetweenAttempts: 15000,
},
);
if (!success) {
CloudRunnerLogger.log(message);
}
return success;
}
}
export default KubernetesTaskRunner;

View File

@@ -1,14 +1,13 @@
import BuildParameters from '../../../build-parameters';
import { OrchestratorSystem } from '../../services/core/orchestrator-system';
import OrchestratorEnvironmentVariable from '../../options/orchestrator-environment-variable';
import OrchestratorLogger from '../../services/core/orchestrator-logger';
import { CloudRunnerSystem } from '../../services/core/cloud-runner-system';
import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable';
import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
import { ProviderInterface } from '../provider-interface';
import OrchestratorSecret from '../../options/orchestrator-secret';
import CloudRunnerSecret from '../../options/cloud-runner-secret';
import { ProviderResource } from '../provider-resource';
import { ProviderWorkflow } from '../provider-workflow';
import { quote } from 'shell-quote';
class LocalOrchestrator implements ProviderInterface {
class LocalCloudRunner implements ProviderInterface {
listResources(): Promise<ProviderResource[]> {
throw new Error('Method not implemented.');
}
@@ -33,6 +32,8 @@ class LocalOrchestrator implements ProviderInterface {
throw new Error('Method not implemented.');
}
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
@@ -59,29 +60,15 @@ class LocalOrchestrator implements ProviderInterface {
// eslint-disable-next-line no-unused-vars
workingdir: string,
// eslint-disable-next-line no-unused-vars
environment: OrchestratorEnvironmentVariable[],
environment: CloudRunnerEnvironmentVariable[],
// eslint-disable-next-line no-unused-vars
secrets: OrchestratorSecret[],
secrets: CloudRunnerSecret[],
): Promise<string> {
OrchestratorLogger.log(image);
OrchestratorLogger.log(buildGuid);
OrchestratorLogger.log(commands);
CloudRunnerLogger.log(image);
CloudRunnerLogger.log(buildGuid);
CloudRunnerLogger.log(commands);
// On Windows, many built-in hooks use POSIX shell syntax. Execute via bash if available.
if (process.platform === 'win32') {
const inline = commands
.replace(/\r/g, '')
.split('\n')
.filter((x) => x.trim().length > 0)
.join(' ; ');
// Use shell-quote to properly escape the command string, preventing command injection
const bashWrapped = `bash -lc ${quote([inline])}`;
return await OrchestratorSystem.Run(bashWrapped);
}
return await OrchestratorSystem.Run(commands);
return await CloudRunnerSystem.Run(commands);
}
}
export default LocalOrchestrator;
export default LocalCloudRunner;

View File

@@ -1,11 +1,13 @@
import BuildParameters from '../../build-parameters';
import OrchestratorEnvironmentVariable from '../options/orchestrator-environment-variable';
import OrchestratorSecret from '../options/orchestrator-secret';
import CloudRunnerEnvironmentVariable from '../options/cloud-runner-environment-variable';
import CloudRunnerSecret from '../options/cloud-runner-secret';
import { ProviderResource } from './provider-resource';
import { ProviderWorkflow } from './provider-workflow';
export interface ProviderInterface {
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
@@ -35,9 +37,9 @@ export interface ProviderInterface {
// eslint-disable-next-line no-unused-vars
workingdir: string,
// eslint-disable-next-line no-unused-vars
environment: OrchestratorEnvironmentVariable[],
environment: CloudRunnerEnvironmentVariable[],
// eslint-disable-next-line no-unused-vars
secrets: OrchestratorSecret[],
secrets: CloudRunnerSecret[],
): Promise<string>;
garbageCollect(
// eslint-disable-next-line no-unused-vars

View File

@@ -1,12 +1,12 @@
import BuildParameters from '../../../build-parameters';
import OrchestratorEnvironmentVariable from '../../options/orchestrator-environment-variable';
import OrchestratorLogger from '../../services/core/orchestrator-logger';
import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable';
import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
import { ProviderInterface } from '../provider-interface';
import OrchestratorSecret from '../../options/orchestrator-secret';
import CloudRunnerSecret from '../../options/cloud-runner-secret';
import { ProviderResource } from '../provider-resource';
import { ProviderWorkflow } from '../provider-workflow';
class TestOrchestrator implements ProviderInterface {
class TestCloudRunner implements ProviderInterface {
listResources(): Promise<ProviderResource[]> {
throw new Error('Method not implemented.');
}
@@ -25,6 +25,8 @@ class TestOrchestrator implements ProviderInterface {
throw new Error('Method not implemented.');
}
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
@@ -51,17 +53,17 @@ class TestOrchestrator implements ProviderInterface {
// eslint-disable-next-line no-unused-vars
workingdir: string,
// eslint-disable-next-line no-unused-vars
environment: OrchestratorEnvironmentVariable[],
environment: CloudRunnerEnvironmentVariable[],
// eslint-disable-next-line no-unused-vars
secrets: OrchestratorSecret[],
secrets: CloudRunnerSecret[],
): Promise<string> {
OrchestratorLogger.log(image);
OrchestratorLogger.log(buildGuid);
OrchestratorLogger.log(commands);
CloudRunnerLogger.log(image);
CloudRunnerLogger.log(buildGuid);
CloudRunnerLogger.log(commands);
return await new Promise((result) => {
result(commands);
});
}
}
export default TestOrchestrator;
export default TestCloudRunner;

View File

@@ -0,0 +1,181 @@
import { assert } from 'node:console';
import fs from 'node:fs';
import path from 'node:path';
import CloudRunner from '../cloud-runner';
import CloudRunnerLogger from '../services/core/cloud-runner-logger';
import { CloudRunnerFolders } from '../options/cloud-runner-folders';
import { CloudRunnerSystem } from '../services/core/cloud-runner-system';
import { LfsHashing } from '../services/utility/lfs-hashing';
import { RemoteClientLogger } from './remote-client-logger';
import { Cli } from '../../cli/cli';
import { CliFunction } from '../../cli/cli-functions-repository';
// eslint-disable-next-line github/no-then
const fileExists = async (fpath: fs.PathLike) => !!(await fs.promises.stat(fpath).catch(() => false));
export class Caching {
@CliFunction(`cache-push`, `push to cache`)
static async cachePush() {
try {
const buildParameter = JSON.parse(process.env.BUILD_PARAMETERS || '{}');
CloudRunner.buildParameters = buildParameter;
await Caching.PushToCache(
Cli.options!['cachePushTo'],
Cli.options!['cachePushFrom'],
Cli.options!['artifactName'] || '',
);
} catch (error: any) {
CloudRunnerLogger.log(`${error}`);
}
}
@CliFunction(`cache-pull`, `pull from cache`)
static async cachePull() {
try {
const buildParameter = JSON.parse(process.env.BUILD_PARAMETERS || '{}');
CloudRunner.buildParameters = buildParameter;
await Caching.PullFromCache(
Cli.options!['cachePushFrom'],
Cli.options!['cachePushTo'],
Cli.options!['artifactName'] || '',
);
} catch (error: any) {
CloudRunnerLogger.log(`${error}`);
}
}
public static async PushToCache(cacheFolder: string, sourceFolder: string, cacheArtifactName: string) {
CloudRunnerLogger.log(`Pushing to cache ${sourceFolder}`);
cacheArtifactName = cacheArtifactName.replace(' ', '');
const startPath = process.cwd();
let compressionSuffix = '';
if (CloudRunner.buildParameters.useCompressionStrategy === true) {
compressionSuffix = `.lz4`;
}
CloudRunnerLogger.log(`Compression: ${CloudRunner.buildParameters.useCompressionStrategy} ${compressionSuffix}`);
try {
if (!(await fileExists(cacheFolder))) {
await CloudRunnerSystem.Run(`mkdir -p ${cacheFolder}`);
}
process.chdir(path.resolve(sourceFolder, '..'));
if (CloudRunner.buildParameters.cloudRunnerDebug === true) {
CloudRunnerLogger.log(
`Hashed cache folder ${await LfsHashing.hashAllFiles(sourceFolder)} ${sourceFolder} ${path.basename(
sourceFolder,
)}`,
);
}
const contents = await fs.promises.readdir(path.basename(sourceFolder));
CloudRunnerLogger.log(
`There is ${contents.length} files/dir in the source folder ${path.basename(sourceFolder)}`,
);
if (contents.length === 0) {
CloudRunnerLogger.log(
`Did not push source folder to cache because it was empty ${path.basename(sourceFolder)}`,
);
process.chdir(`${startPath}`);
return;
}
await CloudRunnerSystem.Run(
`tar -cf ${cacheArtifactName}.tar${compressionSuffix} ${path.basename(sourceFolder)}`,
);
await CloudRunnerSystem.Run(`du ${cacheArtifactName}.tar${compressionSuffix}`);
assert(await fileExists(`${cacheArtifactName}.tar${compressionSuffix}`), 'cache archive exists');
assert(await fileExists(path.basename(sourceFolder)), 'source folder exists');
await CloudRunnerSystem.Run(`mv ${cacheArtifactName}.tar${compressionSuffix} ${cacheFolder}`);
RemoteClientLogger.log(`moved cache entry ${cacheArtifactName} to ${cacheFolder}`);
assert(
await fileExists(`${path.join(cacheFolder, cacheArtifactName)}.tar${compressionSuffix}`),
'cache archive exists inside cache folder',
);
} catch (error) {
process.chdir(`${startPath}`);
throw error;
}
process.chdir(`${startPath}`);
}
public static async PullFromCache(cacheFolder: string, destinationFolder: string, cacheArtifactName: string = ``) {
CloudRunnerLogger.log(`Pulling from cache ${destinationFolder} ${CloudRunner.buildParameters.skipCache}`);
if (`${CloudRunner.buildParameters.skipCache}` === `true`) {
CloudRunnerLogger.log(`Skipping cache debugSkipCache is true`);
return;
}
cacheArtifactName = cacheArtifactName.replace(' ', '');
let compressionSuffix = '';
if (CloudRunner.buildParameters.useCompressionStrategy === true) {
compressionSuffix = `.lz4`;
}
const startPath = process.cwd();
RemoteClientLogger.log(`Caching for (lz4 ${compressionSuffix}) ${path.basename(destinationFolder)}`);
try {
if (!(await fileExists(cacheFolder))) {
await fs.promises.mkdir(cacheFolder);
}
if (!(await fileExists(destinationFolder))) {
await fs.promises.mkdir(destinationFolder);
}
const latestInBranch = await (
await CloudRunnerSystem.Run(`ls -t "${cacheFolder}" | grep .tar${compressionSuffix}$ | head -1`)
)
.replace(/\n/g, ``)
.replace(`.tar${compressionSuffix}`, '');
process.chdir(cacheFolder);
const cacheSelection =
cacheArtifactName !== `` && (await fileExists(`${cacheArtifactName}.tar${compressionSuffix}`))
? cacheArtifactName
: latestInBranch;
await CloudRunnerLogger.log(`cache key ${cacheArtifactName} selection ${cacheSelection}`);
if (await fileExists(`${cacheSelection}.tar${compressionSuffix}`)) {
const resultsFolder = `results${CloudRunner.buildParameters.buildGuid}`;
await CloudRunnerSystem.Run(`mkdir -p ${resultsFolder}`);
RemoteClientLogger.log(`cache item exists ${cacheFolder}/${cacheSelection}.tar${compressionSuffix}`);
const fullResultsFolder = path.join(cacheFolder, resultsFolder);
await CloudRunnerSystem.Run(`tar -xf ${cacheSelection}.tar${compressionSuffix} -C ${fullResultsFolder}`);
RemoteClientLogger.log(`cache item extracted to ${fullResultsFolder}`);
assert(await fileExists(fullResultsFolder), `cache extraction results folder exists`);
const destinationParentFolder = path.resolve(destinationFolder, '..');
if (await fileExists(destinationFolder)) {
await fs.promises.rmdir(destinationFolder, { recursive: true });
}
await CloudRunnerSystem.Run(
`mv "${path.join(fullResultsFolder, path.basename(destinationFolder))}" "${destinationParentFolder}"`,
);
const contents = await fs.promises.readdir(
path.join(destinationParentFolder, path.basename(destinationFolder)),
);
CloudRunnerLogger.log(
`There is ${contents.length} files/dir in the cache pulled contents for ${path.basename(destinationFolder)}`,
);
} else {
RemoteClientLogger.logWarning(`cache item ${cacheArtifactName} doesn't exist ${destinationFolder}`);
if (cacheSelection !== ``) {
RemoteClientLogger.logWarning(
`cache item ${cacheArtifactName}.tar${compressionSuffix} doesn't exist ${destinationFolder}`,
);
throw new Error(`Failed to get cache item, but cache hit was found: ${cacheSelection}`);
}
}
} catch (error) {
process.chdir(startPath);
throw error;
}
process.chdir(startPath);
}
public static async handleCachePurging() {
if (process.env.PURGE_REMOTE_BUILDER_CACHE !== undefined) {
RemoteClientLogger.log(`purging ${CloudRunnerFolders.purgeRemoteCaching}`);
fs.promises.rmdir(CloudRunnerFolders.cacheFolder, { recursive: true });
}
}
}

View File

@@ -0,0 +1,185 @@
import fs from 'node:fs';
import CloudRunner from '../cloud-runner';
import { CloudRunnerFolders } from '../options/cloud-runner-folders';
import { Caching } from './caching';
import { LfsHashing } from '../services/utility/lfs-hashing';
import { RemoteClientLogger } from './remote-client-logger';
import path from 'node:path';
import { assert } from 'node:console';
import CloudRunnerLogger from '../services/core/cloud-runner-logger';
import { CliFunction } from '../../cli/cli-functions-repository';
import { CloudRunnerSystem } from '../services/core/cloud-runner-system';
import YAML from 'yaml';
import GitHub from '../../github';
import BuildParameters from '../../build-parameters';
export class RemoteClient {
@CliFunction(`remote-cli-pre-build`, `sets up a repository, usually before a game-ci build`)
static async runRemoteClientJob() {
CloudRunnerLogger.log(`bootstrap game ci cloud runner...`);
if (!(await RemoteClient.handleRetainedWorkspace())) {
await RemoteClient.bootstrapRepository();
}
await RemoteClient.runCustomHookFiles(`before-build`);
}
static async runCustomHookFiles(hookLifecycle: string) {
RemoteClientLogger.log(`RunCustomHookFiles: ${hookLifecycle}`);
const gameCiCustomHooksPath = path.join(CloudRunnerFolders.repoPathAbsolute, `game-ci`, `hooks`);
try {
const files = fs.readdirSync(gameCiCustomHooksPath);
for (const file of files) {
const fileContents = fs.readFileSync(path.join(gameCiCustomHooksPath, file), `utf8`);
const fileContentsObject = YAML.parse(fileContents.toString());
if (fileContentsObject.hook === hookLifecycle) {
RemoteClientLogger.log(`Active Hook File ${file} \n \n file contents: \n ${fileContents}`);
await CloudRunnerSystem.Run(fileContentsObject.commands);
}
}
} catch (error) {
RemoteClientLogger.log(JSON.stringify(error, undefined, 4));
}
}
public static async bootstrapRepository() {
await CloudRunnerSystem.Run(
`mkdir -p ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)}`,
);
await CloudRunnerSystem.Run(
`mkdir -p ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.cacheFolderForCacheKeyFull)}`,
);
await RemoteClient.cloneRepoWithoutLFSFiles();
await RemoteClient.replaceLargePackageReferencesWithSharedReferences();
await RemoteClient.sizeOfFolder(
'repo before lfs cache pull',
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute),
);
const lfsHashes = await LfsHashing.createLFSHashFiles();
if (fs.existsSync(CloudRunnerFolders.libraryFolderAbsolute)) {
RemoteClientLogger.logWarning(`!Warning!: The Unity library was included in the git repository`);
}
await Caching.PullFromCache(
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsCacheFolderFull),
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsFolderAbsolute),
`${lfsHashes.lfsGuidSum}`,
);
await RemoteClient.sizeOfFolder('repo after lfs cache pull', CloudRunnerFolders.repoPathAbsolute);
await RemoteClient.pullLatestLFS();
await RemoteClient.sizeOfFolder('repo before lfs git pull', CloudRunnerFolders.repoPathAbsolute);
await Caching.PushToCache(
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsCacheFolderFull),
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsFolderAbsolute),
`${lfsHashes.lfsGuidSum}`,
);
await Caching.PullFromCache(
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.libraryCacheFolderFull),
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.libraryFolderAbsolute),
);
await RemoteClient.sizeOfFolder('repo after library cache pull', CloudRunnerFolders.repoPathAbsolute);
await Caching.handleCachePurging();
}
private static async sizeOfFolder(message: string, folder: string) {
if (CloudRunner.buildParameters.cloudRunnerDebug) {
CloudRunnerLogger.log(`Size of ${message}`);
await CloudRunnerSystem.Run(`du -sh ${folder}`);
}
}
private static async cloneRepoWithoutLFSFiles() {
process.chdir(`${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`);
if (
fs.existsSync(CloudRunnerFolders.repoPathAbsolute) &&
!fs.existsSync(path.join(CloudRunnerFolders.repoPathAbsolute, `.git`))
) {
await CloudRunnerSystem.Run(`rm -r ${CloudRunnerFolders.repoPathAbsolute}`);
CloudRunnerLogger.log(`${CloudRunnerFolders.repoPathAbsolute} repo exists, but no git folder, cleaning up`);
}
if (
BuildParameters.shouldUseRetainedWorkspaceMode(CloudRunner.buildParameters) &&
fs.existsSync(path.join(CloudRunnerFolders.repoPathAbsolute, `.git`))
) {
process.chdir(CloudRunnerFolders.repoPathAbsolute);
RemoteClientLogger.log(
`${
CloudRunnerFolders.repoPathAbsolute
} repo exists - skipping clone - retained workspace mode ${BuildParameters.shouldUseRetainedWorkspaceMode(
CloudRunner.buildParameters,
)}`,
);
await CloudRunnerSystem.Run(`git fetch && git reset --hard ${CloudRunner.buildParameters.gitSha}`);
return;
}
RemoteClientLogger.log(`Initializing source repository for cloning with caching of LFS files`);
await CloudRunnerSystem.Run(`git config --global advice.detachedHead false`);
RemoteClientLogger.log(`Cloning the repository being built:`);
await CloudRunnerSystem.Run(`git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"`);
await CloudRunnerSystem.Run(`git config --global filter.lfs.process "git-lfs filter-process --skip"`);
try {
await CloudRunnerSystem.Run(
`git clone ${CloudRunnerFolders.targetBuildRepoUrl} ${path.basename(CloudRunnerFolders.repoPathAbsolute)}`,
);
} catch (error: any) {
throw error;
}
process.chdir(CloudRunnerFolders.repoPathAbsolute);
await CloudRunnerSystem.Run(`git lfs install`);
assert(fs.existsSync(`.git`), 'git folder exists');
RemoteClientLogger.log(`${CloudRunner.buildParameters.branch}`);
if (CloudRunner.buildParameters.gitSha !== undefined) {
await CloudRunnerSystem.Run(`git checkout ${CloudRunner.buildParameters.gitSha}`);
} else {
await CloudRunnerSystem.Run(`git checkout ${CloudRunner.buildParameters.branch}`);
RemoteClientLogger.log(`buildParameter Git Sha is empty`);
}
assert(fs.existsSync(path.join(`.git`, `lfs`)), 'LFS folder should not exist before caching');
RemoteClientLogger.log(`Checked out ${CloudRunner.buildParameters.branch}`);
}
static async replaceLargePackageReferencesWithSharedReferences() {
CloudRunnerLogger.log(`Use Shared Pkgs ${CloudRunner.buildParameters.useLargePackages}`);
GitHub.updateGitHubCheck(`Use Shared Pkgs ${CloudRunner.buildParameters.useLargePackages}`, ``);
if (CloudRunner.buildParameters.useLargePackages) {
const filePath = path.join(CloudRunnerFolders.projectPathAbsolute, `Packages/manifest.json`);
let manifest = fs.readFileSync(filePath, 'utf8');
manifest = manifest.replace(/LargeContent/g, '../../../LargeContent');
fs.writeFileSync(filePath, manifest);
CloudRunnerLogger.log(`Package Manifest \n ${manifest}`);
GitHub.updateGitHubCheck(`Package Manifest \n ${manifest}`, ``);
}
}
private static async pullLatestLFS() {
process.chdir(CloudRunnerFolders.repoPathAbsolute);
await CloudRunnerSystem.Run(`git config --global filter.lfs.smudge "git-lfs smudge -- %f"`);
await CloudRunnerSystem.Run(`git config --global filter.lfs.process "git-lfs filter-process"`);
if (!CloudRunner.buildParameters.skipLfs) {
await CloudRunnerSystem.Run(`git lfs pull`);
RemoteClientLogger.log(`pulled latest LFS files`);
assert(fs.existsSync(CloudRunnerFolders.lfsFolderAbsolute));
}
}
static async handleRetainedWorkspace() {
RemoteClientLogger.log(
`Retained Workspace: ${BuildParameters.shouldUseRetainedWorkspaceMode(CloudRunner.buildParameters)}`,
);
if (
BuildParameters.shouldUseRetainedWorkspaceMode(CloudRunner.buildParameters) &&
fs.existsSync(CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)) &&
fs.existsSync(CloudRunnerFolders.ToLinuxFolder(path.join(CloudRunnerFolders.repoPathAbsolute, `.git`)))
) {
CloudRunnerLogger.log(`Retained Workspace Already Exists!`);
process.chdir(CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute));
await CloudRunnerSystem.Run(`git fetch`);
await CloudRunnerSystem.Run(`git lfs pull`);
await CloudRunnerSystem.Run(`git reset --hard "${CloudRunner.buildParameters.gitSha}"`);
await CloudRunnerSystem.Run(`git checkout ${CloudRunner.buildParameters.gitSha}`);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,19 @@
import CloudRunnerLogger from '../services/core/cloud-runner-logger';
export class RemoteClientLogger {
public static log(message: string) {
CloudRunnerLogger.log(`[Client] ${message}`);
}
public static logCliError(message: string) {
CloudRunnerLogger.log(`[Client][Error] ${message}`);
}
public static logCliDiagnostic(message: string) {
CloudRunnerLogger.log(`[Client][Diagnostic] ${message}`);
}
public static logWarning(message: string) {
CloudRunnerLogger.logWarning(message);
}
}

View File

@@ -1,6 +1,6 @@
import * as core from '@actions/core';
class OrchestratorLogger {
class CloudRunnerLogger {
private static timestamp: number;
private static globalTimestamp: number;
@@ -44,4 +44,4 @@ class OrchestratorLogger {
return Date.now();
}
}
export default OrchestratorLogger;
export default CloudRunnerLogger;

View File

@@ -1,9 +1,9 @@
import { exec } from 'child_process';
import { RemoteClientLogger } from '../../remote-client/remote-client-logger';
export class OrchestratorSystem {
export class CloudRunnerSystem {
public static async RunAndReadLines(command: string): Promise<string[]> {
const result = await OrchestratorSystem.Run(command, false, true);
const result = await CloudRunnerSystem.Run(command, false, true);
return result
.split(`\n`)
@@ -16,13 +16,7 @@ export class OrchestratorSystem {
});
}
public static async Run(
command: string,
suppressError = false,
suppressLogs = false,
// eslint-disable-next-line no-unused-vars
outputCallback?: (output: string) => void,
) {
public static async Run(command: string, suppressError = false, suppressLogs = false) {
for (const element of command.split(`\n`)) {
if (!suppressLogs) {
RemoteClientLogger.log(element);
@@ -31,7 +25,7 @@ export class OrchestratorSystem {
return await new Promise<string>((promise, throwError) => {
let output = '';
const child = exec(command, { maxBuffer: 1024 * 10000 }, (error, stdout, stderr) => {
const child = exec(command, (error, stdout, stderr) => {
if (!suppressError && error) {
RemoteClientLogger.log(error.toString());
throwError(error);
@@ -44,9 +38,6 @@ export class OrchestratorSystem {
output += diagnosticOutput;
}
const outputChunk = `${stdout}`;
if (outputCallback) {
outputCallback(outputChunk);
}
output += outputChunk;
});
child.on('close', (code) => {

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