Compare commits
35 Commits
Author | SHA1 | Date |
---|---|---|
|
cfdebb67c1 | |
|
ab64768ceb | |
|
00fa0d3772 | |
|
d587557287 | |
|
6e0bf17345 | |
|
2822af505e | |
|
8ec161b981 | |
|
88a89c94a0 | |
|
f7f3f70c57 | |
|
c6c8236152 | |
|
9e91ca9749 | |
|
9cd9f7e0e7 | |
|
0b822c28fb | |
|
65607f9ebb | |
|
a1ebdb7abd | |
|
3b26780ddf | |
|
819c2511e0 | |
|
81ed299e10 | |
|
9d6bdcbdc5 | |
|
3ae9ec8536 | |
|
83c85328dd | |
|
b11b6a6f2c | |
|
461ecf7cea | |
|
f2250e958e | |
|
dd427466ce | |
|
0c16aab353 | |
|
fc0a52b805 | |
|
e820c9ce7b | |
|
f4d2cceeb5 | |
|
4ae184ca89 | |
|
082ea39498 | |
|
e73b48fb38 | |
|
2800d14403 | |
|
5ba81971e2 | |
|
ff23166e30 |
|
@ -1,22 +1,11 @@
|
|||
{
|
||||
"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
|
||||
},
|
||||
|
@ -25,7 +14,8 @@
|
|||
"env": {
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"jest/globals": true
|
||||
"jest/globals": true,
|
||||
"es2020": true
|
||||
},
|
||||
"rules": {
|
||||
// Error out for code formatting errors
|
||||
|
@ -33,10 +23,7 @@
|
|||
// Namespaces or sometimes needed
|
||||
"import/no-namespace": "off",
|
||||
// Properly format comments
|
||||
"spaced-comment": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"spaced-comment": ["error", "always"],
|
||||
"lines-around-comment": [
|
||||
"error",
|
||||
{
|
||||
|
@ -71,12 +58,7 @@
|
|||
// 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
|
||||
|
|
|
@ -12,6 +12,9 @@
|
|||
|
||||
#### 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
|
||||
|
|
|
@ -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@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.requestActivationFile.outputs.filePath }}
|
||||
path: ${{ steps.requestActivationFile.outputs.filePath }}
|
||||
|
|
|
@ -18,13 +18,19 @@ jobs:
|
|||
projectPath:
|
||||
- test-project
|
||||
unityVersion:
|
||||
- 2021.3.32f1
|
||||
- 2021.3.45f1
|
||||
- 2022.3.13f1
|
||||
- 2023.1.19f1
|
||||
- 2023.2.2f1
|
||||
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:
|
||||
###########################
|
||||
|
@ -37,7 +43,7 @@ jobs:
|
|||
###########################
|
||||
# Cache #
|
||||
###########################
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ matrix.projectPath }}/Library
|
||||
key: Library-${{ matrix.projectPath }}-macos-${{ matrix.targetPlatform }}
|
||||
|
@ -60,10 +66,13 @@ 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
|
||||
|
@ -71,8 +80,8 @@ jobs:
|
|||
###########################
|
||||
# Upload #
|
||||
###########################
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Build MacOS (${{ matrix.unityVersion }})
|
||||
name: Build ${{ matrix.targetPlatform }} on MacOS (${{ matrix.unityVersion }})${{ matrix.buildProfile && ' With Build Profile' || '' }}
|
||||
path: build
|
||||
retention-days: 14
|
||||
|
|
|
@ -36,7 +36,8 @@ env:
|
|||
|
||||
jobs:
|
||||
buildForAllPlatformsUbuntu:
|
||||
name: ${{ matrix.targetPlatform }} on ${{ matrix.unityVersion }}
|
||||
name:
|
||||
"${{ matrix.targetPlatform }} on ${{ matrix.unityVersion}}${{startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }}"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
@ -49,16 +50,60 @@ jobs:
|
|||
unityVersion:
|
||||
- 2021.3.32f1
|
||||
- 2022.3.13f1
|
||||
- 2023.1.19f1
|
||||
- 2023.2.2f1
|
||||
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 backend.
|
||||
- iOS # Build an iOS player.
|
||||
- StandaloneLinux64 # Build a Linux 64-bit standalone with mono/il2cpp backend.
|
||||
- iOS # Build an iOS project.
|
||||
- Android # Build an Android .apk.
|
||||
- WebGL # WebGL.
|
||||
buildWithIl2cpp:
|
||||
- false
|
||||
- true
|
||||
additionalParameters:
|
||||
- -param value
|
||||
- -standaloneBuildSubtarget Server
|
||||
# Skipping configurations that are not supported
|
||||
exclude:
|
||||
# No il2cpp support on Linux Host
|
||||
- targetPlatform: StandaloneOSX
|
||||
buildWithIl2cpp: true
|
||||
- targetPlatform: StandaloneWindows64
|
||||
buildWithIl2cpp: true
|
||||
# Only builds with Il2cpp
|
||||
- targetPlatform: iOS
|
||||
buildWithIl2cpp: false
|
||||
- targetPlatform: Android
|
||||
buildWithIl2cpp: false
|
||||
- targetPlatform: WebGL
|
||||
buildWithIl2cpp: false
|
||||
# No dedicated server support
|
||||
- targetPlatform: WebGL
|
||||
additionalParameters: -standaloneBuildSubtarget Server
|
||||
- targetPlatform: Android
|
||||
additionalParameters: -standaloneBuildSubtarget Server
|
||||
- targetPlatform: iOS
|
||||
additionalParameters: -standaloneBuildSubtarget Server
|
||||
# No dedicated server support on Linux Host
|
||||
- targetPlatform: StandaloneOSX
|
||||
additionalParameters: -standaloneBuildSubtarget Server
|
||||
# No il2cpp dedicated server support on Linux Host
|
||||
- targetPlatform: StandaloneWindows64
|
||||
additionalParameters: -standaloneBuildSubtarget Server
|
||||
buildWithIl2cpp: true
|
||||
include:
|
||||
- unityVersion: 6000.0.36f1
|
||||
targetPlatform: WebGL
|
||||
- unityVersion: 6000.0.36f1
|
||||
targetPlatform: WebGL
|
||||
buildProfile: 'Assets/Settings/Build Profiles/Sample WebGL Build Profile.asset'
|
||||
|
||||
steps:
|
||||
- name: Clear Space for Android Build
|
||||
if: matrix.targetPlatform == 'Android'
|
||||
uses: jlumbroso/free-disk-space@v1.3.1
|
||||
|
||||
###########################
|
||||
# Checkout #
|
||||
###########################
|
||||
|
@ -69,7 +114,7 @@ jobs:
|
|||
###########################
|
||||
# Cache #
|
||||
###########################
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ matrix.projectPath }}/Library
|
||||
key: Library-${{ matrix.projectPath }}-ubuntu-${{ matrix.targetPlatform }}
|
||||
|
@ -77,6 +122,14 @@ 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 #
|
||||
###########################
|
||||
|
@ -88,11 +141,14 @@ jobs:
|
|||
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
|
||||
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue ${{ matrix.additionalParameters }}
|
||||
providerStrategy: ${{ matrix.providerStrategy }}
|
||||
allowDirtyBuild: true
|
||||
|
||||
- name: Sleep for Retry
|
||||
if: ${{ steps.build-1.outcome == 'failure' }}
|
||||
|
@ -108,10 +164,12 @@ jobs:
|
|||
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
|
||||
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue ${{ matrix.additionalParameters }}
|
||||
providerStrategy: ${{ matrix.providerStrategy }}
|
||||
allowDirtyBuild: true
|
||||
|
||||
|
@ -128,18 +186,21 @@ jobs:
|
|||
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
|
||||
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue ${{ matrix.additionalParameters }}
|
||||
providerStrategy: ${{ matrix.providerStrategy }}
|
||||
allowDirtyBuild: true
|
||||
|
||||
###########################
|
||||
# Upload #
|
||||
###########################
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Build Ubuntu (${{ matrix.unityVersion }})
|
||||
name:
|
||||
"Build ${{ matrix.targetPlatform }}${{ startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }} on Ubuntu (${{ matrix.unityVersion }}_il2cpp_${{ matrix.buildWithIl2cpp }}_params_${{ matrix.additionalParameters }})"
|
||||
path: build
|
||||
retention-days: 14
|
||||
|
|
|
@ -20,13 +20,25 @@ jobs:
|
|||
unityVersion:
|
||||
- 2021.3.32f1
|
||||
- 2022.3.13f1
|
||||
- 2023.1.19f1
|
||||
- 2023.2.2f1
|
||||
targetPlatform:
|
||||
- Android # Build an Android apk.
|
||||
- StandaloneWindows64 # Build a Windows 64-bit standalone.
|
||||
- WSAPlayer # Build a UWP App
|
||||
- tvOS # Build an Apple TV XCode project
|
||||
enableGpu:
|
||||
- false
|
||||
include:
|
||||
# Additionally test enableGpu build for a standalone windows target
|
||||
- projectPath: test-project
|
||||
unityVersion: 2023.2.2f1
|
||||
targetPlatform: StandaloneWindows64
|
||||
enableGpu: true
|
||||
- unityVersion: 6000.0.36f1
|
||||
targetPlatform: StandaloneWindows64
|
||||
- unityVersion: 6000.0.36f1
|
||||
targetPlatform: StandaloneWindows64
|
||||
buildProfile: 'Assets/Settings/Build Profiles/Sample Windows Build Profile.asset'
|
||||
|
||||
steps:
|
||||
###########################
|
||||
|
@ -39,7 +51,7 @@ jobs:
|
|||
###########################
|
||||
# Cache #
|
||||
###########################
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ matrix.projectPath }}/Library
|
||||
key: Library-${{ matrix.projectPath }}-windows-${{ matrix.targetPlatform }}
|
||||
|
@ -66,10 +78,14 @@ 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
|
||||
|
@ -89,10 +105,13 @@ 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
|
||||
|
@ -111,10 +130,13 @@ 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
|
||||
|
@ -122,8 +144,8 @@ jobs:
|
|||
###########################
|
||||
# Upload #
|
||||
###########################
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Build Windows (${{ matrix.unityVersion }})
|
||||
name: Build ${{ matrix.targetPlatform }} on Windows (${{ matrix.unityVersion }})${{ matrix.enableGpu && ' With GPU' || '' }}${{ matrix.buildProfile && ' With Build Profile' || '' }}
|
||||
path: build
|
||||
retention-days: 14
|
||||
|
|
|
@ -3,6 +3,11 @@ name: Cloud Runner CI Pipeline
|
|||
on:
|
||||
push: { branches: [cloud-runner-develop, cloud-runner-preview, main] }
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
runGithubIntegrationTests:
|
||||
description: 'Run GitHub Checks integration tests'
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
permissions:
|
||||
checks: write
|
||||
|
@ -18,42 +23,37 @@ env:
|
|||
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 }}
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
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
|
||||
tests:
|
||||
name: 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-end2end-locking'
|
||||
- 'cloud-runner-end2end-caching'
|
||||
- 'cloud-runner-end2end-retaining'
|
||||
- 'cloud-runner-caching'
|
||||
# - 'cloud-runner-end2end-caching'
|
||||
# - 'cloud-runner-end2end-retaining'
|
||||
- 'cloud-runner-environment'
|
||||
- 'cloud-runner-image'
|
||||
- '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@v4
|
||||
|
@ -65,58 +65,80 @@ jobs:
|
|||
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
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
PROJECT_PATH: test-project
|
||||
TARGET_PLATFORM: StandaloneWindows64
|
||||
cloudRunnerTests: true
|
||||
versioning: None
|
||||
CLOUD_RUNNER_CLUSTER: ${{ matrix.providerStrategy }}
|
||||
tests:
|
||||
# needs:
|
||||
# - smokeTests
|
||||
# - buildTargetTests
|
||||
name: Integration Tests
|
||||
KUBE_STORAGE_CLASS: local-path
|
||||
PROVIDER_STRATEGY: local-docker
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
GIT_PRIVATE_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
k8sTests:
|
||||
name: K8s 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-end2end-locking'
|
||||
- 'cloud-runner-end2end-caching'
|
||||
- 'cloud-runner-end2end-retaining'
|
||||
- 'cloud-runner-kubernetes'
|
||||
- 'cloud-runner-environment'
|
||||
- 'cloud-runner-github-checks'
|
||||
steps:
|
||||
- name: Checkout (default)
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
lfs: false
|
||||
- run: yarn
|
||||
- name: actions-k3s
|
||||
uses: debianmaster/actions-k3s@v1.0.5
|
||||
with:
|
||||
version: 'latest'
|
||||
- run: yarn run test "${{ matrix.test }}" --detectOpenHandles --forceExit --runInBand
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
PROJECT_PATH: test-project
|
||||
TARGET_PLATFORM: StandaloneWindows64
|
||||
cloudRunnerTests: true
|
||||
versioning: None
|
||||
KUBE_STORAGE_CLASS: local-path
|
||||
PROVIDER_STRATEGY: k8s
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
GIT_PRIVATE_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
awsTests:
|
||||
name: AWS 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@v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
lfs: false
|
||||
- name: Configure AWS Credentials
|
||||
|
@ -125,29 +147,24 @@ jobs:
|
|||
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 }}
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
PROJECT_PATH: test-project
|
||||
TARGET_PLATFORM: StandaloneWindows64
|
||||
cloudRunnerTests: true
|
||||
versioning: None
|
||||
PROVIDER_STRATEGY: ${{ matrix.providerStrategy }}
|
||||
KUBE_STORAGE_CLASS: local-path
|
||||
PROVIDER_STRATEGY: aws
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
GIT_PRIVATE_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
buildTargetTests:
|
||||
name: Local Build Target Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -164,7 +181,7 @@ jobs:
|
|||
- StandaloneLinux64 # Build a Linux 64-bit standalone.
|
||||
- WebGL # WebGL.
|
||||
- iOS # Build an iOS player.
|
||||
- Android # Build an Android .apk.
|
||||
# - Android # Build an Android .apk.
|
||||
steps:
|
||||
- name: Checkout (default)
|
||||
uses: actions/checkout@v4
|
||||
|
@ -175,7 +192,14 @@ jobs:
|
|||
id: unity-build
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
GIT_PRIVATE_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
cloudRunnerTests: true
|
||||
versioning: None
|
||||
|
@ -183,8 +207,25 @@ jobs:
|
|||
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
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.providerStrategy }} Build (${{ matrix.targetPlatform }})
|
||||
path: ${{ steps.unity-build.outputs.BUILD_ARTIFACT }}
|
||||
retention-days: 14
|
||||
|
||||
githubChecksIntegration:
|
||||
name: GitHub Checks Integration
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.runGithubIntegrationTests == 'true'
|
||||
env:
|
||||
RUN_GITHUB_INTEGRATION_TESTS: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'yarn'
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn test cloud-runner-github-checks-integration-test --detectOpenHandles --forceExit --runInBand
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
20
action.yml
20
action.yml
|
@ -18,7 +18,11 @@ inputs:
|
|||
projectPath:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'Relative path to the project to be built.'
|
||||
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.'
|
||||
buildName:
|
||||
required: false
|
||||
default: ''
|
||||
|
@ -35,6 +39,10 @@ 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: ''
|
||||
|
@ -186,11 +194,11 @@ inputs:
|
|||
description:
|
||||
'[CloudRunner] Either local, k8s or aws can be used to run builds on a remote cluster. Additional parameters must
|
||||
be configured.'
|
||||
cloudRunnerCpu:
|
||||
containerCpu:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[CloudRunner] Amount of CPU time to assign the remote build container'
|
||||
cloudRunnerMemory:
|
||||
containerMemory:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[CloudRunner] Amount of memory to assign the remote build container'
|
||||
|
@ -253,6 +261,10 @@ 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.'
|
||||
|
||||
outputs:
|
||||
volume:
|
||||
|
@ -270,5 +282,5 @@ branding:
|
|||
icon: 'box'
|
||||
color: 'gray-dark'
|
||||
runs:
|
||||
using: 'node16'
|
||||
using: 'node20'
|
||||
main: 'dist/index.js'
|
||||
|
|
Binary file not shown.
|
@ -6,6 +6,9 @@ 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
|
||||
|
@ -17,47 +20,9 @@ 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.
|
||||
|
@ -74,10 +39,89 @@ 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);
|
||||
|
||||
#if !BUILD_PROFILE_LOADED
|
||||
throw new Exception("Build profile's define symbol not present before script execution; shouldn't happen");
|
||||
#endif // BUILD_PROFILE_LOADED
|
||||
|
||||
// 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);
|
||||
|
||||
|
|
|
@ -56,17 +56,17 @@ namespace UnityBuilderAction.Input
|
|||
case "androidStudioProject":
|
||||
EditorUserBuildSettings.exportAsGoogleAndroidProject = true;
|
||||
if (buildAppBundle != null)
|
||||
buildAppBundle.SetValue(null, false);
|
||||
buildAppBundle.SetValue(null, false, null);
|
||||
break;
|
||||
case "androidAppBundle":
|
||||
EditorUserBuildSettings.exportAsGoogleAndroidProject = false;
|
||||
if (buildAppBundle != null)
|
||||
buildAppBundle.SetValue(null, true);
|
||||
buildAppBundle.SetValue(null, true, null);
|
||||
break;
|
||||
case "androidPackage":
|
||||
EditorUserBuildSettings.exportAsGoogleAndroidProject = false;
|
||||
if (buildAppBundle != null)
|
||||
buildAppBundle.SetValue(null, false);
|
||||
buildAppBundle.SetValue(null, false, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,20 @@ namespace UnityBuilderAction.Input
|
|||
string symbolType;
|
||||
if (options.TryGetValue("androidSymbolType", out symbolType) && !string.IsNullOrEmpty(symbolType))
|
||||
{
|
||||
#if UNITY_2021_1_OR_NEWER
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
switch (symbolType)
|
||||
{
|
||||
case "public":
|
||||
SetDebugSymbols("SymbolTable");
|
||||
break;
|
||||
case "debugging":
|
||||
SetDebugSymbols("Full");
|
||||
break;
|
||||
case "none":
|
||||
SetDebugSymbols("None");
|
||||
break;
|
||||
}
|
||||
#elif UNITY_2021_1_OR_NEWER
|
||||
switch (symbolType)
|
||||
{
|
||||
case "public":
|
||||
|
@ -101,5 +114,37 @@ 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,19 @@ 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");
|
||||
|
@ -28,9 +41,10 @@ namespace UnityBuilderAction.Input
|
|||
}
|
||||
|
||||
if (!Enum.IsDefined(typeof(BuildTarget), buildTarget)) {
|
||||
Console.WriteLine($"{buildTarget} is not a defined {nameof(BuildTarget)}");
|
||||
Console.WriteLine(buildTarget + " is not a defined " + typeof(BuildTarget).Name);
|
||||
EditorApplication.Exit(121);
|
||||
}
|
||||
}
|
||||
|
||||
string customBuildPath;
|
||||
if (!validatedOptions.TryGetValue("customBuildPath", out customBuildPath)) {
|
||||
|
@ -41,10 +55,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);
|
||||
}
|
||||
|
||||
|
@ -57,11 +71,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
|
||||
|
@ -78,7 +92,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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ namespace UnityBuilderAction.Reporting
|
|||
prefix = "error";
|
||||
break;
|
||||
}
|
||||
Console.WriteLine($"{Environment.NewLine}::{prefix} ::{condition}{Environment.NewLine}{stackTrace}");
|
||||
Console.WriteLine(Environment.NewLine + "::" + prefix + "::" + condition + Environment.NewLine + stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -1,32 +1,40 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
#
|
||||
# Create directories for license activation
|
||||
# Perform Activation
|
||||
#
|
||||
|
||||
UNITY_LICENSE_PATH="/Library/Application Support/Unity"
|
||||
if [ "$SKIP_ACTIVATION" != "true" ]; then
|
||||
UNITY_LICENSE_PATH="/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;
|
||||
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"
|
||||
ACTIVATE_LICENSE_PATH="$ACTION_FOLDER/BlankProject"
|
||||
mkdir -p "$ACTIVATE_LICENSE_PATH"
|
||||
|
||||
source $ACTION_FOLDER/platforms/mac/steps/activate.sh
|
||||
else
|
||||
echo "Skipping activation"
|
||||
fi
|
||||
|
||||
#
|
||||
# Run steps
|
||||
# Run Build
|
||||
#
|
||||
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
|
||||
|
||||
#
|
||||
# Remove license activation directory
|
||||
# License Cleanup
|
||||
#
|
||||
|
||||
rm -r "$ACTIVATE_LICENSE_PATH"
|
||||
if [ "$SKIP_ACTIVATION" != "true" ]; then
|
||||
source $ACTION_FOLDER/platforms/mac/steps/return_license.sh
|
||||
rm -r "$ACTIVATE_LICENSE_PATH"
|
||||
fi
|
||||
|
||||
#
|
||||
# Instructions for debugging
|
||||
|
|
|
@ -4,21 +4,69 @@
|
|||
echo "Changing to \"$ACTIVATE_LICENSE_PATH\" directory."
|
||||
pushd "$ACTIVATE_LICENSE_PATH"
|
||||
|
||||
echo "Requesting activation"
|
||||
if [[ -n "$UNITY_SERIAL" && -n "$UNITY_EMAIL" && -n "$UNITY_PASSWORD" ]]; then
|
||||
#
|
||||
# SERIAL LICENSE MODE
|
||||
#
|
||||
# This will activate unity, using the serial activation process.
|
||||
#
|
||||
|
||||
# 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"
|
||||
echo "Requesting activation"
|
||||
|
||||
# Store the exit code from the verify command
|
||||
UNITY_EXIT_CODE=$?
|
||||
# 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
|
||||
|
||||
#
|
||||
# Display information about the result
|
||||
|
|
|
@ -19,6 +19,23 @@ 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
|
||||
#
|
||||
|
@ -129,16 +146,16 @@ echo ""
|
|||
|
||||
/Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/MacOS/Unity \
|
||||
-logFile - \
|
||||
-quit \
|
||||
$( [ "${MANUAL_EXIT}" == "true" ] || echo "-quit" ) \
|
||||
-batchmode \
|
||||
-nographics \
|
||||
-username "$UNITY_EMAIL" \
|
||||
-password "$UNITY_PASSWORD" \
|
||||
$( [ "${ENABLE_GPU}" == "true" ] || echo "-nographics" ) \
|
||||
-customBuildName "$BUILD_NAME" \
|
||||
-projectPath "$UNITY_PROJECT_PATH" \
|
||||
-buildTarget "$BUILD_TARGET" \
|
||||
$( [ -z "$BUILD_PROFILE" ] && echo "-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" \
|
||||
|
|
|
@ -4,15 +4,29 @@
|
|||
echo "Changing to \"$ACTIVATE_LICENSE_PATH\" directory."
|
||||
pushd "$ACTIVATE_LICENSE_PATH"
|
||||
|
||||
/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"
|
||||
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
|
||||
|
||||
# Return to previous working directory
|
||||
popd
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
#!/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
|
||||
|
||||
if [[ -n "$UNITY_SERIAL" && -n "$UNITY_EMAIL" && -n "$UNITY_PASSWORD" ]]; then
|
||||
#
|
||||
# SERIAL LICENSE MODE
|
||||
|
@ -59,14 +68,18 @@ 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
|
||||
|
@ -75,11 +88,12 @@ else
|
|||
#
|
||||
echo "License activation strategy could not be determined."
|
||||
echo ""
|
||||
echo "Visit https://game.ci/docs/github/getting-started for more"
|
||||
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."
|
||||
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;
|
||||
|
||||
|
@ -98,6 +112,3 @@ else
|
|||
echo "::error ::There was an error while trying to activate the Unity license."
|
||||
exit $UNITY_EXIT_CODE
|
||||
fi
|
||||
|
||||
# Return to previous working directory
|
||||
popd
|
||||
|
|
|
@ -19,6 +19,22 @@ 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
|
||||
#
|
||||
|
@ -109,9 +125,11 @@ unity-editor \
|
|||
$( [ "${MANUAL_EXIT}" == "true" ] || echo "-quit" ) \
|
||||
-customBuildName "$BUILD_NAME" \
|
||||
-projectPath "$UNITY_PROJECT_PATH" \
|
||||
-buildTarget "$BUILD_TARGET" \
|
||||
$( [ -z "$BUILD_PROFILE" ] && echo "-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" \
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Run in ACTIVATE_LICENSE_PATH directory
|
||||
echo "Changing to \"$ACTIVATE_LICENSE_PATH\" directory."
|
||||
pushd "$ACTIVATE_LICENSE_PATH"
|
||||
|
||||
|
||||
if [[ -n "$UNITY_LICENSING_SERVER" ]]; then #
|
||||
if [[ -n "$UNITY_LICENSING_SERVER" ]]; then
|
||||
#
|
||||
# Return any floating license used.
|
||||
#
|
||||
|
@ -25,6 +20,3 @@ elif [[ -n "$UNITY_SERIAL" ]]; then
|
|||
-password "$UNITY_PASSWORD" \
|
||||
-projectPath "/BlankProject"
|
||||
fi
|
||||
|
||||
# Return to previous working directory
|
||||
popd
|
||||
|
|
|
@ -5,15 +5,23 @@
|
|||
#
|
||||
source /steps/set_extra_git_configs.sh
|
||||
source /steps/set_gitcredential.sh
|
||||
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
|
||||
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
|
||||
source /steps/return_license.sh
|
||||
|
||||
if [ "$SKIP_ACTIVATION" != "true" ]; then
|
||||
source /steps/return_license.sh
|
||||
fi
|
||||
|
||||
#
|
||||
# Instructions for debugging
|
||||
|
|
|
@ -6,11 +6,88 @@ Write-Output "# Activating #"
|
|||
Write-Output "###########################"
|
||||
Write-Output ""
|
||||
|
||||
& "$Env:UNITY_PATH/Editor/Unity.exe" -batchmode -quit -nographics `
|
||||
-username $Env:UNITY_EMAIL `
|
||||
-password $Env:UNITY_PASSWORD `
|
||||
-serial $Env:UNITY_SERIAL `
|
||||
-projectPath "c:/BlankProject" `
|
||||
-logfile - | Out-Host
|
||||
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_EXIT_CODE = $LASTEXITCODE
|
||||
$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;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,25 @@ 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
|
||||
#
|
||||
|
@ -129,37 +148,52 @@ 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",
|
||||
"-nographics",
|
||||
$unityGraphics,
|
||||
"-silent-crashes",
|
||||
"-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,
|
||||
"-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,
|
||||
"-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`"")
|
||||
}
|
||||
|
||||
# Remove null items as that will fail the Start-Process call
|
||||
$unityArgs = $unityArgs | Where-Object { $_ -ne $null }
|
||||
|
||||
$unityProcess = Start-Process -FilePath "$Env:UNITY_PATH\Editor\Unity.exe" `
|
||||
-ArgumentList $unityArgs `
|
||||
-PassThru `
|
||||
-NoNewWindow
|
||||
$unityProcess = Start-Process -FilePath "$Env:UNITY_PATH/Editor/Unity.exe" `
|
||||
-ArgumentList $unityArgs `
|
||||
-PassThru `
|
||||
-NoNewWindow
|
||||
|
||||
# Cache the handle so exit code works properly
|
||||
# https://stackoverflow.com/questions/10262231/obtaining-exitcode-using-start-process-and-waitforexit-instead-of-wait
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
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-Object { 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
|
||||
|
@ -13,19 +21,31 @@ Get-Process -Name regsvr32 | ForEach-Object { Stop-Process -Id $_.Id -Force }
|
|||
# Setup Git Credentials
|
||||
. "c:\steps\set_gitcredential.ps1"
|
||||
|
||||
# Activate Unity
|
||||
. "c:\steps\activate.ps1"
|
||||
if ($env:ENABLE_GPU -eq "true") {
|
||||
# Install LLVMpipe software graphics driver
|
||||
. "c:\steps\install_llvmpipe.ps1"
|
||||
}
|
||||
|
||||
# If we didn't activate successfully, exit with the exit code from the activation step.
|
||||
if ($ACTIVATION_EXIT_CODE -ne 0) {
|
||||
# Activate Unity
|
||||
if ($env:SKIP_ACTIVATION -ne "true") {
|
||||
. "c:\steps\activate.ps1"
|
||||
|
||||
# If we didn't activate successfully, exit with the exit code from the activation step.
|
||||
if ($ACTIVATION_EXIT_CODE -ne 0) {
|
||||
exit $ACTIVATION_EXIT_CODE
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "Skipping activation"
|
||||
}
|
||||
|
||||
# Build the project
|
||||
. "c:\steps\build.ps1"
|
||||
|
||||
# Free the seat for the activated license
|
||||
. "c:\steps\return_license.ps1"
|
||||
if ($env:SKIP_ACTIVATION -ne "true") {
|
||||
. "c:\steps\return_license.ps1"
|
||||
}
|
||||
|
||||
Get-Process
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
$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
|
||||
}
|
||||
}
|
|
@ -6,9 +6,56 @@ Write-Output "# Return License #"
|
|||
Write-Output "###########################"
|
||||
Write-Output ""
|
||||
|
||||
& "$Env:UNITY_PATH\Editor\Unity.exe" -batchmode -quit -nographics `
|
||||
-username $Env:UNITY_EMAIL `
|
||||
-password $Env:UNITY_PASSWORD `
|
||||
-returnlicense `
|
||||
-projectPath "c:/BlankProject" `
|
||||
-logfile - | Out-Host
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
if ([string]::IsNullOrEmpty($env:GIT_PRIVATE_TOKEN)) {
|
||||
if ($null -eq ${env:GIT_PRIVATE_TOKEN}) {
|
||||
Write-Host "GIT_PRIVATE_TOKEN unset skipping"
|
||||
}
|
||||
else {
|
||||
Write-Host "GIT_PRIVATE_TOKEN is set configuring git credentials"
|
||||
|
||||
git config --global credential.helper store
|
||||
git config --global --replace-all "url.https://token:$env:GIT_PRIVATE_TOKEN@github.com/".insteadOf "ssh://git@github.com/"
|
||||
git config --global --add "url.https://token:$env:GIT_PRIVATE_TOKEN@github.com/".insteadOf "git@github.com"
|
||||
git config --global --add "url.https://token:$env:GIT_PRIVATE_TOKEN@github.com/".insteadOf "https://github.com/"
|
||||
git config --global --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 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 -------------"
|
||||
|
|
|
@ -25,6 +25,8 @@ 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/'],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// Files that will be run before Jest is loaded to set globals like fetch
|
||||
setupFiles: ['<rootDir>/src/jest.globals.ts'],
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework after the environment is ready
|
||||
setupFilesAfterEnv: ['<rootDir>/src/jest.setup.ts'],
|
||||
};
|
||||
|
|
31
package.json
31
package.json
|
@ -7,14 +7,14 @@
|
|||
"author": "Webber <webber@takken.io>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prepare": "lefthook install && npx husky uninstall -y",
|
||||
"prepare": "lefthook install",
|
||||
"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 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",
|
||||
"gcp-secrets-tests": "cross-env providerStrategy=aws cloudRunnerTests=true inputPullCommand=\"gcp-secret-manager\" populateOverride=true pullInputList=UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD yarn test -i -t \"cloud runner\"",
|
||||
"gcp-secrets-cli": "cross-env cloudRunnerTests=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 cloudRunnerTests=true inputPullCommand=\"aws-secret-manager\" yarn ts-node src/index.ts -m cli --populateOverride true --pullInputList 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 cloudRunnerTests=true yarn ts-node src/index.ts -m cli --projectPath test-project",
|
||||
|
@ -28,27 +28,33 @@
|
|||
"node": ">=18.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/cache": "^3.1.3",
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/exec": "^1.1.0",
|
||||
"@actions/github": "^5.0.0",
|
||||
"@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",
|
||||
"@kubernetes/client-node": "^0.16.3",
|
||||
"@octokit/core": "^3.5.1",
|
||||
"@octokit/core": "^5.1.0",
|
||||
"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",
|
||||
"unity-changeset": "^2.0.0",
|
||||
"ts-md5": "^1.3.1",
|
||||
"unity-changeset": "^3.1.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",
|
||||
|
@ -67,9 +73,10 @@
|
|||
"jest-circus": "^27.5.1",
|
||||
"jest-fail-on-console": "^3.0.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lefthook": "^1.6.1",
|
||||
"prettier": "^2.5.1",
|
||||
"ts-jest": "^27.1.3",
|
||||
"ts-node": "10.4.0",
|
||||
"ts-node": "10.8.1",
|
||||
"typescript": "4.7.4",
|
||||
"yarn-audit-fix": "^9.3.8"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
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
|
|
@ -34,6 +34,7 @@ async function runMain() {
|
|||
});
|
||||
} else {
|
||||
await CloudRunner.run(buildParameters, baseImage.toString());
|
||||
exitCode = 0;
|
||||
}
|
||||
|
||||
// Set output
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
// Integration test for exercising real GitHub check creation and updates.
|
||||
import CloudRunner from '../model/cloud-runner/cloud-runner';
|
||||
import UnityVersioning from '../model/unity-versioning';
|
||||
import GitHub from '../model/github';
|
||||
import { TIMEOUT_INFINITE, createParameters } from '../test-utils/cloud-runner-test-helpers';
|
||||
|
||||
const runIntegration = process.env.RUN_GITHUB_INTEGRATION_TESTS === 'true';
|
||||
const describeOrSkip = runIntegration ? describe : describe.skip;
|
||||
|
||||
describeOrSkip('Cloud Runner 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'),
|
||||
asyncCloudRunner: `true`,
|
||||
githubChecks: `true`,
|
||||
});
|
||||
await CloudRunner.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,
|
||||
);
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import { fetch as undiciFetch, Headers, Request, Response } from 'undici';
|
||||
|
||||
Object.assign(globalThis, { fetch: undiciFetch, Headers, Request, Response });
|
|
@ -71,6 +71,12 @@ 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);
|
||||
|
|
|
@ -22,15 +22,18 @@ 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;
|
||||
|
@ -59,7 +62,7 @@ class BuildParameters {
|
|||
public kubeVolumeSize!: string;
|
||||
public kubeVolume!: string;
|
||||
public kubeStorageClass!: string;
|
||||
public runAsHostUser!: String;
|
||||
public runAsHostUser!: string;
|
||||
public chownFilesTo!: string;
|
||||
public commandHooks!: string;
|
||||
public pullInputList!: string[];
|
||||
|
@ -146,15 +149,18 @@ class BuildParameters {
|
|||
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,
|
||||
|
@ -168,7 +174,7 @@ class BuildParameters {
|
|||
customParameters: Input.customParameters,
|
||||
sshAgent: Input.sshAgent,
|
||||
sshPublicKeysDirectoryPath: Input.sshPublicKeysDirectoryPath,
|
||||
gitPrivateToken: Input.gitPrivateToken || (await GithubCliReader.GetGitHubAuthToken()),
|
||||
gitPrivateToken: Input.gitPrivateToken ?? (await GithubCliReader.GetGitHubAuthToken()),
|
||||
runAsHostUser: Input.runAsHostUser,
|
||||
chownFilesTo: Input.chownFilesTo,
|
||||
dockerCpuLimit: Input.dockerCpuLimit,
|
||||
|
@ -190,7 +196,7 @@ class BuildParameters {
|
|||
branch: Input.branch.replace('/head', '') || (await GitRepoReader.GetBranch()),
|
||||
cloudRunnerBranch: CloudRunnerOptions.cloudRunnerBranch.split('/').reverse()[0],
|
||||
cloudRunnerDebug: CloudRunnerOptions.cloudRunnerDebug,
|
||||
githubRepo: Input.githubRepo || (await GitRepoReader.GetRemote()) || 'game-ci/unity-builder',
|
||||
githubRepo: (Input.githubRepo ?? (await GitRepoReader.GetRemote())) || 'game-ci/unity-builder',
|
||||
isCliMode: Cli.isCliMode,
|
||||
awsStackName: CloudRunnerOptions.awsStackName,
|
||||
gitSha: Input.gitSha,
|
||||
|
|
|
@ -10,8 +10,6 @@ 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';
|
||||
|
||||
|
@ -54,6 +52,7 @@ 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();
|
||||
|
||||
|
@ -110,7 +109,7 @@ export class Cli {
|
|||
const buildParameter = await BuildParameters.create();
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
|
||||
return await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
return (await CloudRunner.run(buildParameter, baseImage.toString())).BuildResults;
|
||||
}
|
||||
|
||||
@CliFunction(`async-workflow`, `runs a cloud runner build`)
|
||||
|
@ -119,7 +118,7 @@ export class Cli {
|
|||
const baseImage = new ImageTag(buildParameter);
|
||||
await CloudRunner.setup(buildParameter);
|
||||
|
||||
return await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
return (await CloudRunner.run(buildParameter, baseImage.toString())).BuildResults;
|
||||
}
|
||||
|
||||
@CliFunction(`checks-update`, `runs a cloud runner build`)
|
||||
|
@ -173,31 +172,4 @@ export class Cli {
|
|||
|
||||
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(``));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ 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';
|
||||
import CloudRunnerResult from './services/core/cloud-runner-result';
|
||||
|
||||
class CloudRunner {
|
||||
public static Provider: ProviderInterface;
|
||||
|
@ -83,15 +84,16 @@ class CloudRunner {
|
|||
}
|
||||
|
||||
static async run(buildParameters: BuildParameters, baseImage: string) {
|
||||
if (baseImage.includes(`undefined`)) {
|
||||
throw new Error(`baseImage is undefined`);
|
||||
}
|
||||
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();
|
||||
|
@ -114,11 +116,7 @@ class CloudRunner {
|
|||
CloudRunner.lockedWorkspace = ``;
|
||||
}
|
||||
}
|
||||
const content = { ...CloudRunner.buildParameters };
|
||||
content.gitPrivateToken = ``;
|
||||
content.unitySerial = ``;
|
||||
const jsonContent = JSON.stringify(content, undefined, 4);
|
||||
await GitHub.updateGitHubCheck(jsonContent, CloudRunner.buildParameters.buildGuid);
|
||||
await CloudRunner.updateStatusWithBuildParameters();
|
||||
const output = await new WorkflowCompositionRoot().run(
|
||||
new CloudRunnerStepParameters(
|
||||
baseImage,
|
||||
|
@ -126,16 +124,15 @@ class CloudRunner {
|
|||
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.asyncWorkflow && this.isCloudRunnerEnvironment && this.isCloudRunnerAsyncEnvironment) {
|
||||
await GitHub.updateGitHubCheck(CloudRunner.buildParameters.buildGuid, `success`, `success`, `completed`);
|
||||
}
|
||||
|
||||
if (BuildParameters.shouldUseRetainedWorkspaceMode(buildParameters)) {
|
||||
const workspace = CloudRunner.lockedWorkspace || ``;
|
||||
|
@ -162,7 +159,7 @@ class CloudRunner {
|
|||
CloudRunner.Provider.garbageCollect(``, true, buildParameters.garbageMaxAge, true, true);
|
||||
}
|
||||
|
||||
return output;
|
||||
return new CloudRunnerResult(buildParameters, output, true, true, false);
|
||||
} catch (error: any) {
|
||||
CloudRunnerLogger.log(JSON.stringify(error, undefined, 4));
|
||||
await GitHub.updateGitHubCheck(
|
||||
|
@ -176,5 +173,15 @@ class CloudRunner {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private static async updateStatusWithBuildParameters() {
|
||||
const content = { ...CloudRunner.buildParameters };
|
||||
content.gitPrivateToken = ``;
|
||||
content.unitySerial = ``;
|
||||
content.unityEmail = ``;
|
||||
content.unityPassword = ``;
|
||||
const jsonContent = JSON.stringify(content, undefined, 4);
|
||||
await GitHub.updateGitHubCheck(jsonContent, CloudRunner.buildParameters.buildGuid);
|
||||
}
|
||||
}
|
||||
export default CloudRunner;
|
||||
|
|
|
@ -9,12 +9,7 @@ export class CloudRunnerError {
|
|||
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,
|
||||
);
|
||||
await CloudRunner.Provider.cleanupWorkflow(buildParameters, buildParameters.branch, secrets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,14 +103,14 @@ class CloudRunnerOptions {
|
|||
|
||||
static get buildPlatform(): string {
|
||||
const input = CloudRunnerOptions.getInput('buildPlatform');
|
||||
if (input) {
|
||||
if (input && input !== '') {
|
||||
return input;
|
||||
}
|
||||
if (CloudRunnerOptions.providerStrategy !== 'local') {
|
||||
return 'linux';
|
||||
}
|
||||
|
||||
return ``;
|
||||
return process.platform;
|
||||
}
|
||||
|
||||
static get cloudRunnerBranch(): string {
|
||||
|
|
|
@ -1,6 +1,18 @@
|
|||
import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
|
||||
import * as core from '@actions/core';
|
||||
import * as SDK from 'aws-sdk';
|
||||
import {
|
||||
CloudFormation,
|
||||
CreateStackCommand,
|
||||
CreateStackCommandInput,
|
||||
DescribeStacksCommand,
|
||||
DescribeStacksCommandInput,
|
||||
ListStacksCommand,
|
||||
Parameter,
|
||||
UpdateStackCommand,
|
||||
UpdateStackCommandInput,
|
||||
waitUntilStackCreateComplete,
|
||||
waitUntilStackUpdateComplete,
|
||||
} from '@aws-sdk/client-cloudformation';
|
||||
import { BaseStackFormation } from './cloud-formations/base-stack-formation';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
|
@ -10,51 +22,49 @@ export class AWSBaseStack {
|
|||
}
|
||||
private baseStackName: string;
|
||||
|
||||
async setupBaseStack(CF: SDK.CloudFormation) {
|
||||
async setupBaseStack(CF: CloudFormation) {
|
||||
const baseStackName = this.baseStackName;
|
||||
|
||||
const baseStack = BaseStackFormation.formation;
|
||||
|
||||
// Cloud Formation Input
|
||||
const describeStackInput: SDK.CloudFormation.DescribeStacksInput = {
|
||||
const describeStackInput: DescribeStacksCommandInput = {
|
||||
StackName: baseStackName,
|
||||
};
|
||||
const parametersWithoutHash: SDK.CloudFormation.Parameter[] = [
|
||||
{ ParameterKey: 'EnvironmentName', ParameterValue: baseStackName },
|
||||
];
|
||||
const parametersWithoutHash: Parameter[] = [{ ParameterKey: 'EnvironmentName', ParameterValue: baseStackName }];
|
||||
const parametersHash = crypto
|
||||
.createHash('md5')
|
||||
.update(baseStack + JSON.stringify(parametersWithoutHash))
|
||||
.digest('hex');
|
||||
const parameters: SDK.CloudFormation.Parameter[] = [
|
||||
const parameters: Parameter[] = [
|
||||
...parametersWithoutHash,
|
||||
...[{ ParameterKey: 'Version', ParameterValue: parametersHash }],
|
||||
];
|
||||
const updateInput: SDK.CloudFormation.UpdateStackInput = {
|
||||
const updateInput: UpdateStackCommandInput = {
|
||||
StackName: baseStackName,
|
||||
TemplateBody: baseStack,
|
||||
Parameters: parameters,
|
||||
Capabilities: ['CAPABILITY_IAM'],
|
||||
};
|
||||
const createStackInput: SDK.CloudFormation.CreateStackInput = {
|
||||
const createStackInput: CreateStackCommandInput = {
|
||||
StackName: baseStackName,
|
||||
TemplateBody: baseStack,
|
||||
Parameters: parameters,
|
||||
Capabilities: ['CAPABILITY_IAM'],
|
||||
};
|
||||
|
||||
const stacks = await CF.listStacks({
|
||||
StackStatusFilter: ['UPDATE_COMPLETE', 'CREATE_COMPLETE', 'ROLLBACK_COMPLETE'],
|
||||
}).promise();
|
||||
const stacks = await CF.send(
|
||||
new ListStacksCommand({ StackStatusFilter: ['UPDATE_COMPLETE', 'CREATE_COMPLETE', 'ROLLBACK_COMPLETE'] }),
|
||||
);
|
||||
const stackNames = stacks.StackSummaries?.map((x) => x.StackName) || [];
|
||||
const stackExists: Boolean = stackNames.includes(baseStackName) || false;
|
||||
const describeStack = async () => {
|
||||
return await CF.describeStacks(describeStackInput).promise();
|
||||
return await CF.send(new DescribeStacksCommand(describeStackInput));
|
||||
};
|
||||
try {
|
||||
if (!stackExists) {
|
||||
CloudRunnerLogger.log(`${baseStackName} stack does not exist (${JSON.stringify(stackNames)})`);
|
||||
await CF.createStack(createStackInput).promise();
|
||||
await CF.send(new CreateStackCommand(createStackInput));
|
||||
CloudRunnerLogger.log(`created stack (version: ${parametersHash})`);
|
||||
}
|
||||
const CFState = await describeStack();
|
||||
|
@ -65,7 +75,13 @@ export class AWSBaseStack {
|
|||
const stackVersion = stack.Parameters?.find((x) => x.ParameterKey === 'Version')?.ParameterValue;
|
||||
|
||||
if (stack.StackStatus === 'CREATE_IN_PROGRESS') {
|
||||
await CF.waitFor('stackCreateComplete', describeStackInput).promise();
|
||||
await waitUntilStackCreateComplete(
|
||||
{
|
||||
client: CF,
|
||||
maxWaitTime: 200,
|
||||
},
|
||||
describeStackInput,
|
||||
);
|
||||
}
|
||||
|
||||
if (stackExists) {
|
||||
|
@ -73,7 +89,7 @@ export class AWSBaseStack {
|
|||
if (parametersHash !== stackVersion) {
|
||||
CloudRunnerLogger.log(`Attempting update of base stack`);
|
||||
try {
|
||||
await CF.updateStack(updateInput).promise();
|
||||
await CF.send(new UpdateStackCommand(updateInput));
|
||||
} catch (error: any) {
|
||||
if (error['message'].includes('No updates are to be performed')) {
|
||||
CloudRunnerLogger.log(`No updates are to be performed`);
|
||||
|
@ -93,7 +109,13 @@ export class AWSBaseStack {
|
|||
);
|
||||
}
|
||||
if (stack.StackStatus === 'UPDATE_IN_PROGRESS') {
|
||||
await CF.waitFor('stackUpdateComplete', describeStackInput).promise();
|
||||
await waitUntilStackUpdateComplete(
|
||||
{
|
||||
client: CF,
|
||||
maxWaitTime: 200,
|
||||
},
|
||||
describeStackInput,
|
||||
);
|
||||
}
|
||||
}
|
||||
CloudRunnerLogger.log('base stack is now ready');
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
|
||||
import * as SDK from 'aws-sdk';
|
||||
import { CloudFormation, DescribeStackEventsCommand } from '@aws-sdk/client-cloudformation';
|
||||
import * as core from '@actions/core';
|
||||
import CloudRunner from '../../cloud-runner';
|
||||
|
||||
export class AWSError {
|
||||
static async handleStackCreationFailure(error: any, CF: SDK.CloudFormation, taskDefStackName: string) {
|
||||
static async handleStackCreationFailure(error: any, CF: 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;
|
||||
const events = (await CF.send(new DescribeStackEventsCommand({ StackName: taskDefStackName }))).StackEvents;
|
||||
CloudRunnerLogger.log(JSON.stringify(events, undefined, 4));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import * as SDK from 'aws-sdk';
|
||||
import {
|
||||
CloudFormation,
|
||||
CreateStackCommand,
|
||||
CreateStackCommandInput,
|
||||
DescribeStackResourcesCommand,
|
||||
DescribeStacksCommand,
|
||||
ListStacksCommand,
|
||||
waitUntilStackCreateComplete,
|
||||
} from '@aws-sdk/client-cloudformation';
|
||||
import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def';
|
||||
import CloudRunnerSecret from '../../options/cloud-runner-secret';
|
||||
import { AWSCloudFormationTemplates } from './aws-cloud-formation-templates';
|
||||
|
@ -16,7 +24,7 @@ export class AWSJobStack {
|
|||
}
|
||||
|
||||
public async setupCloudFormations(
|
||||
CF: SDK.CloudFormation,
|
||||
CF: CloudFormation,
|
||||
buildGuid: string,
|
||||
image: string,
|
||||
entrypoint: string[],
|
||||
|
@ -119,7 +127,7 @@ export class AWSJobStack {
|
|||
let previousStackExists = true;
|
||||
while (previousStackExists) {
|
||||
previousStackExists = false;
|
||||
const stacks = await CF.listStacks().promise();
|
||||
const stacks = await CF.send(new ListStacksCommand({}));
|
||||
if (!stacks.StackSummaries) {
|
||||
throw new Error('Faild to get stacks');
|
||||
}
|
||||
|
@ -132,7 +140,7 @@ export class AWSJobStack {
|
|||
}
|
||||
}
|
||||
}
|
||||
const createStackInput: SDK.CloudFormation.CreateStackInput = {
|
||||
const createStackInput: CreateStackCommandInput = {
|
||||
StackName: taskDefStackName,
|
||||
TemplateBody: taskDefCloudFormation,
|
||||
Capabilities: ['CAPABILITY_IAM'],
|
||||
|
@ -140,9 +148,15 @@ export class AWSJobStack {
|
|||
};
|
||||
try {
|
||||
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();
|
||||
await CF.send(new CreateStackCommand(createStackInput));
|
||||
await waitUntilStackCreateComplete(
|
||||
{
|
||||
client: CF,
|
||||
maxWaitTime: 200,
|
||||
},
|
||||
{ StackName: taskDefStackName },
|
||||
);
|
||||
const describeStack = await CF.send(new DescribeStacksCommand({ StackName: taskDefStackName }));
|
||||
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`);
|
||||
|
@ -153,7 +167,7 @@ export class AWSJobStack {
|
|||
throw error;
|
||||
}
|
||||
|
||||
const createCleanupStackInput: SDK.CloudFormation.CreateStackInput = {
|
||||
const createCleanupStackInput: CreateStackCommandInput = {
|
||||
StackName: `${taskDefStackName}-cleanup`,
|
||||
TemplateBody: CleanupCronFormation.formation,
|
||||
Capabilities: ['CAPABILITY_IAM'],
|
||||
|
@ -183,7 +197,7 @@ export class AWSJobStack {
|
|||
if (CloudRunnerOptions.useCleanupCron) {
|
||||
try {
|
||||
CloudRunnerLogger.log(`Creating job cleanup formation`);
|
||||
await CF.createStack(createCleanupStackInput).promise();
|
||||
await CF.send(new CreateStackCommand(createCleanupStackInput));
|
||||
|
||||
// await CF.waitFor('stackCreateComplete', { StackName: createCleanupStackInput.StackName }).promise();
|
||||
} catch (error) {
|
||||
|
@ -193,12 +207,15 @@ export class AWSJobStack {
|
|||
}
|
||||
|
||||
const taskDefResources = (
|
||||
await CF.describeStackResources({
|
||||
StackName: taskDefStackName,
|
||||
}).promise()
|
||||
await CF.send(
|
||||
new DescribeStackResourcesCommand({
|
||||
StackName: taskDefStackName,
|
||||
}),
|
||||
)
|
||||
).StackResources;
|
||||
|
||||
const baseResources = (await CF.describeStackResources({ StackName: this.baseStackName }).promise()).StackResources;
|
||||
const baseResources = (await CF.send(new DescribeStackResourcesCommand({ StackName: this.baseStackName })))
|
||||
.StackResources;
|
||||
|
||||
return {
|
||||
taskDefStackName,
|
||||
|
|
|
@ -1,4 +1,19 @@
|
|||
import * as AWS from 'aws-sdk';
|
||||
import {
|
||||
DescribeTasksCommand,
|
||||
ECS,
|
||||
RunTaskCommand,
|
||||
RunTaskCommandInput,
|
||||
Task,
|
||||
waitUntilTasksRunning,
|
||||
} from '@aws-sdk/client-ecs';
|
||||
import {
|
||||
DescribeStreamCommand,
|
||||
DescribeStreamCommandOutput,
|
||||
GetRecordsCommand,
|
||||
GetRecordsCommandOutput,
|
||||
GetShardIteratorCommand,
|
||||
Kinesis,
|
||||
} from '@aws-sdk/client-kinesis';
|
||||
import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable';
|
||||
import * as core from '@actions/core';
|
||||
import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def';
|
||||
|
@ -12,8 +27,8 @@ import CloudRunnerOptions from '../../options/cloud-runner-options';
|
|||
import GitHub from '../../../github';
|
||||
|
||||
class AWSTaskRunner {
|
||||
public static ECS: AWS.ECS;
|
||||
public static Kinesis: AWS.Kinesis;
|
||||
public static ECS: ECS;
|
||||
public static Kinesis: Kinesis;
|
||||
private static readonly encodedUnderscore = `$252F`;
|
||||
static async runTask(
|
||||
taskDef: CloudRunnerAWSTaskDef,
|
||||
|
@ -60,7 +75,7 @@ class AWSTaskRunner {
|
|||
throw new Error(`Container Overrides length must be at most 8192`);
|
||||
}
|
||||
|
||||
const task = await AWSTaskRunner.ECS.runTask(runParameters).promise();
|
||||
const task = await AWSTaskRunner.ECS.send(new RunTaskCommand(runParameters as RunTaskCommandInput));
|
||||
const taskArn = task.tasks?.[0].taskArn || '';
|
||||
CloudRunnerLogger.log('Cloud runner job is starting');
|
||||
await AWSTaskRunner.waitUntilTaskRunning(taskArn, cluster);
|
||||
|
@ -108,7 +123,13 @@ class AWSTaskRunner {
|
|||
|
||||
private static async waitUntilTaskRunning(taskArn: string, cluster: string) {
|
||||
try {
|
||||
await AWSTaskRunner.ECS.waitFor('tasksRunning', { tasks: [taskArn], cluster }).promise();
|
||||
await waitUntilTasksRunning(
|
||||
{
|
||||
client: AWSTaskRunner.ECS,
|
||||
maxWaitTime: 120,
|
||||
},
|
||||
{ tasks: [taskArn], cluster },
|
||||
);
|
||||
} catch (error_) {
|
||||
const error = error_ as Error;
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
@ -124,10 +145,7 @@ class AWSTaskRunner {
|
|||
}
|
||||
|
||||
static async describeTasks(clusterName: string, taskArn: string) {
|
||||
const tasks = await AWSTaskRunner.ECS.describeTasks({
|
||||
cluster: clusterName,
|
||||
tasks: [taskArn],
|
||||
}).promise();
|
||||
const tasks = await AWSTaskRunner.ECS.send(new DescribeTasksCommand({ cluster: clusterName, tasks: [taskArn] }));
|
||||
if (tasks.tasks?.[0]) {
|
||||
return tasks.tasks?.[0];
|
||||
} else {
|
||||
|
@ -169,9 +187,7 @@ class AWSTaskRunner {
|
|||
output: string,
|
||||
shouldCleanup: boolean,
|
||||
) {
|
||||
const records = await AWSTaskRunner.Kinesis.getRecords({
|
||||
ShardIterator: iterator,
|
||||
}).promise();
|
||||
const records = await AWSTaskRunner.Kinesis.send(new GetRecordsCommand({ ShardIterator: iterator }));
|
||||
iterator = records.NextShardIterator || '';
|
||||
({ shouldReadLogs, output, shouldCleanup } = AWSTaskRunner.logRecords(
|
||||
records,
|
||||
|
@ -184,7 +200,7 @@ class AWSTaskRunner {
|
|||
return { iterator, shouldReadLogs, output, shouldCleanup };
|
||||
}
|
||||
|
||||
private static checkStreamingShouldContinue(taskData: AWS.ECS.Task, timestamp: number, shouldReadLogs: boolean) {
|
||||
private static checkStreamingShouldContinue(taskData: Task, timestamp: number, shouldReadLogs: boolean) {
|
||||
if (taskData?.lastStatus === 'UNKNOWN') {
|
||||
CloudRunnerLogger.log('## Cloud runner job unknwon');
|
||||
}
|
||||
|
@ -204,15 +220,17 @@ class AWSTaskRunner {
|
|||
}
|
||||
|
||||
private static logRecords(
|
||||
records: AWS.Kinesis.GetRecordsOutput,
|
||||
records: GetRecordsCommandOutput,
|
||||
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 ((records.Records ?? []).length > 0 && iterator) {
|
||||
for (const record of records.Records ?? []) {
|
||||
const json = JSON.parse(
|
||||
zlib.gunzipSync(Buffer.from(record.Data as unknown as string, 'base64')).toString('utf8'),
|
||||
);
|
||||
if (json.messageType === 'DATA_MESSAGE') {
|
||||
for (const logEvent of json.logEvents) {
|
||||
({ shouldReadLogs, shouldCleanup, output } = FollowLogStreamService.handleIteration(
|
||||
|
@ -230,19 +248,19 @@ class AWSTaskRunner {
|
|||
}
|
||||
|
||||
private static async getLogStream(kinesisStreamName: string) {
|
||||
return await AWSTaskRunner.Kinesis.describeStream({
|
||||
StreamName: kinesisStreamName,
|
||||
}).promise();
|
||||
return await AWSTaskRunner.Kinesis.send(new DescribeStreamCommand({ StreamName: kinesisStreamName }));
|
||||
}
|
||||
|
||||
private static async getLogIterator(stream: AWS.Kinesis.DescribeStreamOutput) {
|
||||
private static async getLogIterator(stream: DescribeStreamCommandOutput) {
|
||||
return (
|
||||
(
|
||||
await AWSTaskRunner.Kinesis.getShardIterator({
|
||||
ShardIteratorType: 'TRIM_HORIZON',
|
||||
StreamName: stream.StreamDescription.StreamName,
|
||||
ShardId: stream.StreamDescription.Shards[0].ShardId,
|
||||
}).promise()
|
||||
await AWSTaskRunner.Kinesis.send(
|
||||
new GetShardIteratorCommand({
|
||||
ShardIteratorType: 'TRIM_HORIZON',
|
||||
StreamName: stream.StreamDescription?.StreamName ?? '',
|
||||
ShardId: stream.StreamDescription?.Shards?.[0]?.ShardId || '',
|
||||
}),
|
||||
)
|
||||
).ShardIterator || ''
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import CloudRunner from '../../../cloud-runner';
|
||||
|
||||
export class TaskDefinitionFormation {
|
||||
public static readonly description: string = `Game CI Cloud Runner Task Stack`;
|
||||
public static readonly formation: string = `AWSTemplateFormatVersion: 2010-09-09
|
||||
public static get formation(): string {
|
||||
return `AWSTemplateFormatVersion: 2010-09-09
|
||||
Description: ${TaskDefinitionFormation.description}
|
||||
Parameters:
|
||||
EnvironmentName:
|
||||
|
@ -26,11 +29,11 @@ Parameters:
|
|||
Default: 80
|
||||
Description: What port number the application inside the docker container is binding to
|
||||
ContainerCpu:
|
||||
Default: 1024
|
||||
Default: ${CloudRunner.buildParameters.containerCpu}
|
||||
Type: Number
|
||||
Description: How much CPU to give the container. 1024 is 1 CPU
|
||||
ContainerMemory:
|
||||
Default: 4096
|
||||
Default: ${CloudRunner.buildParameters.containerMemory}
|
||||
Type: Number
|
||||
Description: How much memory in megabytes to give the container
|
||||
BUILDGUID:
|
||||
|
@ -92,7 +95,7 @@ Resources:
|
|||
EFSVolumeConfiguration:
|
||||
FilesystemId:
|
||||
'Fn::ImportValue': !Sub '${'${EnvironmentName}'}:EfsFileStorageId'
|
||||
TransitEncryption: ENABLED
|
||||
TransitEncryption: DISABLED
|
||||
RequiresCompatibilities:
|
||||
- FARGATE
|
||||
ExecutionRoleArn:
|
||||
|
@ -135,6 +138,7 @@ Resources:
|
|||
DependsOn:
|
||||
- LogGroup
|
||||
`;
|
||||
}
|
||||
public static streamLogs = `
|
||||
SubscriptionFilter:
|
||||
Type: 'AWS::Logs::SubscriptionFilter'
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import * as AWS from 'aws-sdk';
|
||||
import { StackResource } from '@aws-sdk/client-cloudformation';
|
||||
|
||||
class CloudRunnerAWSTaskDef {
|
||||
public taskDefStackName!: string;
|
||||
public taskDefCloudFormation!: string;
|
||||
public taskDefResources: AWS.CloudFormation.StackResources | undefined;
|
||||
public baseResources: AWS.CloudFormation.StackResources | undefined;
|
||||
public taskDefResources: StackResource[] | undefined;
|
||||
public baseResources: StackResource[] | undefined;
|
||||
}
|
||||
export default CloudRunnerAWSTaskDef;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import * as SDK from 'aws-sdk';
|
||||
import { CloudFormation, DeleteStackCommand, waitUntilStackDeleteComplete } from '@aws-sdk/client-cloudformation';
|
||||
import { ECS as ECSClient } from '@aws-sdk/client-ecs';
|
||||
import { Kinesis } from '@aws-sdk/client-kinesis';
|
||||
import CloudRunnerSecret from '../../options/cloud-runner-secret';
|
||||
import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable';
|
||||
import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def';
|
||||
|
@ -57,8 +59,6 @@ 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
|
||||
|
@ -77,7 +77,7 @@ class AWSBuildEnvironment implements ProviderInterface {
|
|||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
) {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const CF = new SDK.CloudFormation();
|
||||
const CF = new CloudFormation({ region: Input.region });
|
||||
await new AwsBaseStack(this.baseStackName).setupBaseStack(CF);
|
||||
}
|
||||
|
||||
|
@ -91,10 +91,10 @@ class AWSBuildEnvironment implements ProviderInterface {
|
|||
secrets: CloudRunnerSecret[],
|
||||
): Promise<string> {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const ECS = new SDK.ECS();
|
||||
const CF = new SDK.CloudFormation();
|
||||
const ECS = new ECSClient({ region: Input.region });
|
||||
const CF = new CloudFormation({ region: Input.region });
|
||||
AwsTaskRunner.ECS = ECS;
|
||||
AwsTaskRunner.Kinesis = new SDK.Kinesis();
|
||||
AwsTaskRunner.Kinesis = new Kinesis({ region: Input.region });
|
||||
CloudRunnerLogger.log(`AWS Region: ${CF.config.region}`);
|
||||
const entrypoint = ['/bin/sh'];
|
||||
const startTimeMs = Date.now();
|
||||
|
@ -131,23 +131,31 @@ class AWSBuildEnvironment implements ProviderInterface {
|
|||
}
|
||||
}
|
||||
|
||||
async cleanupResources(CF: SDK.CloudFormation, taskDef: CloudRunnerAWSTaskDef) {
|
||||
async cleanupResources(CF: CloudFormation, taskDef: CloudRunnerAWSTaskDef) {
|
||||
CloudRunnerLogger.log('Cleanup starting');
|
||||
await CF.deleteStack({
|
||||
StackName: taskDef.taskDefStackName,
|
||||
}).promise();
|
||||
await CF.send(new DeleteStackCommand({ StackName: taskDef.taskDefStackName }));
|
||||
if (CloudRunnerOptions.useCleanupCron) {
|
||||
await CF.deleteStack({
|
||||
StackName: `${taskDef.taskDefStackName}-cleanup`,
|
||||
}).promise();
|
||||
await CF.send(new DeleteStackCommand({ StackName: `${taskDef.taskDefStackName}-cleanup` }));
|
||||
}
|
||||
|
||||
await CF.waitFor('stackDeleteComplete', {
|
||||
StackName: taskDef.taskDefStackName,
|
||||
}).promise();
|
||||
await CF.waitFor('stackDeleteComplete', {
|
||||
StackName: `${taskDef.taskDefStackName}-cleanup`,
|
||||
}).promise();
|
||||
await waitUntilStackDeleteComplete(
|
||||
{
|
||||
client: CF,
|
||||
maxWaitTime: 200,
|
||||
},
|
||||
{
|
||||
StackName: taskDef.taskDefStackName,
|
||||
},
|
||||
);
|
||||
await waitUntilStackDeleteComplete(
|
||||
{
|
||||
client: CF,
|
||||
maxWaitTime: 200,
|
||||
},
|
||||
{
|
||||
StackName: `${taskDef.taskDefStackName}-cleanup`,
|
||||
},
|
||||
);
|
||||
CloudRunnerLogger.log(`Deleted Stack: ${taskDef.taskDefStackName}`);
|
||||
CloudRunnerLogger.log('Cleanup complete');
|
||||
}
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import AWS from 'aws-sdk';
|
||||
import {
|
||||
CloudFormation,
|
||||
DeleteStackCommand,
|
||||
DeleteStackCommandInput,
|
||||
DescribeStackResourcesCommand,
|
||||
} from '@aws-sdk/client-cloudformation';
|
||||
import { CloudWatchLogs, DeleteLogGroupCommand } from '@aws-sdk/client-cloudwatch-logs';
|
||||
import { ECS, StopTaskCommand } from '@aws-sdk/client-ecs';
|
||||
import Input from '../../../../input';
|
||||
import CloudRunnerLogger from '../../../services/core/cloud-runner-logger';
|
||||
import { TaskService } from './task-service';
|
||||
|
@ -12,9 +19,9 @@ export class GarbageCollectionService {
|
|||
|
||||
public static async cleanup(deleteResources = false, OneDayOlderOnly: boolean = false) {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const CF = new AWS.CloudFormation();
|
||||
const ecs = new AWS.ECS();
|
||||
const cwl = new AWS.CloudWatchLogs();
|
||||
const CF = new CloudFormation({ region: Input.region });
|
||||
const ecs = new ECS({ region: Input.region });
|
||||
const cwl = new CloudWatchLogs({ region: Input.region });
|
||||
const taskDefinitionsInUse = new Array();
|
||||
const tasks = await TaskService.getTasks();
|
||||
|
||||
|
@ -23,14 +30,14 @@ export class GarbageCollectionService {
|
|||
taskDefinitionsInUse.push(taskElement.taskDefinitionArn);
|
||||
if (deleteResources && (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(taskElement.createdAt!))) {
|
||||
CloudRunnerLogger.log(`Stopping task ${taskElement.containers?.[0].name}`);
|
||||
await ecs.stopTask({ task: taskElement.taskArn || '', cluster: element }).promise();
|
||||
await ecs.send(new StopTaskCommand({ task: taskElement.taskArn || '', cluster: element }));
|
||||
}
|
||||
}
|
||||
|
||||
const jobStacks = await TaskService.getCloudFormationJobStacks();
|
||||
for (const element of jobStacks) {
|
||||
if (
|
||||
(await CF.describeStackResources({ StackName: element.StackName }).promise()).StackResources?.some(
|
||||
(await CF.send(new DescribeStackResourcesCommand({ StackName: element.StackName }))).StackResources?.some(
|
||||
(x) => x.ResourceType === 'AWS::ECS::TaskDefinition' && taskDefinitionsInUse.includes(x.PhysicalResourceId),
|
||||
)
|
||||
) {
|
||||
|
@ -39,7 +46,10 @@ export class GarbageCollectionService {
|
|||
return;
|
||||
}
|
||||
|
||||
if (deleteResources && (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(element.CreationTime))) {
|
||||
if (
|
||||
deleteResources &&
|
||||
(!OneDayOlderOnly || (element.CreationTime && GarbageCollectionService.isOlderThan1day(element.CreationTime)))
|
||||
) {
|
||||
if (element.StackName === 'game-ci' || element.TemplateDescription === 'Game-CI base stack') {
|
||||
CloudRunnerLogger.log(`Skipping ${element.StackName} ignore list`);
|
||||
|
||||
|
@ -47,8 +57,8 @@ export class GarbageCollectionService {
|
|||
}
|
||||
|
||||
CloudRunnerLogger.log(`Deleting ${element.StackName}`);
|
||||
const deleteStackInput: AWS.CloudFormation.DeleteStackInput = { StackName: element.StackName };
|
||||
await CF.deleteStack(deleteStackInput).promise();
|
||||
const deleteStackInput: DeleteStackCommandInput = { StackName: element.StackName };
|
||||
await CF.send(new DeleteStackCommand(deleteStackInput));
|
||||
}
|
||||
}
|
||||
const logGroups = await TaskService.getLogGroups();
|
||||
|
@ -58,7 +68,7 @@ export class GarbageCollectionService {
|
|||
(!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(new Date(element.creationTime!)))
|
||||
) {
|
||||
CloudRunnerLogger.log(`Deleting ${element.logGroupName}`);
|
||||
await cwl.deleteLogGroup({ logGroupName: element.logGroupName || '' }).promise();
|
||||
await cwl.send(new DeleteLogGroupCommand({ logGroupName: element.logGroupName || '' }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,31 @@
|
|||
import AWS from 'aws-sdk';
|
||||
import {
|
||||
CloudFormation,
|
||||
DescribeStackResourcesCommand,
|
||||
DescribeStacksCommand,
|
||||
ListStacksCommand,
|
||||
StackSummary,
|
||||
} from '@aws-sdk/client-cloudformation';
|
||||
import {
|
||||
CloudWatchLogs,
|
||||
DescribeLogGroupsCommand,
|
||||
DescribeLogGroupsCommandInput,
|
||||
LogGroup,
|
||||
} from '@aws-sdk/client-cloudwatch-logs';
|
||||
import {
|
||||
DescribeTasksCommand,
|
||||
DescribeTasksCommandInput,
|
||||
ECS,
|
||||
ListClustersCommand,
|
||||
ListTasksCommand,
|
||||
ListTasksCommandInput,
|
||||
Task,
|
||||
} from '@aws-sdk/client-ecs';
|
||||
import { ListObjectsCommand, ListObjectsCommandInput, S3 } from '@aws-sdk/client-s3';
|
||||
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() {
|
||||
|
@ -20,20 +39,24 @@ export class TaskService {
|
|||
return output;
|
||||
}
|
||||
public static async getCloudFormationJobStacks() {
|
||||
const result: StackSummaries = [];
|
||||
const result: StackSummary[] = [];
|
||||
CloudRunnerLogger.log(``);
|
||||
CloudRunnerLogger.log(`List Cloud Formation Stacks`);
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const CF = new AWS.CloudFormation();
|
||||
const CF = new CloudFormation({ region: Input.region });
|
||||
const stacks =
|
||||
(await CF.listStacks().promise()).StackSummaries?.filter(
|
||||
(await CF.send(new ListStacksCommand({}))).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());
|
||||
if (!element.CreationTime) {
|
||||
CloudRunnerLogger.log(`${element.StackName} due to undefined CreationTime`);
|
||||
}
|
||||
|
||||
const ageDate: Date = new Date(Date.now() - (element.CreationTime?.getTime() ?? 0));
|
||||
|
||||
CloudRunnerLogger.log(
|
||||
`Task Stack ${element.StackName} - Age D${Math.floor(
|
||||
|
@ -43,14 +66,18 @@ export class TaskService {
|
|||
result.push(element);
|
||||
}
|
||||
const baseStacks =
|
||||
(await CF.listStacks().promise()).StackSummaries?.filter(
|
||||
(await CF.send(new ListStacksCommand({}))).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());
|
||||
if (!element.CreationTime) {
|
||||
CloudRunnerLogger.log(`${element.StackName} due to undefined CreationTime`);
|
||||
}
|
||||
|
||||
const ageDate: Date = new Date(Date.now() - (element.CreationTime?.getTime() ?? 0));
|
||||
|
||||
CloudRunnerLogger.log(
|
||||
`Task Stack ${element.StackName} - Age D${Math.floor(
|
||||
|
@ -64,22 +91,22 @@ export class TaskService {
|
|||
return result;
|
||||
}
|
||||
public static async getTasks() {
|
||||
const result: { taskElement: AWS.ECS.Task; element: string }[] = [];
|
||||
const result: { taskElement: 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 || [];
|
||||
const ecs = new ECS({ region: Input.region });
|
||||
const clusters = (await ecs.send(new ListClustersCommand({}))).clusterArns || [];
|
||||
CloudRunnerLogger.log(`Task Clusters ${clusters.length}`);
|
||||
for (const element of clusters) {
|
||||
const input: AWS.ECS.ListTasksRequest = {
|
||||
const input: ListTasksCommandInput = {
|
||||
cluster: element,
|
||||
};
|
||||
|
||||
const list = (await ecs.listTasks(input).promise()).taskArns || [];
|
||||
const list = (await ecs.send(new ListTasksCommand(input))).taskArns || [];
|
||||
if (list.length > 0) {
|
||||
const describeInput: AWS.ECS.DescribeTasksRequest = { tasks: list, cluster: element };
|
||||
const describeList = (await ecs.describeTasks(describeInput).promise()).tasks || [];
|
||||
const describeInput: DescribeTasksCommandInput = { tasks: list, cluster: element };
|
||||
const describeList = (await ecs.send(new DescribeTasksCommand(describeInput))).tasks || [];
|
||||
if (describeList.length === 0) {
|
||||
CloudRunnerLogger.log(`No Tasks`);
|
||||
continue;
|
||||
|
@ -105,37 +132,48 @@ export class TaskService {
|
|||
}
|
||||
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 = `
|
||||
const CF = new CloudFormation({ region: Input.region });
|
||||
try {
|
||||
const stack =
|
||||
(await CF.send(new ListStacksCommand({}))).StackSummaries?.find((_x) => _x.StackName === job) || undefined;
|
||||
const stackInfo = (await CF.send(new DescribeStackResourcesCommand({ StackName: job }))) || undefined;
|
||||
const stackInfo2 = (await CF.send(new DescribeStacksCommand({ StackName: job }))) || undefined;
|
||||
if (stack === undefined) {
|
||||
throw new Error('stack not defined');
|
||||
}
|
||||
if (!stack.CreationTime) {
|
||||
CloudRunnerLogger.log(`${stack.StackName} due to undefined CreationTime`);
|
||||
}
|
||||
const ageDate: Date = new Date(Date.now() - (stack.CreationTime?.getTime() ?? 0));
|
||||
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);
|
||||
CloudRunnerLogger.log(message);
|
||||
|
||||
return message;
|
||||
return message;
|
||||
} catch (error) {
|
||||
CloudRunnerLogger.error(
|
||||
`Failed to describe job ${job}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
public static async getLogGroups() {
|
||||
const result: LogGroups = [];
|
||||
const result: Array<LogGroup> = [];
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const ecs = new AWS.CloudWatchLogs();
|
||||
let logStreamInput: AWS.CloudWatchLogs.DescribeLogGroupsRequest = {
|
||||
const ecs = new CloudWatchLogs();
|
||||
let logStreamInput: DescribeLogGroupsCommandInput = {
|
||||
/* logGroupNamePrefix: 'game-ci' */
|
||||
};
|
||||
let logGroupsDescribe = await ecs.describeLogGroups(logStreamInput).promise();
|
||||
let logGroupsDescribe = await ecs.send(new DescribeLogGroupsCommand(logStreamInput));
|
||||
const logGroups = logGroupsDescribe.logGroups || [];
|
||||
while (logGroupsDescribe.nextToken) {
|
||||
logStreamInput = { /* logGroupNamePrefix: 'game-ci',*/ nextToken: logGroupsDescribe.nextToken };
|
||||
logGroupsDescribe = await ecs.describeLogGroups(logStreamInput).promise();
|
||||
logGroupsDescribe = await ecs.send(new DescribeLogGroupsCommand(logStreamInput));
|
||||
logGroups.push(...(logGroupsDescribe?.logGroups || []));
|
||||
}
|
||||
|
||||
|
@ -159,11 +197,12 @@ export class TaskService {
|
|||
}
|
||||
public static async getLocks() {
|
||||
process.env.AWS_REGION = Input.region;
|
||||
const s3 = new AWS.S3();
|
||||
const listRequest: ListObjectsRequest = {
|
||||
const s3 = new S3({ region: Input.region });
|
||||
const listRequest: ListObjectsCommandInput = {
|
||||
Bucket: CloudRunner.buildParameters.awsStackName,
|
||||
};
|
||||
const results = await s3.listObjects(listRequest).promise();
|
||||
|
||||
const results = await s3.send(new ListObjectsCommand(listRequest));
|
||||
|
||||
return results.Contents || [];
|
||||
}
|
||||
|
|
|
@ -41,7 +41,6 @@ class LocalDockerCloudRunner implements ProviderInterface {
|
|||
return new Promise((result) => result(``));
|
||||
}
|
||||
async cleanupWorkflow(
|
||||
buildGuid: string,
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
|
|
|
@ -14,12 +14,17 @@ import { CoreV1Api } from '@kubernetes/client-node';
|
|||
import CloudRunner from '../../cloud-runner';
|
||||
import { ProviderResource } from '../provider-resource';
|
||||
import { ProviderWorkflow } from '../provider-workflow';
|
||||
import { RemoteClientLogger } from '../../remote-client/remote-client-logger';
|
||||
import { KubernetesRole } from './kubernetes-role';
|
||||
import { CloudRunnerSystem } from '../../services/core/cloud-runner-system';
|
||||
|
||||
class Kubernetes implements ProviderInterface {
|
||||
public static Instance: Kubernetes;
|
||||
public kubeConfig!: k8s.KubeConfig;
|
||||
public kubeClient!: k8s.CoreV1Api;
|
||||
public kubeClientApps!: k8s.AppsV1Api;
|
||||
public kubeClientBatch!: k8s.BatchV1Api;
|
||||
public rbacAuthorizationV1Api!: k8s.RbacAuthorizationV1Api;
|
||||
public buildGuid: string = '';
|
||||
public buildParameters!: BuildParameters;
|
||||
public pvcName: string = '';
|
||||
|
@ -30,6 +35,7 @@ class Kubernetes implements ProviderInterface {
|
|||
public containerName: string = '';
|
||||
public cleanupCronJobName: string = '';
|
||||
public serviceAccountName: string = '';
|
||||
public ip: string = '';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
constructor(buildParameters: BuildParameters) {
|
||||
|
@ -37,11 +43,30 @@ class Kubernetes implements ProviderInterface {
|
|||
this.kubeConfig = new k8s.KubeConfig();
|
||||
this.kubeConfig.loadFromDefault();
|
||||
this.kubeClient = this.kubeConfig.makeApiClient(k8s.CoreV1Api);
|
||||
this.kubeClientApps = this.kubeConfig.makeApiClient(k8s.AppsV1Api);
|
||||
this.kubeClientBatch = this.kubeConfig.makeApiClient(k8s.BatchV1Api);
|
||||
this.rbacAuthorizationV1Api = this.kubeConfig.makeApiClient(k8s.RbacAuthorizationV1Api);
|
||||
this.namespace = 'default';
|
||||
CloudRunnerLogger.log('Loaded default Kubernetes configuration for this environment');
|
||||
}
|
||||
|
||||
async PushLogUpdate(logs: string) {
|
||||
// push logs to nginx file server via 'LOG_SERVICE_IP' env var
|
||||
const ip = process.env[`LOG_SERVICE_IP`];
|
||||
if (ip === undefined) {
|
||||
RemoteClientLogger.logWarning(`LOG_SERVICE_IP not set, skipping log push`);
|
||||
|
||||
return;
|
||||
}
|
||||
const url = `http://${ip}/api/log`;
|
||||
RemoteClientLogger.log(`Pushing logs to ${url}`);
|
||||
|
||||
// logs to base64
|
||||
logs = Buffer.from(logs).toString('base64');
|
||||
const response = await CloudRunnerSystem.Run(`curl -X POST -d "${logs}" ${url}`, false, true);
|
||||
RemoteClientLogger.log(`Pushed logs to ${url} ${response}`);
|
||||
}
|
||||
|
||||
async listResources(): Promise<ProviderResource[]> {
|
||||
const pods = await this.kubeClient.listNamespacedPod(this.namespace);
|
||||
const serviceAccounts = await this.kubeClient.listNamespacedServiceAccount(this.namespace);
|
||||
|
@ -115,9 +140,10 @@ class Kubernetes implements ProviderInterface {
|
|||
CloudRunnerLogger.log('Cloud Runner K8s workflow!');
|
||||
|
||||
// Setup
|
||||
const id = BuildParameters.shouldUseRetainedWorkspaceMode(this.buildParameters)
|
||||
? CloudRunner.lockedWorkspace
|
||||
: this.buildParameters.buildGuid;
|
||||
const id =
|
||||
BuildParameters && BuildParameters.shouldUseRetainedWorkspaceMode(this.buildParameters)
|
||||
? CloudRunner.lockedWorkspace
|
||||
: this.buildParameters.buildGuid;
|
||||
this.pvcName = `unity-builder-pvc-${id}`;
|
||||
await KubernetesStorage.createPersistentVolumeClaim(
|
||||
this.buildParameters,
|
||||
|
@ -137,10 +163,7 @@ class Kubernetes implements ProviderInterface {
|
|||
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}`,
|
||||
);
|
||||
CloudRunnerLogger.log('Pod is running');
|
||||
output += await KubernetesTaskRunner.runTask(
|
||||
this.kubeConfig,
|
||||
this.kubeClient,
|
||||
|
@ -232,8 +255,12 @@ class Kubernetes implements ProviderInterface {
|
|||
this.jobName,
|
||||
k8s,
|
||||
this.containerName,
|
||||
this.ip,
|
||||
);
|
||||
await new Promise((promise) => setTimeout(promise, 15000));
|
||||
|
||||
// await KubernetesRole.createRole(this.serviceAccountName, this.namespace, this.rbacAuthorizationV1Api);
|
||||
|
||||
const result = await this.kubeClientBatch.createNamespacedJob(this.namespace, jobSpec);
|
||||
CloudRunnerLogger.log(`Build job created`);
|
||||
await new Promise((promise) => setTimeout(promise, 5000));
|
||||
|
@ -257,6 +284,7 @@ class Kubernetes implements ProviderInterface {
|
|||
try {
|
||||
await this.kubeClientBatch.deleteNamespacedJob(this.jobName, this.namespace);
|
||||
await this.kubeClient.deleteNamespacedPod(this.podName, this.namespace);
|
||||
await KubernetesRole.deleteRole(this.serviceAccountName, this.namespace, this.rbacAuthorizationV1Api);
|
||||
} catch (error: any) {
|
||||
CloudRunnerLogger.log(`Failed to cleanup`);
|
||||
if (error.response.body.reason !== `NotFound`) {
|
||||
|
@ -275,14 +303,13 @@ class Kubernetes implements ProviderInterface {
|
|||
}
|
||||
|
||||
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)) {
|
||||
if (BuildParameters && BuildParameters.shouldUseRetainedWorkspaceMode(buildParameters)) {
|
||||
return;
|
||||
}
|
||||
CloudRunnerLogger.log(`deleting PVC`);
|
||||
|
|
|
@ -20,6 +20,7 @@ class KubernetesJobSpecFactory {
|
|||
jobName: string,
|
||||
k8s: any,
|
||||
containerName: string,
|
||||
ip: string = '',
|
||||
) {
|
||||
const job = new k8s.V1Job();
|
||||
job.apiVersion = 'batch/v1';
|
||||
|
@ -81,6 +82,7 @@ class KubernetesJobSpecFactory {
|
|||
|
||||
return environmentVariable;
|
||||
}),
|
||||
{ name: 'LOG_SERVICE_IP', value: ip },
|
||||
],
|
||||
volumeMounts: [
|
||||
{
|
||||
|
@ -92,9 +94,8 @@ class KubernetesJobSpecFactory {
|
|||
preStop: {
|
||||
exec: {
|
||||
command: [
|
||||
'bin/bash',
|
||||
'-c',
|
||||
`cd /data/builder/action/steps;
|
||||
`wait 60s;
|
||||
cd /data/builder/action/steps;
|
||||
chmod +x /return_license.sh;
|
||||
/return_license.sh;`,
|
||||
],
|
||||
|
@ -108,6 +109,16 @@ class KubernetesJobSpecFactory {
|
|||
},
|
||||
};
|
||||
|
||||
if (process.env['CLOUD_RUNNER_MINIKUBE']) {
|
||||
job.spec.template.spec.volumes[0] = {
|
||||
name: 'build-mount',
|
||||
hostPath: {
|
||||
path: `/data`,
|
||||
type: `Directory`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
job.spec.template.spec.containers[0].resources.requests[`ephemeral-storage`] = '10Gi';
|
||||
|
||||
return job;
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import { RbacAuthorizationV1Api } from '@kubernetes/client-node';
|
||||
|
||||
class KubernetesRole {
|
||||
static async createRole(serviceAccountName: string, namespace: string, rbac: RbacAuthorizationV1Api) {
|
||||
// create admin kubernetes role and role binding
|
||||
const roleBinding = {
|
||||
apiVersion: 'rbac.authorization.k8s.io/v1',
|
||||
kind: 'RoleBinding',
|
||||
metadata: {
|
||||
name: `${serviceAccountName}-admin`,
|
||||
namespace,
|
||||
},
|
||||
subjects: [
|
||||
{
|
||||
kind: 'ServiceAccount',
|
||||
name: serviceAccountName,
|
||||
namespace,
|
||||
},
|
||||
],
|
||||
roleRef: {
|
||||
apiGroup: 'rbac.authorization.k8s.io',
|
||||
kind: 'Role',
|
||||
name: `${serviceAccountName}-admin`,
|
||||
},
|
||||
};
|
||||
|
||||
const role = {
|
||||
apiVersion: 'rbac.authorization.k8s.io/v1',
|
||||
kind: 'Role',
|
||||
metadata: {
|
||||
name: `${serviceAccountName}-admin`,
|
||||
namespace,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
apiGroups: ['*'],
|
||||
resources: ['*'],
|
||||
verbs: ['*'],
|
||||
},
|
||||
],
|
||||
};
|
||||
const roleBindingResponse = await rbac.createNamespacedRoleBinding(namespace, roleBinding);
|
||||
const roleResponse = await rbac.createNamespacedRole(namespace, role);
|
||||
|
||||
return { roleBindingResponse, roleResponse };
|
||||
}
|
||||
|
||||
public static async deleteRole(serviceAccountName: string, namespace: string, rbac: RbacAuthorizationV1Api) {
|
||||
await rbac.deleteNamespacedRoleBinding(`${serviceAccountName}-admin`, namespace);
|
||||
await rbac.deleteNamespacedRole(`${serviceAccountName}-admin`, namespace);
|
||||
}
|
||||
}
|
||||
export { KubernetesRole };
|
|
@ -9,7 +9,7 @@ class KubernetesServiceAccount {
|
|||
serviceAccount.metadata = {
|
||||
name: serviceAccountName,
|
||||
};
|
||||
serviceAccount.automountServiceAccountToken = false;
|
||||
serviceAccount.automountServiceAccountToken = true;
|
||||
|
||||
return kubeClient.createNamespacedServiceAccount(namespace, serviceAccount);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ 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 = ``;
|
||||
|
||||
|
@ -22,38 +21,33 @@ class KubernetesTaskRunner {
|
|||
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}`,
|
||||
`Streaming logs from pod: ${podName} container: ${containerName} namespace: ${namespace} ${CloudRunner.buildParameters.kubeVolumeSize}/${CloudRunner.buildParameters.containerCpu}/${CloudRunner.buildParameters.containerMemory}`,
|
||||
);
|
||||
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;
|
||||
const callback = (outputChunk: string) => {
|
||||
output += outputChunk;
|
||||
|
||||
// split output chunk and handle per line
|
||||
for (const chunk of outputChunk.split(`\n`)) {
|
||||
({ shouldReadLogs, shouldCleanup, output } = FollowLogStreamService.handleIteration(
|
||||
chunk,
|
||||
shouldReadLogs,
|
||||
shouldCleanup,
|
||||
output,
|
||||
));
|
||||
}
|
||||
};
|
||||
try {
|
||||
logs = await CloudRunnerSystem.Run(
|
||||
`kubectl logs ${podName}${extraFlags} --timestamps${sinceTime}`,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
await CloudRunnerSystem.Run(`kubectl logs ${podName}${extraFlags}`, false, true, callback);
|
||||
} catch (error: any) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
const continueStreaming = await KubernetesPods.IsPodRunning(podName, namespace, kubeClient);
|
||||
|
@ -68,34 +62,6 @@ class KubernetesTaskRunner {
|
|||
}
|
||||
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;
|
||||
|
@ -106,14 +72,14 @@ class KubernetesTaskRunner {
|
|||
}
|
||||
|
||||
static async watchUntilPodRunning(kubeClient: CoreV1Api, podName: string, namespace: string) {
|
||||
let success: boolean = false;
|
||||
let waitComplete: 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';
|
||||
waitComplete = phase !== 'Pending';
|
||||
message = `Phase:${status.body.status?.phase} \n Reason:${
|
||||
status.body.status?.conditions?.[0].reason || ''
|
||||
} \n Message:${status.body.status?.conditions?.[0].message || ''}`;
|
||||
|
@ -133,7 +99,7 @@ class KubernetesTaskRunner {
|
|||
// 4,
|
||||
// ),
|
||||
// );
|
||||
if (success || phase !== 'Pending') return true;
|
||||
if (waitComplete || phase !== 'Pending') return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
|
@ -142,11 +108,11 @@ class KubernetesTaskRunner {
|
|||
intervalBetweenAttempts: 15000,
|
||||
},
|
||||
);
|
||||
if (!success) {
|
||||
if (!waitComplete) {
|
||||
CloudRunnerLogger.log(message);
|
||||
}
|
||||
|
||||
return success;
|
||||
return waitComplete;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,8 +32,6 @@ class LocalCloudRunner 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
|
||||
|
|
|
@ -6,8 +6,6 @@ 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
|
||||
|
|
|
@ -25,8 +25,6 @@ class TestCloudRunner 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
|
||||
|
|
|
@ -80,7 +80,7 @@ export class Caching {
|
|||
}
|
||||
|
||||
await CloudRunnerSystem.Run(
|
||||
`tar -cf ${cacheArtifactName}.tar${compressionSuffix} ${path.basename(sourceFolder)}`,
|
||||
`tar -cf ${cacheArtifactName}.tar${compressionSuffix} "${path.basename(sourceFolder)}"`,
|
||||
);
|
||||
await CloudRunnerSystem.Run(`du ${cacheArtifactName}.tar${compressionSuffix}`);
|
||||
assert(await fileExists(`${cacheArtifactName}.tar${compressionSuffix}`), 'cache archive exists');
|
||||
|
|
|
@ -12,16 +12,83 @@ import { CloudRunnerSystem } from '../services/core/cloud-runner-system';
|
|||
import YAML from 'yaml';
|
||||
import GitHub from '../../github';
|
||||
import BuildParameters from '../../build-parameters';
|
||||
import { Cli } from '../../cli/cli';
|
||||
import CloudRunnerOptions from '../options/cloud-runner-options';
|
||||
|
||||
export class RemoteClient {
|
||||
@CliFunction(`remote-cli-pre-build`, `sets up a repository, usually before a game-ci build`)
|
||||
static async runRemoteClientJob() {
|
||||
static async setupRemoteClient() {
|
||||
CloudRunnerLogger.log(`bootstrap game ci cloud runner...`);
|
||||
if (!(await RemoteClient.handleRetainedWorkspace())) {
|
||||
await RemoteClient.bootstrapRepository();
|
||||
}
|
||||
await RemoteClient.replaceLargePackageReferencesWithSharedReferences();
|
||||
await RemoteClient.runCustomHookFiles(`before-build`);
|
||||
}
|
||||
|
||||
@CliFunction('remote-cli-log-stream', `log stream from standard input`)
|
||||
public static async remoteClientLogStream() {
|
||||
const logFile = Cli.options!['logFile'];
|
||||
process.stdin.resume();
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
let lingeringLine = '';
|
||||
|
||||
process.stdin.on('data', (chunk) => {
|
||||
const lines = chunk.toString().split('\n');
|
||||
|
||||
lines[0] = lingeringLine + lines[0];
|
||||
lingeringLine = lines.pop() || '';
|
||||
|
||||
for (const element of lines) {
|
||||
if (CloudRunnerOptions.providerStrategy !== 'k8s') {
|
||||
CloudRunnerLogger.log(element);
|
||||
} else {
|
||||
fs.appendFileSync(logFile, element);
|
||||
CloudRunnerLogger.log(element);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
if (CloudRunnerOptions.providerStrategy !== 'k8s') {
|
||||
CloudRunnerLogger.log(lingeringLine);
|
||||
} else {
|
||||
fs.appendFileSync(logFile, lingeringLine);
|
||||
CloudRunnerLogger.log(lingeringLine);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@CliFunction(`remote-cli-post-build`, `runs a cloud runner build`)
|
||||
public static async remoteClientPostBuild(): Promise<string> {
|
||||
RemoteClientLogger.log(`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`);
|
||||
|
||||
// WIP - need to give the pod permissions to create config map
|
||||
await RemoteClientLogger.handleLogManagementPostJob();
|
||||
|
||||
return new Promise((result) => result(``));
|
||||
}
|
||||
static async runCustomHookFiles(hookLifecycle: string) {
|
||||
RemoteClientLogger.log(`RunCustomHookFiles: ${hookLifecycle}`);
|
||||
const gameCiCustomHooksPath = path.join(CloudRunnerFolders.repoPathAbsolute, `game-ci`, `hooks`);
|
||||
|
@ -47,7 +114,6 @@ export class RemoteClient {
|
|||
`mkdir -p ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.cacheFolderForCacheKeyFull)}`,
|
||||
);
|
||||
await RemoteClient.cloneRepoWithoutLFSFiles();
|
||||
await RemoteClient.replaceLargePackageReferencesWithSharedReferences();
|
||||
await RemoteClient.sizeOfFolder(
|
||||
'repo before lfs cache pull',
|
||||
CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute),
|
||||
|
|
|
@ -1,8 +1,18 @@
|
|||
import CloudRunnerLogger from '../services/core/cloud-runner-logger';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import CloudRunner from '../cloud-runner';
|
||||
import CloudRunnerOptions from '../options/cloud-runner-options';
|
||||
|
||||
export class RemoteClientLogger {
|
||||
private static get LogFilePath() {
|
||||
return path.join(`/home`, `job-log.txt`);
|
||||
}
|
||||
|
||||
public static log(message: string) {
|
||||
CloudRunnerLogger.log(`[Client] ${message}`);
|
||||
const finalMessage = `[Client] ${message}`;
|
||||
this.appendToFile(finalMessage);
|
||||
CloudRunnerLogger.log(finalMessage);
|
||||
}
|
||||
|
||||
public static logCliError(message: string) {
|
||||
|
@ -16,4 +26,57 @@ export class RemoteClientLogger {
|
|||
public static logWarning(message: string) {
|
||||
CloudRunnerLogger.logWarning(message);
|
||||
}
|
||||
|
||||
public static appendToFile(message: string) {
|
||||
if (CloudRunner.isCloudRunnerEnvironment) {
|
||||
fs.appendFileSync(RemoteClientLogger.LogFilePath, `${message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
public static async handleLogManagementPostJob() {
|
||||
if (CloudRunnerOptions.providerStrategy !== 'k8s') {
|
||||
return;
|
||||
}
|
||||
CloudRunnerLogger.log(`Collected Logs`);
|
||||
|
||||
// check for log file not existing
|
||||
if (!fs.existsSync(RemoteClientLogger.LogFilePath)) {
|
||||
CloudRunnerLogger.log(`Log file does not exist`);
|
||||
|
||||
// check if CloudRunner.isCloudRunnerEnvironment is true, log
|
||||
if (!CloudRunner.isCloudRunnerEnvironment) {
|
||||
CloudRunnerLogger.log(`Cloud Runner is not running in a cloud environment, not collecting logs`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
CloudRunnerLogger.log(`Log file exist`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
|
||||
// let hashedLogs = fs.readFileSync(RemoteClientLogger.LogFilePath).toString();
|
||||
//
|
||||
// hashedLogs = md5(hashedLogs);
|
||||
//
|
||||
// for (let index = 0; index < 3; index++) {
|
||||
// CloudRunnerLogger.log(`LOGHASH: ${hashedLogs}`);
|
||||
// const logs = fs.readFileSync(RemoteClientLogger.LogFilePath).toString();
|
||||
// CloudRunnerLogger.log(`LOGS: ${Buffer.from(logs).toString('base64')}`);
|
||||
// CloudRunnerLogger.log(
|
||||
// `Game CI's "Cloud Runner System" will cancel the log when it has successfully received the log data to verify all logs have been received.`,
|
||||
// );
|
||||
//
|
||||
// // wait for 15 seconds to allow the log to be sent
|
||||
// await new Promise((resolve) => setTimeout(resolve, 15000));
|
||||
// }
|
||||
}
|
||||
public static HandleLog(message: string): boolean {
|
||||
if (RemoteClientLogger.value !== '') {
|
||||
RemoteClientLogger.value += `\n`;
|
||||
}
|
||||
|
||||
RemoteClientLogger.value += message;
|
||||
|
||||
return false;
|
||||
}
|
||||
static value: string = '';
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import BuildParameters from '../../../build-parameters';
|
||||
|
||||
class CloudRunnerResult {
|
||||
public BuildParameters: BuildParameters;
|
||||
public BuildResults: string;
|
||||
public BuildSucceeded: boolean;
|
||||
public BuildFinished: boolean;
|
||||
public LibraryCacheUsed: boolean;
|
||||
|
||||
public constructor(
|
||||
buildParameters: BuildParameters,
|
||||
buildResults: string,
|
||||
buildSucceeded: boolean,
|
||||
buildFinished: boolean,
|
||||
libraryCacheUsed: boolean,
|
||||
) {
|
||||
this.BuildParameters = buildParameters;
|
||||
this.BuildResults = buildResults;
|
||||
this.BuildSucceeded = buildSucceeded;
|
||||
this.BuildFinished = buildFinished;
|
||||
this.LibraryCacheUsed = libraryCacheUsed;
|
||||
}
|
||||
}
|
||||
export default CloudRunnerResult;
|
|
@ -16,7 +16,13 @@ export class CloudRunnerSystem {
|
|||
});
|
||||
}
|
||||
|
||||
public static async Run(command: string, suppressError = false, suppressLogs = false) {
|
||||
public static async Run(
|
||||
command: string,
|
||||
suppressError = false,
|
||||
suppressLogs = false,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
outputCallback?: (output: string) => void,
|
||||
) {
|
||||
for (const element of command.split(`\n`)) {
|
||||
if (!suppressLogs) {
|
||||
RemoteClientLogger.log(element);
|
||||
|
@ -25,7 +31,7 @@ export class CloudRunnerSystem {
|
|||
|
||||
return await new Promise<string>((promise, throwError) => {
|
||||
let output = '';
|
||||
const child = exec(command, (error, stdout, stderr) => {
|
||||
const child = exec(command, { maxBuffer: 1024 * 10000 }, (error, stdout, stderr) => {
|
||||
if (!suppressError && error) {
|
||||
RemoteClientLogger.log(error.toString());
|
||||
throwError(error);
|
||||
|
@ -38,6 +44,9 @@ export class CloudRunnerSystem {
|
|||
output += diagnosticOutput;
|
||||
}
|
||||
const outputChunk = `${stdout}`;
|
||||
if (outputCallback) {
|
||||
outputCallback(outputChunk);
|
||||
}
|
||||
output += outputChunk;
|
||||
});
|
||||
child.on('close', (code) => {
|
||||
|
|
|
@ -146,7 +146,8 @@ export class TaskParameterSerializer {
|
|||
array = TaskParameterSerializer.tryAddInput(array, 'UNITY_SERIAL');
|
||||
array = TaskParameterSerializer.tryAddInput(array, 'UNITY_EMAIL');
|
||||
array = TaskParameterSerializer.tryAddInput(array, 'UNITY_PASSWORD');
|
||||
array = TaskParameterSerializer.tryAddInput(array, 'UNITY_LICENSE');
|
||||
|
||||
// array = TaskParameterSerializer.tryAddInput(array, 'UNITY_LICENSE');
|
||||
array = TaskParameterSerializer.tryAddInput(array, 'GIT_PRIVATE_TOKEN');
|
||||
|
||||
return array;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import YAML from 'yaml';
|
||||
import CloudRunner from '../../cloud-runner';
|
||||
import * as core from '@actions/core';
|
||||
import { CustomWorkflow } from '../../workflows/custom-workflow';
|
||||
import { RemoteClientLogger } from '../../remote-client/remote-client-logger';
|
||||
import path from 'node:path';
|
||||
|
@ -237,13 +236,11 @@ export class ContainerHookService {
|
|||
];
|
||||
|
||||
if (steps.length > 0) {
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.startGroup('post build steps');
|
||||
output += await CustomWorkflow.runContainerJob(
|
||||
steps,
|
||||
cloudRunnerStepState.environment,
|
||||
cloudRunnerStepState.secrets,
|
||||
);
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
|
||||
}
|
||||
|
||||
return output;
|
||||
|
@ -256,13 +253,11 @@ export class ContainerHookService {
|
|||
];
|
||||
|
||||
if (steps.length > 0) {
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.startGroup('pre build steps');
|
||||
output += await CustomWorkflow.runContainerJob(
|
||||
steps,
|
||||
cloudRunnerStepState.environment,
|
||||
cloudRunnerStepState.secrets,
|
||||
);
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
|
||||
}
|
||||
|
||||
return output;
|
||||
|
|
|
@ -24,11 +24,17 @@ describe('Cloud Runner Async Workflows', () => {
|
|||
unityVersion: UnityVersioning.read('test-project'),
|
||||
asyncCloudRunner: `true`,
|
||||
githubChecks: `true`,
|
||||
providerStrategy: 'k8s',
|
||||
buildPlatform: 'linux',
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
});
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
|
||||
// Run the job
|
||||
await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
|
||||
// wait for 15 seconds
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * 60 * 12));
|
||||
}, 1_000_000_000);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -34,6 +34,7 @@ describe('Cloud Runner Sync Environments', () => {
|
|||
versioning: 'None',
|
||||
projectPath: 'test-project',
|
||||
unityVersion: UnityVersioning.read('test-project'),
|
||||
targetPlatform: 'StandaloneWindows64',
|
||||
customJob: `
|
||||
- name: 'step 1'
|
||||
image: 'ubuntu'
|
||||
|
@ -44,9 +45,12 @@ describe('Cloud Runner Sync Environments', () => {
|
|||
`,
|
||||
});
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
if (baseImage.toString().includes('undefined')) {
|
||||
throw new Error(`Base image is undefined`);
|
||||
}
|
||||
|
||||
// Run the job
|
||||
const file = await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
const file = (await CloudRunner.run(buildParameter, baseImage.toString())).BuildResults;
|
||||
|
||||
// Assert results
|
||||
// expect(file).toContain(JSON.stringify(buildParameter));
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import CloudRunner from '../cloud-runner';
|
||||
import UnityVersioning from '../../unity-versioning';
|
||||
import setups from './cloud-runner-suite.test';
|
||||
import GitHub from '../../github';
|
||||
import { TIMEOUT_INFINITE, createParameters } from '../../../test-utils/cloud-runner-test-helpers';
|
||||
describe('Cloud Runner Github Checks', () => {
|
||||
setups();
|
||||
it('Responds', () => {});
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock GitHub API requests to avoid real network calls
|
||||
jest.spyOn(GitHub as any, 'createGitHubCheckRequest').mockResolvedValue({
|
||||
status: 201,
|
||||
data: { id: '1' },
|
||||
});
|
||||
jest.spyOn(GitHub as any, 'updateGitHubCheckRequest').mockResolvedValue({
|
||||
status: 200,
|
||||
data: {},
|
||||
});
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
jest.spyOn(GitHub as any, 'runUpdateAsyncChecksWorkflow').mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it(
|
||||
'Check Handling Direct',
|
||||
async () => {
|
||||
// Setup parameters
|
||||
const buildParameter = await createParameters({
|
||||
versioning: 'None',
|
||||
projectPath: 'test-project',
|
||||
unityVersion: UnityVersioning.read('test-project'),
|
||||
asyncCloudRunner: `true`,
|
||||
githubChecks: `true`,
|
||||
});
|
||||
await CloudRunner.setup(buildParameter);
|
||||
CloudRunner.buildParameters.githubCheckId = await GitHub.createGitHubCheck(`direct create`);
|
||||
await GitHub.updateGitHubCheck(`1 ${new Date().toISOString()}`, `direct`);
|
||||
await GitHub.updateGitHubCheck(`2 ${new Date().toISOString()}`, `direct`, `success`, `completed`);
|
||||
},
|
||||
TIMEOUT_INFINITE,
|
||||
);
|
||||
it(
|
||||
'Check Handling Via Async Workflow',
|
||||
async () => {
|
||||
// Setup parameters
|
||||
const buildParameter = await createParameters({
|
||||
versioning: 'None',
|
||||
projectPath: 'test-project',
|
||||
unityVersion: UnityVersioning.read('test-project'),
|
||||
asyncCloudRunner: `true`,
|
||||
githubChecks: `true`,
|
||||
});
|
||||
GitHub.forceAsyncTest = true;
|
||||
await CloudRunner.setup(buildParameter);
|
||||
CloudRunner.buildParameters.githubCheckId = await GitHub.createGitHubCheck(`async create`);
|
||||
await GitHub.updateGitHubCheck(`1 ${new Date().toISOString()}`, `async`);
|
||||
await GitHub.updateGitHubCheck(`2 ${new Date().toISOString()}`, `async`, `success`, `completed`);
|
||||
GitHub.forceAsyncTest = false;
|
||||
},
|
||||
TIMEOUT_INFINITE,
|
||||
);
|
||||
});
|
|
@ -30,6 +30,7 @@ commands: echo "test"`;
|
|||
projectPath: 'test-project',
|
||||
unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')),
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
image: 'ubuntu',
|
||||
cacheKey: `test-case-${uuidv4()}`,
|
||||
};
|
||||
CloudRunner.setup(await CreateParameters(overrides));
|
||||
|
@ -51,6 +52,7 @@ commands: echo "test"`;
|
|||
it('Should be 1 before and 1 after hook', async () => {
|
||||
const overrides = {
|
||||
versioning: 'None',
|
||||
image: 'ubuntu',
|
||||
projectPath: 'test-project',
|
||||
unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')),
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
|
@ -72,6 +74,7 @@ commands: echo "test"`;
|
|||
unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')),
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
cacheKey: `test-case-${uuidv4()}`,
|
||||
image: 'ubuntu',
|
||||
containerHookFiles: `my-test-step-pre-build,my-test-step-post-build`,
|
||||
commandHookFiles: `my-test-hook-pre-build,my-test-hook-post-build`,
|
||||
};
|
||||
|
@ -94,7 +97,8 @@ commands: echo "test"`;
|
|||
};
|
||||
const buildParameter2 = await CreateParameters(overrides);
|
||||
const baseImage2 = new ImageTag(buildParameter2);
|
||||
const results2 = await CloudRunner.run(buildParameter2, baseImage2.toString());
|
||||
const results2Object = await CloudRunner.run(buildParameter2, baseImage2.toString());
|
||||
const results2 = results2Object.BuildResults;
|
||||
CloudRunnerLogger.log(`run 2 succeeded`);
|
||||
|
||||
const buildContainsBuildSucceeded = results2.includes('Build succeeded');
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import { BuildParameters, ImageTag } from '../..';
|
||||
import UnityVersioning from '../../unity-versioning';
|
||||
import { Cli } from '../../cli/cli';
|
||||
import GitHub from '../../github';
|
||||
import setups from './cloud-runner-suite.test';
|
||||
|
||||
async function CreateParameters(overrides: any) {
|
||||
if (overrides) {
|
||||
Cli.options = overrides;
|
||||
}
|
||||
const originalValue = GitHub.githubInputEnabled;
|
||||
GitHub.githubInputEnabled = false;
|
||||
const results = await BuildParameters.create();
|
||||
GitHub.githubInputEnabled = originalValue;
|
||||
delete Cli.options;
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
describe('Cloud Runner Image', () => {
|
||||
setups();
|
||||
const testSecretName = 'testSecretName';
|
||||
const testSecretValue = 'testSecretValue';
|
||||
it('Can create valid image from normal config', async () => {
|
||||
// Setup parameters
|
||||
const buildParameter = await CreateParameters({
|
||||
versioning: 'None',
|
||||
projectPath: 'test-project',
|
||||
unityVersion: UnityVersioning.read('test-project'),
|
||||
targetPlatform: 'StandaloneWindows64',
|
||||
customJob: `
|
||||
- name: 'step 1'
|
||||
image: 'ubuntu'
|
||||
commands: 'printenv'
|
||||
secrets:
|
||||
- name: '${testSecretName}'
|
||||
value: '${testSecretValue}'
|
||||
`,
|
||||
});
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
if (buildParameter.targetPlatform === undefined) {
|
||||
throw new Error(`target platform includes undefined`);
|
||||
}
|
||||
if (baseImage.toString().includes('undefined')) {
|
||||
throw new Error(`Base image ${baseImage.toString()} includes undefined`);
|
||||
}
|
||||
if (baseImage.toString().includes('NaN')) {
|
||||
throw new Error(`Base image ${baseImage.toString()} includes nan`);
|
||||
}
|
||||
}, 1_000_000_000);
|
||||
});
|
|
@ -32,7 +32,8 @@ describe('Cloud Runner pre-built S3 steps', () => {
|
|||
};
|
||||
const buildParameter2 = await CreateParameters(overrides);
|
||||
const baseImage2 = new ImageTag(buildParameter2);
|
||||
const results2 = await CloudRunner.run(buildParameter2, baseImage2.toString());
|
||||
const results2Object = await CloudRunner.run(buildParameter2, baseImage2.toString());
|
||||
const results2 = results2Object.BuildResults;
|
||||
CloudRunnerLogger.log(`run 2 succeeded`);
|
||||
|
||||
const build2ContainsBuildSucceeded = results2.includes('Build succeeded');
|
||||
|
|
|
@ -24,11 +24,13 @@ describe('Cloud Runner Caching', () => {
|
|||
it('Run one build it should not use cache, run subsequent build which should use cache', async () => {
|
||||
const overrides = {
|
||||
versioning: 'None',
|
||||
image: 'ubuntu',
|
||||
projectPath: 'test-project',
|
||||
unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')),
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
cacheKey: `test-case-${uuidv4()}`,
|
||||
containerHookFiles: `debug-cache`,
|
||||
cloudRunnerBranch: `cloud-runner-develop`,
|
||||
};
|
||||
if (CloudRunnerOptions.providerStrategy === `k8s`) {
|
||||
overrides.containerHookFiles += `,aws-s3-pull-cache,aws-s3-upload-cache`;
|
||||
|
@ -37,7 +39,8 @@ describe('Cloud Runner Caching', () => {
|
|||
expect(buildParameter.projectPath).toEqual(overrides.projectPath);
|
||||
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
const results = await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
const resultsObject = await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
const results = resultsObject.BuildResults;
|
||||
const libraryString = 'Rebuilding Library because the asset database could not be found!';
|
||||
const cachePushFail = 'Did not push source folder to cache because it was empty Library';
|
||||
const buildSucceededString = 'Build succeeded';
|
||||
|
@ -63,12 +66,12 @@ describe('Cloud Runner Caching', () => {
|
|||
|
||||
buildParameter2.cacheKey = buildParameter.cacheKey;
|
||||
const baseImage2 = new ImageTag(buildParameter2);
|
||||
const results2 = await CloudRunner.run(buildParameter2, baseImage2.toString());
|
||||
const results2Object = await CloudRunner.run(buildParameter2, baseImage2.toString());
|
||||
const results2 = results2Object.BuildResults;
|
||||
CloudRunnerLogger.log(`run 2 succeeded`);
|
||||
|
||||
const build2ContainsCacheKey = results2.includes(buildParameter.cacheKey);
|
||||
const build2ContainsBuildSucceeded = results2.includes(buildSucceededString);
|
||||
const build2NotContainsNoLibraryMessage = !results2.includes(libraryString);
|
||||
const build2NotContainsZeroLibraryCacheFilesMessage = !results2.includes(
|
||||
'There is 0 files/dir in the cache pulled contents for Library',
|
||||
);
|
||||
|
@ -77,10 +80,13 @@ describe('Cloud Runner Caching', () => {
|
|||
);
|
||||
|
||||
expect(build2ContainsCacheKey).toBeTruthy();
|
||||
expect(results2).toContain('Activation successful');
|
||||
expect(build2ContainsBuildSucceeded).toBeTruthy();
|
||||
expect(results2).toContain(buildSucceededString);
|
||||
const splitResults = results2.split('Activation successful');
|
||||
expect(splitResults[splitResults.length - 1]).not.toContain(libraryString);
|
||||
expect(build2NotContainsZeroLibraryCacheFilesMessage).toBeTruthy();
|
||||
expect(build2NotContainsZeroLFSCacheFilesMessage).toBeTruthy();
|
||||
expect(build2NotContainsNoLibraryMessage).toBeTruthy();
|
||||
}, 1_000_000_000);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -29,7 +29,8 @@ describe('Cloud Runner Retain Workspace', () => {
|
|||
expect(buildParameter.projectPath).toEqual(overrides.projectPath);
|
||||
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
const results = await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
const resultsObject = await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
const results = resultsObject.BuildResults;
|
||||
const libraryString = 'Rebuilding Library because the asset database could not be found!';
|
||||
const cachePushFail = 'Did not push source folder to cache because it was empty Library';
|
||||
const buildSucceededString = 'Build succeeded';
|
||||
|
@ -51,7 +52,8 @@ describe('Cloud Runner Retain Workspace', () => {
|
|||
|
||||
buildParameter2.cacheKey = buildParameter.cacheKey;
|
||||
const baseImage2 = new ImageTag(buildParameter2);
|
||||
const results2 = await CloudRunner.run(buildParameter2, baseImage2.toString());
|
||||
const results2Object = await CloudRunner.run(buildParameter2, baseImage2.toString());
|
||||
const results2 = results2Object.BuildResults;
|
||||
CloudRunnerLogger.log(`run 2 succeeded`);
|
||||
|
||||
const build2ContainsCacheKey = results2.includes(buildParameter.cacheKey);
|
||||
|
@ -59,7 +61,6 @@ describe('Cloud Runner Retain Workspace', () => {
|
|||
const build2ContainsRetainedWorkspacePhrase = results2.includes(`Retained Workspace:`);
|
||||
const build2ContainsWorkspaceExistsAlreadyPhrase = results2.includes(`Retained Workspace Already Exists!`);
|
||||
const build2ContainsBuildSucceeded = results2.includes(buildSucceededString);
|
||||
const build2NotContainsNoLibraryMessage = !results2.includes(libraryString);
|
||||
const build2NotContainsZeroLibraryCacheFilesMessage = !results2.includes(
|
||||
'There is 0 files/dir in the cache pulled contents for Library',
|
||||
);
|
||||
|
@ -74,7 +75,8 @@ describe('Cloud Runner Retain Workspace', () => {
|
|||
expect(build2ContainsBuildSucceeded).toBeTruthy();
|
||||
expect(build2NotContainsZeroLibraryCacheFilesMessage).toBeTruthy();
|
||||
expect(build2NotContainsZeroLFSCacheFilesMessage).toBeTruthy();
|
||||
expect(build2NotContainsNoLibraryMessage).toBeTruthy();
|
||||
const splitResults = results2.split('Activation successful');
|
||||
expect(splitResults[splitResults.length - 1]).not.toContain(libraryString);
|
||||
}, 1_000_000_000);
|
||||
afterAll(async () => {
|
||||
await SharedWorkspaceLocking.CleanupWorkspace(CloudRunner.lockedWorkspace || ``, CloudRunner.buildParameters);
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import CloudRunner from '../../cloud-runner';
|
||||
import UnityVersioning from '../../../unity-versioning';
|
||||
import { Cli } from '../../../cli/cli';
|
||||
import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import CloudRunnerOptions from '../../options/cloud-runner-options';
|
||||
import setups from '../cloud-runner-suite.test';
|
||||
import BuildParameters from '../../../build-parameters';
|
||||
import ImageTag from '../../../image-tag';
|
||||
|
||||
async function CreateParameters(overrides: any) {
|
||||
if (overrides) {
|
||||
Cli.options = overrides;
|
||||
}
|
||||
|
||||
return await BuildParameters.create();
|
||||
}
|
||||
|
||||
describe('Cloud Runner Kubernetes', () => {
|
||||
it('Responds', () => {});
|
||||
setups();
|
||||
|
||||
if (CloudRunnerOptions.cloudRunnerDebug) {
|
||||
it('Run one build it using K8s without error', async () => {
|
||||
if (CloudRunnerOptions.providerStrategy !== `k8s`) {
|
||||
return;
|
||||
}
|
||||
process.env.USE_IL2CPP = 'false';
|
||||
const overrides = {
|
||||
versioning: 'None',
|
||||
projectPath: 'test-project',
|
||||
unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')),
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
cacheKey: `test-case-${uuidv4()}`,
|
||||
providerStrategy: 'k8s',
|
||||
buildPlatform: 'linux',
|
||||
};
|
||||
const buildParameter = await CreateParameters(overrides);
|
||||
expect(buildParameter.projectPath).toEqual(overrides.projectPath);
|
||||
|
||||
const baseImage = new ImageTag(buildParameter);
|
||||
const resultsObject = await CloudRunner.run(buildParameter, baseImage.toString());
|
||||
const results = resultsObject.BuildResults;
|
||||
const libraryString = 'Rebuilding Library because the asset database could not be found!';
|
||||
const cachePushFail = 'Did not push source folder to cache because it was empty Library';
|
||||
const buildSucceededString = 'Build succeeded';
|
||||
|
||||
expect(results).toContain('Collected Logs');
|
||||
expect(results).toContain(libraryString);
|
||||
expect(results).toContain(buildSucceededString);
|
||||
expect(results).not.toContain(cachePushFail);
|
||||
|
||||
CloudRunnerLogger.log(`run 1 succeeded`);
|
||||
}, 1_000_000_000);
|
||||
}
|
||||
});
|
|
@ -41,6 +41,11 @@ node /builder/dist/index.js -m async-workflow`,
|
|||
[
|
||||
...secrets,
|
||||
...[
|
||||
{
|
||||
ParameterKey: `GITHUB_TOKEN`,
|
||||
EnvironmentVariable: `GITHUB_TOKEN`,
|
||||
ParameterValue: process.env.GITHUB_TOKEN || ``,
|
||||
},
|
||||
{
|
||||
ParameterKey: `AWS_ACCESS_KEY_ID`,
|
||||
EnvironmentVariable: `AWS_ACCESS_KEY_ID`,
|
||||
|
|
|
@ -2,7 +2,6 @@ import CloudRunnerLogger from '../services/core/cloud-runner-logger';
|
|||
import { CloudRunnerFolders } from '../options/cloud-runner-folders';
|
||||
import { CloudRunnerStepParameters } from '../options/cloud-runner-step-parameters';
|
||||
import { WorkflowInterface } from './workflow-interface';
|
||||
import * as core from '@actions/core';
|
||||
import { CommandHookService } from '../services/hooks/command-hook-service';
|
||||
import path from 'node:path';
|
||||
import CloudRunner from '../cloud-runner';
|
||||
|
@ -21,8 +20,6 @@ export class BuildAutomationWorkflow implements WorkflowInterface {
|
|||
|
||||
output += await ContainerHookService.RunPreBuildSteps(cloudRunnerStepState);
|
||||
CloudRunnerLogger.logWithTime('Configurable pre build step(s) time');
|
||||
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.startGroup('build');
|
||||
CloudRunnerLogger.log(baseImage);
|
||||
CloudRunnerLogger.logLine(` `);
|
||||
CloudRunnerLogger.logLine('Starting build automation job');
|
||||
|
@ -36,7 +33,6 @@ export class BuildAutomationWorkflow implements WorkflowInterface {
|
|||
cloudRunnerStepState.environment,
|
||||
cloudRunnerStepState.secrets,
|
||||
);
|
||||
if (!CloudRunner.buildParameters.isCliMode) core.endGroup();
|
||||
CloudRunnerLogger.logWithTime('Build time');
|
||||
|
||||
output += await ContainerHookService.RunPostBuildSteps(cloudRunnerStepState);
|
||||
|
@ -58,11 +54,14 @@ export class BuildAutomationWorkflow implements WorkflowInterface {
|
|||
path.join(CloudRunnerFolders.builderPathAbsolute, 'dist', `index.js`),
|
||||
);
|
||||
|
||||
return `apt-get update > /dev/null
|
||||
return `echo "cloud runner build workflow starting"
|
||||
apt-get update > /dev/null
|
||||
apt-get install -y curl tar tree npm git-lfs jq git > /dev/null
|
||||
npm i -g n > /dev/null
|
||||
n 16.15.1 > /dev/null
|
||||
npm --version
|
||||
npm i -g n > /dev/null
|
||||
npm i -g semver > /dev/null
|
||||
npm install --global yarn > /dev/null
|
||||
n 20.8.0
|
||||
node --version
|
||||
${setupHooks.filter((x) => x.hook.includes(`before`)).map((x) => x.commands) || ' '}
|
||||
export GITHUB_WORKSPACE="${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute)}"
|
||||
|
@ -91,6 +90,7 @@ export class BuildAutomationWorkflow implements WorkflowInterface {
|
|||
|
||||
return `export GIT_DISCOVERY_ACROSS_FILESYSTEM=1
|
||||
${cloneBuilderCommands}
|
||||
echo "log start" >> /home/job-log.txt
|
||||
node ${builderPath} -m remote-cli-pre-build`;
|
||||
}
|
||||
|
||||
|
@ -98,7 +98,7 @@ node ${builderPath} -m remote-cli-pre-build`;
|
|||
const distFolder = path.join(CloudRunnerFolders.builderPathAbsolute, 'dist');
|
||||
const ubuntuPlatformsFolder = path.join(CloudRunnerFolders.builderPathAbsolute, 'dist', 'platforms', 'ubuntu');
|
||||
|
||||
return `echo "game ci cloud runner initalized"
|
||||
return `
|
||||
mkdir -p ${`${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.projectBuildFolderAbsolute)}/build`}
|
||||
cd ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.projectPathAbsolute)}
|
||||
cp -r "${CloudRunnerFolders.ToLinuxFolder(path.join(distFolder, 'default-build-script'))}" "/UnityBuilderAction"
|
||||
|
@ -107,9 +107,8 @@ node ${builderPath} -m remote-cli-pre-build`;
|
|||
chmod -R +x "/entrypoint.sh"
|
||||
chmod -R +x "/steps"
|
||||
echo "game ci start"
|
||||
/entrypoint.sh
|
||||
echo "game ci caching results"
|
||||
chmod +x ${builderPath}
|
||||
echo "game ci start" >> /home/job-log.txt
|
||||
/entrypoint.sh | node ${builderPath} -m remote-cli-log-stream --logFile /home/job-log.txt
|
||||
node ${builderPath} -m remote-cli-post-build`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,9 @@ class Docker {
|
|||
break;
|
||||
case 'win32':
|
||||
runCommand = this.getWindowsCommand(image, parameters);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Operation system, ${process.platform}, is not supported yet.`);
|
||||
}
|
||||
|
||||
options.silent = silent;
|
||||
|
@ -89,6 +92,7 @@ class Docker {
|
|||
const {
|
||||
workspace,
|
||||
actionFolder,
|
||||
runnerTempPath,
|
||||
gitPrivateToken,
|
||||
dockerWorkspacePath,
|
||||
dockerCpuLimit,
|
||||
|
@ -96,13 +100,18 @@ class Docker {
|
|||
dockerIsolationMode,
|
||||
} = parameters;
|
||||
|
||||
const githubHome = path.join(runnerTempPath, '_github_home');
|
||||
if (!existsSync(githubHome)) mkdirSync(githubHome);
|
||||
|
||||
return `docker run \
|
||||
--workdir c:${dockerWorkspacePath} \
|
||||
--rm \
|
||||
${ImageEnvironmentFactory.getEnvVarString(parameters)} \
|
||||
--env BEE_CACHE_DIRECTORY=c:${dockerWorkspacePath}/Library/bee_cache \
|
||||
--env GITHUB_WORKSPACE=c:${dockerWorkspacePath} \
|
||||
${gitPrivateToken ? `--env GIT_PRIVATE_TOKEN="${gitPrivateToken}"` : ''} \
|
||||
--volume "${workspace}":"c:${dockerWorkspacePath}" \
|
||||
--volume "${githubHome}":"C:/githubhome" \
|
||||
--volume "c:/regkeys":"c:/regkeys" \
|
||||
--volume "C:/Program Files/Microsoft Visual Studio":"C:/Program Files/Microsoft Visual Studio" \
|
||||
--volume "C:/Program Files (x86)/Microsoft Visual Studio":"C:/Program Files (x86)/Microsoft Visual Studio" \
|
||||
|
@ -110,6 +119,7 @@ class Docker {
|
|||
--volume "C:/ProgramData/Microsoft/VisualStudio":"C:/ProgramData/Microsoft/VisualStudio" \
|
||||
--volume "${actionFolder}/default-build-script":"c:/UnityBuilderAction" \
|
||||
--volume "${actionFolder}/platforms/windows":"c:/steps" \
|
||||
--volume "${actionFolder}/unity-config":"C:/ProgramData/Unity/config" \
|
||||
--volume "${actionFolder}/BlankProject":"c:/BlankProject" \
|
||||
--cpus=${dockerCpuLimit} \
|
||||
--memory=${dockerMemoryLimit} \
|
||||
|
|
|
@ -11,6 +11,7 @@ class GitHub {
|
|||
private static startedDate: string;
|
||||
private static endedDate: string;
|
||||
static result: string = ``;
|
||||
static forceAsyncTest: boolean;
|
||||
private static get octokitDefaultToken() {
|
||||
return new Octokit({
|
||||
auth: process.env.GITHUB_TOKEN,
|
||||
|
@ -51,7 +52,7 @@ class GitHub {
|
|||
}
|
||||
GitHub.startedDate = new Date().toISOString();
|
||||
|
||||
CloudRunnerLogger.log(`Creating inital github check`);
|
||||
CloudRunnerLogger.log(`Creating github check`);
|
||||
const data = {
|
||||
owner: GitHub.owner,
|
||||
repo: GitHub.repo,
|
||||
|
@ -78,6 +79,8 @@ class GitHub {
|
|||
};
|
||||
const result = await GitHub.createGitHubCheckRequest(data);
|
||||
|
||||
CloudRunnerLogger.log(`Creating github check ${result.status}`);
|
||||
|
||||
return result.data.id.toString();
|
||||
}
|
||||
|
||||
|
@ -127,7 +130,7 @@ class GitHub {
|
|||
data.conclusion = result;
|
||||
}
|
||||
|
||||
await (CloudRunner.isCloudRunnerAsyncEnvironment
|
||||
await (CloudRunner.isCloudRunnerAsyncEnvironment || GitHub.forceAsyncTest
|
||||
? GitHub.runUpdateAsyncChecksWorkflow(data, `update`)
|
||||
: GitHub.updateGitHubCheckRequest(data));
|
||||
}
|
||||
|
@ -174,38 +177,46 @@ class GitHub {
|
|||
|
||||
static async triggerWorkflowOnComplete(triggerWorkflowOnComplete: string[]) {
|
||||
const isLocalAsync = CloudRunner.buildParameters.asyncWorkflow && !CloudRunner.isCloudRunnerAsyncEnvironment;
|
||||
if (isLocalAsync) {
|
||||
if (isLocalAsync || triggerWorkflowOnComplete === undefined || triggerWorkflowOnComplete.length === 0) {
|
||||
return;
|
||||
}
|
||||
const workflowsResult = await GitHub.octokitPAT.request(`GET /repos/{owner}/{repo}/actions/workflows`, {
|
||||
owner: GitHub.owner,
|
||||
repo: GitHub.repo,
|
||||
});
|
||||
const workflows = workflowsResult.data.workflows;
|
||||
CloudRunnerLogger.log(`Got ${workflows.length} workflows`);
|
||||
for (const element of triggerWorkflowOnComplete) {
|
||||
let selectedId = ``;
|
||||
for (let index = 0; index < workflowsResult.data.total_count; index++) {
|
||||
if (workflows[index].name === element) {
|
||||
selectedId = workflows[index].id.toString();
|
||||
}
|
||||
}
|
||||
if (selectedId === ``) {
|
||||
core.info(JSON.stringify(workflows));
|
||||
throw new Error(`no workflow with name "${GitHub.asyncChecksApiWorkflowName}"`);
|
||||
}
|
||||
await GitHub.octokitPAT.request(`POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches`, {
|
||||
try {
|
||||
const workflowsResult = await GitHub.octokitPAT.request(`GET /repos/{owner}/{repo}/actions/workflows`, {
|
||||
owner: GitHub.owner,
|
||||
repo: GitHub.repo,
|
||||
// eslint-disable-next-line camelcase
|
||||
workflow_id: selectedId,
|
||||
ref: CloudRunnerOptions.branch,
|
||||
inputs: {
|
||||
buildGuid: CloudRunner.buildParameters.buildGuid,
|
||||
},
|
||||
});
|
||||
const workflows = workflowsResult.data.workflows;
|
||||
CloudRunnerLogger.log(`Got ${workflows.length} workflows`);
|
||||
for (const element of triggerWorkflowOnComplete) {
|
||||
let selectedId = ``;
|
||||
for (let index = 0; index < workflowsResult.data.total_count; index++) {
|
||||
if (workflows[index].name === element) {
|
||||
selectedId = workflows[index].id.toString();
|
||||
}
|
||||
}
|
||||
if (selectedId === ``) {
|
||||
core.info(JSON.stringify(workflows));
|
||||
throw new Error(`no workflow with name "${GitHub.asyncChecksApiWorkflowName}"`);
|
||||
}
|
||||
await GitHub.octokitPAT.request(`POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches`, {
|
||||
owner: GitHub.owner,
|
||||
repo: GitHub.repo,
|
||||
// eslint-disable-next-line camelcase
|
||||
workflow_id: selectedId,
|
||||
ref: CloudRunnerOptions.branch,
|
||||
inputs: {
|
||||
buildGuid: CloudRunner.buildParameters.buildGuid,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
core.info(`github workflow complete hook not found`);
|
||||
}
|
||||
}
|
||||
|
||||
public static async getCheckStatus() {
|
||||
return await GitHub.octokitDefaultToken.request(`GET /repos/{owner}/{repo}/check-runs/{check_run_id}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default GitHub;
|
||||
|
|
|
@ -29,18 +29,21 @@ class ImageEnvironmentFactory {
|
|||
name: 'UNITY_LICENSING_SERVER',
|
||||
value: parameters.unityLicensingServer,
|
||||
},
|
||||
{ name: 'SKIP_ACTIVATION', value: parameters.skipActivation },
|
||||
{ name: 'UNITY_VERSION', value: parameters.editorVersion },
|
||||
{
|
||||
name: 'USYM_UPLOAD_AUTH_TOKEN',
|
||||
value: process.env.USYM_UPLOAD_AUTH_TOKEN,
|
||||
},
|
||||
{ name: 'PROJECT_PATH', value: parameters.projectPath },
|
||||
{ name: 'BUILD_PROFILE', value: parameters.buildProfile },
|
||||
{ name: 'BUILD_TARGET', value: parameters.targetPlatform },
|
||||
{ name: 'BUILD_NAME', value: parameters.buildName },
|
||||
{ name: 'BUILD_PATH', value: parameters.buildPath },
|
||||
{ name: 'BUILD_FILE', value: parameters.buildFile },
|
||||
{ name: 'BUILD_METHOD', value: parameters.buildMethod },
|
||||
{ name: 'MANUAL_EXIT', value: parameters.manualExit },
|
||||
{ name: 'ENABLE_GPU', value: parameters.enableGpu },
|
||||
{ name: 'VERSION', value: parameters.buildVersion },
|
||||
{ name: 'ANDROID_VERSION_CODE', value: parameters.androidVersionCode },
|
||||
{ name: 'ANDROID_KEYSTORE_NAME', value: parameters.androidKeystoreName },
|
||||
|
@ -81,20 +84,12 @@ class ImageEnvironmentFactory {
|
|||
];
|
||||
if (parameters.providerStrategy === 'local-docker') {
|
||||
for (const element of additionalVariables) {
|
||||
if (
|
||||
environmentVariables.find(
|
||||
(x) => element !== undefined && element.name !== undefined && x.name === element.name,
|
||||
) === undefined
|
||||
) {
|
||||
if (!environmentVariables.some((x) => element?.name === x?.name)) {
|
||||
environmentVariables.push(element);
|
||||
}
|
||||
}
|
||||
for (const variable of environmentVariables) {
|
||||
if (
|
||||
environmentVariables.find(
|
||||
(x) => variable !== undefined && variable.name !== undefined && x.name === variable.name,
|
||||
) === undefined
|
||||
) {
|
||||
if (!environmentVariables.some((x) => variable?.name === x?.name)) {
|
||||
environmentVariables = environmentVariables.filter((x) => x !== variable);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import ImageTag from './image-tag';
|
|||
|
||||
describe('ImageTag', () => {
|
||||
const testImageParameters = {
|
||||
editorVersion: '2099.9.f9f9',
|
||||
editorVersion: '2099.9.9f9',
|
||||
targetPlatform: 'Test',
|
||||
builderPlatform: '',
|
||||
containerRegistryRepository: 'unityci/editor',
|
||||
|
@ -27,7 +27,7 @@ describe('ImageTag', () => {
|
|||
expect(image.builderPlatform).toStrictEqual(testImageParameters.builderPlatform);
|
||||
});
|
||||
|
||||
test.each(['2000.0.0f0', '2011.1.11f1'])('accepts %p version format', (version) => {
|
||||
test.each(['2000.0.0f0', '2011.1.11f1', '6000.0.0f1'])('accepts %p version format', (version) => {
|
||||
expect(
|
||||
() =>
|
||||
new ImageTag({
|
||||
|
@ -50,23 +50,23 @@ describe('ImageTag', () => {
|
|||
describe('toString', () => {
|
||||
it('returns the correct version', () => {
|
||||
const image = new ImageTag({
|
||||
editorVersion: '2099.1.1111',
|
||||
editorVersion: '2099.1.1111f1',
|
||||
targetPlatform: testImageParameters.targetPlatform,
|
||||
containerRegistryRepository: 'unityci/editor',
|
||||
containerRegistryImageVersion: '3',
|
||||
});
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
expect(image.toString()).toStrictEqual(`${defaults.image}:windows-2099.1.1111-3`);
|
||||
expect(image.toString()).toStrictEqual(`${defaults.image}:windows-2099.1.1111f1-3`);
|
||||
break;
|
||||
case 'linux':
|
||||
expect(image.toString()).toStrictEqual(`${defaults.image}:ubuntu-2099.1.1111-3`);
|
||||
expect(image.toString()).toStrictEqual(`${defaults.image}:ubuntu-2099.1.1111f1-3`);
|
||||
break;
|
||||
}
|
||||
});
|
||||
it('returns customImage if given', () => {
|
||||
const image = new ImageTag({
|
||||
editorVersion: '2099.1.1111',
|
||||
editorVersion: '2099.1.1111f1',
|
||||
targetPlatform: testImageParameters.targetPlatform,
|
||||
customImage: `${defaults.image}:2099.1.1111@347598437689743986`,
|
||||
containerRegistryRepository: 'unityci/editor',
|
||||
|
|
|
@ -2,7 +2,6 @@ import Platform from './platform';
|
|||
|
||||
class ImageTag {
|
||||
public repository: string;
|
||||
public cloudRunnerBuilderPlatform!: string;
|
||||
public editorVersion: string;
|
||||
public targetPlatform: string;
|
||||
public builderPlatform: string;
|
||||
|
@ -15,9 +14,10 @@ class ImageTag {
|
|||
editorVersion,
|
||||
targetPlatform,
|
||||
customImage,
|
||||
cloudRunnerBuilderPlatform,
|
||||
buildPlatform,
|
||||
containerRegistryRepository,
|
||||
containerRegistryImageVersion,
|
||||
providerStrategy,
|
||||
} = imageProperties;
|
||||
|
||||
if (!ImageTag.versionPattern.test(editorVersion)) {
|
||||
|
@ -32,17 +32,17 @@ class ImageTag {
|
|||
this.repository = containerRegistryRepository;
|
||||
this.editorVersion = editorVersion;
|
||||
this.targetPlatform = targetPlatform;
|
||||
this.cloudRunnerBuilderPlatform = cloudRunnerBuilderPlatform;
|
||||
const isCloudRunnerLocal = cloudRunnerBuilderPlatform === 'local' || cloudRunnerBuilderPlatform === undefined;
|
||||
this.builderPlatform = ImageTag.getTargetPlatformToTargetPlatformSuffixMap(targetPlatform, editorVersion);
|
||||
this.imagePlatformPrefix = ImageTag.getImagePlatformPrefixes(
|
||||
isCloudRunnerLocal ? process.platform : cloudRunnerBuilderPlatform,
|
||||
this.builderPlatform = ImageTag.getTargetPlatformToTargetPlatformSuffixMap(
|
||||
targetPlatform,
|
||||
editorVersion,
|
||||
providerStrategy,
|
||||
);
|
||||
this.imagePlatformPrefix = ImageTag.getImagePlatformPrefixes(buildPlatform);
|
||||
this.imageRollingVersion = Number(containerRegistryImageVersion); // Will automatically roll to the latest non-breaking version.
|
||||
}
|
||||
|
||||
static get versionPattern(): RegExp {
|
||||
return /^(20\d{2}\.\d\.\w{3,4}|3)$/;
|
||||
return /^\d+\.\d+\.\d+[a-z]\d+$/;
|
||||
}
|
||||
|
||||
static get targetPlatformSuffixes() {
|
||||
|
@ -58,11 +58,16 @@ class ImageTag {
|
|||
android: 'android',
|
||||
ios: 'ios',
|
||||
tvos: 'appletv',
|
||||
visionos: 'visionos',
|
||||
facebook: 'facebook',
|
||||
};
|
||||
}
|
||||
|
||||
static getImagePlatformPrefixes(platform: string): string {
|
||||
if (!platform || platform === '') {
|
||||
platform = process.platform;
|
||||
}
|
||||
|
||||
switch (platform) {
|
||||
case 'win32':
|
||||
return 'windows';
|
||||
|
@ -73,9 +78,26 @@ class ImageTag {
|
|||
}
|
||||
}
|
||||
|
||||
static getTargetPlatformToTargetPlatformSuffixMap(platform: string, version: string): string {
|
||||
const { generic, webgl, mac, windows, windowsIl2cpp, wsaPlayer, linux, linuxIl2cpp, android, ios, tvos, facebook } =
|
||||
ImageTag.targetPlatformSuffixes;
|
||||
static getTargetPlatformToTargetPlatformSuffixMap(
|
||||
platform: string,
|
||||
version: string,
|
||||
providerStrategy: string,
|
||||
): string {
|
||||
const {
|
||||
generic,
|
||||
webgl,
|
||||
mac,
|
||||
windows,
|
||||
windowsIl2cpp,
|
||||
wsaPlayer,
|
||||
linux,
|
||||
linuxIl2cpp,
|
||||
android,
|
||||
ios,
|
||||
tvos,
|
||||
visionos,
|
||||
facebook,
|
||||
} = ImageTag.targetPlatformSuffixes;
|
||||
|
||||
const [major, minor] = version.split('.').map((digit) => Number(digit));
|
||||
|
||||
|
@ -102,7 +124,11 @@ class ImageTag {
|
|||
case Platform.types.StandaloneLinux64: {
|
||||
// Unity versions before 2019.3 do not support il2cpp
|
||||
if (major >= 2020 || (major === 2019 && minor >= 3)) {
|
||||
return linuxIl2cpp;
|
||||
if (providerStrategy === 'local') {
|
||||
return linuxIl2cpp;
|
||||
} else {
|
||||
return process.env.USE_IL2CPP === 'true' ? linuxIl2cpp : linux;
|
||||
}
|
||||
}
|
||||
|
||||
return linux;
|
||||
|
@ -124,11 +150,17 @@ class ImageTag {
|
|||
case Platform.types.XboxOne:
|
||||
return windows;
|
||||
case Platform.types.tvOS:
|
||||
if (process.platform !== 'win32') {
|
||||
throw new Error(`tvOS can only be built on a windows base OS`);
|
||||
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
||||
throw new Error(`tvOS can only be built on Windows or macOS base OS`);
|
||||
}
|
||||
|
||||
return tvos;
|
||||
case Platform.types.VisionOS:
|
||||
if (process.platform !== 'darwin') {
|
||||
throw new Error(`visionOS can only be built on a macOS base OS`);
|
||||
}
|
||||
|
||||
return visionos;
|
||||
case Platform.types.Switch:
|
||||
return windows;
|
||||
|
||||
|
@ -169,7 +201,7 @@ class ImageTag {
|
|||
|
||||
if (customImage) return customImage;
|
||||
|
||||
return `${image}:${tag}`; // '0' here represents the docker repo version
|
||||
return `${image}:${tag}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -59,6 +59,19 @@ describe('Input', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('buildProfile', () => {
|
||||
it('returns the default value', () => {
|
||||
expect(Input.buildProfile).toStrictEqual('');
|
||||
});
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
const mockValue = 'path/to/build_profile.asset';
|
||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||
expect(Input.buildProfile).toStrictEqual(mockValue);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildName', () => {
|
||||
it('returns the default value', () => {
|
||||
expect(Input.buildName).toStrictEqual(Input.targetPlatform);
|
||||
|
@ -122,6 +135,24 @@ describe('Input', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('enableGpu', () => {
|
||||
it('returns the default value', () => {
|
||||
expect(Input.enableGpu).toStrictEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when string true is passed', () => {
|
||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.enableGpu).toStrictEqual(true);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('returns false when string false is passed', () => {
|
||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.enableGpu).toStrictEqual(false);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('versioningStrategy', () => {
|
||||
it('returns the default value', () => {
|
||||
expect(Input.versioningStrategy).toStrictEqual('Semantic');
|
||||
|
|
|
@ -46,11 +46,11 @@ class Input {
|
|||
}
|
||||
|
||||
static get region(): string {
|
||||
return Input.getInput('region') || 'eu-west-2';
|
||||
return Input.getInput('region') ?? 'eu-west-2';
|
||||
}
|
||||
|
||||
static get githubRepo(): string | undefined {
|
||||
return Input.getInput('GITHUB_REPOSITORY') || Input.getInput('GITHUB_REPO') || undefined;
|
||||
return Input.getInput('GITHUB_REPOSITORY') ?? Input.getInput('GITHUB_REPO') ?? undefined;
|
||||
}
|
||||
|
||||
static get branch(): string {
|
||||
|
@ -74,19 +74,19 @@ class Input {
|
|||
}
|
||||
|
||||
static get runNumber(): string {
|
||||
return Input.getInput('GITHUB_RUN_NUMBER') || '0';
|
||||
return Input.getInput('GITHUB_RUN_NUMBER') ?? '0';
|
||||
}
|
||||
|
||||
static get targetPlatform(): string {
|
||||
return Input.getInput('targetPlatform') || Platform.default;
|
||||
return Input.getInput('targetPlatform') ?? Platform.default;
|
||||
}
|
||||
|
||||
static get unityVersion(): string {
|
||||
return Input.getInput('unityVersion') || 'auto';
|
||||
return Input.getInput('unityVersion') ?? 'auto';
|
||||
}
|
||||
|
||||
static get customImage(): string {
|
||||
return Input.getInput('customImage') || '';
|
||||
return Input.getInput('customImage') ?? '';
|
||||
}
|
||||
|
||||
static get projectPath(): string {
|
||||
|
@ -107,86 +107,96 @@ class Input {
|
|||
return rawProjectPath.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
static get buildProfile(): string {
|
||||
return Input.getInput('buildProfile') ?? '';
|
||||
}
|
||||
|
||||
static get runnerTempPath(): string {
|
||||
return Input.getInput('RUNNER_TEMP') || '';
|
||||
return Input.getInput('RUNNER_TEMP') ?? '';
|
||||
}
|
||||
|
||||
static get buildName(): string {
|
||||
return Input.getInput('buildName') || Input.targetPlatform;
|
||||
return Input.getInput('buildName') ?? Input.targetPlatform;
|
||||
}
|
||||
|
||||
static get buildsPath(): string {
|
||||
return Input.getInput('buildsPath') || 'build';
|
||||
return Input.getInput('buildsPath') ?? 'build';
|
||||
}
|
||||
|
||||
static get unityLicensingServer(): string {
|
||||
return Input.getInput('unityLicensingServer') || '';
|
||||
return Input.getInput('unityLicensingServer') ?? '';
|
||||
}
|
||||
|
||||
static get buildMethod(): string {
|
||||
return Input.getInput('buildMethod') || ''; // Processed in docker file
|
||||
return Input.getInput('buildMethod') ?? ''; // Processed in docker file
|
||||
}
|
||||
|
||||
static get manualExit(): boolean {
|
||||
const input = Input.getInput('manualExit') || false;
|
||||
const input = Input.getInput('manualExit') ?? false;
|
||||
|
||||
return input === 'true';
|
||||
}
|
||||
|
||||
static get enableGpu(): boolean {
|
||||
const input = Input.getInput('enableGpu') ?? false;
|
||||
|
||||
return input === 'true';
|
||||
}
|
||||
|
||||
static get customParameters(): string {
|
||||
return Input.getInput('customParameters') || '';
|
||||
return Input.getInput('customParameters') ?? '';
|
||||
}
|
||||
|
||||
static get versioningStrategy(): string {
|
||||
return Input.getInput('versioning') || 'Semantic';
|
||||
return Input.getInput('versioning') ?? 'Semantic';
|
||||
}
|
||||
|
||||
static get specifiedVersion(): string {
|
||||
return Input.getInput('version') || '';
|
||||
return Input.getInput('version') ?? '';
|
||||
}
|
||||
|
||||
static get androidVersionCode(): string {
|
||||
return Input.getInput('androidVersionCode') || '';
|
||||
return Input.getInput('androidVersionCode') ?? '';
|
||||
}
|
||||
|
||||
static get androidExportType(): string {
|
||||
return Input.getInput('androidExportType') || 'androidPackage';
|
||||
return Input.getInput('androidExportType') ?? 'androidPackage';
|
||||
}
|
||||
|
||||
static get androidKeystoreName(): string {
|
||||
return Input.getInput('androidKeystoreName') || '';
|
||||
return Input.getInput('androidKeystoreName') ?? '';
|
||||
}
|
||||
|
||||
static get androidKeystoreBase64(): string {
|
||||
return Input.getInput('androidKeystoreBase64') || '';
|
||||
return Input.getInput('androidKeystoreBase64') ?? '';
|
||||
}
|
||||
|
||||
static get androidKeystorePass(): string {
|
||||
return Input.getInput('androidKeystorePass') || '';
|
||||
return Input.getInput('androidKeystorePass') ?? '';
|
||||
}
|
||||
|
||||
static get androidKeyaliasName(): string {
|
||||
return Input.getInput('androidKeyaliasName') || '';
|
||||
return Input.getInput('androidKeyaliasName') ?? '';
|
||||
}
|
||||
|
||||
static get androidKeyaliasPass(): string {
|
||||
return Input.getInput('androidKeyaliasPass') || '';
|
||||
return Input.getInput('androidKeyaliasPass') ?? '';
|
||||
}
|
||||
|
||||
static get androidTargetSdkVersion(): string {
|
||||
return Input.getInput('androidTargetSdkVersion') || '';
|
||||
return Input.getInput('androidTargetSdkVersion') ?? '';
|
||||
}
|
||||
|
||||
static get androidSymbolType(): string {
|
||||
return Input.getInput('androidSymbolType') || 'none';
|
||||
return Input.getInput('androidSymbolType') ?? 'none';
|
||||
}
|
||||
|
||||
static get sshAgent(): string {
|
||||
return Input.getInput('sshAgent') || '';
|
||||
return Input.getInput('sshAgent') ?? '';
|
||||
}
|
||||
|
||||
static get sshPublicKeysDirectoryPath(): string {
|
||||
return Input.getInput('sshPublicKeysDirectoryPath') || '';
|
||||
return Input.getInput('sshPublicKeysDirectoryPath') ?? '';
|
||||
}
|
||||
|
||||
static get gitPrivateToken(): string | undefined {
|
||||
|
@ -194,27 +204,27 @@ class Input {
|
|||
}
|
||||
|
||||
static get runAsHostUser(): string {
|
||||
return Input.getInput('runAsHostUser') || 'false';
|
||||
return Input.getInput('runAsHostUser')?.toLowerCase() ?? 'false';
|
||||
}
|
||||
|
||||
static get chownFilesTo() {
|
||||
return Input.getInput('chownFilesTo') || '';
|
||||
return Input.getInput('chownFilesTo') ?? '';
|
||||
}
|
||||
|
||||
static get allowDirtyBuild(): boolean {
|
||||
const input = Input.getInput('allowDirtyBuild') || false;
|
||||
const input = Input.getInput('allowDirtyBuild') ?? false;
|
||||
|
||||
return input === 'true';
|
||||
}
|
||||
|
||||
static get cacheUnityInstallationOnMac(): boolean {
|
||||
const input = Input.getInput('cacheUnityInstallationOnMac') || false;
|
||||
const input = Input.getInput('cacheUnityInstallationOnMac') ?? false;
|
||||
|
||||
return input === 'true';
|
||||
}
|
||||
|
||||
static get unityHubVersionOnMac(): string {
|
||||
const input = Input.getInput('unityHubVersionOnMac') || '';
|
||||
const input = Input.getInput('unityHubVersionOnMac') ?? '';
|
||||
|
||||
return input !== '' ? input : '';
|
||||
}
|
||||
|
@ -228,11 +238,11 @@ class Input {
|
|||
}
|
||||
|
||||
static get dockerWorkspacePath(): string {
|
||||
return Input.getInput('dockerWorkspacePath') || '/github/workspace';
|
||||
return Input.getInput('dockerWorkspacePath') ?? '/github/workspace';
|
||||
}
|
||||
|
||||
static get dockerCpuLimit(): string {
|
||||
return Input.getInput('dockerCpuLimit') || os.cpus().length.toString();
|
||||
return Input.getInput('dockerCpuLimit') ?? os.cpus().length.toString();
|
||||
}
|
||||
|
||||
static get dockerMemoryLimit(): string {
|
||||
|
@ -252,20 +262,24 @@ class Input {
|
|||
}
|
||||
|
||||
return (
|
||||
Input.getInput('dockerMemoryLimit') || `${Math.floor((os.totalmem() / bytesInMegabyte) * memoryMultiplier)}m`
|
||||
Input.getInput('dockerMemoryLimit') ?? `${Math.floor((os.totalmem() / bytesInMegabyte) * memoryMultiplier)}m`
|
||||
);
|
||||
}
|
||||
|
||||
static get dockerIsolationMode(): string {
|
||||
return Input.getInput('dockerIsolationMode') || 'default';
|
||||
return Input.getInput('dockerIsolationMode') ?? 'default';
|
||||
}
|
||||
|
||||
static get containerRegistryRepository(): string {
|
||||
return Input.getInput('containerRegistryRepository')!;
|
||||
return Input.getInput('containerRegistryRepository') ?? 'unityci/editor';
|
||||
}
|
||||
|
||||
static get containerRegistryImageVersion(): string {
|
||||
return Input.getInput('containerRegistryImageVersion')!;
|
||||
return Input.getInput('containerRegistryImageVersion') ?? '3';
|
||||
}
|
||||
|
||||
static get skipActivation(): string {
|
||||
return Input.getInput('skipActivation')?.toLowerCase() ?? 'false';
|
||||
}
|
||||
|
||||
public static ToEnvVarFormat(input: string) {
|
||||
|
|
|
@ -101,7 +101,10 @@ class SetupMac {
|
|||
moduleArgument.push('--module', 'ios');
|
||||
break;
|
||||
case 'tvOS':
|
||||
moduleArgument.push('--module', 'tvos');
|
||||
moduleArgument.push('--module', 'appletv');
|
||||
break;
|
||||
case 'VisionOS':
|
||||
moduleArgument.push('--module', 'visionos');
|
||||
break;
|
||||
case 'StandaloneOSX':
|
||||
moduleArgument.push('--module', 'mac-il2cpp');
|
||||
|
@ -168,7 +171,9 @@ class SetupMac {
|
|||
process.env.UNITY_VERSION = buildParameters.editorVersion;
|
||||
process.env.UNITY_SERIAL = buildParameters.unitySerial;
|
||||
process.env.UNITY_LICENSING_SERVER = buildParameters.unityLicensingServer;
|
||||
process.env.SKIP_ACTIVATION = buildParameters.skipActivation;
|
||||
process.env.PROJECT_PATH = buildParameters.projectPath;
|
||||
process.env.BUILD_PROFILE = buildParameters.buildProfile;
|
||||
process.env.BUILD_TARGET = buildParameters.targetPlatform;
|
||||
process.env.BUILD_NAME = buildParameters.buildName;
|
||||
process.env.BUILD_PATH = buildParameters.buildPath;
|
||||
|
@ -187,6 +192,8 @@ class SetupMac {
|
|||
process.env.ANDROID_SYMBOL_TYPE = buildParameters.androidSymbolType;
|
||||
process.env.CUSTOM_PARAMETERS = buildParameters.customParameters;
|
||||
process.env.CHOWN_FILES_TO = buildParameters.chownFilesTo;
|
||||
process.env.MANUAL_EXIT = buildParameters.manualExit.toString();
|
||||
process.env.ENABLE_GPU = buildParameters.enableGpu.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,9 +4,14 @@ import { BuildParameters } from '..';
|
|||
class ValidateWindows {
|
||||
public static validate(buildParameters: BuildParameters) {
|
||||
ValidateWindows.validateWindowsPlatformRequirements(buildParameters.targetPlatform);
|
||||
if (!(process.env.UNITY_EMAIL && process.env.UNITY_PASSWORD)) {
|
||||
throw new Error(`Unity email and password must be set for Windows based builds to
|
||||
authenticate the license. Make sure to set them inside UNITY_EMAIL
|
||||
|
||||
const { unityLicensingServer } = buildParameters;
|
||||
const hasLicensingCredentials = process.env.UNITY_EMAIL && process.env.UNITY_PASSWORD;
|
||||
const hasValidLicensingStrategy = hasLicensingCredentials || unityLicensingServer;
|
||||
|
||||
if (!hasValidLicensingStrategy) {
|
||||
throw new Error(`Unity email and password or alternatively a Unity licensing server url must be set for
|
||||
Windows based builds to authenticate the license. Make sure to set them inside UNITY_EMAIL
|
||||
and UNITY_PASSWORD in Github Secrets and pass them into the environment.`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ class Platform {
|
|||
PS4: 'PS4',
|
||||
XboxOne: 'XboxOne',
|
||||
tvOS: 'tvOS',
|
||||
VisionOS: 'VisionOS',
|
||||
Switch: 'Switch',
|
||||
|
||||
// Unsupported
|
||||
|
|
|
@ -7,9 +7,15 @@ describe('Unity Versioning', () => {
|
|||
});
|
||||
|
||||
it('parses from ProjectVersion.txt', () => {
|
||||
const projectVersionContents = `m_EditorVersion: 2021.3.4f1
|
||||
m_EditorVersionWithRevision: 2021.3.4f1 (cb45f9cae8b7)`;
|
||||
expect(UnityVersioning.parse(projectVersionContents)).toBe('2021.3.4f1');
|
||||
const projectVersionContents = `m_EditorVersion: 2021.3.45f1
|
||||
m_EditorVersionWithRevision: 2021.3.45f1 (cb45f9cae8b7)`;
|
||||
expect(UnityVersioning.parse(projectVersionContents)).toBe('2021.3.45f1');
|
||||
});
|
||||
|
||||
it('parses Unity 6000 and newer from ProjectVersion.txt', () => {
|
||||
const projectVersionContents = `m_EditorVersion: 6000.0.0f1
|
||||
m_EditorVersionWithRevision: 6000.0.0f1 (cb45f9cae8b7)`;
|
||||
expect(UnityVersioning.parse(projectVersionContents)).toBe('6000.0.0f1');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -19,13 +25,13 @@ describe('Unity Versioning', () => {
|
|||
});
|
||||
|
||||
it('reads from test-project', () => {
|
||||
expect(UnityVersioning.read('./test-project')).toBe('2021.3.4f1');
|
||||
expect(UnityVersioning.read('./test-project')).toBe('2021.3.45f1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('determineUnityVersion', () => {
|
||||
it('defaults to parsed version', () => {
|
||||
expect(UnityVersioning.determineUnityVersion('./test-project', 'auto')).toBe('2021.3.4f1');
|
||||
expect(UnityVersioning.determineUnityVersion('./test-project', 'auto')).toBe('2021.3.45f1');
|
||||
});
|
||||
|
||||
it('use specified unityVersion', () => {
|
||||
|
|
|
@ -2,10 +2,6 @@ import fs from 'node:fs';
|
|||
import path from 'node:path';
|
||||
|
||||
export default class UnityVersioning {
|
||||
static get versionPattern() {
|
||||
return /20\d{2}\.\d\.\w{3,4}|3/;
|
||||
}
|
||||
|
||||
static determineUnityVersion(projectPath: string, unityVersion: string) {
|
||||
if (unityVersion === 'auto') {
|
||||
return UnityVersioning.read(projectPath);
|
||||
|
@ -24,11 +20,13 @@ export default class UnityVersioning {
|
|||
}
|
||||
|
||||
static parse(projectVersionTxt: string) {
|
||||
const matches = projectVersionTxt.match(UnityVersioning.versionPattern);
|
||||
if (!matches || matches.length === 0) {
|
||||
throw new Error(`Failed to parse version from "${projectVersionTxt}".`);
|
||||
const versionRegex = /m_EditorVersion: (\d+\.\d+\.\d+[A-Za-z]?\d+)/;
|
||||
const matches = projectVersionTxt.match(versionRegex);
|
||||
|
||||
if (!matches || matches.length < 2) {
|
||||
throw new Error(`Failed to extract version from "${projectVersionTxt}".`);
|
||||
}
|
||||
|
||||
return matches[0];
|
||||
return matches[1];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -207,7 +207,21 @@ export default class Versioning {
|
|||
* identifies the current commit.
|
||||
*/
|
||||
static async getVersionDescription() {
|
||||
return this.git(['describe', '--long', '--tags', '--always', 'HEAD']);
|
||||
const versionTags = (await this.git(['tag', '--list', '--merged', 'HEAD', '--sort=-creatordate']))
|
||||
.split('\n')
|
||||
.filter((tag) => new RegExp(this.grepCompatibleInputVersionRegex).test(tag));
|
||||
|
||||
if (versionTags.length === 0) {
|
||||
core.warning('No valid version tags found. Using fallback description.');
|
||||
|
||||
return this.git(['describe', '--long', '--tags', '--always', 'HEAD']);
|
||||
}
|
||||
|
||||
const latestVersionTag = versionTags[0];
|
||||
const commitsCount = (await this.git(['rev-list', `${latestVersionTag}..HEAD`, '--count'])).trim();
|
||||
const commitHash = (await this.git(['rev-parse', '--short', 'HEAD'])).trim();
|
||||
|
||||
return `${latestVersionTag}-${commitsCount}-g${commitHash}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { BuildParameters } from '../model';
|
||||
import { Cli } from '../model/cli/cli';
|
||||
import { OptionValues } from 'commander';
|
||||
|
||||
export const TIMEOUT_INFINITE = 1e9;
|
||||
|
||||
export async function createParameters(overrides?: OptionValues) {
|
||||
if (overrides) Cli.options = overrides;
|
||||
|
||||
return BuildParameters.create();
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 28bfc999a135648538355bfcb6a23aee
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: cd91492ed9aca40c49d42156a4a8f387
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,47 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 15003, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name: Sample WebGL Build Profile
|
||||
m_EditorClassIdentifier:
|
||||
m_AssetVersion: 1
|
||||
m_BuildTarget: 20
|
||||
m_Subtarget: 0
|
||||
m_PlatformId: 84a3bb9e7420477f885e98145999eb20
|
||||
m_PlatformBuildProfile:
|
||||
rid: 200022742090383361
|
||||
m_OverrideGlobalSceneList: 0
|
||||
m_Scenes: []
|
||||
m_ScriptingDefines:
|
||||
- BUILD_PROFILE_LOADED
|
||||
m_PlayerSettingsYaml:
|
||||
m_Settings: []
|
||||
references:
|
||||
version: 2
|
||||
RefIds:
|
||||
- rid: 200022742090383361
|
||||
type: {class: WebGLPlatformSettings, ns: UnityEditor.WebGL, asm: UnityEditor.WebGL.Extensions}
|
||||
data:
|
||||
m_Development: 0
|
||||
m_ConnectProfiler: 0
|
||||
m_BuildWithDeepProfilingSupport: 0
|
||||
m_AllowDebugging: 0
|
||||
m_WaitForManagedDebugger: 0
|
||||
m_ManagedDebuggerFixedPort: 0
|
||||
m_ExplicitNullChecks: 0
|
||||
m_ExplicitDivideByZeroChecks: 0
|
||||
m_ExplicitArrayBoundsChecks: 0
|
||||
m_CompressionType: -1
|
||||
m_InstallInBuildFolder: 0
|
||||
m_CodeOptimization: 0
|
||||
m_WebGLClientBrowserPath:
|
||||
m_WebGLClientBrowserType: 0
|
||||
m_WebGLTextureSubtarget: 0
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b9aac23ad2add4b439decb0cf65b0d68
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,49 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 15003, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name: Sample Windows Build Profile
|
||||
m_EditorClassIdentifier:
|
||||
m_AssetVersion: 1
|
||||
m_BuildTarget: 19
|
||||
m_Subtarget: 2
|
||||
m_PlatformId: 4e3c793746204150860bf175a9a41a05
|
||||
m_PlatformBuildProfile:
|
||||
rid: 9120355575023534081
|
||||
m_OverrideGlobalSceneList: 0
|
||||
m_Scenes: []
|
||||
m_ScriptingDefines:
|
||||
- BUILD_PROFILE_LOADED
|
||||
m_PlayerSettingsYaml:
|
||||
m_Settings: []
|
||||
references:
|
||||
version: 2
|
||||
RefIds:
|
||||
- rid: 9120355575023534081
|
||||
type: {class: WindowsPlatformSettings, ns: UnityEditor.WindowsStandalone, asm: UnityEditor.WindowsStandalone.Extensions}
|
||||
data:
|
||||
m_Development: 1
|
||||
m_ConnectProfiler: 0
|
||||
m_BuildWithDeepProfilingSupport: 0
|
||||
m_AllowDebugging: 0
|
||||
m_WaitForManagedDebugger: 0
|
||||
m_ManagedDebuggerFixedPort: 0
|
||||
m_ExplicitNullChecks: 0
|
||||
m_ExplicitDivideByZeroChecks: 0
|
||||
m_ExplicitArrayBoundsChecks: 0
|
||||
m_CompressionType: 0
|
||||
m_InstallInBuildFolder: 0
|
||||
m_WindowsBuildAndRunDeployTarget: 0
|
||||
m_Architecture: 0
|
||||
m_CreateSolution: 0
|
||||
m_CopyPDBFiles: 0
|
||||
m_WindowsDevicePortalAddress:
|
||||
m_WindowsDevicePortalUsername:
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 89540e92f0e247d4084f426eb3bdb288
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue