mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-06-01 06:16:14 -07:00
Compare commits
13 Commits
feature/cl
...
linux-exte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a38ec6caef | ||
|
|
2240bedf08 | ||
|
|
37ce35f1a5 | ||
|
|
9d20e0b607 | ||
|
|
16c5c20793 | ||
|
|
821ba97789 | ||
|
|
365bdb5e12 | ||
|
|
c7a43cde4d | ||
|
|
d10fd10a95 | ||
|
|
2321712bb4 | ||
|
|
ef0555fb0d | ||
|
|
4a7fc08e63 | ||
|
|
ce7ce7a416 |
@@ -1,4 +0,0 @@
|
|||||||
dist/
|
|
||||||
lib/
|
|
||||||
node_modules/
|
|
||||||
jest.config.js
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
{
|
|
||||||
"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"],
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"impliedStrict": true
|
|
||||||
},
|
|
||||||
"project": "./tsconfig.json"
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"node": true,
|
|
||||||
"es6": true,
|
|
||||||
"jest/globals": true,
|
|
||||||
"es2020": true
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
// Error out for code formatting errors
|
|
||||||
"prettier/prettier": "error",
|
|
||||||
// Namespaces or sometimes needed
|
|
||||||
"import/no-namespace": "off",
|
|
||||||
// Properly format comments
|
|
||||||
"spaced-comment": ["error", "always"],
|
|
||||||
"lines-around-comment": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"beforeBlockComment": true,
|
|
||||||
"beforeLineComment": true,
|
|
||||||
"allowBlockStart": true,
|
|
||||||
"allowObjectStart": true,
|
|
||||||
"allowArrayStart": true,
|
|
||||||
"allowClassStart": true,
|
|
||||||
"ignorePattern": "pragma|ts-ignore"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// Mandatory spacing
|
|
||||||
"padding-line-between-statements": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"blankLine": "always",
|
|
||||||
"prev": "*",
|
|
||||||
"next": "return"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"blankLine": "always",
|
|
||||||
"prev": "directive",
|
|
||||||
"next": "*"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"blankLine": "any",
|
|
||||||
"prev": "directive",
|
|
||||||
"next": "directive"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// Enforce camelCase
|
|
||||||
"camelcase": "error",
|
|
||||||
// Allow forOfStatements
|
|
||||||
"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
|
|
||||||
"import/prefer-default-export": "off",
|
|
||||||
// Unused vars are useful to keep method signatures consistent and documented
|
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
|
||||||
// For this project only use kebab-case
|
|
||||||
"unicorn/filename-case": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"cases": {
|
|
||||||
"kebabCase": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// Allow Array.from(set) mitigate TS2569 which would require '--downlevelIteration'
|
|
||||||
"unicorn/prefer-spread": "off",
|
|
||||||
// Temp disable to prevent mixing changes with other PRs
|
|
||||||
"i18n-text/no-en": "off"
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["jest.setup.js"],
|
|
||||||
"rules": {
|
|
||||||
"import/no-commonjs": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -14,9 +14,7 @@ assignees: ''
|
|||||||
|
|
||||||
<!--Steps to reproduce the behavior:-->
|
<!--Steps to reproduce the behavior:-->
|
||||||
|
|
||||||
-
|
- **Expected behavior**
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
|
|
||||||
<!--A clear and concise description of what you expected to happen.-->
|
<!--A clear and concise description of what you expected to happen.-->
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/build-tests-mac.yml
vendored
6
.github/workflows/build-tests-mac.yml
vendored
@@ -18,9 +18,9 @@ jobs:
|
|||||||
projectPath:
|
projectPath:
|
||||||
- test-project
|
- test-project
|
||||||
unityVersion:
|
unityVersion:
|
||||||
- 2021.3.45f1
|
- 2021.3.45f2
|
||||||
- 2022.3.13f1
|
- 2022.3.62f3
|
||||||
- 2023.2.2f1
|
- 2023.2.22f1
|
||||||
targetPlatform:
|
targetPlatform:
|
||||||
- StandaloneOSX # Build a MacOS executable
|
- StandaloneOSX # Build a MacOS executable
|
||||||
- iOS # Build an iOS executable
|
- iOS # Build an iOS executable
|
||||||
|
|||||||
15
.github/workflows/build-tests-ubuntu.yml
vendored
15
.github/workflows/build-tests-ubuntu.yml
vendored
@@ -9,8 +9,7 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
UNITY_LICENSE:
|
UNITY_LICENSE: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n <License
|
||||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n <License
|
|
||||||
id=\"Terms\">\n <MachineBindings>\n <Binding Key=\"1\"
|
id=\"Terms\">\n <MachineBindings>\n <Binding Key=\"1\"
|
||||||
Value=\"576562626572264761624c65526f7578\"/>\n <Binding Key=\"2\"
|
Value=\"576562626572264761624c65526f7578\"/>\n <Binding Key=\"2\"
|
||||||
Value=\"576562626572264761624c65526f7578\"/>\n </MachineBindings>\n <MachineID
|
Value=\"576562626572264761624c65526f7578\"/>\n </MachineBindings>\n <MachineID
|
||||||
@@ -36,8 +35,7 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
buildForAllPlatformsUbuntu:
|
buildForAllPlatformsUbuntu:
|
||||||
name:
|
name: "${{ matrix.targetPlatform }} on ${{ matrix.unityVersion}}${{startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }}"
|
||||||
"${{ matrix.targetPlatform }} on ${{ matrix.unityVersion}}${{startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }}"
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -48,9 +46,9 @@ jobs:
|
|||||||
projectPath:
|
projectPath:
|
||||||
- test-project
|
- test-project
|
||||||
unityVersion:
|
unityVersion:
|
||||||
- 2021.3.32f1
|
- 2021.3.45f2
|
||||||
- 2022.3.13f1
|
- 2022.3.62f3
|
||||||
- 2023.2.2f1
|
- 2023.2.22f1
|
||||||
targetPlatform:
|
targetPlatform:
|
||||||
- StandaloneOSX # Build a macOS standalone (Intel 64-bit) with mono backend.
|
- StandaloneOSX # Build a macOS standalone (Intel 64-bit) with mono backend.
|
||||||
- StandaloneWindows64 # Build a Windows 64-bit standalone with mono backend.
|
- StandaloneWindows64 # Build a Windows 64-bit standalone with mono backend.
|
||||||
@@ -200,7 +198,6 @@ jobs:
|
|||||||
###########################
|
###########################
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name:
|
name: "Build ${{ matrix.targetPlatform }}${{ startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }} on Ubuntu (${{ matrix.unityVersion }}_il2cpp_${{ matrix.buildWithIl2cpp }}_params_${{ matrix.additionalParameters }})"
|
||||||
"Build ${{ matrix.targetPlatform }}${{ startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }} on Ubuntu (${{ matrix.unityVersion }}_il2cpp_${{ matrix.buildWithIl2cpp }}_params_${{ matrix.additionalParameters }})"
|
|
||||||
path: build
|
path: build
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|||||||
38
.github/workflows/build-tests-windows.yml
vendored
38
.github/workflows/build-tests-windows.yml
vendored
@@ -18,9 +18,9 @@ jobs:
|
|||||||
projectPath:
|
projectPath:
|
||||||
- test-project
|
- test-project
|
||||||
unityVersion:
|
unityVersion:
|
||||||
- 2021.3.32f1
|
- 2021.3.45f2
|
||||||
- 2022.3.13f1
|
- 2022.3.62f3
|
||||||
- 2023.2.2f1
|
- 2023.2.22f1
|
||||||
targetPlatform:
|
targetPlatform:
|
||||||
- Android # Build an Android apk.
|
- Android # Build an Android apk.
|
||||||
- StandaloneWindows64 # Build a Windows 64-bit standalone.
|
- StandaloneWindows64 # Build a Windows 64-bit standalone.
|
||||||
@@ -66,6 +66,34 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
Move-Item -Path "./test-project/ProjectSettings/ProjectSettingsIl2cpp.asset" -Destination "./test-project/ProjectSettings/ProjectSettings.asset" -Force
|
Move-Item -Path "./test-project/ProjectSettings/ProjectSettingsIl2cpp.asset" -Destination "./test-project/ProjectSettings/ProjectSettings.asset" -Force
|
||||||
|
|
||||||
|
###########################
|
||||||
|
# Docker Readiness #
|
||||||
|
###########################
|
||||||
|
- name: Ensure Docker daemon is ready
|
||||||
|
timeout-minutes: 2
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$maxRetries = 10
|
||||||
|
$retryDelay = 6
|
||||||
|
for ($i = 0; $i -lt $maxRetries; $i++) {
|
||||||
|
$svc = Get-Service docker -ErrorAction SilentlyContinue
|
||||||
|
if ($svc -and $svc.Status -eq 'Running') {
|
||||||
|
docker version 2>$null
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host "Docker is ready."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($svc -and $svc.Status -eq 'Stopped') {
|
||||||
|
Write-Host "Docker service stopped, attempting to start..."
|
||||||
|
Start-Service docker -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
Write-Host "Waiting for Docker daemon (attempt $($i+1)/$maxRetries)..."
|
||||||
|
Start-Sleep -Seconds $retryDelay
|
||||||
|
}
|
||||||
|
Write-Error "Docker daemon did not start within $($maxRetries * $retryDelay) seconds"
|
||||||
|
exit 1
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
# Build #
|
# Build #
|
||||||
###########################
|
###########################
|
||||||
@@ -146,6 +174,8 @@ jobs:
|
|||||||
###########################
|
###########################
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Build ${{ matrix.targetPlatform }} on Windows (${{ matrix.unityVersion }})${{ matrix.enableGpu && ' With GPU' || '' }}${{ matrix.buildProfile && ' With Build Profile' || '' }}
|
name:
|
||||||
|
Build ${{ matrix.targetPlatform }} on Windows (${{ matrix.unityVersion }})${{ matrix.enableGpu && ' With
|
||||||
|
GPU' || '' }}${{ matrix.buildProfile && ' With Build Profile' || '' }}
|
||||||
path: build
|
path: build
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|||||||
34
.github/workflows/integrity-check.yml
vendored
34
.github/workflows/integrity-check.yml
vendored
@@ -2,7 +2,8 @@ name: Integrity
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push: { branches: [main] }
|
push: { branches: [main] }
|
||||||
pull_request: {}
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, labeled]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -22,17 +23,40 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install package manager (from package.json)
|
||||||
|
run: |
|
||||||
|
corepack enable
|
||||||
|
corepack install
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
- run: yarn
|
- name: Resolve yarn cache folder
|
||||||
|
id: yarn-config
|
||||||
|
run: echo "cacheFolder=$(yarn config get cacheFolder)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Restore yarn install cache (node_modules + cacheFolder + install-state)
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ steps.yarn-config.outputs.cacheFolder }}
|
||||||
|
.yarn/install-state.gz
|
||||||
|
key: yarn-v2-${{ runner.os }}-node-18-${{ hashFiles('yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
yarn-v2-${{ runner.os }}-node-18-
|
||||||
|
- name: Install deps
|
||||||
|
env:
|
||||||
|
YARN_ENABLE_HARDENED_MODE: 'false'
|
||||||
|
run: |
|
||||||
|
case "$(yarn --version)" in 1.*) echo 'expected up-to-date yarn version'; exit 1 ;; esac
|
||||||
|
yarn install --immutable
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
- run: yarn test:ci --coverage
|
- run: yarn test:ci --coverage
|
||||||
- run: bash <(curl -s https://codecov.io/bash)
|
- run: bash <(curl -s https://codecov.io/bash)
|
||||||
- run: yarn build || { echo "build command should always succeed" ; exit 61; }
|
- run: yarn build || { echo "build command should always succeed" ; exit 61; }
|
||||||
# - run: yarn build --quiet && git diff --quiet dist || { echo "dist should be auto generated" ; git diff dist ; exit 62; }
|
# - run: yarn build --quiet && git diff --quiet dist || { echo "dist should be auto generated" ; git diff dist ; exit 62; }
|
||||||
|
|
||||||
orchestrator:
|
orchestrator-integration:
|
||||||
name: Orchestrator Integrity
|
name: Orchestrator Integration
|
||||||
uses: ./.github/workflows/orchestrator-integrity.yml
|
if: >-
|
||||||
|
github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'run-integration')
|
||||||
|
uses: ./.github/workflows/validate-orchestrator-integration.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|||||||
61
.github/workflows/orchestrator-async-checks.yml
vendored
61
.github/workflows/orchestrator-async-checks.yml
vendored
@@ -1,61 +0,0 @@
|
|||||||
name: Async Checks API
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
checksObject:
|
|
||||||
description: ''
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
checks: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
GKE_ZONE: 'us-central1'
|
|
||||||
GKE_REGION: 'us-central1'
|
|
||||||
GKE_PROJECT: 'unitykubernetesbuilder'
|
|
||||||
GKE_CLUSTER: 'game-ci-github-pipelines'
|
|
||||||
GCP_LOGGING: true
|
|
||||||
GCP_PROJECT: unitykubernetesbuilder
|
|
||||||
GCP_LOG_FILE: ${{ github.workspace }}/orchestrator-logs.txt
|
|
||||||
# Commented out: Using LocalStack tests instead of real AWS
|
|
||||||
# AWS_REGION: eu-west-2
|
|
||||||
# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
||||||
# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
||||||
# AWS_DEFAULT_REGION: eu-west-2
|
|
||||||
# AWS_STACK_NAME: game-ci-github-pipelines
|
|
||||||
ORCHESTRATOR_BRANCH: ${{ github.ref }}
|
|
||||||
ORCHESTRATOR_DEBUG: true
|
|
||||||
ORCHESTRATOR_DEBUG_TREE: true
|
|
||||||
DEBUG: true
|
|
||||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
|
||||||
PROJECT_PATH: test-project
|
|
||||||
UNITY_VERSION: 2019.3.15f1
|
|
||||||
USE_IL2CPP: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
asyncChecks:
|
|
||||||
name: Async Checks
|
|
||||||
if: github.event.event_type != 'pull_request_target'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- timeout-minutes: 180
|
|
||||||
env:
|
|
||||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
|
||||||
PROJECT_PATH: test-project
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
GIT_PRIVATE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TARGET_PLATFORM: StandaloneWindows64
|
|
||||||
orchestratorTests: true
|
|
||||||
versioning: None
|
|
||||||
ORCHESTRATOR_CLUSTER: local-docker
|
|
||||||
# Commented out: Using LocalStack tests instead of real AWS
|
|
||||||
# AWS_STACK_NAME: game-ci-github-pipelines
|
|
||||||
CHECKS_UPDATE: ${{ github.event.inputs.checksObject }}
|
|
||||||
run: |
|
|
||||||
git clone -b main https://github.com/game-ci/unity-builder
|
|
||||||
cd unity-builder
|
|
||||||
yarn
|
|
||||||
ls
|
|
||||||
yarn run cli -m checks-update
|
|
||||||
1109
.github/workflows/orchestrator-integrity.yml
vendored
1109
.github/workflows/orchestrator-integrity.yml
vendored
File diff suppressed because it is too large
Load Diff
170
.github/workflows/release-cli.yml
vendored
170
.github/workflows/release-cli.yml
vendored
@@ -1,170 +0,0 @@
|
|||||||
name: Release CLI
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tag:
|
|
||||||
description: 'Release tag to build (e.g., v2.0.0). Uses latest release if empty.'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
publish-npm:
|
|
||||||
description: 'Publish to npm'
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.release.tag_name || inputs.tag || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-binaries:
|
|
||||||
name: Build ${{ matrix.target }}
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- target: linux-x64
|
|
||||||
os: ubuntu-latest
|
|
||||||
pkg-target: node20-linux-x64
|
|
||||||
binary-name: game-ci-linux-x64
|
|
||||||
- target: linux-arm64
|
|
||||||
os: ubuntu-latest
|
|
||||||
pkg-target: node20-linux-arm64
|
|
||||||
binary-name: game-ci-linux-arm64
|
|
||||||
- target: macos-x64
|
|
||||||
os: macos-latest
|
|
||||||
pkg-target: node20-macos-x64
|
|
||||||
binary-name: game-ci-macos-x64
|
|
||||||
- target: macos-arm64
|
|
||||||
os: macos-latest
|
|
||||||
pkg-target: node20-macos-arm64
|
|
||||||
binary-name: game-ci-macos-arm64
|
|
||||||
- target: windows-x64
|
|
||||||
os: windows-latest
|
|
||||||
pkg-target: node20-win-x64
|
|
||||||
binary-name: game-ci-windows-x64.exe
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.release.tag_name || inputs.tag || github.ref }}
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build TypeScript
|
|
||||||
run: yarn build
|
|
||||||
|
|
||||||
- name: Verify CLI before packaging
|
|
||||||
run: node lib/cli.js version
|
|
||||||
|
|
||||||
- name: Build standalone binary
|
|
||||||
run: npx pkg lib/cli.js --target ${{ matrix.pkg-target }} --output ${{ matrix.binary-name }} --compress GZip
|
|
||||||
|
|
||||||
- name: Verify standalone binary (non-cross-compiled)
|
|
||||||
if: |
|
|
||||||
(matrix.target == 'linux-x64' && runner.os == 'Linux') ||
|
|
||||||
(matrix.target == 'macos-arm64' && runner.os == 'macOS' && runner.arch == 'ARM64') ||
|
|
||||||
(matrix.target == 'macos-x64' && runner.os == 'macOS' && runner.arch == 'X64') ||
|
|
||||||
(matrix.target == 'windows-x64' && runner.os == 'Windows')
|
|
||||||
run: ./${{ matrix.binary-name }} version
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: binary-${{ matrix.target }}
|
|
||||||
path: ${{ matrix.binary-name }}
|
|
||||||
retention-days: 5
|
|
||||||
|
|
||||||
create-checksums-and-upload:
|
|
||||||
name: Checksums and release upload
|
|
||||||
needs: build-binaries
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: binaries
|
|
||||||
pattern: binary-*
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: List binaries
|
|
||||||
run: ls -la binaries/
|
|
||||||
|
|
||||||
- name: Generate SHA256 checksums
|
|
||||||
run: |
|
|
||||||
cd binaries
|
|
||||||
sha256sum game-ci-* > checksums.txt
|
|
||||||
echo "=== checksums.txt ==="
|
|
||||||
cat checksums.txt
|
|
||||||
|
|
||||||
- name: Determine release tag
|
|
||||||
id: tag
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.event_name }}" = "release" ]; then
|
|
||||||
echo "tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
|
|
||||||
elif [ -n "${{ inputs.tag }}" ]; then
|
|
||||||
echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "No release tag available. Skipping upload."
|
|
||||||
echo "tag=" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload binaries to release
|
|
||||||
if: steps.tag.outputs.tag != ''
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
cd binaries
|
|
||||||
for f in game-ci-* checksums.txt; do
|
|
||||||
echo "Uploading $f..."
|
|
||||||
gh release upload "${{ steps.tag.outputs.tag }}" "$f" \
|
|
||||||
--repo "${{ github.repository }}" \
|
|
||||||
--clobber
|
|
||||||
done
|
|
||||||
|
|
||||||
publish-npm:
|
|
||||||
name: Publish to npm
|
|
||||||
needs: build-binaries
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
(github.event_name == 'release') || (github.event_name == 'workflow_dispatch' && inputs.publish-npm)
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.release.tag_name || inputs.tag || github.ref }}
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
registry-url: 'https://registry.npmjs.org'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: yarn build
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: yarn test
|
|
||||||
|
|
||||||
- name: Verify CLI
|
|
||||||
run: |
|
|
||||||
node lib/cli.js version
|
|
||||||
node lib/cli.js --help
|
|
||||||
|
|
||||||
- name: Publish to npm
|
|
||||||
run: npm publish --provenance --access public
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
91
.github/workflows/sync-secrets.yml
vendored
Normal file
91
.github/workflows/sync-secrets.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
name: Sync Secrets to Repositories
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
target_repo:
|
||||||
|
description: 'Target repository (org/repo format)'
|
||||||
|
required: true
|
||||||
|
default: 'game-ci/orchestrator'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- game-ci/orchestrator
|
||||||
|
- game-ci/cli
|
||||||
|
dry_run:
|
||||||
|
description: 'Dry run (list secrets to sync without writing)'
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync-secrets:
|
||||||
|
name: Sync secrets to ${{ inputs.target_repo }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Sync secrets
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||||
|
TARGET_REPO: ${{ inputs.target_repo }}
|
||||||
|
DRY_RUN: ${{ inputs.dry_run }}
|
||||||
|
# Secrets to sync — values come from repo + org secrets available here
|
||||||
|
SECRET_UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||||
|
SECRET_UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||||
|
SECRET_UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||||
|
SECRET_GIT_PRIVATE_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||||
|
SECRET_GOOGLE_SERVICE_ACCOUNT_EMAIL: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }}
|
||||||
|
SECRET_GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
|
||||||
|
SECRET_CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
SECRET_UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||||
|
SECRET_NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
run: |
|
||||||
|
SECRETS=(
|
||||||
|
"UNITY_EMAIL:SECRET_UNITY_EMAIL"
|
||||||
|
"UNITY_PASSWORD:SECRET_UNITY_PASSWORD"
|
||||||
|
"UNITY_SERIAL:SECRET_UNITY_SERIAL"
|
||||||
|
"UNITY_LICENSE:SECRET_UNITY_LICENSE"
|
||||||
|
"GIT_PRIVATE_TOKEN:SECRET_GIT_PRIVATE_TOKEN"
|
||||||
|
"GOOGLE_SERVICE_ACCOUNT_EMAIL:SECRET_GOOGLE_SERVICE_ACCOUNT_EMAIL"
|
||||||
|
"GOOGLE_SERVICE_ACCOUNT_KEY:SECRET_GOOGLE_SERVICE_ACCOUNT_KEY"
|
||||||
|
"CODECOV_TOKEN:SECRET_CODECOV_TOKEN"
|
||||||
|
"NPM_TOKEN:SECRET_NPM_TOKEN"
|
||||||
|
)
|
||||||
|
|
||||||
|
synced=0
|
||||||
|
skipped=0
|
||||||
|
|
||||||
|
for entry in "${SECRETS[@]}"; do
|
||||||
|
name="${entry%%:*}"
|
||||||
|
env_var="${entry##*:}"
|
||||||
|
value="${!env_var}"
|
||||||
|
|
||||||
|
if [ -z "$value" ]; then
|
||||||
|
echo "⏭ SKIP: $name (not available in this repo's context)"
|
||||||
|
skipped=$((skipped + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" = "true" ]; then
|
||||||
|
echo "🔍 DRY RUN: would sync $name → $TARGET_REPO"
|
||||||
|
else
|
||||||
|
if echo "$value" | gh secret set "$name" -R "$TARGET_REPO" --body - 2>/dev/null; then
|
||||||
|
echo "✅ SYNCED: $name → $TARGET_REPO"
|
||||||
|
else
|
||||||
|
echo "⚠️ FAILED: $name → $TARGET_REPO (continuing)"
|
||||||
|
skipped=$((skipped + 1))
|
||||||
|
synced=$((synced - 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
synced=$((synced + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Summary ==="
|
||||||
|
echo "Synced: $synced"
|
||||||
|
echo "Skipped (not available): $skipped"
|
||||||
|
echo "Target: $TARGET_REPO"
|
||||||
|
if [ "$DRY_RUN" = "true" ]; then
|
||||||
|
echo "Mode: DRY RUN (no secrets were written)"
|
||||||
|
fi
|
||||||
205
.github/workflows/validate-community-plugins.yml
vendored
Normal file
205
.github/workflows/validate-community-plugins.yml
vendored
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
name: Validate Community Plugins
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Run weekly on Sunday at 02:00 UTC
|
||||||
|
- cron: '0 2 * * 0'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
plugin_filter:
|
||||||
|
description: 'Filter plugins by name (regex pattern, empty = all)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
unity_version:
|
||||||
|
description: 'Override Unity version (empty = use plugin default)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
load-plugins:
|
||||||
|
name: Load Plugin Registry
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
matrix: ${{ steps.parse.outputs.matrix }}
|
||||||
|
plugin_count: ${{ steps.parse.outputs.count }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Parse plugin registry
|
||||||
|
id: parse
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
|
||||||
|
const registry = yaml.load(fs.readFileSync('community-plugins.yml', 'utf8'));
|
||||||
|
let plugins = registry.plugins || [];
|
||||||
|
|
||||||
|
// Apply name filter if provided
|
||||||
|
const filter = '${{ github.event.inputs.plugin_filter }}';
|
||||||
|
if (filter) {
|
||||||
|
const regex = new RegExp(filter, 'i');
|
||||||
|
plugins = plugins.filter(p => regex.test(p.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand platform matrix
|
||||||
|
const matrix = [];
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
const platforms = plugin.platforms || ['StandaloneLinux64'];
|
||||||
|
for (const platform of platforms) {
|
||||||
|
matrix.push({
|
||||||
|
name: plugin.name,
|
||||||
|
package: plugin.package,
|
||||||
|
source: plugin.source || 'git',
|
||||||
|
unity: '${{ github.event.inputs.unity_version }}' || plugin.unity || '2021.3',
|
||||||
|
platform: platform,
|
||||||
|
timeout: plugin.timeout || 30
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
core.setOutput('matrix', JSON.stringify({ include: matrix }));
|
||||||
|
core.setOutput('count', matrix.length);
|
||||||
|
console.log(`Found ${matrix.length} plugin-platform combinations to validate`);
|
||||||
|
|
||||||
|
validate:
|
||||||
|
name: '${{ matrix.name }} (${{ matrix.platform }})'
|
||||||
|
needs: load-plugins
|
||||||
|
if: needs.load-plugins.outputs.plugin_count > 0
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: ${{ fromJson(matrix.timeout) }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix: ${{ fromJson(needs.load-plugins.outputs.matrix) }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Create test project
|
||||||
|
run: |
|
||||||
|
mkdir -p test-project/Assets
|
||||||
|
mkdir -p test-project/Packages
|
||||||
|
mkdir -p test-project/ProjectSettings
|
||||||
|
|
||||||
|
# Create minimal manifest.json
|
||||||
|
if [ "${{ matrix.source }}" = "git" ]; then
|
||||||
|
cat > test-project/Packages/manifest.json << 'MANIFEST'
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"com.unity.modules.imgui": "1.0.0",
|
||||||
|
"com.unity.modules.jsonserialize": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Add git package via manifest
|
||||||
|
cd test-project
|
||||||
|
python3 -c "
|
||||||
|
import sys, json
|
||||||
|
manifest = json.load(sys.stdin)
|
||||||
|
manifest['dependencies']['${{ matrix.name }}'] = '${{ matrix.package }}'
|
||||||
|
json.dump(manifest, sys.stdout, indent=2)
|
||||||
|
" < Packages/manifest.json > Packages/manifest.tmp && mv Packages/manifest.tmp Packages/manifest.json
|
||||||
|
cd ..
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create minimal ProjectSettings
|
||||||
|
cat > test-project/ProjectSettings/ProjectVersion.txt << EOF
|
||||||
|
m_EditorVersion: ${{ matrix.unity }}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Build with unity-builder
|
||||||
|
uses: ./
|
||||||
|
id: build
|
||||||
|
with:
|
||||||
|
projectPath: test-project
|
||||||
|
targetPlatform: ${{ matrix.platform }}
|
||||||
|
unityVersion: ${{ matrix.unity }}
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Record result
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
STATUS="${{ steps.build.outcome }}"
|
||||||
|
{
|
||||||
|
echo "## ${{ matrix.name }} — ${{ matrix.platform }}"
|
||||||
|
echo ""
|
||||||
|
if [ "$STATUS" = "success" ]; then
|
||||||
|
echo "✅ **PASSED** — Compiled and built successfully"
|
||||||
|
else
|
||||||
|
echo "❌ **FAILED** — Build or compilation failed"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo "- Unity: ${{ matrix.unity }}"
|
||||||
|
echo "- Platform: ${{ matrix.platform }}"
|
||||||
|
echo "- Source: ${{ matrix.source }}"
|
||||||
|
echo "- Package: \`${{ matrix.package }}\`"
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
|
report:
|
||||||
|
name: Validation Report
|
||||||
|
needs: [load-plugins, validate]
|
||||||
|
if: always()
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Generate summary
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { data: run } = await github.rest.actions.listJobsForWorkflowRun({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: context.runId
|
||||||
|
});
|
||||||
|
|
||||||
|
const validateJobs = run.jobs.filter(j => j.name.startsWith('validate'));
|
||||||
|
const passed = validateJobs.filter(j => j.conclusion === 'success').length;
|
||||||
|
const failed = validateJobs.filter(j => j.conclusion === 'failure').length;
|
||||||
|
const total = validateJobs.length;
|
||||||
|
|
||||||
|
let summary = `# Community Plugin Validation Report\n\n`;
|
||||||
|
summary += `**${passed}/${total} passed** | ${failed} failed\n\n`;
|
||||||
|
summary += `| Plugin | Platform | Status |\n|--------|----------|--------|\n`;
|
||||||
|
|
||||||
|
for (const job of validateJobs) {
|
||||||
|
const icon = job.conclusion === 'success' ? '✅' : '❌';
|
||||||
|
summary += `| ${job.name} | | ${icon} ${job.conclusion} |\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await core.summary.addRaw(summary).write();
|
||||||
|
|
||||||
|
// Create or update issue if there are failures
|
||||||
|
if (failed > 0) {
|
||||||
|
const title = `Community Plugin Validation: ${failed} failure(s) — ${new Date().toISOString().split('T')[0]}`;
|
||||||
|
const body = summary + `\n\n[Workflow Run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`;
|
||||||
|
|
||||||
|
const { data: issues } = await github.rest.issues.listForRepo({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
state: 'open',
|
||||||
|
labels: 'community-plugin-validation'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (issues.length > 0) {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: issues[0].number,
|
||||||
|
body: body
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.create({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
title: title,
|
||||||
|
body: body,
|
||||||
|
labels: ['community-plugin-validation']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
1242
.github/workflows/validate-orchestrator-integration.yml
vendored
Normal file
1242
.github/workflows/validate-orchestrator-integration.yml
vendored
Normal file
File diff suppressed because it is too large
Load Diff
255
.github/workflows/validate-orchestrator.yml
vendored
Normal file
255
.github/workflows/validate-orchestrator.yml
vendored
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
name: Validate Orchestrator Compatibility
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Essential plugin health checks — runs on every PR and push.
|
||||||
|
# Fast (~5 min): compilation, unit tests, plugin interface, type declarations.
|
||||||
|
#
|
||||||
|
# For exhaustive integration tests (k8s, AWS, local-docker, rclone) see
|
||||||
|
# validate-orchestrator-integration.yml which runs on a daily cron.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [main, 'release/**', 'feature/**', 'refactor/**']
|
||||||
|
paths:
|
||||||
|
- 'src/model/orchestrator-plugin.ts'
|
||||||
|
- 'src/model/build-parameters.ts'
|
||||||
|
- 'src/model/input.ts'
|
||||||
|
- 'src/model/github.ts'
|
||||||
|
- 'src/model/cli/cli.ts'
|
||||||
|
- 'src/model/input-readers/**'
|
||||||
|
- 'src/index.ts'
|
||||||
|
- 'src/types/game-ci-orchestrator.d.ts'
|
||||||
|
- 'action.yml'
|
||||||
|
- 'package.json'
|
||||||
|
- 'yarn.lock'
|
||||||
|
- '.github/workflows/validate-orchestrator.yml'
|
||||||
|
pull_request:
|
||||||
|
branches: [main, 'release/**']
|
||||||
|
paths:
|
||||||
|
- 'src/model/orchestrator-plugin.ts'
|
||||||
|
- 'src/model/build-parameters.ts'
|
||||||
|
- 'src/model/input.ts'
|
||||||
|
- 'src/model/github.ts'
|
||||||
|
- 'src/model/cli/cli.ts'
|
||||||
|
- 'src/model/input-readers/**'
|
||||||
|
- 'src/index.ts'
|
||||||
|
- 'src/types/game-ci-orchestrator.d.ts'
|
||||||
|
- 'action.yml'
|
||||||
|
- 'package.json'
|
||||||
|
- 'yarn.lock'
|
||||||
|
- '.github/workflows/validate-orchestrator.yml'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ============================================================================
|
||||||
|
# PLUGIN ARCHITECTURE HEALTH CHECK
|
||||||
|
# ============================================================================
|
||||||
|
# Validates that:
|
||||||
|
# 1. unity-builder compiles and its unit tests pass
|
||||||
|
# 2. Plugin loader degrades gracefully without orchestrator
|
||||||
|
# 3. Orchestrator compiles and its unit tests pass
|
||||||
|
# 4. Plugin loader loads all services when orchestrator is installed
|
||||||
|
# 5. Type declarations match actual exports
|
||||||
|
# ============================================================================
|
||||||
|
plugin-health:
|
||||||
|
name: Plugin Architecture Health
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout unity-builder
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Checkout orchestrator
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: game-ci/orchestrator
|
||||||
|
ref: ${{ github.head_ref || github.ref_name }}
|
||||||
|
path: orchestrator-standalone
|
||||||
|
continue-on-error: true
|
||||||
|
id: orchestrator-branch
|
||||||
|
|
||||||
|
- name: Fallback to orchestrator main branch
|
||||||
|
if: steps.orchestrator-branch.outcome == 'failure'
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: game-ci/orchestrator
|
||||||
|
path: orchestrator-standalone
|
||||||
|
|
||||||
|
- name: Install package manager (from package.json)
|
||||||
|
run: |
|
||||||
|
corepack enable
|
||||||
|
corepack install
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- name: Resolve yarn cache folder
|
||||||
|
id: yarn-config
|
||||||
|
run: echo "cacheFolder=$(yarn config get cacheFolder)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Restore yarn install cache (node_modules + cacheFolder + install-state)
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ steps.yarn-config.outputs.cacheFolder }}
|
||||||
|
.yarn/install-state.gz
|
||||||
|
key: yarn-v2-${{ runner.os }}-node-20-${{ hashFiles('yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
yarn-v2-${{ runner.os }}-node-20-
|
||||||
|
|
||||||
|
# --- unity-builder compilation and tests ---
|
||||||
|
- name: Install unity-builder dependencies
|
||||||
|
env:
|
||||||
|
YARN_ENABLE_HARDENED_MODE: 'false'
|
||||||
|
run: |
|
||||||
|
case "$(yarn --version)" in 1.*) echo 'expected up-to-date yarn version'; exit 1 ;; esac
|
||||||
|
yarn install --immutable
|
||||||
|
|
||||||
|
- name: Build unity-builder
|
||||||
|
run: |
|
||||||
|
echo "Building unity-builder TypeScript..."
|
||||||
|
npx tsc
|
||||||
|
echo "✓ unity-builder compiles successfully"
|
||||||
|
|
||||||
|
- name: Run orchestrator-plugin unit tests
|
||||||
|
run: |
|
||||||
|
echo "Running orchestrator-plugin unit tests..."
|
||||||
|
yarn vitest run orchestrator-plugin
|
||||||
|
|
||||||
|
# --- Plugin loader without orchestrator ---
|
||||||
|
- name: Verify plugin loader returns undefined without orchestrator
|
||||||
|
run: |
|
||||||
|
echo "Checking plugin loader handles missing @game-ci/orchestrator..."
|
||||||
|
node -e "
|
||||||
|
const { loadOrchestratorPlugin } = require('./lib/model/orchestrator-plugin');
|
||||||
|
(async () => {
|
||||||
|
const plugin = await loadOrchestratorPlugin();
|
||||||
|
if (plugin !== undefined) {
|
||||||
|
console.error('ERROR: loadOrchestratorPlugin should return undefined when package not installed');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('✓ loadOrchestratorPlugin() returns undefined when package not installed');
|
||||||
|
})();
|
||||||
|
"
|
||||||
|
|
||||||
|
- name: Verify orchestrator type declarations exist
|
||||||
|
run: |
|
||||||
|
if [ -f "src/types/game-ci-orchestrator.d.ts" ]; then
|
||||||
|
echo "✓ Type declarations for @game-ci/orchestrator exist"
|
||||||
|
else
|
||||||
|
echo "::error::Missing type declarations: src/types/game-ci-orchestrator.d.ts"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Orchestrator compilation and tests ---
|
||||||
|
- name: Build and pack orchestrator
|
||||||
|
working-directory: orchestrator-standalone
|
||||||
|
run: |
|
||||||
|
yarn install --immutable
|
||||||
|
echo "Building orchestrator..."
|
||||||
|
npx tsc
|
||||||
|
echo "✓ orchestrator compiles successfully"
|
||||||
|
echo "Packing orchestrator as tarball..."
|
||||||
|
npm pack
|
||||||
|
|
||||||
|
- name: Run orchestrator unit tests
|
||||||
|
working-directory: orchestrator-standalone
|
||||||
|
run: |
|
||||||
|
echo "Running orchestrator unit tests..."
|
||||||
|
yarn vitest run 2>&1 | tail -30
|
||||||
|
|
||||||
|
# --- Plugin loader with orchestrator installed ---
|
||||||
|
- name: Install orchestrator into unity-builder
|
||||||
|
run: |
|
||||||
|
echo "Installing orchestrator into unity-builder workspace..."
|
||||||
|
npm install ./orchestrator-standalone/game-ci-orchestrator-*.tgz --no-save --legacy-peer-deps
|
||||||
|
|
||||||
|
- name: Verify plugin loader returns exports with orchestrator installed
|
||||||
|
run: |
|
||||||
|
echo "Checking plugin loader returns defined exports..."
|
||||||
|
node -e "
|
||||||
|
const { loadOrchestratorPlugin } = require('./lib/model/orchestrator-plugin');
|
||||||
|
(async () => {
|
||||||
|
const plugin = await loadOrchestratorPlugin();
|
||||||
|
if (plugin === undefined) {
|
||||||
|
console.error('ERROR: loadOrchestratorPlugin should return defined plugin when package is installed');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const lifecycleMethods = [
|
||||||
|
'initialize', 'canHandleBuild', 'handleBuild',
|
||||||
|
'beforeLocalBuild', 'afterLocalBuild', 'handlePostBuild',
|
||||||
|
];
|
||||||
|
for (const method of lifecycleMethods) {
|
||||||
|
if (typeof plugin[method] !== 'function') {
|
||||||
|
console.error('ERROR: plugin.' + method + ' should be a function, got ' + typeof plugin[method]);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('✓ loadOrchestratorPlugin() returns plugin with all ' + lifecycleMethods.length + ' lifecycle methods');
|
||||||
|
})();
|
||||||
|
"
|
||||||
|
|
||||||
|
- name: Verify type declarations match orchestrator exports
|
||||||
|
run: |
|
||||||
|
echo "Checking type declarations align with orchestrator exports..."
|
||||||
|
node -e "
|
||||||
|
const orch = require('@game-ci/orchestrator');
|
||||||
|
const expectedExports = [
|
||||||
|
'Orchestrator', 'BuildReliabilityService', 'TestWorkflowService',
|
||||||
|
'HotRunnerService', 'OutputService', 'OutputTypeRegistry',
|
||||||
|
'ArtifactUploadHandler', 'IncrementalSyncService',
|
||||||
|
'ChildWorkspaceService', 'LocalCacheService', 'SubmoduleProfileService',
|
||||||
|
'LfsAgentService', 'GitHooksService',
|
||||||
|
];
|
||||||
|
const missing = expectedExports.filter(e => orch[e] === undefined);
|
||||||
|
if (missing.length > 0) {
|
||||||
|
console.error('ERROR: Missing exports from @game-ci/orchestrator:', missing.join(', '));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('✓ All ' + expectedExports.length + ' declared exports present in orchestrator package');
|
||||||
|
"
|
||||||
|
|
||||||
|
- name: Smoke test orchestrator build wiring
|
||||||
|
run: |
|
||||||
|
echo "Verifying orchestrator build wiring end-to-end..."
|
||||||
|
node -e "
|
||||||
|
const { loadOrchestratorPlugin } = require('./lib/model/orchestrator-plugin');
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// Verify plugin loads successfully with orchestrator installed
|
||||||
|
const plugin = await loadOrchestratorPlugin();
|
||||||
|
if (plugin === undefined) {
|
||||||
|
console.error('ERROR: plugin should be defined when orchestrator is installed');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all lifecycle methods are callable
|
||||||
|
const lifecycleMethods = [
|
||||||
|
'initialize', 'canHandleBuild', 'handleBuild',
|
||||||
|
'beforeLocalBuild', 'afterLocalBuild', 'handlePostBuild',
|
||||||
|
];
|
||||||
|
for (const m of lifecycleMethods) {
|
||||||
|
if (typeof plugin[m] !== 'function') {
|
||||||
|
console.error('ERROR: plugin.' + m + ' should be a function, got ' + typeof plugin[m]);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('✓ Plugin has all ' + lifecycleMethods.length + ' lifecycle methods');
|
||||||
|
|
||||||
|
// Verify canHandleBuild returns a boolean
|
||||||
|
const canHandle = plugin.canHandleBuild();
|
||||||
|
if (typeof canHandle !== 'boolean') {
|
||||||
|
console.error('ERROR: canHandleBuild() should return a boolean, got ' + typeof canHandle);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('✓ canHandleBuild() returns boolean');
|
||||||
|
|
||||||
|
console.log('✓ Plugin architecture wiring verified');
|
||||||
|
})();
|
||||||
|
"
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -7,3 +7,12 @@ yarn-error.log
|
|||||||
.orig
|
.orig
|
||||||
$LOG_FILE
|
$LOG_FILE
|
||||||
temp/
|
temp/
|
||||||
|
|
||||||
|
# yarn 4 (berry)
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|||||||
7
.husky/pre-commit
Executable file
7
.husky/pre-commit
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
yarn lint-staged
|
||||||
|
yarn typecheck
|
||||||
|
|
||||||
|
if command -v gitleaks >/dev/null 2>&1; then
|
||||||
|
gitleaks protect --staged --no-banner --redact
|
||||||
|
fi
|
||||||
17
.oxfmtrc.json
Normal file
17
.oxfmtrc.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 100,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"sortPackageJson": false,
|
||||||
|
"ignorePatterns": [
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/dist/**",
|
||||||
|
"**/coverage/**",
|
||||||
|
"**/.yarn/**",
|
||||||
|
"default-build-script/**",
|
||||||
|
"test-runner/**",
|
||||||
|
"platforms/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
58
.oxlintrc.json
Normal file
58
.oxlintrc.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": ["typescript", "vitest", "unicorn", "oxc"],
|
||||||
|
"categories": {
|
||||||
|
"correctness": "error",
|
||||||
|
"suspicious": "error",
|
||||||
|
"perf": "error"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"vitest/require-mock-type-parameters": "off",
|
||||||
|
"vitest/valid-title": "off",
|
||||||
|
"vitest/valid-describe-callback": "off",
|
||||||
|
"vitest/expect-expect": "off",
|
||||||
|
"vitest/no-conditional-tests": "off",
|
||||||
|
"vitest/no-conditional-expect": "off",
|
||||||
|
"vitest/require-to-throw-message": "off",
|
||||||
|
"vitest/no-disabled-tests": "warn",
|
||||||
|
"unicorn/prefer-array-flat-map": "warn",
|
||||||
|
"typescript/no-explicit-any": "warn",
|
||||||
|
"typescript/ban-ts-comment": "off",
|
||||||
|
"typescript/no-namespace": "off",
|
||||||
|
"typescript/no-extraneous-class": "off",
|
||||||
|
"no-bitwise": "off",
|
||||||
|
"no-shadow": "off",
|
||||||
|
"no-await-in-loop": "off",
|
||||||
|
"no-underscore-dangle": "off",
|
||||||
|
"unicorn/no-array-sort": "off",
|
||||||
|
"unicorn/prefer-set-has": "off",
|
||||||
|
"unicorn/consistent-function-scoping": "off",
|
||||||
|
"unicorn/no-useless-spread": "warn",
|
||||||
|
"eslint/preserve-caught-error": "warn",
|
||||||
|
"oxc/no-map-spread": "warn"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["**/*.test.ts", "**/*.spec.ts"],
|
||||||
|
"rules": {
|
||||||
|
"typescript/no-explicit-any": "off",
|
||||||
|
"no-unused-vars": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"browser": false,
|
||||||
|
"node": true,
|
||||||
|
"es2024": true,
|
||||||
|
"vitest/globals": true
|
||||||
|
},
|
||||||
|
"ignorePatterns": [
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/dist/**",
|
||||||
|
"**/coverage/**",
|
||||||
|
"**/.yarn/**",
|
||||||
|
"default-build-script/**",
|
||||||
|
"test-runner/**",
|
||||||
|
"platforms/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
**/node_modules/**
|
|
||||||
**/dist/**
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"printWidth": 120,
|
|
||||||
"proseWrap": "always"
|
|
||||||
}
|
|
||||||
10
.yarnrc.yml
Normal file
10
.yarnrc.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
approvedGitRepositories:
|
||||||
|
- '**'
|
||||||
|
|
||||||
|
compressionLevel: mixed
|
||||||
|
|
||||||
|
enableGlobalCache: false
|
||||||
|
|
||||||
|
enableHardenedMode: false
|
||||||
|
|
||||||
|
nodeLinker: node-modules
|
||||||
130
action.yml
130
action.yml
@@ -9,8 +9,7 @@ inputs:
|
|||||||
unityVersion:
|
unityVersion:
|
||||||
required: false
|
required: false
|
||||||
default: 'auto'
|
default: 'auto'
|
||||||
description:
|
description: 'Version of unity to use for building the project. Use "auto" to get from your ProjectSettings/ProjectVersion.txt'
|
||||||
'Version of unity to use for building the project. Use "auto" to get from your ProjectSettings/ProjectVersion.txt'
|
|
||||||
customImage:
|
customImage:
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
@@ -47,6 +46,10 @@ inputs:
|
|||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
description: 'Custom parameters to configure the build.'
|
description: 'Custom parameters to configure the build.'
|
||||||
|
useHostNetwork:
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
description: 'Initialises Docker using the host network. (Linux only)'
|
||||||
versioning:
|
versioning:
|
||||||
required: false
|
required: false
|
||||||
default: 'Semantic'
|
default: 'Semantic'
|
||||||
@@ -104,11 +107,13 @@ inputs:
|
|||||||
gitPrivateToken:
|
gitPrivateToken:
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
description: '[Orchestrator] Github private token to pull from github'
|
description: 'Github private token to pull from github'
|
||||||
githubOwner:
|
providerStrategy:
|
||||||
|
default: 'local'
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
description:
|
||||||
description: '[Orchestrator] GitHub owner name or organization/team name'
|
'Build execution strategy. Use "local" for local Docker/Mac builds. For remote builds (aws, k8s, etc.), install
|
||||||
|
@game-ci/orchestrator and use the game-ci/orchestrator action which declares its own inputs.'
|
||||||
runAsHostUser:
|
runAsHostUser:
|
||||||
required: false
|
required: false
|
||||||
default: 'false'
|
default: 'false'
|
||||||
@@ -118,8 +123,7 @@ inputs:
|
|||||||
chownFilesTo:
|
chownFilesTo:
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
description:
|
description: 'User and optionally group (user or user:group or uid:gid) to give ownership of the resulting build artifacts'
|
||||||
'User and optionally group (user or user:group or uid:gid) to give ownership of the resulting build artifacts'
|
|
||||||
dockerCpuLimit:
|
dockerCpuLimit:
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
@@ -149,102 +153,7 @@ inputs:
|
|||||||
allowDirtyBuild:
|
allowDirtyBuild:
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
description: '[Orchestrator] Allows the branch of the build to be dirty, and still generate the build.'
|
description: 'Allows the branch of the build to be dirty, and still generate the build.'
|
||||||
postBuildSteps:
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
description:
|
|
||||||
'[Orchestrator] run a post build job in yaml format with the keys image, secrets (name, value object array),
|
|
||||||
command string'
|
|
||||||
preBuildSteps:
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
description:
|
|
||||||
'[Orchestrator] Run a pre build job after the repository setup but before the build job (in yaml format with the
|
|
||||||
keys image, secrets (name, value object array), command line string)'
|
|
||||||
containerHookFiles:
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
description:
|
|
||||||
'[Orchestrator] Specify the names (by file name) of custom steps to run before or after orchestrator jobs, must
|
|
||||||
match a yaml step file inside your repo in the folder .game-ci/steps/'
|
|
||||||
customHookFiles:
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
description:
|
|
||||||
'[Orchestrator] Specify the names (by file name) of custom hooks to run before or after orchestrator jobs, must
|
|
||||||
match a yaml step file inside your repo in the folder .game-ci/hooks/'
|
|
||||||
customCommandHooks:
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
description: '[Orchestrator] Specify custom commands and trigger hooks (injects commands into jobs)'
|
|
||||||
customJob:
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
description:
|
|
||||||
'[Orchestrator] Run a custom job instead of the standard build automation for orchestrator (in yaml format with the
|
|
||||||
keys image, secrets (name, value object array), command line string)'
|
|
||||||
awsStackName:
|
|
||||||
default: 'game-ci'
|
|
||||||
required: false
|
|
||||||
description: '[Orchestrator] The Cloud Formation stack name that must be setup before using this option.'
|
|
||||||
providerStrategy:
|
|
||||||
default: 'local'
|
|
||||||
required: false
|
|
||||||
description:
|
|
||||||
'[Orchestrator] Either local, k8s or aws can be used to run builds on a remote cluster. Additional parameters must
|
|
||||||
be configured.'
|
|
||||||
resourceTracking:
|
|
||||||
default: 'false'
|
|
||||||
required: false
|
|
||||||
description: '[Orchestrator] Enable resource tracking logs for disk usage and allocation summaries.'
|
|
||||||
containerCpu:
|
|
||||||
default: ''
|
|
||||||
required: false
|
|
||||||
description: '[Orchestrator] Amount of CPU time to assign the remote build container'
|
|
||||||
containerMemory:
|
|
||||||
default: ''
|
|
||||||
required: false
|
|
||||||
description: '[Orchestrator] Amount of memory to assign the remote build container'
|
|
||||||
readInputFromOverrideList:
|
|
||||||
default: ''
|
|
||||||
required: false
|
|
||||||
description: '[Orchestrator] Comma separated list of input value names to read from "input override command"'
|
|
||||||
readInputOverrideCommand:
|
|
||||||
default: ''
|
|
||||||
required: false
|
|
||||||
description:
|
|
||||||
'[Orchestrator] Extend game ci by specifying a command to execute to pull input from external source e.g cloud
|
|
||||||
provider secret managers'
|
|
||||||
kubeConfig:
|
|
||||||
default: ''
|
|
||||||
required: false
|
|
||||||
description:
|
|
||||||
'[Orchestrator] Supply a base64 encoded kubernetes config to run builds on kubernetes and stream logs until
|
|
||||||
completion.'
|
|
||||||
kubeVolume:
|
|
||||||
default: ''
|
|
||||||
required: false
|
|
||||||
description: '[Orchestrator] Supply a Persistent Volume Claim name to use for the Unity build.'
|
|
||||||
kubeStorageClass:
|
|
||||||
default: ''
|
|
||||||
required: false
|
|
||||||
description:
|
|
||||||
'[Orchestrator] Kubernetes storage class to use for orchestrator jobs, leave empty to install rook cluster.'
|
|
||||||
kubeVolumeSize:
|
|
||||||
default: '5Gi'
|
|
||||||
required: false
|
|
||||||
description: '[Orchestrator] Amount of disc space to assign the Kubernetes Persistent Volume'
|
|
||||||
cacheKey:
|
|
||||||
default: ''
|
|
||||||
required: false
|
|
||||||
description: '[Orchestrator] Cache key to indicate bucket for cache'
|
|
||||||
watchToEnd:
|
|
||||||
default: 'true'
|
|
||||||
required: false
|
|
||||||
description:
|
|
||||||
'[Orchestrator] Whether or not to watch the build to the end. Can be used for especially long running jobs e.g
|
|
||||||
imports or self-hosted ephemeral runners.'
|
|
||||||
cacheUnityInstallationOnMac:
|
cacheUnityInstallationOnMac:
|
||||||
default: 'false'
|
default: 'false'
|
||||||
required: false
|
required: false
|
||||||
@@ -269,16 +178,11 @@ inputs:
|
|||||||
default: 'false'
|
default: 'false'
|
||||||
required: false
|
required: false
|
||||||
description: 'Skip the activation/deactivation of Unity. This assumes Unity is already activated.'
|
description: 'Skip the activation/deactivation of Unity. This assumes Unity is already activated.'
|
||||||
cloneDepth:
|
linux64RemoveExecutableExtension:
|
||||||
default: '50'
|
default: 'false'
|
||||||
required: false
|
|
||||||
description: '[Orchestrator] Specifies the depth of the git clone for the repository. Use 0 for full clone.'
|
|
||||||
orchestratorRepoName:
|
|
||||||
default: 'game-ci/unity-builder'
|
|
||||||
required: false
|
required: false
|
||||||
description:
|
description:
|
||||||
'[Orchestrator] Specifies the repo for the unity builder. Useful if you forked the repo for testing, features, or
|
'When building for StandaloneLinux64, remove the default file extension of `.x86_64`. Set to true to restore the extensionless behavior from v4.'
|
||||||
fixes.'
|
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
volume:
|
volume:
|
||||||
@@ -296,5 +200,5 @@ branding:
|
|||||||
icon: 'box'
|
icon: 'box'
|
||||||
color: 'gray-dark'
|
color: 'gray-dark'
|
||||||
runs:
|
runs:
|
||||||
using: 'node20'
|
using: 'node24'
|
||||||
main: 'dist/index.js'
|
main: 'dist/index.js'
|
||||||
|
|||||||
27
community-plugins.yml
Normal file
27
community-plugins.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Community Plugin Validation Registry
|
||||||
|
# Packages listed here are automatically tested on a schedule
|
||||||
|
# to ensure compatibility with unity-builder.
|
||||||
|
#
|
||||||
|
# Format:
|
||||||
|
# - name: Human-readable name
|
||||||
|
# package: UPM package name or git URL
|
||||||
|
# source: upm | git | asset-store
|
||||||
|
# unity: Minimum Unity version (optional, defaults to 2021.3)
|
||||||
|
# platforms: List of platforms to test (optional, defaults to [StandaloneLinux64])
|
||||||
|
# timeout: Build timeout in minutes (optional, defaults to 30)
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
# Example entries — community members can submit PRs to add their packages
|
||||||
|
- name: UniTask
|
||||||
|
package: https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask
|
||||||
|
source: git
|
||||||
|
platforms: [StandaloneLinux64, StandaloneWindows64]
|
||||||
|
|
||||||
|
- name: NaughtyAttributes
|
||||||
|
package: https://github.com/dbrizov/NaughtyAttributes.git?path=Assets/NaughtyAttributes
|
||||||
|
source: git
|
||||||
|
|
||||||
|
- name: Unity Atoms
|
||||||
|
package: https://github.com/unity-atoms/unity-atoms.git
|
||||||
|
source: git
|
||||||
|
platforms: [StandaloneLinux64]
|
||||||
381691
dist/index.js
generated
vendored
381691
dist/index.js
generated
vendored
File diff suppressed because one or more lines are too long
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
18938
dist/licenses.txt
generated
vendored
18938
dist/licenses.txt
generated
vendored
File diff suppressed because it is too large
Load Diff
122
install.ps1
122
install.ps1
@@ -1,122 +0,0 @@
|
|||||||
# game-ci CLI installer for Windows
|
|
||||||
# Usage: irm https://raw.githubusercontent.com/game-ci/unity-builder/main/install.ps1 | iex
|
|
||||||
#
|
|
||||||
# Environment variables:
|
|
||||||
# GAME_CI_VERSION - Install a specific version (e.g., v2.0.0). Defaults to latest.
|
|
||||||
# GAME_CI_INSTALL - Installation directory. Defaults to $HOME\.game-ci\bin.
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
$Repo = "game-ci/unity-builder"
|
|
||||||
$InstallDir = if ($env:GAME_CI_INSTALL) { $env:GAME_CI_INSTALL } else { Join-Path $env:USERPROFILE ".game-ci\bin" }
|
|
||||||
$AssetName = "game-ci-windows-x64.exe"
|
|
||||||
$BinaryName = "game-ci.exe"
|
|
||||||
|
|
||||||
function Write-Info($Message) {
|
|
||||||
Write-Host "info: " -ForegroundColor Green -NoNewline
|
|
||||||
Write-Host $Message
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-Warn($Message) {
|
|
||||||
Write-Host "warn: " -ForegroundColor Yellow -NoNewline
|
|
||||||
Write-Host $Message
|
|
||||||
}
|
|
||||||
|
|
||||||
# Determine version
|
|
||||||
if ($env:GAME_CI_VERSION) {
|
|
||||||
$Version = $env:GAME_CI_VERSION
|
|
||||||
Write-Info "Using specified version: $Version"
|
|
||||||
} else {
|
|
||||||
Write-Info "Fetching latest release..."
|
|
||||||
try {
|
|
||||||
$Release = Invoke-RestMethod "https://api.github.com/repos/$Repo/releases/latest"
|
|
||||||
$Version = $Release.tag_name
|
|
||||||
} catch {
|
|
||||||
Write-Host "error: Could not determine latest version. Check https://github.com/$Repo/releases" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$DownloadUrl = "https://github.com/$Repo/releases/download/$Version/$AssetName"
|
|
||||||
$ChecksumUrl = "https://github.com/$Repo/releases/download/$Version/checksums.txt"
|
|
||||||
$BinaryPath = Join-Path $InstallDir $BinaryName
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Info "Installing game-ci $Version (windows-x64)"
|
|
||||||
Write-Info " from: $DownloadUrl"
|
|
||||||
Write-Info " to: $BinaryPath"
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
# Create install directory
|
|
||||||
if (-not (Test-Path $InstallDir)) {
|
|
||||||
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Download binary
|
|
||||||
try {
|
|
||||||
Invoke-WebRequest -Uri $DownloadUrl -OutFile $BinaryPath -UseBasicParsing
|
|
||||||
} catch {
|
|
||||||
if ($_.Exception.Response.StatusCode -eq 404) {
|
|
||||||
Write-Host "error: Release asset not found: $AssetName ($Version)" -ForegroundColor Red
|
|
||||||
Write-Host " Check available assets at https://github.com/$Repo/releases/tag/$Version" -ForegroundColor Red
|
|
||||||
} else {
|
|
||||||
Write-Host "error: Download failed: $_" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Verify checksum
|
|
||||||
try {
|
|
||||||
$Checksums = Invoke-WebRequest -Uri $ChecksumUrl -UseBasicParsing | Select-Object -ExpandProperty Content
|
|
||||||
$ExpectedLine = $Checksums -split "`n" | Where-Object { $_ -match $AssetName } | Select-Object -First 1
|
|
||||||
if ($ExpectedLine) {
|
|
||||||
$ExpectedHash = ($ExpectedLine -split '\s+')[0]
|
|
||||||
$ActualHash = (Get-FileHash -Path $BinaryPath -Algorithm SHA256).Hash.ToLower()
|
|
||||||
if ($ExpectedHash -eq $ActualHash) {
|
|
||||||
Write-Info "Checksum verified (SHA256)"
|
|
||||||
} else {
|
|
||||||
Write-Host "error: Checksum verification failed!" -ForegroundColor Red
|
|
||||||
Write-Host " Expected: $ExpectedHash" -ForegroundColor Red
|
|
||||||
Write-Host " Got: $ActualHash" -ForegroundColor Red
|
|
||||||
Remove-Item $BinaryPath -Force
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
# Checksums not available for this release; continue without verification
|
|
||||||
}
|
|
||||||
|
|
||||||
# Verify the binary works
|
|
||||||
try {
|
|
||||||
$VersionOutput = & $BinaryPath version 2>&1
|
|
||||||
Write-Info "Verified: $($VersionOutput | Select-Object -First 1)"
|
|
||||||
} catch {
|
|
||||||
Write-Warn "Binary downloaded but could not verify. It may still work."
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "game-ci installed successfully!" -ForegroundColor Green -BackgroundColor Black
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
# Check PATH and offer to add
|
|
||||||
$UserPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
|
|
||||||
if ($UserPath -notlike "*$InstallDir*") {
|
|
||||||
Write-Warn "game-ci is not in your PATH."
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "To add it permanently, run:" -ForegroundColor Yellow
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host " [Environment]::SetEnvironmentVariable('PATH', ""$InstallDir;"" + [Environment]::GetEnvironmentVariable('PATH', 'User'), 'User')"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Info "Then restart your terminal."
|
|
||||||
|
|
||||||
# Offer to add automatically
|
|
||||||
Write-Host ""
|
|
||||||
$AddToPath = Read-Host "Add to PATH now? (Y/n)"
|
|
||||||
if ($AddToPath -ne 'n' -and $AddToPath -ne 'N') {
|
|
||||||
[Environment]::SetEnvironmentVariable('PATH', "$InstallDir;$UserPath", 'User')
|
|
||||||
$env:PATH = "$InstallDir;$env:PATH"
|
|
||||||
Write-Info "Added to PATH. You can now run: game-ci --help"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Info "game-ci is already in your PATH. Run: game-ci --help"
|
|
||||||
}
|
|
||||||
196
install.sh
196
install.sh
@@ -1,196 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# game-ci CLI installer
|
|
||||||
# Usage: curl -fsSL https://raw.githubusercontent.com/game-ci/unity-builder/main/install.sh | sh
|
|
||||||
#
|
|
||||||
# Environment variables:
|
|
||||||
# GAME_CI_VERSION - Install a specific version (e.g., v2.0.0). Defaults to latest.
|
|
||||||
# GAME_CI_INSTALL - Installation directory. Defaults to ~/.game-ci/bin.
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
REPO="game-ci/unity-builder"
|
|
||||||
INSTALL_DIR="${GAME_CI_INSTALL:-$HOME/.game-ci/bin}"
|
|
||||||
BINARY_NAME="game-ci"
|
|
||||||
|
|
||||||
# Colors (disabled if not a terminal)
|
|
||||||
if [ -t 1 ]; then
|
|
||||||
BOLD='\033[1m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[0;33m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
RESET='\033[0m'
|
|
||||||
else
|
|
||||||
BOLD=''
|
|
||||||
GREEN=''
|
|
||||||
YELLOW=''
|
|
||||||
RED=''
|
|
||||||
RESET=''
|
|
||||||
fi
|
|
||||||
|
|
||||||
info() {
|
|
||||||
printf "${GREEN}info${RESET}: %s\n" "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
warn() {
|
|
||||||
printf "${YELLOW}warn${RESET}: %s\n" "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
error() {
|
|
||||||
printf "${RED}error${RESET}: %s\n" "$1" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Detect OS and architecture
|
|
||||||
detect_platform() {
|
|
||||||
OS="$(uname -s)"
|
|
||||||
ARCH="$(uname -m)"
|
|
||||||
|
|
||||||
case "$OS" in
|
|
||||||
Linux*) PLATFORM="linux" ;;
|
|
||||||
Darwin*) PLATFORM="macos" ;;
|
|
||||||
MINGW*|MSYS*|CYGWIN*)
|
|
||||||
PLATFORM="windows"
|
|
||||||
warn "For Windows, consider using install.ps1 instead:"
|
|
||||||
warn " irm https://raw.githubusercontent.com/game-ci/unity-builder/main/install.ps1 | iex"
|
|
||||||
;;
|
|
||||||
*) error "Unsupported operating system: $OS" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$ARCH" in
|
|
||||||
x86_64|amd64) ARCH="x64" ;;
|
|
||||||
aarch64|arm64) ARCH="arm64" ;;
|
|
||||||
*) error "Unsupported architecture: $ARCH" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
ASSET_NAME="game-ci-${PLATFORM}-${ARCH}"
|
|
||||||
if [ "$PLATFORM" = "windows" ]; then
|
|
||||||
ASSET_NAME="${ASSET_NAME}.exe"
|
|
||||||
BINARY_NAME="game-ci.exe"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get latest release tag from GitHub API
|
|
||||||
get_latest_version() {
|
|
||||||
if [ -n "$GAME_CI_VERSION" ]; then
|
|
||||||
VERSION="$GAME_CI_VERSION"
|
|
||||||
info "Using specified version: $VERSION"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
info "Fetching latest release..."
|
|
||||||
|
|
||||||
if command -v curl > /dev/null 2>&1; then
|
|
||||||
VERSION=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
|
|
||||||
elif command -v wget > /dev/null 2>&1; then
|
|
||||||
VERSION=$(wget -qO- "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
|
|
||||||
else
|
|
||||||
error "Neither curl nor wget found. Please install one of them."
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$VERSION" ]; then
|
|
||||||
error "Could not determine latest version. Check https://github.com/${REPO}/releases"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Download and install the binary
|
|
||||||
install() {
|
|
||||||
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${VERSION}/${ASSET_NAME}"
|
|
||||||
|
|
||||||
printf "\n"
|
|
||||||
info "Installing game-ci ${VERSION} (${PLATFORM}-${ARCH})"
|
|
||||||
info " from: ${DOWNLOAD_URL}"
|
|
||||||
info " to: ${INSTALL_DIR}/${BINARY_NAME}"
|
|
||||||
printf "\n"
|
|
||||||
|
|
||||||
mkdir -p "$INSTALL_DIR"
|
|
||||||
|
|
||||||
# Download with progress
|
|
||||||
if command -v curl > /dev/null 2>&1; then
|
|
||||||
HTTP_CODE=$(curl -fSL "$DOWNLOAD_URL" -o "${INSTALL_DIR}/${BINARY_NAME}" -w "%{http_code}" 2>/dev/null) || true
|
|
||||||
if [ "$HTTP_CODE" = "404" ]; then
|
|
||||||
error "Release asset not found: ${ASSET_NAME} (${VERSION}). Check available assets at https://github.com/${REPO}/releases/tag/${VERSION}"
|
|
||||||
elif [ ! -f "${INSTALL_DIR}/${BINARY_NAME}" ]; then
|
|
||||||
error "Download failed. URL: ${DOWNLOAD_URL}"
|
|
||||||
fi
|
|
||||||
elif command -v wget > /dev/null 2>&1; then
|
|
||||||
wget -q "$DOWNLOAD_URL" -O "${INSTALL_DIR}/${BINARY_NAME}" || error "Download failed. URL: ${DOWNLOAD_URL}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
chmod +x "${INSTALL_DIR}/${BINARY_NAME}"
|
|
||||||
|
|
||||||
# Verify the binary works
|
|
||||||
if "${INSTALL_DIR}/${BINARY_NAME}" version > /dev/null 2>&1; then
|
|
||||||
INSTALLED_VERSION=$("${INSTALL_DIR}/${BINARY_NAME}" version 2>&1 | head -1)
|
|
||||||
info "Verified: ${INSTALLED_VERSION}"
|
|
||||||
else
|
|
||||||
warn "Binary downloaded but could not verify. It may still work."
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf "\n"
|
|
||||||
printf "${BOLD}game-ci installed successfully!${RESET}\n"
|
|
||||||
printf "\n"
|
|
||||||
|
|
||||||
# Check if install dir is in PATH
|
|
||||||
case ":$PATH:" in
|
|
||||||
*":${INSTALL_DIR}:"*)
|
|
||||||
info "game-ci is already in your PATH. Run: game-ci --help"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
SHELL_NAME=$(basename "$SHELL" 2>/dev/null || echo "sh")
|
|
||||||
case "$SHELL_NAME" in
|
|
||||||
zsh) PROFILE="~/.zshrc" ;;
|
|
||||||
bash) PROFILE="~/.bashrc" ;;
|
|
||||||
fish) PROFILE="~/.config/fish/config.fish" ;;
|
|
||||||
*) PROFILE="~/.profile" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
printf "${YELLOW}Add game-ci to your PATH by adding this to ${PROFILE}:${RESET}\n"
|
|
||||||
printf "\n"
|
|
||||||
if [ "$SHELL_NAME" = "fish" ]; then
|
|
||||||
printf " set -gx PATH \"%s\" \$PATH\n" "$INSTALL_DIR"
|
|
||||||
else
|
|
||||||
printf " export PATH=\"%s:\$PATH\"\n" "$INSTALL_DIR"
|
|
||||||
fi
|
|
||||||
printf "\n"
|
|
||||||
info "Then restart your shell or run: source ${PROFILE}"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# Verify checksum if checksums.txt is available
|
|
||||||
verify_checksum() {
|
|
||||||
if ! command -v sha256sum > /dev/null 2>&1; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
CHECKSUM_URL="https://github.com/${REPO}/releases/download/${VERSION}/checksums.txt"
|
|
||||||
|
|
||||||
CHECKSUMS=""
|
|
||||||
if command -v curl > /dev/null 2>&1; then
|
|
||||||
CHECKSUMS=$(curl -fsSL "$CHECKSUM_URL" 2>/dev/null) || return 0
|
|
||||||
elif command -v wget > /dev/null 2>&1; then
|
|
||||||
CHECKSUMS=$(wget -qO- "$CHECKSUM_URL" 2>/dev/null) || return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$CHECKSUMS" ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
EXPECTED=$(echo "$CHECKSUMS" | grep "$ASSET_NAME" | awk '{print $1}')
|
|
||||||
if [ -z "$EXPECTED" ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
ACTUAL=$(sha256sum "${INSTALL_DIR}/${BINARY_NAME}" | awk '{print $1}')
|
|
||||||
if [ "$EXPECTED" != "$ACTUAL" ]; then
|
|
||||||
error "Checksum verification failed!\n Expected: ${EXPECTED}\n Got: ${ACTUAL}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
info "Checksum verified (SHA256)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main
|
|
||||||
detect_platform
|
|
||||||
get_latest_version
|
|
||||||
install
|
|
||||||
verify_checksum
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
const base = require('./jest.config.js');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
...base,
|
|
||||||
forceExit: true,
|
|
||||||
detectOpenHandles: true,
|
|
||||||
testTimeout: 120000,
|
|
||||||
maxWorkers: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
// Automatically clear mock calls and instances between every test
|
|
||||||
clearMocks: true,
|
|
||||||
|
|
||||||
// An array of file extensions your modules use
|
|
||||||
moduleFileExtensions: ['js', 'ts'],
|
|
||||||
|
|
||||||
// The test environment that will be used for testing
|
|
||||||
testEnvironment: 'node',
|
|
||||||
|
|
||||||
// The glob patterns Jest uses to detect test files
|
|
||||||
testMatch: ['**/*.test.ts'],
|
|
||||||
|
|
||||||
// This option allows use of a custom test runner
|
|
||||||
testRunner: 'jest-circus/runner',
|
|
||||||
|
|
||||||
// A map with regular expressions for transformers to paths
|
|
||||||
transform: {
|
|
||||||
'^.+\\.ts$': 'ts-jest',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Indicates whether each individual test should be reported during the run
|
|
||||||
verbose: true,
|
|
||||||
|
|
||||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
|
||||||
modulePathIgnorePatterns: ['<rootDir>/lib/', '<rootDir>/dist/'],
|
|
||||||
|
|
||||||
// Use jest.setup.js to polyfill fetch for all tests
|
|
||||||
setupFiles: ['<rootDir>/jest.setup.js'],
|
|
||||||
};
|
|
||||||
31
lefthook.yml
31
lefthook.yml
@@ -1,31 +0,0 @@
|
|||||||
# EXAMPLE USAGE
|
|
||||||
# Refer for explanation to following link:
|
|
||||||
# https://github.com/evilmartians/lefthook/blob/master/docs/full_guide.md
|
|
||||||
#
|
|
||||||
|
|
||||||
color: true
|
|
||||||
extends: {}
|
|
||||||
|
|
||||||
pre-commit:
|
|
||||||
parallel: true
|
|
||||||
commands:
|
|
||||||
format documents:
|
|
||||||
glob: '*.{md,mdx}'
|
|
||||||
run: yarn prettier --write {staged_files}
|
|
||||||
format configs:
|
|
||||||
glob: '*.{json,yml,yaml}'
|
|
||||||
run: yarn prettier --write {staged_files}
|
|
||||||
format code:
|
|
||||||
glob: '*.{js,jsx,ts,tsx}'
|
|
||||||
exclude: 'dist/'
|
|
||||||
run: yarn prettier --write {staged_files} && yarn eslint {staged_files} && git add {staged_files}
|
|
||||||
run tests:
|
|
||||||
glob: '*.{js,jsx,ts,tsx}'
|
|
||||||
exclude: 'dist/'
|
|
||||||
run: yarn jest --passWithNoTests --findRelatedTests {staged_files}
|
|
||||||
build distributables:
|
|
||||||
skip: ['merge', 'rebase']
|
|
||||||
run: yarn build && git add dist
|
|
||||||
make shell script executable:
|
|
||||||
glob: '*.sh'
|
|
||||||
run: git update-index --chmod=+x
|
|
||||||
6
mise.toml
Normal file
6
mise.toml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[tools]
|
||||||
|
node = "20.18.0"
|
||||||
|
yarn = "4.14.1"
|
||||||
|
actionlint = "latest"
|
||||||
|
shellcheck = "latest"
|
||||||
|
gitleaks = "latest"
|
||||||
106
package.json
106
package.json
@@ -3,46 +3,30 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"description": "Build Unity projects for different platforms.",
|
"description": "Build Unity projects for different platforms.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"bin": {
|
|
||||||
"game-ci": "./lib/cli.js"
|
|
||||||
},
|
|
||||||
"pkg": {
|
|
||||||
"scripts": "lib/**/*.js",
|
|
||||||
"assets": [
|
|
||||||
"lib/**/*.json",
|
|
||||||
"package.json"
|
|
||||||
],
|
|
||||||
"targets": [
|
|
||||||
"node20-linux-x64",
|
|
||||||
"node20-linux-arm64",
|
|
||||||
"node20-macos-x64",
|
|
||||||
"node20-macos-arm64",
|
|
||||||
"node20-win-x64"
|
|
||||||
],
|
|
||||||
"outputPath": "dist-binaries"
|
|
||||||
},
|
|
||||||
"repository": "git@github.com:game-ci/unity-builder.git",
|
"repository": "git@github.com:game-ci/unity-builder.git",
|
||||||
"author": "Webber <webber@takken.io>",
|
"author": "Webber <webber@takken.io>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "lefthook install",
|
"prepare": "husky",
|
||||||
"build": "yarn && tsc && ncc build lib --source-map --license licenses.txt",
|
"build": "yarn && tsc && ncc build lib --source-map --license licenses.txt",
|
||||||
"lint": "prettier --check \"src/**/*.{js,ts}\" && eslint src/**/*.ts",
|
"test": "node scripts/ensure-husky.mjs && vitest run",
|
||||||
"format": "prettier --write \"src/**/*.{js,ts}\"",
|
"test:watch": "vitest",
|
||||||
"cli": "yarn ts-node src/index.ts -m cli",
|
"test:ci": "vitest run",
|
||||||
"game-ci": "ts-node src/cli.ts",
|
"coverage": "vitest run --coverage",
|
||||||
"gcp-secrets-tests": "cross-env providerStrategy=aws orchestratorTests=true inputPullCommand=\"gcp-secret-manager\" populateOverride=true pullInputList=UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD yarn test -i -t \"orchestrator\"",
|
"lint": "yarn oxlint --report-unused-disable-directives",
|
||||||
"gcp-secrets-cli": "cross-env orchestratorTests=true USE_IL2CPP=false inputPullCommand=\"gcp-secret-manager\" yarn ts-node src/index.ts -m cli --populateOverride true --pullInputList UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD",
|
"format": "oxfmt --write",
|
||||||
"aws-secrets-cli": "cross-env orchestratorTests=true inputPullCommand=\"aws-secret-manager\" yarn ts-node src/index.ts -m cli --populateOverride true --pullInputList UNITY_EMAIL,UNITY_SERIAL,UNITY_PASSWORD",
|
"format:check": "oxfmt --check",
|
||||||
"cli-aws": "cross-env providerStrategy=aws yarn run test-cli",
|
"typecheck": "tsc --noEmit",
|
||||||
"cli-k8s": "cross-env providerStrategy=k8s yarn run test-cli",
|
"typecheck:tsgo": "tsgo --noEmit",
|
||||||
"test-cli": "cross-env orchestratorTests=true yarn ts-node src/index.ts -m cli --projectPath test-project",
|
"setup:hooks": "node scripts/ensure-husky.mjs"
|
||||||
"test": "jest",
|
},
|
||||||
"test:ci": "jest --config=jest.ci.config.js --runInBand",
|
"lint-staged": {
|
||||||
"test-i": "cross-env orchestratorTests=true yarn test -i -t \"orchestrator\"",
|
"*.@(ts|tsx|mts|js|jsx|mjs|cjs)": [
|
||||||
"test-i-*": "yarn run test-i-aws && yarn run test-i-k8s",
|
"oxlint --fix --quiet",
|
||||||
"test-i-aws": "cross-env orchestratorTests=true providerStrategy=aws yarn test -i -t \"orchestrator\"",
|
"oxfmt --write"
|
||||||
"test-i-k8s": "cross-env orchestratorTests=true providerStrategy=k8s yarn test -i -t \"orchestrator\""
|
],
|
||||||
|
"*.@(json|jsonc|json5|md|mdx|yaml|yml|css|scss|sass|html|toml)": "oxfmt --write",
|
||||||
|
".github/workflows/*.@(yml|yaml)": "actionlint"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.x"
|
"node": ">=18.x"
|
||||||
@@ -52,61 +36,41 @@
|
|||||||
"@actions/core": "^1.11.1",
|
"@actions/core": "^1.11.1",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"@actions/github": "^6.0.0",
|
"@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": "^5.1.0",
|
|
||||||
"async-wait-until": "^2.0.12",
|
|
||||||
"aws-sdk": "^2.1081.0",
|
|
||||||
"base-64": "^1.0.0",
|
|
||||||
"commander": "^9.0.0",
|
"commander": "^9.0.0",
|
||||||
"commander-ts": "^0.2.0",
|
"commander-ts": "^0.2.0",
|
||||||
"kubernetes-client": "^9.0.0",
|
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"nanoid": "^3.3.1",
|
"nanoid": "^3.3.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"semver": "^7.5.2",
|
"semver": "^7.5.2",
|
||||||
"shell-quote": "^1.8.3",
|
|
||||||
"ts-md5": "^1.3.1",
|
"ts-md5": "^1.3.1",
|
||||||
"unity-changeset": "^3.1.0",
|
"unity-changeset": "^3.1.0",
|
||||||
"uuid": "^9.0.0",
|
"yaml": "^2.2.2"
|
||||||
"yaml": "^2.2.2",
|
|
||||||
"yargs": "^17.7.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/base-64": "^1.0.0",
|
|
||||||
"@types/jest": "^27.4.1",
|
|
||||||
"@types/node": "^17.0.23",
|
"@types/node": "^17.0.23",
|
||||||
"@types/semver": "^7.3.9",
|
"@types/semver": "^7.3.9",
|
||||||
"@types/uuid": "^9.0.0",
|
"@typescript/native-preview": "^7.0.0-dev.20260505.1",
|
||||||
"@types/yargs": "^17.0.35",
|
|
||||||
"@typescript-eslint/parser": "4.8.1",
|
|
||||||
"@vercel/ncc": "^0.36.1",
|
"@vercel/ncc": "^0.36.1",
|
||||||
|
"@vitest/coverage-istanbul": "^4.1.5",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^7.23.0",
|
"eslint": "^10.3.0",
|
||||||
"eslint-config-prettier": "8.1.0",
|
"eslint-plugin-unicorn": "^64.0.0",
|
||||||
"eslint-plugin-github": "^4.1.1",
|
"husky": "9",
|
||||||
"eslint-plugin-jest": "24.1.3",
|
|
||||||
"eslint-plugin-prettier": "^3.3.1",
|
|
||||||
"eslint-plugin-unicorn": "28.0.2",
|
|
||||||
"jest": "^27.5.1",
|
|
||||||
"jest-circus": "^27.5.1",
|
|
||||||
"jest-fail-on-console": "^3.0.2",
|
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lefthook": "^1.6.1",
|
"lint-staged": "^16.4.0",
|
||||||
"node-fetch": "2",
|
"node-fetch": "2",
|
||||||
"pkg": "^5.8.1",
|
"oxfmt": "^0.48.0",
|
||||||
"prettier": "^2.5.1",
|
"oxlint": "^1.63.0",
|
||||||
"ts-jest": "^27.1.3",
|
|
||||||
"ts-node": "10.8.1",
|
"ts-node": "10.8.1",
|
||||||
"typescript": "4.7.4",
|
"typescript": "4.7.4",
|
||||||
|
"vite": "^7",
|
||||||
|
"vitest": "^4",
|
||||||
"yarn-audit-fix": "^9.3.8"
|
"yarn-audit-fix": "^9.3.8"
|
||||||
},
|
},
|
||||||
"volta": {
|
"packageManager": "yarn@4.14.1",
|
||||||
"node": "20.5.1",
|
"dependenciesMeta": {
|
||||||
"yarn": "1.22.19"
|
"lefthook": {
|
||||||
|
"built": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
55
scripts/ensure-husky.mjs
Executable file
55
scripts/ensure-husky.mjs
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Self-heals husky git hooks before local dev workflows.
|
||||||
|
//
|
||||||
|
// Why this exists: Yarn 4 skips lifecycle scripts (`prepare`, `postinstall`) on
|
||||||
|
// no-op installs, so `yarn install --immutable` does NOT reinstall hooks once
|
||||||
|
// `.husky/_/` has been wiped. `.husky/_/` is gitignored, so it is also missing
|
||||||
|
// in fresh worktrees and after `git clean -fdx`. Without this guard, commits
|
||||||
|
// silently skip the pre-commit hook (git treats a missing hook file as "no hook").
|
||||||
|
//
|
||||||
|
// Behaviour: ~20 ms no-op when hooks are already installed. Skipped in CI and
|
||||||
|
// when HUSKY=0. Fails loudly (non-zero exit) on real install errors so the
|
||||||
|
// caller stops before commits are made without hooks.
|
||||||
|
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
|
||||||
|
if (process.env.CI || process.env.HUSKY === '0') process.exit(0);
|
||||||
|
|
||||||
|
const expectedHooksPath = '.husky/_';
|
||||||
|
const sentinelHook = '.husky/_/pre-commit';
|
||||||
|
// husky 9.1+ ships bin.js; husky 9.0 ships bin.mjs. Try both.
|
||||||
|
const huskyBin = ['node_modules/husky/bin.js', 'node_modules/husky/bin.mjs'].find(existsSync);
|
||||||
|
|
||||||
|
let configuredHooksPath = '';
|
||||||
|
try {
|
||||||
|
configuredHooksPath = execSync('git config --get core.hooksPath', {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['ignore', 'pipe', 'ignore'],
|
||||||
|
}).trim();
|
||||||
|
} catch {
|
||||||
|
// not a git repo or config unset — fall through and try to install
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuredHooksPath === expectedHooksPath && existsSync(sentinelHook)) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!huskyBin) {
|
||||||
|
// husky not installed yet (yarn install hasn't run) — silent no-op
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('· installing git hooks (husky self-heal)…');
|
||||||
|
try {
|
||||||
|
execSync(`node ${huskyBin}`, { stdio: 'inherit' });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(
|
||||||
|
`\n❌ husky install failed: ${message}\n\n` +
|
||||||
|
` git pre-commit hooks are NOT installed; commits will skip lint/format/tests.\n` +
|
||||||
|
` Fix the underlying error above, then run \`yarn setup:hooks\` to retry.\n` +
|
||||||
|
` To bypass this guard temporarily (NOT recommended): HUSKY=0 yarn <cmd>.\n`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
42
src/cli.ts
42
src/cli.ts
@@ -1,42 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import yargs from 'yargs';
|
|
||||||
// eslint-disable-next-line import/no-unresolved
|
|
||||||
import { hideBin } from 'yargs/helpers';
|
|
||||||
import buildCommand from './cli/commands/build';
|
|
||||||
import testCommand from './cli/commands/test';
|
|
||||||
import orchestrateCommand from './cli/commands/orchestrate';
|
|
||||||
import activateCommand from './cli/commands/activate';
|
|
||||||
import statusCommand from './cli/commands/status';
|
|
||||||
import versionCommand from './cli/commands/version';
|
|
||||||
import updateCommand from './cli/commands/update';
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
|
|
||||||
const cli = yargs(hideBin(process.argv))
|
|
||||||
.scriptName('game-ci')
|
|
||||||
.usage('$0 <command> [options]')
|
|
||||||
.command(buildCommand)
|
|
||||||
.command(testCommand)
|
|
||||||
.command(orchestrateCommand)
|
|
||||||
.command(activateCommand)
|
|
||||||
.command(statusCommand)
|
|
||||||
.command(versionCommand)
|
|
||||||
.command(updateCommand)
|
|
||||||
.demandCommand(1, 'You must specify a command. Run game-ci --help for available commands.')
|
|
||||||
.strict()
|
|
||||||
.alias('h', 'help')
|
|
||||||
.epilogue('For more information, visit https://game.ci')
|
|
||||||
.wrap(Math.min(120, process.stdout.columns || 80));
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
await cli.parse();
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.name !== 'YError') {
|
|
||||||
core.error(`Error: ${error.message}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import { execFile } from 'node:child_process';
|
|
||||||
import path from 'node:path';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Integration tests that spawn the CLI as a child process and verify
|
|
||||||
* exit codes and output. Uses node with --require ts-node/register to
|
|
||||||
* run the TypeScript entry point directly so no build step is required.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const CLI_ENTRY = path.resolve(__dirname, '..', '..', 'cli.ts');
|
|
||||||
|
|
||||||
function runCli(cliArguments: string[]): Promise<{ code: number | null; stdout: string; stderr: string }> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
execFile(
|
|
||||||
process.execPath,
|
|
||||||
['--require', 'ts-node/register/transpile-only', CLI_ENTRY, ...cliArguments],
|
|
||||||
{ timeout: 30_000, cwd: path.resolve(__dirname, '..', '..', '..') },
|
|
||||||
(error, stdout, stderr) => {
|
|
||||||
resolve({
|
|
||||||
code: error ? error.code ?? 1 : 0,
|
|
||||||
stdout: stdout.toString(),
|
|
||||||
stderr: stderr.toString(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Integration tests spawn child processes which need more time than the default 5s
|
|
||||||
jest.setTimeout(30_000);
|
|
||||||
|
|
||||||
describe('CLI integration', () => {
|
|
||||||
it('exits 0 and shows all commands for --help', async () => {
|
|
||||||
const result = await runCli(['--help']);
|
|
||||||
|
|
||||||
expect(result.code).toStrictEqual(0);
|
|
||||||
expect(result.stdout).toContain('game-ci');
|
|
||||||
expect(result.stdout).toContain('build');
|
|
||||||
expect(result.stdout).toContain('test');
|
|
||||||
expect(result.stdout).toContain('orchestrate');
|
|
||||||
expect(result.stdout).toContain('activate');
|
|
||||||
expect(result.stdout).toContain('status');
|
|
||||||
expect(result.stdout).toContain('version');
|
|
||||||
expect(result.stdout).toContain('update');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exits 0 and shows version info for version command', async () => {
|
|
||||||
const result = await runCli(['version']);
|
|
||||||
|
|
||||||
expect(result.code).toStrictEqual(0);
|
|
||||||
expect(result.stdout).toContain('unity-builder');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exits 0 and shows build flags for build --help', async () => {
|
|
||||||
const result = await runCli(['build', '--help']);
|
|
||||||
|
|
||||||
expect(result.code).toStrictEqual(0);
|
|
||||||
expect(result.stdout).toContain('--target-platform');
|
|
||||||
expect(result.stdout).toContain('--unity-version');
|
|
||||||
expect(result.stdout).toContain('--project-path');
|
|
||||||
expect(result.stdout).toContain('--build-name');
|
|
||||||
expect(result.stdout).toContain('--builds-path');
|
|
||||||
expect(result.stdout).toContain('--build-method');
|
|
||||||
expect(result.stdout).toContain('--custom-parameters');
|
|
||||||
expect(result.stdout).toContain('--versioning');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exits 0 and shows test flags for test --help', async () => {
|
|
||||||
const result = await runCli(['test', '--help']);
|
|
||||||
|
|
||||||
expect(result.code).toStrictEqual(0);
|
|
||||||
expect(result.stdout).toContain('--target-platform');
|
|
||||||
expect(result.stdout).toContain('--test-mode');
|
|
||||||
expect(result.stdout).toContain('--test-results-path');
|
|
||||||
expect(result.stdout).toContain('--enable-code-coverage');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exits 0 for test alias t --help', async () => {
|
|
||||||
const result = await runCli(['t', '--help']);
|
|
||||||
|
|
||||||
expect(result.code).toStrictEqual(0);
|
|
||||||
expect(result.stdout).toContain('--test-mode');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exits non-zero for an unknown command', async () => {
|
|
||||||
const result = await runCli(['nonexistent']);
|
|
||||||
|
|
||||||
expect(result.code).not.toStrictEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exits non-zero when no command is provided', async () => {
|
|
||||||
const result = await runCli([]);
|
|
||||||
|
|
||||||
expect(result.code).not.toStrictEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exits 0 for orchestrate --help', async () => {
|
|
||||||
const result = await runCli(['orchestrate', '--help']);
|
|
||||||
|
|
||||||
expect(result.code).toStrictEqual(0);
|
|
||||||
expect(result.stdout).toContain('--target-platform');
|
|
||||||
expect(result.stdout).toContain('--provider-strategy');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exits 0 for orchestrate alias o --help', async () => {
|
|
||||||
const result = await runCli(['o', '--help']);
|
|
||||||
|
|
||||||
expect(result.code).toStrictEqual(0);
|
|
||||||
expect(result.stdout).toContain('--provider-strategy');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exits 0 for activate --help', async () => {
|
|
||||||
const result = await runCli(['activate', '--help']);
|
|
||||||
|
|
||||||
expect(result.code).toStrictEqual(0);
|
|
||||||
expect(result.stdout).toContain('activate');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exits 0 for status --help', async () => {
|
|
||||||
const result = await runCli(['status', '--help']);
|
|
||||||
|
|
||||||
expect(result.code).toStrictEqual(0);
|
|
||||||
expect(result.stdout).toContain('status');
|
|
||||||
expect(result.stdout).toContain('--cache-dir');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exits 0 for update --help', async () => {
|
|
||||||
const result = await runCli(['update', '--help']);
|
|
||||||
|
|
||||||
expect(result.code).toStrictEqual(0);
|
|
||||||
expect(result.stdout).toContain('update');
|
|
||||||
expect(result.stdout).toContain('--force');
|
|
||||||
expect(result.stdout).toContain('--version');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
import buildCommand from '../commands/build';
|
|
||||||
import testCommand from '../commands/test';
|
|
||||||
import activateCommand from '../commands/activate';
|
|
||||||
import orchestrateCommand from '../commands/orchestrate';
|
|
||||||
import statusCommand from '../commands/status';
|
|
||||||
import versionCommand from '../commands/version';
|
|
||||||
import updateCommand from '../commands/update';
|
|
||||||
|
|
||||||
function createFakeYargs(): { yargs: any; options: Record<string, any> } {
|
|
||||||
const options: Record<string, any> = {};
|
|
||||||
const yargs: any = {
|
|
||||||
option: jest.fn(),
|
|
||||||
positional: jest.fn(),
|
|
||||||
example: jest.fn(),
|
|
||||||
env: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
yargs.option.mockImplementation((name: string, config: any) => {
|
|
||||||
options[name] = config;
|
|
||||||
|
|
||||||
return yargs;
|
|
||||||
});
|
|
||||||
yargs.positional.mockImplementation((name: string, config: any) => {
|
|
||||||
options[name] = config;
|
|
||||||
|
|
||||||
return yargs;
|
|
||||||
});
|
|
||||||
yargs.example.mockReturnValue(yargs);
|
|
||||||
yargs.env.mockReturnValue(yargs);
|
|
||||||
|
|
||||||
return { yargs, options };
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('CLI commands', () => {
|
|
||||||
describe('build command', () => {
|
|
||||||
it('exports the correct command name', () => {
|
|
||||||
expect(buildCommand.command).toStrictEqual('build');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a description', () => {
|
|
||||||
expect(buildCommand.describe).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a builder function', () => {
|
|
||||||
expect(typeof buildCommand.builder).toStrictEqual('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a handler function', () => {
|
|
||||||
expect(typeof buildCommand.handler).toStrictEqual('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('defines all expected build flags via builder', () => {
|
|
||||||
const { yargs, options } = createFakeYargs();
|
|
||||||
|
|
||||||
(buildCommand.builder as Function)(yargs);
|
|
||||||
|
|
||||||
// Core build flags (from shared + build-specific)
|
|
||||||
expect(options['target-platform']).toBeDefined();
|
|
||||||
expect(options['target-platform'].demandOption).toStrictEqual(true);
|
|
||||||
expect(options['unity-version']).toBeDefined();
|
|
||||||
expect(options['project-path']).toBeDefined();
|
|
||||||
expect(options['build-profile']).toBeDefined();
|
|
||||||
expect(options['build-name']).toBeDefined();
|
|
||||||
expect(options['builds-path']).toBeDefined();
|
|
||||||
expect(options['build-method']).toBeDefined();
|
|
||||||
expect(options['custom-parameters']).toBeDefined();
|
|
||||||
expect(options['versioning']).toBeDefined();
|
|
||||||
expect(options['version']).toBeDefined();
|
|
||||||
expect(options['custom-image']).toBeDefined();
|
|
||||||
expect(options['manual-exit']).toBeDefined();
|
|
||||||
expect(options['enable-gpu']).toBeDefined();
|
|
||||||
|
|
||||||
// Android flags
|
|
||||||
expect(options['android-version-code']).toBeDefined();
|
|
||||||
expect(options['android-export-type']).toBeDefined();
|
|
||||||
expect(options['android-keystore-name']).toBeDefined();
|
|
||||||
expect(options['android-keystore-base64']).toBeDefined();
|
|
||||||
expect(options['android-keystore-pass']).toBeDefined();
|
|
||||||
expect(options['android-keyalias-name']).toBeDefined();
|
|
||||||
expect(options['android-keyalias-pass']).toBeDefined();
|
|
||||||
expect(options['android-target-sdk-version']).toBeDefined();
|
|
||||||
expect(options['android-symbol-type']).toBeDefined();
|
|
||||||
|
|
||||||
// Docker flags
|
|
||||||
expect(options['docker-cpu-limit']).toBeDefined();
|
|
||||||
expect(options['docker-memory-limit']).toBeDefined();
|
|
||||||
expect(options['docker-workspace-path']).toBeDefined();
|
|
||||||
expect(options['run-as-host-user']).toBeDefined();
|
|
||||||
expect(options['chown-files-to']).toBeDefined();
|
|
||||||
|
|
||||||
// Build should NOT have orchestrator-specific flags
|
|
||||||
expect(options['provider-strategy']).toBeUndefined();
|
|
||||||
expect(options['aws-stack-name']).toBeUndefined();
|
|
||||||
expect(options['kube-config']).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets correct default values', () => {
|
|
||||||
const { yargs, options } = createFakeYargs();
|
|
||||||
|
|
||||||
(buildCommand.builder as Function)(yargs);
|
|
||||||
|
|
||||||
expect(options['unity-version'].default).toStrictEqual('auto');
|
|
||||||
expect(options['project-path'].default).toStrictEqual('.');
|
|
||||||
expect(options['builds-path'].default).toStrictEqual('build');
|
|
||||||
expect(options['versioning'].default).toStrictEqual('Semantic');
|
|
||||||
expect(options['manual-exit'].default).toStrictEqual(false);
|
|
||||||
expect(options['enable-gpu'].default).toStrictEqual(false);
|
|
||||||
expect(options['android-export-type'].default).toStrictEqual('androidPackage');
|
|
||||||
expect(options['android-symbol-type'].default).toStrictEqual('none');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('provides camelCase aliases for kebab-case options', () => {
|
|
||||||
const { yargs, options } = createFakeYargs();
|
|
||||||
|
|
||||||
(buildCommand.builder as Function)(yargs);
|
|
||||||
|
|
||||||
expect(options['target-platform'].alias).toStrictEqual('targetPlatform');
|
|
||||||
expect(options['unity-version'].alias).toStrictEqual('unityVersion');
|
|
||||||
expect(options['project-path'].alias).toStrictEqual('projectPath');
|
|
||||||
expect(options['build-name'].alias).toStrictEqual('buildName');
|
|
||||||
expect(options['builds-path'].alias).toStrictEqual('buildsPath');
|
|
||||||
expect(options['build-method'].alias).toStrictEqual('buildMethod');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('test command', () => {
|
|
||||||
it('exports command with alias', () => {
|
|
||||||
expect(testCommand.command).toStrictEqual(['test', 't']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a description', () => {
|
|
||||||
expect(testCommand.describe).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a builder function', () => {
|
|
||||||
expect(typeof testCommand.builder).toStrictEqual('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a handler function', () => {
|
|
||||||
expect(typeof testCommand.handler).toStrictEqual('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('defines test-specific flags', () => {
|
|
||||||
const { yargs, options } = createFakeYargs();
|
|
||||||
|
|
||||||
(testCommand.builder as Function)(yargs);
|
|
||||||
|
|
||||||
expect(options['test-mode']).toBeDefined();
|
|
||||||
expect(options['test-mode'].default).toStrictEqual('All');
|
|
||||||
expect(options['test-mode'].choices).toStrictEqual(['EditMode', 'PlayMode', 'All']);
|
|
||||||
expect(options['test-results-path']).toBeDefined();
|
|
||||||
expect(options['test-category']).toBeDefined();
|
|
||||||
expect(options['test-filter']).toBeDefined();
|
|
||||||
expect(options['enable-code-coverage']).toBeDefined();
|
|
||||||
expect(options['coverage-options']).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('includes shared project options', () => {
|
|
||||||
const { yargs, options } = createFakeYargs();
|
|
||||||
|
|
||||||
(testCommand.builder as Function)(yargs);
|
|
||||||
|
|
||||||
expect(options['target-platform']).toBeDefined();
|
|
||||||
expect(options['target-platform'].demandOption).toStrictEqual(true);
|
|
||||||
expect(options['unity-version']).toBeDefined();
|
|
||||||
expect(options['project-path']).toBeDefined();
|
|
||||||
expect(options['custom-image']).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('includes docker options but not orchestrator options', () => {
|
|
||||||
const { yargs, options } = createFakeYargs();
|
|
||||||
|
|
||||||
(testCommand.builder as Function)(yargs);
|
|
||||||
|
|
||||||
expect(options['docker-cpu-limit']).toBeDefined();
|
|
||||||
expect(options['docker-memory-limit']).toBeDefined();
|
|
||||||
expect(options['provider-strategy']).toBeUndefined();
|
|
||||||
expect(options['aws-stack-name']).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('activate command', () => {
|
|
||||||
it('exports the correct command name', () => {
|
|
||||||
expect(activateCommand.command).toStrictEqual('activate');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a description', () => {
|
|
||||||
expect(activateCommand.describe).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a builder function', () => {
|
|
||||||
expect(typeof activateCommand.builder).toStrictEqual('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a handler function', () => {
|
|
||||||
expect(typeof activateCommand.handler).toStrictEqual('function');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('orchestrate command', () => {
|
|
||||||
it('exports command with alias', () => {
|
|
||||||
expect(orchestrateCommand.command).toStrictEqual(['orchestrate', 'o']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a description', () => {
|
|
||||||
expect(orchestrateCommand.describe).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a builder function', () => {
|
|
||||||
expect(typeof orchestrateCommand.builder).toStrictEqual('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a handler function', () => {
|
|
||||||
expect(typeof orchestrateCommand.handler).toStrictEqual('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('defines key orchestrator flags', () => {
|
|
||||||
const { yargs, options } = createFakeYargs();
|
|
||||||
|
|
||||||
(orchestrateCommand.builder as Function)(yargs);
|
|
||||||
|
|
||||||
expect(options['target-platform']).toBeDefined();
|
|
||||||
expect(options['target-platform'].demandOption).toStrictEqual(true);
|
|
||||||
expect(options['provider-strategy']).toBeDefined();
|
|
||||||
expect(options['provider-strategy'].default).toStrictEqual('aws');
|
|
||||||
expect(options['aws-stack-name']).toBeDefined();
|
|
||||||
expect(options['kube-config']).toBeDefined();
|
|
||||||
expect(options['kube-volume']).toBeDefined();
|
|
||||||
expect(options['cache-key']).toBeDefined();
|
|
||||||
expect(options['watch-to-end']).toBeDefined();
|
|
||||||
expect(options['clone-depth']).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not include build-only options', () => {
|
|
||||||
const { yargs, options } = createFakeYargs();
|
|
||||||
|
|
||||||
(orchestrateCommand.builder as Function)(yargs);
|
|
||||||
|
|
||||||
expect(options['build-profile']).toBeUndefined();
|
|
||||||
expect(options['manual-exit']).toBeUndefined();
|
|
||||||
expect(options['enable-gpu']).toBeUndefined();
|
|
||||||
expect(options['android-version-code']).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('status command', () => {
|
|
||||||
it('exports the correct command name', () => {
|
|
||||||
expect(statusCommand.command).toStrictEqual('status');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a description', () => {
|
|
||||||
expect(statusCommand.describe).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a handler function', () => {
|
|
||||||
expect(typeof statusCommand.handler).toStrictEqual('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('includes cache-dir option', () => {
|
|
||||||
const { yargs, options } = createFakeYargs();
|
|
||||||
|
|
||||||
(statusCommand.builder as Function)(yargs);
|
|
||||||
|
|
||||||
expect(options['cache-dir']).toBeDefined();
|
|
||||||
expect(options['project-path']).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('version command', () => {
|
|
||||||
it('exports the correct command name', () => {
|
|
||||||
expect(versionCommand.command).toStrictEqual('version');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a description', () => {
|
|
||||||
expect(versionCommand.describe).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a handler function', () => {
|
|
||||||
expect(typeof versionCommand.handler).toStrictEqual('function');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('update command', () => {
|
|
||||||
it('exports the correct command name', () => {
|
|
||||||
expect(updateCommand.command).toStrictEqual('update');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a description', () => {
|
|
||||||
expect(updateCommand.describe).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a builder function', () => {
|
|
||||||
expect(typeof updateCommand.builder).toStrictEqual('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a handler function', () => {
|
|
||||||
expect(typeof updateCommand.handler).toStrictEqual('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('defines force and version flags', () => {
|
|
||||||
const { yargs, options } = createFakeYargs();
|
|
||||||
|
|
||||||
(updateCommand.builder as Function)(yargs);
|
|
||||||
|
|
||||||
expect(options['force']).toBeDefined();
|
|
||||||
expect(options['force'].type).toStrictEqual('boolean');
|
|
||||||
expect(options['force'].default).toStrictEqual(false);
|
|
||||||
expect(options['version']).toBeDefined();
|
|
||||||
expect(options['version'].type).toStrictEqual('string');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
import { mapCliArgumentsToInput, CliArguments } from '../input-mapper';
|
|
||||||
import { Cli } from '../../model/cli/cli';
|
|
||||||
import GitHub from '../../model/github';
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
Cli.options = undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('mapCliArgumentsToInput', () => {
|
|
||||||
describe('basic mapping', () => {
|
|
||||||
it('populates Cli.options from CLI arguments', () => {
|
|
||||||
const cliArguments: CliArguments = {
|
|
||||||
targetPlatform: 'StandaloneLinux64',
|
|
||||||
unityVersion: '2022.3.56f1',
|
|
||||||
projectPath: './my-project',
|
|
||||||
};
|
|
||||||
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
expect(Cli.options).toBeDefined();
|
|
||||||
expect(Cli.options!['targetPlatform']).toStrictEqual('StandaloneLinux64');
|
|
||||||
expect(Cli.options!['unityVersion']).toStrictEqual('2022.3.56f1');
|
|
||||||
expect(Cli.options!['projectPath']).toStrictEqual('./my-project');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('disables GitHub Actions input reading', () => {
|
|
||||||
const cliArguments: CliArguments = { targetPlatform: 'WebGL' };
|
|
||||||
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
expect(GitHub.githubInputEnabled).toStrictEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets mode to cli by default when not provided', () => {
|
|
||||||
const cliArguments: CliArguments = { targetPlatform: 'Android' };
|
|
||||||
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
expect(Cli.options!['mode']).toStrictEqual('cli');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves an explicitly provided mode', () => {
|
|
||||||
const cliArguments: CliArguments = { targetPlatform: 'Android', mode: 'custom-mode' };
|
|
||||||
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
expect(Cli.options!['mode']).toStrictEqual('custom-mode');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('default values', () => {
|
|
||||||
it('omits undefined values from Cli.options', () => {
|
|
||||||
const cliArguments: CliArguments = {
|
|
||||||
targetPlatform: 'StandaloneLinux64',
|
|
||||||
unityVersion: undefined,
|
|
||||||
buildName: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
expect(Cli.options!['targetPlatform']).toStrictEqual('StandaloneLinux64');
|
|
||||||
expect(Cli.options!).not.toHaveProperty('unityVersion');
|
|
||||||
expect(Cli.options!).not.toHaveProperty('buildName');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('boolean conversion', () => {
|
|
||||||
it('converts boolean true to string "true"', () => {
|
|
||||||
const cliArguments: CliArguments = { manualExit: true };
|
|
||||||
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
expect(Cli.options!['manualExit']).toStrictEqual('true');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('converts boolean false to string "false"', () => {
|
|
||||||
const cliArguments: CliArguments = { enableGpu: false };
|
|
||||||
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
expect(Cli.options!['enableGpu']).toStrictEqual('false');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('converts allowDirtyBuild boolean to string', () => {
|
|
||||||
const cliArguments: CliArguments = { allowDirtyBuild: true };
|
|
||||||
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
expect(Cli.options!['allowDirtyBuild']).toStrictEqual('true');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('yargs internal properties', () => {
|
|
||||||
it('filters out yargs _ property', () => {
|
|
||||||
const cliArguments: CliArguments = {
|
|
||||||
targetPlatform: 'iOS',
|
|
||||||
_: ['build'] as any,
|
|
||||||
};
|
|
||||||
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
expect(Cli.options!).not.toHaveProperty('_');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('filters out yargs $0 property', () => {
|
|
||||||
const cliArguments: CliArguments = {
|
|
||||||
targetPlatform: 'iOS',
|
|
||||||
$0: 'game-ci' as any,
|
|
||||||
};
|
|
||||||
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
expect(Cli.options!).not.toHaveProperty('$0');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('flag name conversion', () => {
|
|
||||||
it('passes camelCase keys through directly', () => {
|
|
||||||
const cliArguments: CliArguments = {
|
|
||||||
androidKeystoreName: 'my.keystore',
|
|
||||||
androidKeystorePass: 'secret',
|
|
||||||
dockerCpuLimit: '4',
|
|
||||||
dockerMemoryLimit: '8g',
|
|
||||||
};
|
|
||||||
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
expect(Cli.options!['androidKeystoreName']).toStrictEqual('my.keystore');
|
|
||||||
expect(Cli.options!['androidKeystorePass']).toStrictEqual('secret');
|
|
||||||
expect(Cli.options!['dockerCpuLimit']).toStrictEqual('4');
|
|
||||||
expect(Cli.options!['dockerMemoryLimit']).toStrictEqual('8g');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('maps all android-related arguments', () => {
|
|
||||||
const cliArguments: CliArguments = {
|
|
||||||
androidVersionCode: '42',
|
|
||||||
androidExportType: 'androidAppBundle',
|
|
||||||
androidKeystoreBase64: 'base64data',
|
|
||||||
androidKeyaliasName: 'myalias',
|
|
||||||
androidKeyaliasPass: 'aliaspass',
|
|
||||||
androidTargetSdkVersion: '33',
|
|
||||||
androidSymbolType: 'public',
|
|
||||||
};
|
|
||||||
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
expect(Cli.options!['androidVersionCode']).toStrictEqual('42');
|
|
||||||
expect(Cli.options!['androidExportType']).toStrictEqual('androidAppBundle');
|
|
||||||
expect(Cli.options!['androidKeystoreBase64']).toStrictEqual('base64data');
|
|
||||||
expect(Cli.options!['androidKeyaliasName']).toStrictEqual('myalias');
|
|
||||||
expect(Cli.options!['androidKeyaliasPass']).toStrictEqual('aliaspass');
|
|
||||||
expect(Cli.options!['androidTargetSdkVersion']).toStrictEqual('33');
|
|
||||||
expect(Cli.options!['androidSymbolType']).toStrictEqual('public');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('maps docker and container arguments', () => {
|
|
||||||
const cliArguments: CliArguments = {
|
|
||||||
dockerIsolationMode: 'hyperv',
|
|
||||||
dockerWorkspacePath: '/custom/workspace',
|
|
||||||
containerRegistryRepository: 'custom/editor',
|
|
||||||
containerRegistryImageVersion: '5',
|
|
||||||
runAsHostUser: 'true',
|
|
||||||
chownFilesTo: 'root:root',
|
|
||||||
};
|
|
||||||
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
expect(Cli.options!['dockerIsolationMode']).toStrictEqual('hyperv');
|
|
||||||
expect(Cli.options!['dockerWorkspacePath']).toStrictEqual('/custom/workspace');
|
|
||||||
expect(Cli.options!['containerRegistryRepository']).toStrictEqual('custom/editor');
|
|
||||||
expect(Cli.options!['containerRegistryImageVersion']).toStrictEqual('5');
|
|
||||||
expect(Cli.options!['runAsHostUser']).toStrictEqual('true');
|
|
||||||
expect(Cli.options!['chownFilesTo']).toStrictEqual('root:root');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('maps orchestrator-related arguments', () => {
|
|
||||||
const cliArguments: CliArguments = {
|
|
||||||
providerStrategy: 'k8s',
|
|
||||||
awsStackName: 'my-stack',
|
|
||||||
kubeConfig: 'base64config',
|
|
||||||
kubeVolume: 'my-pvc',
|
|
||||||
kubeVolumeSize: '10Gi',
|
|
||||||
kubeStorageClass: 'gp3',
|
|
||||||
containerCpu: '2048',
|
|
||||||
containerMemory: '4096',
|
|
||||||
cacheKey: 'my-cache',
|
|
||||||
watchToEnd: 'false',
|
|
||||||
cloneDepth: '100',
|
|
||||||
};
|
|
||||||
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
expect(Cli.options!['providerStrategy']).toStrictEqual('k8s');
|
|
||||||
expect(Cli.options!['awsStackName']).toStrictEqual('my-stack');
|
|
||||||
expect(Cli.options!['kubeConfig']).toStrictEqual('base64config');
|
|
||||||
expect(Cli.options!['kubeVolume']).toStrictEqual('my-pvc');
|
|
||||||
expect(Cli.options!['kubeVolumeSize']).toStrictEqual('10Gi');
|
|
||||||
expect(Cli.options!['kubeStorageClass']).toStrictEqual('gp3');
|
|
||||||
expect(Cli.options!['containerCpu']).toStrictEqual('2048');
|
|
||||||
expect(Cli.options!['containerMemory']).toStrictEqual('4096');
|
|
||||||
expect(Cli.options!['cacheKey']).toStrictEqual('my-cache');
|
|
||||||
expect(Cli.options!['watchToEnd']).toStrictEqual('false');
|
|
||||||
expect(Cli.options!['cloneDepth']).toStrictEqual('100');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Cli.isCliMode integration', () => {
|
|
||||||
it('enables CLI mode after mapping', () => {
|
|
||||||
const cliArguments: CliArguments = { targetPlatform: 'WebGL' };
|
|
||||||
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
expect(Cli.isCliMode).toStrictEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is not in CLI mode before mapping', () => {
|
|
||||||
expect(Cli.isCliMode).toStrictEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import type { CommandModule } from 'yargs';
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
import { mapCliArgumentsToInput, CliArguments } from '../input-mapper';
|
|
||||||
|
|
||||||
interface ActivateArguments extends CliArguments {
|
|
||||||
unityVersion?: string;
|
|
||||||
unitySerial?: string;
|
|
||||||
unityLicensingServer?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activateCommand: CommandModule<object, ActivateArguments> = {
|
|
||||||
command: 'activate',
|
|
||||||
describe: 'Verify Unity license configuration',
|
|
||||||
builder: (yargs) => {
|
|
||||||
return yargs
|
|
||||||
.option('unity-version', {
|
|
||||||
alias: 'unityVersion',
|
|
||||||
type: 'string',
|
|
||||||
description: 'Version of Unity to activate',
|
|
||||||
default: 'auto',
|
|
||||||
})
|
|
||||||
.option('unity-licensing-server', {
|
|
||||||
alias: 'unityLicensingServer',
|
|
||||||
type: 'string',
|
|
||||||
description: 'The Unity licensing server address for floating licenses',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.env('UNITY')
|
|
||||||
.example(
|
|
||||||
'UNITY_SERIAL=XXXX-XXXX-XXXX-XXXX game-ci activate',
|
|
||||||
'Activate Unity using a serial from environment variable',
|
|
||||||
)
|
|
||||||
.example(
|
|
||||||
'game-ci activate --unity-licensing-server http://license-server:8080',
|
|
||||||
'Activate Unity using a floating license server',
|
|
||||||
) as any;
|
|
||||||
},
|
|
||||||
handler: async (cliArguments) => {
|
|
||||||
try {
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
const unitySerial = process.env.UNITY_SERIAL;
|
|
||||||
const unityLicense = process.env.UNITY_LICENSE;
|
|
||||||
const licensingServer = cliArguments.unityLicensingServer || process.env.UNITY_LICENSING_SERVER || '';
|
|
||||||
|
|
||||||
if (licensingServer) {
|
|
||||||
core.info(`Activating Unity via licensing server: ${licensingServer}`);
|
|
||||||
core.info('Floating license activation is handled automatically during builds.');
|
|
||||||
core.info('No manual activation step is needed when using a licensing server.');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!unitySerial && !unityLicense) {
|
|
||||||
throw new Error(
|
|
||||||
'No Unity license found.\n\n' +
|
|
||||||
'Provide one of the following:\n' +
|
|
||||||
' - UNITY_SERIAL environment variable (professional license)\n' +
|
|
||||||
' - UNITY_LICENSE environment variable (personal license file content)\n' +
|
|
||||||
' - --unity-licensing-server flag (floating license)\n\n' +
|
|
||||||
'For more information, visit: https://game.ci/docs/github/activation',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unitySerial) {
|
|
||||||
const maskedSerial = unitySerial.length > 8 ? `${unitySerial.slice(0, 4)}...${unitySerial.slice(-4)}` : '****';
|
|
||||||
core.info(`Unity serial detected: ${maskedSerial}`);
|
|
||||||
core.info('License will be activated automatically when running a build.');
|
|
||||||
} else if (unityLicense) {
|
|
||||||
core.info('Unity license file detected from UNITY_LICENSE environment variable.');
|
|
||||||
core.info('License will be activated automatically when running a build.');
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info('\nActivation verified. You can now run: game-ci build --target-platform <platform>');
|
|
||||||
} catch (error: any) {
|
|
||||||
core.setFailed(`Activation failed: ${error.message}`);
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default activateCommand;
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import type { CommandModule } from 'yargs';
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
import { BuildParameters, ImageTag } from '../../model';
|
|
||||||
import { mapCliArgumentsToInput, CliArguments } from '../input-mapper';
|
|
||||||
import MacBuilder from '../../model/mac-builder';
|
|
||||||
import Docker from '../../model/docker';
|
|
||||||
import Action from '../../model/action';
|
|
||||||
import PlatformSetup from '../../model/platform-setup';
|
|
||||||
import { withProjectOptions, withDockerOptions, withAndroidOptions } from './shared-options';
|
|
||||||
|
|
||||||
interface BuildArguments extends CliArguments {
|
|
||||||
targetPlatform: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildCommand: CommandModule<object, BuildArguments> = {
|
|
||||||
command: 'build',
|
|
||||||
describe: 'Build a Unity project locally via Docker or native runner',
|
|
||||||
builder: (yargs) => {
|
|
||||||
let y = withProjectOptions(yargs);
|
|
||||||
y = withAndroidOptions(y);
|
|
||||||
y = withDockerOptions(y);
|
|
||||||
|
|
||||||
return y
|
|
||||||
.option('build-profile', {
|
|
||||||
alias: 'buildProfile',
|
|
||||||
type: 'string',
|
|
||||||
description: 'Path to the build profile to activate, relative to the project root',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('versioning', {
|
|
||||||
type: 'string',
|
|
||||||
description: 'The versioning scheme to use when building the project',
|
|
||||||
default: 'Semantic',
|
|
||||||
})
|
|
||||||
.option('version', {
|
|
||||||
type: 'string',
|
|
||||||
description: 'The version, when used with the "Custom" versioning scheme',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('manual-exit', {
|
|
||||||
alias: 'manualExit',
|
|
||||||
type: 'boolean',
|
|
||||||
description: 'Suppresses -quit. Exit your build method using EditorApplication.Exit(0) instead.',
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
.option('enable-gpu', {
|
|
||||||
alias: 'enableGpu',
|
|
||||||
type: 'boolean',
|
|
||||||
description: 'Launches unity without specifying -nographics',
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
.option('cache-unity-installation-on-mac', {
|
|
||||||
alias: 'cacheUnityInstallationOnMac',
|
|
||||||
type: 'boolean',
|
|
||||||
description: 'Whether to cache the Unity hub and editor installation on MacOS',
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
.option('unity-hub-version-on-mac', {
|
|
||||||
alias: 'unityHubVersionOnMac',
|
|
||||||
type: 'string',
|
|
||||||
description: 'The version of Unity Hub to install on MacOS (e.g. 3.4.0). Defaults to latest available on brew.',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.example('game-ci build --target-platform StandaloneLinux64', 'Build for Linux using auto-detected Unity version')
|
|
||||||
.example(
|
|
||||||
'game-ci build --target-platform Android --unity-version 2022.3.56f1 --build-method MyBuild.Run',
|
|
||||||
'Build for Android with a specific Unity version and build method',
|
|
||||||
) as any;
|
|
||||||
},
|
|
||||||
handler: async (cliArguments) => {
|
|
||||||
try {
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
const buildParameters = await BuildParameters.create();
|
|
||||||
const baseImage = new ImageTag(buildParameters);
|
|
||||||
|
|
||||||
core.info(`Building locally for ${buildParameters.targetPlatform}...`);
|
|
||||||
core.info(`Unity version: ${buildParameters.editorVersion}`);
|
|
||||||
core.info(`Project path: ${buildParameters.projectPath}`);
|
|
||||||
|
|
||||||
const actionFolder = Action.actionFolder;
|
|
||||||
await PlatformSetup.setup(buildParameters, actionFolder);
|
|
||||||
|
|
||||||
const exitCode =
|
|
||||||
process.platform === 'darwin'
|
|
||||||
? await MacBuilder.run(actionFolder)
|
|
||||||
: await Docker.run(baseImage.toString(), {
|
|
||||||
workspace: process.cwd(),
|
|
||||||
actionFolder,
|
|
||||||
...buildParameters,
|
|
||||||
});
|
|
||||||
|
|
||||||
core.info(`\nBuild completed with exit code: ${exitCode}`);
|
|
||||||
core.info(`Build version: ${buildParameters.buildVersion}`);
|
|
||||||
core.info(`Build path: ${buildParameters.buildPath}`);
|
|
||||||
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
throw new Error(`Build failed with exit code ${exitCode}`);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
core.setFailed(`Build failed: ${error.message}`);
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default buildCommand;
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import type { CommandModule } from 'yargs';
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
import { BuildParameters, ImageTag, Orchestrator } from '../../model';
|
|
||||||
import { mapCliArgumentsToInput, CliArguments } from '../input-mapper';
|
|
||||||
import { withProjectOptions, withOrchestratorOptions } from './shared-options';
|
|
||||||
|
|
||||||
interface OrchestrateArguments extends CliArguments {
|
|
||||||
targetPlatform: string;
|
|
||||||
providerStrategy?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orchestrateCommand: CommandModule<object, OrchestrateArguments> = {
|
|
||||||
command: ['orchestrate', 'o'],
|
|
||||||
describe: 'Run a build via orchestrator providers (AWS, Kubernetes, etc.)',
|
|
||||||
builder: (yargs) => {
|
|
||||||
let y = withProjectOptions(yargs);
|
|
||||||
y = withOrchestratorOptions(y);
|
|
||||||
|
|
||||||
return y
|
|
||||||
.option('versioning', {
|
|
||||||
type: 'string',
|
|
||||||
description: 'The versioning scheme to use',
|
|
||||||
default: 'None',
|
|
||||||
})
|
|
||||||
.example(
|
|
||||||
'game-ci orchestrate --target-platform StandaloneLinux64 --provider-strategy aws',
|
|
||||||
'Build on AWS using the orchestrator',
|
|
||||||
)
|
|
||||||
.example(
|
|
||||||
'game-ci o --target-platform StandaloneLinux64 --provider-strategy k8s --kube-config <base64>',
|
|
||||||
'Build on Kubernetes (short alias)',
|
|
||||||
) as any;
|
|
||||||
},
|
|
||||||
handler: async (cliArguments) => {
|
|
||||||
try {
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
const buildParameters = await BuildParameters.create();
|
|
||||||
const baseImage = new ImageTag(buildParameters);
|
|
||||||
|
|
||||||
core.info(`Orchestrating build via ${buildParameters.providerStrategy}...`);
|
|
||||||
core.info(`Target platform: ${buildParameters.targetPlatform}`);
|
|
||||||
core.info(`Unity version: ${buildParameters.editorVersion}`);
|
|
||||||
core.info(`Build GUID: ${buildParameters.buildGuid}`);
|
|
||||||
|
|
||||||
const result = await Orchestrator.run(buildParameters, baseImage.toString());
|
|
||||||
|
|
||||||
core.info(`\nOrchestrated build completed.`);
|
|
||||||
if (result?.BuildResults) {
|
|
||||||
core.info(`Results: ${result.BuildResults}`);
|
|
||||||
} else {
|
|
||||||
core.warning('Build completed but no build results were returned.');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
core.setFailed(`Orchestrated build failed: ${error.message}`);
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default orchestrateCommand;
|
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
import type { Argv } from 'yargs';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared option groups for CLI commands. Avoids duplicating option
|
|
||||||
* definitions across build, test, and orchestrate commands.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function withProjectOptions<T>(yargs: Argv<T>) {
|
|
||||||
return yargs
|
|
||||||
.option('target-platform', {
|
|
||||||
alias: 'targetPlatform',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Platform that the build should target',
|
|
||||||
demandOption: true,
|
|
||||||
})
|
|
||||||
.option('unity-version', {
|
|
||||||
alias: 'unityVersion',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Version of Unity to use. Use "auto" to detect from ProjectVersion.txt.',
|
|
||||||
default: 'auto',
|
|
||||||
})
|
|
||||||
.option('project-path', {
|
|
||||||
alias: 'projectPath',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Path to the Unity project',
|
|
||||||
default: '.',
|
|
||||||
})
|
|
||||||
.option('build-name', {
|
|
||||||
alias: 'buildName',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Name of the build',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('builds-path', {
|
|
||||||
alias: 'buildsPath',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Path where the builds should be stored',
|
|
||||||
default: 'build',
|
|
||||||
})
|
|
||||||
.option('build-method', {
|
|
||||||
alias: 'buildMethod',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Path to a Namespace.Class.StaticMethod to run to perform the build',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('custom-parameters', {
|
|
||||||
alias: 'customParameters',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Custom parameters to configure the build',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('custom-image', {
|
|
||||||
alias: 'customImage',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Specific docker image that should be used for building the project',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('git-private-token', {
|
|
||||||
alias: 'gitPrivateToken',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'GitHub private token for repository access',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('skip-activation', {
|
|
||||||
alias: 'skipActivation',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Skip Unity activation/deactivation',
|
|
||||||
default: 'false',
|
|
||||||
})
|
|
||||||
.option('unity-licensing-server', {
|
|
||||||
alias: 'unityLicensingServer',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'The Unity licensing server address',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('container-registry-repository', {
|
|
||||||
alias: 'containerRegistryRepository',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Container registry and repository to pull image from. Only applicable if customImage is not set.',
|
|
||||||
default: 'unityci/editor',
|
|
||||||
})
|
|
||||||
.option('container-registry-image-version', {
|
|
||||||
alias: 'containerRegistryImageVersion',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Container registry image version. Only applicable if customImage is not set.',
|
|
||||||
default: '3',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withDockerOptions<T>(yargs: Argv<T>) {
|
|
||||||
return yargs
|
|
||||||
.option('docker-cpu-limit', {
|
|
||||||
alias: 'dockerCpuLimit',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Number of CPU cores to assign the docker container',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('docker-memory-limit', {
|
|
||||||
alias: 'dockerMemoryLimit',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Amount of memory to assign the docker container (e.g. 512m, 4g)',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('docker-workspace-path', {
|
|
||||||
alias: 'dockerWorkspacePath',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'The path to mount the workspace inside the docker container',
|
|
||||||
default: '/github/workspace',
|
|
||||||
})
|
|
||||||
.option('docker-isolation-mode', {
|
|
||||||
alias: 'dockerIsolationMode',
|
|
||||||
type: 'string' as const,
|
|
||||||
description:
|
|
||||||
'Isolation mode to use for the docker container (process, hyperv, or default). Only applicable on Windows.',
|
|
||||||
default: 'default',
|
|
||||||
})
|
|
||||||
.option('run-as-host-user', {
|
|
||||||
alias: 'runAsHostUser',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Whether to run as a user that matches the host system',
|
|
||||||
default: 'false',
|
|
||||||
})
|
|
||||||
.option('chown-files-to', {
|
|
||||||
alias: 'chownFilesTo',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'User and optionally group to give ownership of build artifacts',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('ssh-agent', {
|
|
||||||
alias: 'sshAgent',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'SSH Agent path to forward to the container',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('ssh-public-keys-directory-path', {
|
|
||||||
alias: 'sshPublicKeysDirectoryPath',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Path to a directory containing SSH public keys to forward to the container',
|
|
||||||
default: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withAndroidOptions<T>(yargs: Argv<T>) {
|
|
||||||
return yargs
|
|
||||||
.option('android-version-code', {
|
|
||||||
alias: 'androidVersionCode',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'The android versionCode',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('android-export-type', {
|
|
||||||
alias: 'androidExportType',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'The android export type (androidPackage, androidAppBundle, androidStudioProject)',
|
|
||||||
default: 'androidPackage',
|
|
||||||
})
|
|
||||||
.option('android-keystore-name', {
|
|
||||||
alias: 'androidKeystoreName',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'The android keystoreName',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('android-keystore-base64', {
|
|
||||||
alias: 'androidKeystoreBase64',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'The base64 contents of the android keystore file',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('android-keystore-pass', {
|
|
||||||
alias: 'androidKeystorePass',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'The android keystorePass',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('android-keyalias-name', {
|
|
||||||
alias: 'androidKeyaliasName',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'The android keyaliasName',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('android-keyalias-pass', {
|
|
||||||
alias: 'androidKeyaliasPass',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'The android keyaliasPass',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('android-target-sdk-version', {
|
|
||||||
alias: 'androidTargetSdkVersion',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'The android target API level',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('android-symbol-type', {
|
|
||||||
alias: 'androidSymbolType',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'The android symbol type to export (none, public, debugging)',
|
|
||||||
default: 'none',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withOrchestratorOptions<T>(yargs: Argv<T>) {
|
|
||||||
return yargs
|
|
||||||
.option('provider-strategy', {
|
|
||||||
alias: 'providerStrategy',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Orchestrator provider: aws, k8s, local-docker, local-system',
|
|
||||||
default: 'aws',
|
|
||||||
})
|
|
||||||
.option('aws-stack-name', {
|
|
||||||
alias: 'awsStackName',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'The Cloud Formation stack name (AWS provider)',
|
|
||||||
default: 'game-ci',
|
|
||||||
})
|
|
||||||
.option('kube-config', {
|
|
||||||
alias: 'kubeConfig',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Base64 encoded Kubernetes config (K8s provider)',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('kube-volume', {
|
|
||||||
alias: 'kubeVolume',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Persistent Volume Claim name for Unity build (K8s provider)',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('kube-volume-size', {
|
|
||||||
alias: 'kubeVolumeSize',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Disc space for Kubernetes Persistent Volume',
|
|
||||||
default: '5Gi',
|
|
||||||
})
|
|
||||||
.option('kube-storage-class', {
|
|
||||||
alias: 'kubeStorageClass',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Kubernetes storage class to use for orchestrator jobs. Leave empty to install rook cluster.',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('container-cpu', {
|
|
||||||
alias: 'containerCpu',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'CPU allocation for remote build container',
|
|
||||||
default: '1024',
|
|
||||||
})
|
|
||||||
.option('container-memory', {
|
|
||||||
alias: 'containerMemory',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Memory allocation for remote build container',
|
|
||||||
default: '3072',
|
|
||||||
})
|
|
||||||
.option('cache-key', {
|
|
||||||
alias: 'cacheKey',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Cache key to indicate bucket for cache',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('allow-dirty-build', {
|
|
||||||
alias: 'allowDirtyBuild',
|
|
||||||
type: 'boolean' as const,
|
|
||||||
description: 'Allow builds from dirty branches',
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
.option('watch-to-end', {
|
|
||||||
alias: 'watchToEnd',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Whether to watch the build to completion',
|
|
||||||
default: 'true',
|
|
||||||
})
|
|
||||||
.option('clone-depth', {
|
|
||||||
alias: 'cloneDepth',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Git clone depth (0 for full clone)',
|
|
||||||
default: '50',
|
|
||||||
})
|
|
||||||
.option('read-input-from-override-list', {
|
|
||||||
alias: 'readInputFromOverrideList',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Comma separated list of input value names to read from the input override command',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('read-input-override-command', {
|
|
||||||
alias: 'readInputOverrideCommand',
|
|
||||||
type: 'string' as const,
|
|
||||||
description: 'Command to execute to pull input from an external source (e.g. cloud provider secret managers)',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('post-build-steps', {
|
|
||||||
alias: 'postBuildSteps',
|
|
||||||
type: 'string' as const,
|
|
||||||
description:
|
|
||||||
'Post build job in yaml format with the keys image, secrets (name, value object array), command string',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('pre-build-steps', {
|
|
||||||
alias: 'preBuildSteps',
|
|
||||||
type: 'string' as const,
|
|
||||||
description:
|
|
||||||
'Pre build job after repository setup but before the build job (yaml format with keys image, secrets, command)',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('custom-job', {
|
|
||||||
alias: 'customJob',
|
|
||||||
type: 'string' as const,
|
|
||||||
description:
|
|
||||||
'Custom job instead of the standard build automation (yaml format with keys image, secrets, command)',
|
|
||||||
default: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import type { CommandModule } from 'yargs';
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import UnityVersioning from '../../model/unity-versioning';
|
|
||||||
|
|
||||||
const statusCommand: CommandModule = {
|
|
||||||
command: 'status',
|
|
||||||
describe: 'Show project info, environment, and cache status',
|
|
||||||
builder: (yargs) => {
|
|
||||||
return yargs
|
|
||||||
.option('project-path', {
|
|
||||||
alias: 'projectPath',
|
|
||||||
type: 'string',
|
|
||||||
description: 'Path to the Unity project',
|
|
||||||
default: '.',
|
|
||||||
})
|
|
||||||
.option('cache-dir', {
|
|
||||||
alias: 'cacheDir',
|
|
||||||
type: 'string',
|
|
||||||
description: 'Path to an additional cache directory to inspect',
|
|
||||||
default: '',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handler: async (cliArguments) => {
|
|
||||||
const projectPath = (cliArguments.projectPath as string) || '.';
|
|
||||||
const cacheDirectory = cliArguments.cacheDir as string;
|
|
||||||
|
|
||||||
core.info('game-ci Workspace Status');
|
|
||||||
core.info('========================\n');
|
|
||||||
|
|
||||||
// Project detection
|
|
||||||
const projectVersionPath = path.join(projectPath, 'ProjectSettings', 'ProjectVersion.txt');
|
|
||||||
const hasProject = fs.existsSync(projectVersionPath);
|
|
||||||
|
|
||||||
core.info(`Project Path: ${path.resolve(projectPath)}`);
|
|
||||||
core.info(`Unity Project Found: ${hasProject ? 'Yes' : 'No'}`);
|
|
||||||
|
|
||||||
if (hasProject) {
|
|
||||||
try {
|
|
||||||
const unityVersion = UnityVersioning.determineUnityVersion(projectPath, 'auto');
|
|
||||||
core.info(`Unity Version: ${unityVersion}`);
|
|
||||||
} catch {
|
|
||||||
core.info(`Unity Version: Unable to detect`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Library cache status
|
|
||||||
const libraryPath = path.join(projectPath, 'Library');
|
|
||||||
if (fs.existsSync(libraryPath)) {
|
|
||||||
const stats = fs.statSync(libraryPath);
|
|
||||||
core.info(`Library Cache: Present (modified ${stats.mtime.toISOString()})`);
|
|
||||||
|
|
||||||
const keyDirectories = ['PackageCache', 'ScriptAssemblies', 'ShaderCache', 'Bee'];
|
|
||||||
for (const directory of keyDirectories) {
|
|
||||||
const directoryPath = path.join(libraryPath, directory);
|
|
||||||
if (fs.existsSync(directoryPath)) {
|
|
||||||
const directoryStats = fs.statSync(directoryPath);
|
|
||||||
core.info(` ${directory}/: exists (modified ${directoryStats.mtime.toISOString()})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
core.info(`Library Cache: Not present (clean build required)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache archive detection
|
|
||||||
if (cacheDirectory && fs.existsSync(cacheDirectory)) {
|
|
||||||
core.info(`\nCache Archives (${cacheDirectory}):`);
|
|
||||||
const cacheFiles = fs.readdirSync(cacheDirectory).filter((f) => f.endsWith('.tar') || f.endsWith('.tar.lz4'));
|
|
||||||
if (cacheFiles.length > 0) {
|
|
||||||
for (const file of cacheFiles) {
|
|
||||||
const filePath = path.join(cacheDirectory, file);
|
|
||||||
const fileStats = fs.statSync(filePath);
|
|
||||||
const sizeMegabytes = (fileStats.size / (1024 * 1024)).toFixed(1);
|
|
||||||
core.info(` - ${file} (${sizeMegabytes} MB, ${fileStats.mtime.toISOString()})`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
core.info(' No cache archives found.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build output detection
|
|
||||||
const buildsPath = path.join(projectPath, '..', 'build');
|
|
||||||
if (fs.existsSync(buildsPath)) {
|
|
||||||
const builds = fs.readdirSync(buildsPath);
|
|
||||||
if (builds.length > 0) {
|
|
||||||
core.info(`\nBuild Outputs (${buildsPath}):`);
|
|
||||||
for (const build of builds) {
|
|
||||||
const buildPath = path.join(buildsPath, build);
|
|
||||||
const buildStats = fs.statSync(buildPath);
|
|
||||||
core.info(` - ${build} (${buildStats.isDirectory() ? 'dir' : 'file'}, ${buildStats.mtime.toISOString()})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Environment
|
|
||||||
core.info('\nEnvironment:');
|
|
||||||
core.info(` Platform: ${process.platform}`);
|
|
||||||
core.info(` Node.js: ${process.version}`);
|
|
||||||
core.info(` UNITY_SERIAL: ${process.env.UNITY_SERIAL ? 'Set' : 'Not set'}`);
|
|
||||||
core.info(` UNITY_LICENSE: ${process.env.UNITY_LICENSE ? 'Set' : 'Not set'}`);
|
|
||||||
core.info(` UNITY_EMAIL: ${process.env.UNITY_EMAIL ? 'Set' : 'Not set'}`);
|
|
||||||
core.info(` UNITY_PASSWORD: ${process.env.UNITY_PASSWORD ? 'Set' : 'Not set'}`);
|
|
||||||
|
|
||||||
// Docker availability
|
|
||||||
core.info(`\nDocker: Checking...`);
|
|
||||||
try {
|
|
||||||
const { execSync } = await import('node:child_process');
|
|
||||||
const dockerVersion = execSync('docker --version', { encoding: 'utf8' }).trim();
|
|
||||||
core.info(` ${dockerVersion}`);
|
|
||||||
} catch {
|
|
||||||
core.info(` Docker not found or not accessible`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default statusCommand;
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import type { CommandModule } from 'yargs';
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
import { BuildParameters, ImageTag } from '../../model';
|
|
||||||
import { mapCliArgumentsToInput, CliArguments } from '../input-mapper';
|
|
||||||
import Docker from '../../model/docker';
|
|
||||||
import Action from '../../model/action';
|
|
||||||
import PlatformSetup from '../../model/platform-setup';
|
|
||||||
import { withProjectOptions, withDockerOptions } from './shared-options';
|
|
||||||
|
|
||||||
interface TestArguments extends CliArguments {
|
|
||||||
targetPlatform: string;
|
|
||||||
testMode?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const testCommand: CommandModule<object, TestArguments> = {
|
|
||||||
command: ['test', 't'],
|
|
||||||
describe: 'Run tests for a Unity project',
|
|
||||||
builder: (yargs) => {
|
|
||||||
let y = withProjectOptions(yargs);
|
|
||||||
y = withDockerOptions(y);
|
|
||||||
|
|
||||||
return y
|
|
||||||
.option('test-mode', {
|
|
||||||
alias: 'testMode',
|
|
||||||
type: 'string',
|
|
||||||
description: 'The mode to run tests in (EditMode, PlayMode, or All)',
|
|
||||||
default: 'All',
|
|
||||||
choices: ['EditMode', 'PlayMode', 'All'],
|
|
||||||
})
|
|
||||||
.option('test-results-path', {
|
|
||||||
alias: 'testResultsPath',
|
|
||||||
type: 'string',
|
|
||||||
description: 'Path where test results XML should be stored',
|
|
||||||
default: 'test-results',
|
|
||||||
})
|
|
||||||
.option('test-category', {
|
|
||||||
alias: 'testCategory',
|
|
||||||
type: 'string',
|
|
||||||
description: 'Only run tests in the given category (semicolon-separated)',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('test-filter', {
|
|
||||||
alias: 'testFilter',
|
|
||||||
type: 'string',
|
|
||||||
description: 'Only run tests that match the filter (semicolon-separated)',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('coverage-options', {
|
|
||||||
alias: 'coverageOptions',
|
|
||||||
type: 'string',
|
|
||||||
description: 'Options for code coverage (e.g. assemblyFilters, pathFilters)',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.option('enable-code-coverage', {
|
|
||||||
alias: 'enableCodeCoverage',
|
|
||||||
type: 'boolean',
|
|
||||||
description: 'Enable code coverage when running tests',
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
.option('versioning', {
|
|
||||||
type: 'string',
|
|
||||||
description: 'The versioning scheme to use',
|
|
||||||
default: 'None',
|
|
||||||
})
|
|
||||||
.option('cache-unity-installation-on-mac', {
|
|
||||||
alias: 'cacheUnityInstallationOnMac',
|
|
||||||
type: 'boolean',
|
|
||||||
description: 'Whether to cache the Unity hub and editor installation on MacOS',
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
.option('unity-hub-version-on-mac', {
|
|
||||||
alias: 'unityHubVersionOnMac',
|
|
||||||
type: 'string',
|
|
||||||
description: 'The version of Unity Hub to install on MacOS (e.g. 3.4.0). Defaults to latest available on brew.',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.example('game-ci test --target-platform StandaloneLinux64', 'Run all tests for Linux platform')
|
|
||||||
.example(
|
|
||||||
'game-ci t --target-platform StandaloneLinux64 --test-mode EditMode',
|
|
||||||
'Run only EditMode tests (short alias)',
|
|
||||||
)
|
|
||||||
.example(
|
|
||||||
'game-ci test --target-platform StandaloneLinux64 --enable-code-coverage',
|
|
||||||
'Run tests with code coverage',
|
|
||||||
) as any;
|
|
||||||
},
|
|
||||||
handler: async (cliArguments) => {
|
|
||||||
try {
|
|
||||||
// Map test-specific flags into the input system
|
|
||||||
mapCliArgumentsToInput(cliArguments);
|
|
||||||
|
|
||||||
const buildParameters = await BuildParameters.create();
|
|
||||||
const baseImage = new ImageTag(buildParameters);
|
|
||||||
|
|
||||||
const testMode = cliArguments.testMode || 'All';
|
|
||||||
|
|
||||||
core.info(`Running Unity tests (${testMode})...`);
|
|
||||||
core.info(`Target platform: ${buildParameters.targetPlatform}`);
|
|
||||||
core.info(`Unity version: ${buildParameters.editorVersion}`);
|
|
||||||
core.info(`Project path: ${buildParameters.projectPath}`);
|
|
||||||
|
|
||||||
const actionFolder = Action.actionFolder;
|
|
||||||
await PlatformSetup.setup(buildParameters, actionFolder);
|
|
||||||
|
|
||||||
const exitCode = await Docker.run(baseImage.toString(), {
|
|
||||||
workspace: process.cwd(),
|
|
||||||
actionFolder,
|
|
||||||
...buildParameters,
|
|
||||||
});
|
|
||||||
|
|
||||||
const resultsPath = cliArguments.testResultsPath || 'test-results';
|
|
||||||
core.info(`\nTests completed with exit code: ${exitCode}`);
|
|
||||||
core.info(`Test results: ${resultsPath}`);
|
|
||||||
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
throw new Error(`Tests failed with exit code ${exitCode}`);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
core.setFailed(`Tests failed: ${error.message}`);
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default testCommand;
|
|
||||||
@@ -1,387 +0,0 @@
|
|||||||
import type { CommandModule } from 'yargs';
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import os from 'node:os';
|
|
||||||
import path from 'node:path';
|
|
||||||
import https from 'node:https';
|
|
||||||
import http from 'node:http';
|
|
||||||
import { execFileSync } from 'node:child_process';
|
|
||||||
|
|
||||||
const REPO = 'game-ci/unity-builder';
|
|
||||||
|
|
||||||
interface GitHubRelease {
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
tag_name: string;
|
|
||||||
assets: Array<{
|
|
||||||
name: string;
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
browser_download_url: string;
|
|
||||||
size: number;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateArguments {
|
|
||||||
force?: boolean;
|
|
||||||
version?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches JSON from a URL via HTTPS, following redirects.
|
|
||||||
*/
|
|
||||||
function fetchJson(url: string): Promise<any> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const get = (targetUrl: string, redirectCount: number) => {
|
|
||||||
if (redirectCount > 5) {
|
|
||||||
reject(new Error('Too many redirects'));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
https
|
|
||||||
.get(
|
|
||||||
targetUrl,
|
|
||||||
{
|
|
||||||
headers: { 'User-Agent': 'game-ci-cli', Accept: 'application/json' },
|
|
||||||
},
|
|
||||||
(response) => {
|
|
||||||
if (
|
|
||||||
response.statusCode &&
|
|
||||||
response.statusCode >= 300 &&
|
|
||||||
response.statusCode < 400 &&
|
|
||||||
response.headers.location
|
|
||||||
) {
|
|
||||||
get(response.headers.location, redirectCount + 1);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (response.statusCode !== 200) {
|
|
||||||
reject(new Error(`HTTP ${response.statusCode} from ${targetUrl}`));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let data = '';
|
|
||||||
response.on('data', (chunk) => (data += chunk));
|
|
||||||
response.on('end', () => {
|
|
||||||
try {
|
|
||||||
resolve(JSON.parse(data));
|
|
||||||
} catch {
|
|
||||||
reject(new Error('Invalid JSON response'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.on('error', reject);
|
|
||||||
};
|
|
||||||
get(url, 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads a file from a URL, following redirects. Returns the file content as a Buffer.
|
|
||||||
*/
|
|
||||||
function downloadFile(url: string): Promise<Buffer> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const get = (targetUrl: string, redirectCount: number) => {
|
|
||||||
if (redirectCount > 10) {
|
|
||||||
reject(new Error('Too many redirects'));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const protocol = targetUrl.startsWith('https') ? https : http;
|
|
||||||
protocol
|
|
||||||
.get(targetUrl, { headers: { 'User-Agent': 'game-ci-cli' } }, (response) => {
|
|
||||||
if (
|
|
||||||
response.statusCode &&
|
|
||||||
response.statusCode >= 300 &&
|
|
||||||
response.statusCode < 400 &&
|
|
||||||
response.headers.location
|
|
||||||
) {
|
|
||||||
get(response.headers.location, redirectCount + 1);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (response.statusCode !== 200) {
|
|
||||||
reject(new Error(`HTTP ${response.statusCode} downloading ${targetUrl}`));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
response.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
||||||
response.on('end', () => resolve(Buffer.concat(chunks)));
|
|
||||||
})
|
|
||||||
.on('error', reject);
|
|
||||||
};
|
|
||||||
get(url, 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current version from package.json or the compiled binary.
|
|
||||||
*/
|
|
||||||
function getCurrentVersion(): string {
|
|
||||||
// Try reading from package.json at various relative locations
|
|
||||||
const candidates = [
|
|
||||||
path.join(__dirname, '..', '..', '..', 'package.json'),
|
|
||||||
path.join(__dirname, '..', '..', 'package.json'),
|
|
||||||
path.join(process.cwd(), 'package.json'),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
if (fs.existsSync(candidate)) {
|
|
||||||
try {
|
|
||||||
const packageData = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
||||||
if (packageData.version) {
|
|
||||||
return packageData.version;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Continue to next candidate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'unknown';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the correct asset name for the current platform/architecture.
|
|
||||||
*/
|
|
||||||
function getAssetName(): string {
|
|
||||||
const platform = process.platform;
|
|
||||||
const arch = process.arch;
|
|
||||||
|
|
||||||
let osPart: string;
|
|
||||||
switch (platform) {
|
|
||||||
case 'linux':
|
|
||||||
osPart = 'linux';
|
|
||||||
break;
|
|
||||||
case 'darwin':
|
|
||||||
osPart = 'macos';
|
|
||||||
break;
|
|
||||||
case 'win32':
|
|
||||||
osPart = 'windows';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported platform: ${platform}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let archPart: string;
|
|
||||||
switch (arch) {
|
|
||||||
case 'x64':
|
|
||||||
archPart = 'x64';
|
|
||||||
break;
|
|
||||||
case 'arm64':
|
|
||||||
archPart = 'arm64';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported architecture: ${arch}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetBaseName = `game-ci-${osPart}-${archPart}`;
|
|
||||||
|
|
||||||
return osPart === 'windows' ? `${assetBaseName}.exe` : assetBaseName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the path to the currently running executable.
|
|
||||||
* For standalone binaries (pkg), process.execPath points to the binary itself.
|
|
||||||
* For Node.js execution, we return undefined since self-update does not apply.
|
|
||||||
*/
|
|
||||||
function getExecutablePath(): string | undefined {
|
|
||||||
// When running as a pkg binary, process.pkg is defined
|
|
||||||
if ((process as any).pkg) {
|
|
||||||
return process.execPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When running via Node.js, check if there is a standalone binary in the typical install location
|
|
||||||
const installDirectory = process.env.GAME_CI_INSTALL || path.join(os.homedir(), '.game-ci', 'bin');
|
|
||||||
const binaryName = process.platform === 'win32' ? 'game-ci.exe' : 'game-ci';
|
|
||||||
const installedPath = path.join(installDirectory, binaryName);
|
|
||||||
|
|
||||||
if (fs.existsSync(installedPath)) {
|
|
||||||
return installedPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strips leading 'v' from a version string and splits into numeric parts.
|
|
||||||
*/
|
|
||||||
function parseVersionParts(version: string): number[] {
|
|
||||||
return version
|
|
||||||
.replace(/^v/, '')
|
|
||||||
.split('.')
|
|
||||||
.map((part) => Number(part));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares two semver strings. Returns:
|
|
||||||
* -1 if a < b
|
|
||||||
* 0 if a == b
|
|
||||||
* 1 if a > b
|
|
||||||
*/
|
|
||||||
function compareSemver(a: string, b: string): number {
|
|
||||||
const partsA = parseVersionParts(a);
|
|
||||||
const partsB = parseVersionParts(b);
|
|
||||||
|
|
||||||
for (let index = 0; index < 3; index++) {
|
|
||||||
const x = partsA[index] || 0;
|
|
||||||
const y = partsB[index] || 0;
|
|
||||||
if (x < y) return -1;
|
|
||||||
if (x > y) return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateCommand: CommandModule<object, UpdateArguments> = {
|
|
||||||
command: 'update',
|
|
||||||
describe: 'Update game-ci to the latest version',
|
|
||||||
builder: (yargs) => {
|
|
||||||
return yargs
|
|
||||||
.option('force', {
|
|
||||||
alias: 'f',
|
|
||||||
type: 'boolean',
|
|
||||||
description: 'Force update even if already on latest version',
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
.option('version', {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Update to a specific version (e.g., v2.0.0)',
|
|
||||||
default: '',
|
|
||||||
})
|
|
||||||
.example('game-ci update', 'Update to the latest version')
|
|
||||||
.example('game-ci update --version v2.1.0', 'Update to a specific version')
|
|
||||||
.example('game-ci update --force', 'Force reinstall of the current version') as any;
|
|
||||||
},
|
|
||||||
handler: async (cliArguments) => {
|
|
||||||
try {
|
|
||||||
const currentVersion = getCurrentVersion();
|
|
||||||
core.info(`Current version: v${currentVersion}`);
|
|
||||||
core.info(`Platform: ${process.platform} ${process.arch}`);
|
|
||||||
core.info('');
|
|
||||||
|
|
||||||
// Fetch release info
|
|
||||||
let release: GitHubRelease;
|
|
||||||
const targetVersion = cliArguments.version as string;
|
|
||||||
|
|
||||||
if (targetVersion) {
|
|
||||||
const tag = targetVersion.startsWith('v') ? targetVersion : `v${targetVersion}`;
|
|
||||||
core.info(`Fetching release ${tag}...`);
|
|
||||||
release = await fetchJson(`https://api.github.com/repos/${REPO}/releases/tags/${tag}`);
|
|
||||||
} else {
|
|
||||||
core.info('Checking for updates...');
|
|
||||||
release = await fetchJson(`https://api.github.com/repos/${REPO}/releases/latest`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestVersion = release.tag_name;
|
|
||||||
core.info(`Latest version: ${latestVersion}`);
|
|
||||||
core.info('');
|
|
||||||
|
|
||||||
// Compare versions
|
|
||||||
const comparison = compareSemver(currentVersion, latestVersion);
|
|
||||||
if (comparison >= 0 && !cliArguments.force) {
|
|
||||||
core.info('You are already on the latest version. Use --force to reinstall.');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (comparison > 0 && !targetVersion) {
|
|
||||||
core.info(`Current version (v${currentVersion}) is newer than latest release (${latestVersion}).`);
|
|
||||||
core.info('Use --force to downgrade, or --version to target a specific release.');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the correct asset
|
|
||||||
const assetName = getAssetName();
|
|
||||||
const asset = release.assets.find((a) => a.name === assetName);
|
|
||||||
|
|
||||||
if (!asset) {
|
|
||||||
const available = release.assets.map((a) => a.name).join(', ');
|
|
||||||
throw new Error(
|
|
||||||
`No binary found for ${process.platform}-${process.arch} (looking for ${assetName}).\nAvailable assets: ${available}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sizeMb = (asset.size / (1024 * 1024)).toFixed(1);
|
|
||||||
core.info(`Downloading ${assetName} (${sizeMb} MB)...`);
|
|
||||||
|
|
||||||
// Download the new binary
|
|
||||||
const binaryData = await downloadFile(asset.browser_download_url);
|
|
||||||
|
|
||||||
// Determine where to write the updated binary
|
|
||||||
const executablePath = getExecutablePath();
|
|
||||||
|
|
||||||
if (!executablePath) {
|
|
||||||
core.info('');
|
|
||||||
core.info('game-ci is running via Node.js (not as a standalone binary).');
|
|
||||||
core.info('To update the npm package, run:');
|
|
||||||
core.info(' npm install -g unity-builder@latest');
|
|
||||||
core.info('');
|
|
||||||
core.info('To install the standalone binary instead:');
|
|
||||||
core.info(' curl -fsSL https://raw.githubusercontent.com/game-ci/unity-builder/main/install.sh | sh');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the new binary.
|
|
||||||
// On Windows, we cannot overwrite a running executable directly.
|
|
||||||
// Write to a temporary file, then rename.
|
|
||||||
const temporaryPath = `${executablePath}.update`;
|
|
||||||
const backupPath = `${executablePath}.backup`;
|
|
||||||
|
|
||||||
fs.writeFileSync(temporaryPath, binaryData);
|
|
||||||
|
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
fs.chmodSync(temporaryPath, 0o755);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the downloaded binary
|
|
||||||
try {
|
|
||||||
const output = execFileSync(temporaryPath, ['version'], { encoding: 'utf8', timeout: 10_000 });
|
|
||||||
core.info(`Verified new binary: ${output.trim().split('\n')[0]}`);
|
|
||||||
} catch (verifyError: any) {
|
|
||||||
fs.unlinkSync(temporaryPath);
|
|
||||||
throw new Error(`Downloaded binary failed verification: ${verifyError.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the current binary
|
|
||||||
try {
|
|
||||||
// Backup current
|
|
||||||
if (fs.existsSync(backupPath)) {
|
|
||||||
fs.unlinkSync(backupPath);
|
|
||||||
}
|
|
||||||
fs.renameSync(executablePath, backupPath);
|
|
||||||
fs.renameSync(temporaryPath, executablePath);
|
|
||||||
|
|
||||||
// Clean up backup
|
|
||||||
try {
|
|
||||||
fs.unlinkSync(backupPath);
|
|
||||||
} catch {
|
|
||||||
// On Windows the backup may be locked; that is fine
|
|
||||||
}
|
|
||||||
} catch (replaceError: any) {
|
|
||||||
// Attempt to restore from backup
|
|
||||||
if (fs.existsSync(backupPath) && !fs.existsSync(executablePath)) {
|
|
||||||
fs.renameSync(backupPath, executablePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up temporary file
|
|
||||||
if (fs.existsSync(temporaryPath)) {
|
|
||||||
fs.unlinkSync(temporaryPath);
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to replace binary: ${replaceError.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info('');
|
|
||||||
core.info(`Successfully updated game-ci to ${latestVersion}`);
|
|
||||||
} catch (error: any) {
|
|
||||||
core.error(`Update failed: ${error.message}`);
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default updateCommand;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import type { CommandModule } from 'yargs';
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
|
|
||||||
const versionCommand: CommandModule = {
|
|
||||||
command: 'version',
|
|
||||||
describe: 'Show version info',
|
|
||||||
builder: {},
|
|
||||||
handler: async () => {
|
|
||||||
try {
|
|
||||||
// Read version from package.json
|
|
||||||
let packageJsonPath = path.join(__dirname, '..', '..', '..', 'package.json');
|
|
||||||
if (!fs.existsSync(packageJsonPath)) {
|
|
||||||
packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(packageJsonPath)) {
|
|
||||||
packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fs.existsSync(packageJsonPath)) {
|
|
||||||
const packageData = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
||||||
core.info(`game-ci (unity-builder) v${packageData.version}`);
|
|
||||||
core.info(`Node.js ${process.version}`);
|
|
||||||
core.info(`Platform: ${process.platform} ${process.arch}`);
|
|
||||||
} else {
|
|
||||||
core.info('game-ci (unity-builder)');
|
|
||||||
core.info('Version information unavailable');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
core.info('game-ci (unity-builder)');
|
|
||||||
core.error(`Could not read version: ${error.message}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default versionCommand;
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import { Cli } from '../model/cli/cli';
|
|
||||||
import GitHub from '../model/github';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps CLI arguments (kebab-case flags) to the Input/OrchestratorOptions
|
|
||||||
* interface used by the action. This bridges the gap between user-friendly
|
|
||||||
* CLI flags and the camelCase environment/input system unity-builder expects.
|
|
||||||
*
|
|
||||||
* The existing Input class already queries Cli.options, environment variables,
|
|
||||||
* and GitHub Action inputs in priority order. We populate Cli.options so that
|
|
||||||
* the rest of the codebase works unchanged.
|
|
||||||
*/
|
|
||||||
export interface CliArguments {
|
|
||||||
targetPlatform?: string;
|
|
||||||
unityVersion?: string;
|
|
||||||
projectPath?: string;
|
|
||||||
buildProfile?: string;
|
|
||||||
buildName?: string;
|
|
||||||
buildsPath?: string;
|
|
||||||
buildMethod?: string;
|
|
||||||
customParameters?: string;
|
|
||||||
versioning?: string;
|
|
||||||
version?: string;
|
|
||||||
customImage?: string;
|
|
||||||
manualExit?: boolean;
|
|
||||||
enableGpu?: boolean;
|
|
||||||
|
|
||||||
androidVersionCode?: string;
|
|
||||||
androidExportType?: string;
|
|
||||||
androidKeystoreName?: string;
|
|
||||||
androidKeystoreBase64?: string;
|
|
||||||
androidKeystorePass?: string;
|
|
||||||
androidKeyaliasName?: string;
|
|
||||||
androidKeyaliasPass?: string;
|
|
||||||
androidTargetSdkVersion?: string;
|
|
||||||
androidSymbolType?: string;
|
|
||||||
|
|
||||||
dockerCpuLimit?: string;
|
|
||||||
dockerMemoryLimit?: string;
|
|
||||||
dockerIsolationMode?: string;
|
|
||||||
dockerWorkspacePath?: string;
|
|
||||||
containerRegistryRepository?: string;
|
|
||||||
containerRegistryImageVersion?: string;
|
|
||||||
runAsHostUser?: string;
|
|
||||||
chownFilesTo?: string;
|
|
||||||
|
|
||||||
sshAgent?: string;
|
|
||||||
sshPublicKeysDirectoryPath?: string;
|
|
||||||
gitPrivateToken?: string;
|
|
||||||
|
|
||||||
providerStrategy?: string;
|
|
||||||
awsStackName?: string;
|
|
||||||
kubeConfig?: string;
|
|
||||||
kubeVolume?: string;
|
|
||||||
kubeVolumeSize?: string;
|
|
||||||
kubeStorageClass?: string;
|
|
||||||
containerCpu?: string;
|
|
||||||
containerMemory?: string;
|
|
||||||
cacheKey?: string;
|
|
||||||
watchToEnd?: string;
|
|
||||||
allowDirtyBuild?: boolean;
|
|
||||||
skipActivation?: string;
|
|
||||||
cloneDepth?: string;
|
|
||||||
|
|
||||||
readInputFromOverrideList?: string;
|
|
||||||
readInputOverrideCommand?: string;
|
|
||||||
postBuildSteps?: string;
|
|
||||||
preBuildSteps?: string;
|
|
||||||
customJob?: string;
|
|
||||||
|
|
||||||
unityLicensingServer?: string;
|
|
||||||
|
|
||||||
cacheUnityInstallationOnMac?: boolean;
|
|
||||||
unityHubVersionOnMac?: string;
|
|
||||||
|
|
||||||
testMode?: string;
|
|
||||||
testResultsPath?: string;
|
|
||||||
testCategory?: string;
|
|
||||||
testFilter?: string;
|
|
||||||
coverageOptions?: string;
|
|
||||||
enableCodeCoverage?: boolean;
|
|
||||||
|
|
||||||
mode?: string;
|
|
||||||
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts kebab-case CLI flags to camelCase keys matching the Input class
|
|
||||||
* property names, then injects them into Cli.options so the existing
|
|
||||||
* Input.getInput() / OrchestratorOptions.getInput() chain picks them up.
|
|
||||||
*/
|
|
||||||
export function mapCliArgumentsToInput(cliArguments: CliArguments): void {
|
|
||||||
// Disable GitHub Actions input reading when in CLI mode
|
|
||||||
GitHub.githubInputEnabled = false;
|
|
||||||
|
|
||||||
// The existing Cli.options mechanism is used by Input.getInput() to query
|
|
||||||
// CLI-provided values. We set it directly.
|
|
||||||
const mapped: Record<string, unknown> = {};
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(cliArguments)) {
|
|
||||||
if (value !== undefined && key !== '_' && key !== '$0') {
|
|
||||||
mapped[key] = typeof value === 'boolean' ? String(value) : value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure mode is set so Cli.isCliMode returns true
|
|
||||||
if (!mapped['mode']) {
|
|
||||||
mapped['mode'] = 'cli';
|
|
||||||
}
|
|
||||||
|
|
||||||
Cli.options = mapped;
|
|
||||||
}
|
|
||||||
278
src/index-plugin-features.test.ts
Normal file
278
src/index-plugin-features.test.ts
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi, type Mock } from 'vitest';
|
||||||
|
/**
|
||||||
|
* Integration wiring tests for the plugin lifecycle in index.ts
|
||||||
|
*
|
||||||
|
* These tests verify that:
|
||||||
|
* - The plugin lifecycle hooks are called in the correct order
|
||||||
|
* - Plugin canHandleBuild() controls the execution path
|
||||||
|
* - fallbackToLocal is handled correctly
|
||||||
|
* - When no plugin is installed, local builds still work
|
||||||
|
* - When providerStrategy is non-local without a plugin, an error is thrown
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BuildParameters, Docker } from './model';
|
||||||
|
import * as core from '@actions/core';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mock plugin
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// `vi.mock` hoists to the top of the module, so any factory references must
|
||||||
|
// be hoisted with `vi.hoisted` to be defined at mock-evaluation time.
|
||||||
|
const { mockPlugin, mockLoadPlugin } = vi.hoisted(() => {
|
||||||
|
const plugin = {
|
||||||
|
initialize: vi.fn().mockResolvedValue(undefined),
|
||||||
|
canHandleBuild: vi.fn().mockReturnValue(false),
|
||||||
|
handleBuild: vi.fn().mockResolvedValue({ exitCode: 0 }),
|
||||||
|
beforeLocalBuild: vi.fn().mockResolvedValue(undefined),
|
||||||
|
afterLocalBuild: vi.fn().mockResolvedValue(undefined),
|
||||||
|
handlePostBuild: vi.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
mockPlugin: plugin,
|
||||||
|
mockLoadPlugin: vi.fn().mockResolvedValue(plugin),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('./model/plugin', () => ({
|
||||||
|
loadPlugin: mockLoadPlugin,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@actions/core');
|
||||||
|
vi.mock('./model', () => ({
|
||||||
|
Action: {
|
||||||
|
checkCompatibility: vi.fn(),
|
||||||
|
workspace: '/workspace',
|
||||||
|
actionFolder: '/action',
|
||||||
|
},
|
||||||
|
BuildParameters: {
|
||||||
|
create: vi.fn(),
|
||||||
|
},
|
||||||
|
Cache: {
|
||||||
|
verify: vi.fn(),
|
||||||
|
},
|
||||||
|
Docker: {
|
||||||
|
run: vi.fn().mockResolvedValue(0),
|
||||||
|
},
|
||||||
|
// vitest 4 requires constructor mocks to use regular `function` (or
|
||||||
|
// `class`); arrow fns aren't valid constructors.
|
||||||
|
ImageTag: vi.fn(function () {
|
||||||
|
return { toString: () => 'mock-image:latest' };
|
||||||
|
}),
|
||||||
|
Output: {
|
||||||
|
setBuildVersion: vi.fn().mockResolvedValue(''),
|
||||||
|
setAndroidVersionCode: vi.fn().mockResolvedValue(''),
|
||||||
|
setEngineExitCode: vi.fn().mockResolvedValue(''),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('./model/cli/cli', () => ({
|
||||||
|
Cli: {
|
||||||
|
InitCliMode: vi.fn().mockReturnValue(false),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('./model/mac-builder', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
run: vi.fn().mockResolvedValue(0),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('./model/platform-setup', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
setup: vi.fn().mockResolvedValue(''),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockedBuildParametersCreate = BuildParameters.create as Mock;
|
||||||
|
|
||||||
|
function createMockBuildParameters(overrides: Record<string, any> = {}) {
|
||||||
|
return {
|
||||||
|
providerStrategy: 'local',
|
||||||
|
targetPlatform: 'StandaloneLinux64',
|
||||||
|
editorVersion: '2021.3.1f1',
|
||||||
|
buildVersion: '1.0.0',
|
||||||
|
androidVersionCode: '1',
|
||||||
|
projectPath: '.',
|
||||||
|
branch: 'main',
|
||||||
|
runnerTempPath: '/tmp',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runIndex(overrides: Record<string, any> = {}): Promise<void> {
|
||||||
|
mockedBuildParametersCreate.mockResolvedValue(createMockBuildParameters(overrides));
|
||||||
|
|
||||||
|
// index.ts exports `runMain` for testability (the file used to rely on
|
||||||
|
// top-level execution + jest's `vi.isolateModules`, but vitest 4 dropped
|
||||||
|
// that API). Calling the exported function directly is cleaner than
|
||||||
|
// round-tripping through dynamic imports.
|
||||||
|
const { runMain } = await import('./index');
|
||||||
|
await runMain();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('index.ts plugin lifecycle wiring', () => {
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
const originalEnvironment = { ...process.env };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
process.env.GITHUB_WORKSPACE = '/workspace';
|
||||||
|
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||||
|
|
||||||
|
// Reset plugin to default behavior
|
||||||
|
mockPlugin.canHandleBuild.mockReturnValue(false);
|
||||||
|
mockPlugin.handleBuild.mockResolvedValue({ exitCode: 0 });
|
||||||
|
mockLoadPlugin.mockResolvedValue(mockPlugin);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||||
|
process.env = { ...originalEnvironment };
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Local build with plugin
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('local build with plugin installed', () => {
|
||||||
|
it('should call lifecycle hooks in order: initialize -> beforeLocalBuild -> [build] -> afterLocalBuild -> handlePostBuild', async () => {
|
||||||
|
const callOrder: string[] = [];
|
||||||
|
mockPlugin.initialize.mockImplementation(async () => callOrder.push('initialize'));
|
||||||
|
mockPlugin.beforeLocalBuild.mockImplementation(async () =>
|
||||||
|
callOrder.push('beforeLocalBuild'),
|
||||||
|
);
|
||||||
|
mockPlugin.afterLocalBuild.mockImplementation(async () => callOrder.push('afterLocalBuild'));
|
||||||
|
mockPlugin.handlePostBuild.mockImplementation(async () => callOrder.push('handlePostBuild'));
|
||||||
|
|
||||||
|
await runIndex();
|
||||||
|
|
||||||
|
expect(callOrder).toEqual([
|
||||||
|
'initialize',
|
||||||
|
'beforeLocalBuild',
|
||||||
|
'afterLocalBuild',
|
||||||
|
'handlePostBuild',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass buildParameters and workspace to initialize', async () => {
|
||||||
|
await runIndex({ targetPlatform: 'WebGL' });
|
||||||
|
|
||||||
|
expect(mockPlugin.initialize).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ targetPlatform: 'WebGL' }),
|
||||||
|
'/workspace',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass workspace to beforeLocalBuild', async () => {
|
||||||
|
await runIndex();
|
||||||
|
|
||||||
|
expect(mockPlugin.beforeLocalBuild).toHaveBeenCalledWith('/workspace');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass workspace and exit code to afterLocalBuild', async () => {
|
||||||
|
await runIndex();
|
||||||
|
|
||||||
|
expect(mockPlugin.afterLocalBuild).toHaveBeenCalledWith('/workspace', 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass exit code to handlePostBuild', async () => {
|
||||||
|
await runIndex();
|
||||||
|
|
||||||
|
expect(mockPlugin.handlePostBuild).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Plugin handles build entirely
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('plugin handles build (canHandleBuild = true)', () => {
|
||||||
|
it('should call handleBuild instead of Docker.run', async () => {
|
||||||
|
mockPlugin.canHandleBuild.mockReturnValue(true);
|
||||||
|
mockPlugin.handleBuild.mockResolvedValue({ exitCode: 0 });
|
||||||
|
|
||||||
|
await runIndex();
|
||||||
|
|
||||||
|
expect(mockPlugin.handleBuild).toHaveBeenCalledWith('mock-image:latest');
|
||||||
|
expect(Docker.run).not.toHaveBeenCalled();
|
||||||
|
expect(mockPlugin.beforeLocalBuild).not.toHaveBeenCalled();
|
||||||
|
expect(mockPlugin.afterLocalBuild).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should still call handlePostBuild after handleBuild', async () => {
|
||||||
|
mockPlugin.canHandleBuild.mockReturnValue(true);
|
||||||
|
mockPlugin.handleBuild.mockResolvedValue({ exitCode: 0 });
|
||||||
|
|
||||||
|
await runIndex();
|
||||||
|
|
||||||
|
expect(mockPlugin.handlePostBuild).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Fallback to local
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('fallback to local build', () => {
|
||||||
|
it('should do a local build when handleBuild returns fallbackToLocal', async () => {
|
||||||
|
mockPlugin.canHandleBuild.mockReturnValue(true);
|
||||||
|
mockPlugin.handleBuild.mockResolvedValue({ exitCode: -1, fallbackToLocal: true });
|
||||||
|
|
||||||
|
await runIndex();
|
||||||
|
|
||||||
|
expect(mockPlugin.handleBuild).toHaveBeenCalled();
|
||||||
|
expect(mockPlugin.beforeLocalBuild).toHaveBeenCalled();
|
||||||
|
expect(Docker.run).toHaveBeenCalled();
|
||||||
|
expect(mockPlugin.afterLocalBuild).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// No plugin installed
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('no plugin installed', () => {
|
||||||
|
it('should build locally without errors when providerStrategy is local', async () => {
|
||||||
|
mockLoadPlugin.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await runIndex({ providerStrategy: 'local' });
|
||||||
|
|
||||||
|
expect(Docker.run).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error when providerStrategy is non-local and no plugin', async () => {
|
||||||
|
mockLoadPlugin.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await runIndex({ providerStrategy: 'aws' });
|
||||||
|
|
||||||
|
expect(core.setFailed).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('requires @game-ci/orchestrator'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// canHandleBuild = false with non-local provider
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('plugin installed but canHandleBuild returns false with non-local provider', () => {
|
||||||
|
it('should error when providerStrategy is non-local', async () => {
|
||||||
|
mockPlugin.canHandleBuild.mockReturnValue(false);
|
||||||
|
|
||||||
|
await runIndex({ providerStrategy: 'aws' });
|
||||||
|
|
||||||
|
// The plugin is initialized but says it can't handle the build,
|
||||||
|
// and providerStrategy is not local, so it falls to the error case
|
||||||
|
expect(core.setFailed).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('requires @game-ci/orchestrator'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
72
src/index.ts
72
src/index.ts
@@ -1,10 +1,13 @@
|
|||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
import { Action, BuildParameters, Cache, Orchestrator, Docker, ImageTag, Output } from './model';
|
import { Action, BuildParameters, Cache, Docker, ImageTag, Output } from './model';
|
||||||
import { Cli } from './model/cli/cli';
|
import { Cli } from './model/cli/cli';
|
||||||
import MacBuilder from './model/mac-builder';
|
import MacBuilder from './model/mac-builder';
|
||||||
import PlatformSetup from './model/platform-setup';
|
import PlatformSetup from './model/platform-setup';
|
||||||
|
import { Plugin, loadPlugin } from './model/plugin';
|
||||||
|
|
||||||
async function runMain() {
|
// Exported so tests can drive the lifecycle directly without depending on
|
||||||
|
// vitest's module re-loading (which changed in vitest 4).
|
||||||
|
export async function runMain() {
|
||||||
try {
|
try {
|
||||||
if (Cli.InitCliMode()) {
|
if (Cli.InitCliMode()) {
|
||||||
await Cli.RunCli();
|
await Cli.RunCli();
|
||||||
@@ -15,33 +18,39 @@ async function runMain() {
|
|||||||
Cache.verify();
|
Cache.verify();
|
||||||
|
|
||||||
const { workspace, actionFolder } = Action;
|
const { workspace, actionFolder } = Action;
|
||||||
|
|
||||||
const buildParameters = await BuildParameters.create();
|
const buildParameters = await BuildParameters.create();
|
||||||
const baseImage = new ImageTag(buildParameters);
|
const baseImage = new ImageTag(buildParameters);
|
||||||
|
|
||||||
|
// Load optional plugin. The default implementation is @game-ci/orchestrator.
|
||||||
|
const plugin = await loadPlugin();
|
||||||
|
await plugin?.initialize(buildParameters, workspace);
|
||||||
|
|
||||||
let exitCode = -1;
|
let exitCode = -1;
|
||||||
|
|
||||||
if (buildParameters.providerStrategy === 'local') {
|
if (plugin?.canHandleBuild()) {
|
||||||
core.info('Building locally');
|
// Plugin handles the build entirely (remote providers, hot runner, test workflows)
|
||||||
await PlatformSetup.setup(buildParameters, actionFolder);
|
const result = await plugin.handleBuild(baseImage.toString());
|
||||||
exitCode =
|
|
||||||
process.platform === 'darwin'
|
exitCode = result.fallbackToLocal
|
||||||
? await MacBuilder.run(actionFolder)
|
? await runLocalBuild(buildParameters, baseImage, workspace, actionFolder, plugin)
|
||||||
: await Docker.run(baseImage.toString(), {
|
: result.exitCode;
|
||||||
workspace,
|
} else if (buildParameters.providerStrategy === 'local') {
|
||||||
actionFolder,
|
exitCode = await runLocalBuild(buildParameters, baseImage, workspace, actionFolder, plugin);
|
||||||
...buildParameters,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await Orchestrator.run(buildParameters, baseImage.toString());
|
throw new Error(
|
||||||
exitCode = 0;
|
`Provider strategy "${buildParameters.providerStrategy}" requires @game-ci/orchestrator. ` +
|
||||||
|
'Install it via the game-ci/orchestrator action, or use providerStrategy=local.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set output
|
// Set core outputs
|
||||||
await Output.setBuildVersion(buildParameters.buildVersion);
|
await Output.setBuildVersion(buildParameters.buildVersion);
|
||||||
await Output.setAndroidVersionCode(buildParameters.androidVersionCode);
|
await Output.setAndroidVersionCode(buildParameters.androidVersionCode);
|
||||||
await Output.setEngineExitCode(exitCode);
|
await Output.setEngineExitCode(exitCode);
|
||||||
|
|
||||||
|
// Plugin handles post-build (artifacts, archiving, retention)
|
||||||
|
await plugin?.handlePostBuild(exitCode);
|
||||||
|
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
core.setFailed(`Build failed with exit code ${exitCode}`);
|
core.setFailed(`Build failed with exit code ${exitCode}`);
|
||||||
}
|
}
|
||||||
@@ -50,4 +59,33 @@ async function runMain() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runLocalBuild(
|
||||||
|
buildParameters: BuildParameters,
|
||||||
|
baseImage: ImageTag,
|
||||||
|
workspace: string,
|
||||||
|
actionFolder: string,
|
||||||
|
plugin?: Plugin,
|
||||||
|
): Promise<number> {
|
||||||
|
await plugin?.beforeLocalBuild(workspace);
|
||||||
|
|
||||||
|
await PlatformSetup.setup(buildParameters, actionFolder);
|
||||||
|
const exitCode =
|
||||||
|
process.platform === 'darwin'
|
||||||
|
? await MacBuilder.run(actionFolder)
|
||||||
|
: await Docker.run(baseImage.toString(), {
|
||||||
|
workspace,
|
||||||
|
actionFolder,
|
||||||
|
...buildParameters,
|
||||||
|
});
|
||||||
|
|
||||||
|
await plugin?.afterLocalBuild(workspace, exitCode);
|
||||||
|
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-run when this module is the entry point. Tests import the file via
|
||||||
|
// `await import('./index')` purely to register the mock factories and then
|
||||||
|
// call `runMain()` directly.
|
||||||
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
runMain();
|
runMain();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
// Integration test for exercising real GitHub check creation and updates.
|
|
||||||
import Orchestrator from '../model/orchestrator/orchestrator';
|
|
||||||
import UnityVersioning from '../model/unity-versioning';
|
|
||||||
import GitHub from '../model/github';
|
|
||||||
import { TIMEOUT_INFINITE, createParameters } from '../test-utils/orchestrator-test-helpers';
|
|
||||||
|
|
||||||
const runIntegration = process.env.RUN_GITHUB_INTEGRATION_TESTS === 'true';
|
|
||||||
const describeOrSkip = runIntegration ? describe : describe.skip;
|
|
||||||
|
|
||||||
describeOrSkip('Orchestrator Github Checks Integration', () => {
|
|
||||||
it(
|
|
||||||
'creates and updates a real GitHub check',
|
|
||||||
async () => {
|
|
||||||
const buildParameter = await createParameters({
|
|
||||||
versioning: 'None',
|
|
||||||
projectPath: 'test-project',
|
|
||||||
unityVersion: UnityVersioning.read('test-project'),
|
|
||||||
asyncOrchestrator: `true`,
|
|
||||||
githubChecks: `true`,
|
|
||||||
});
|
|
||||||
await Orchestrator.setup(buildParameter);
|
|
||||||
const checkId = await GitHub.createGitHubCheck(`integration create`);
|
|
||||||
expect(checkId).not.toEqual('');
|
|
||||||
await GitHub.updateGitHubCheck(`1 ${new Date().toISOString()}`, `integration`);
|
|
||||||
await GitHub.updateGitHubCheck(`2 ${new Date().toISOString()}`, `integration`, `success`, `completed`);
|
|
||||||
},
|
|
||||||
TIMEOUT_INFINITE,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||||
import { stat } from 'node:fs/promises';
|
import { stat } from 'node:fs/promises';
|
||||||
|
|
||||||
describe('Integrity tests', () => {
|
describe('Integrity tests', () => {
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import failOnConsole from 'jest-fail-on-console';
|
|
||||||
|
|
||||||
// Fail when console logs something inside a test - use spyOn instead
|
|
||||||
failOnConsole({
|
|
||||||
shouldFailOnWarn: true,
|
|
||||||
shouldFailOnError: true,
|
|
||||||
shouldFailOnLog: true,
|
|
||||||
shouldFailOnAssert: true,
|
|
||||||
});
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { vi } from 'vitest';
|
||||||
// Import this named export into your test file:
|
// Import this named export into your test file:
|
||||||
import Platform from '../platform';
|
import Platform from '../platform';
|
||||||
|
|
||||||
export const mockGetFromUser = jest.fn().mockResolvedValue({
|
export const mockGetFromUser = vi.fn().mockResolvedValue({
|
||||||
editorVersion: '',
|
editorVersion: '',
|
||||||
targetPlatform: Platform.types.Test,
|
targetPlatform: Platform.types.Test,
|
||||||
projectPath: '.',
|
projectPath: '.',
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
|
import { vi } from 'vitest';
|
||||||
/* eslint unicorn/prevent-abbreviations: "off" */
|
/* eslint unicorn/prevent-abbreviations: "off" */
|
||||||
|
|
||||||
// Import these named export into your test file:
|
// Import these named export into your test file:
|
||||||
export const mockProjectPath = jest.fn().mockResolvedValue('mockProjectPath');
|
export const mockProjectPath = vi.fn().mockResolvedValue('mockProjectPath');
|
||||||
export const mockIsDirtyAllowed = jest.fn().mockResolvedValue(false);
|
export const mockIsDirtyAllowed = vi.fn().mockResolvedValue(false);
|
||||||
export const mockBranch = jest.fn().mockResolvedValue('mockBranch');
|
export const mockBranch = vi.fn().mockResolvedValue('mockBranch');
|
||||||
export const mockHeadRef = jest.fn().mockResolvedValue('mockHeadRef');
|
export const mockHeadRef = vi.fn().mockResolvedValue('mockHeadRef');
|
||||||
export const mockRef = jest.fn().mockResolvedValue('mockRef');
|
export const mockRef = vi.fn().mockResolvedValue('mockRef');
|
||||||
export const mockDetermineVersion = jest.fn().mockResolvedValue('1.2.3');
|
export const mockDetermineVersion = vi.fn().mockResolvedValue('1.2.3');
|
||||||
export const mockGenerateSemanticVersion = jest.fn().mockResolvedValue('2.3.4');
|
export const mockGenerateSemanticVersion = vi.fn().mockResolvedValue('2.3.4');
|
||||||
export const mockGenerateTagVersion = jest.fn().mockResolvedValue('1.0');
|
export const mockGenerateTagVersion = vi.fn().mockResolvedValue('1.0');
|
||||||
export const mockParseSemanticVersion = jest.fn().mockResolvedValue({});
|
export const mockParseSemanticVersion = vi.fn().mockResolvedValue({});
|
||||||
export const mockFetch = jest.fn().mockImplementation(() => {});
|
export const mockFetch = vi.fn().mockImplementation(() => {});
|
||||||
export const mockGetVersionDescription = jest.fn().mockResolvedValue('1.2-3-g12345678-dirty');
|
export const mockGetVersionDescription = vi.fn().mockResolvedValue('1.2-3-g12345678-dirty');
|
||||||
export const mockIsDirty = jest.fn().mockResolvedValue(false);
|
export const mockIsDirty = vi.fn().mockResolvedValue(false);
|
||||||
export const mockGetTag = jest.fn().mockResolvedValue('v1.0');
|
export const mockGetTag = vi.fn().mockResolvedValue('v1.0');
|
||||||
export const mockHasAnyVersionTags = jest.fn().mockResolvedValue(true);
|
export const mockHasAnyVersionTags = vi.fn().mockResolvedValue(true);
|
||||||
export const mockGetTotalNumberOfCommits = jest.fn().mockResolvedValue(3);
|
export const mockGetTotalNumberOfCommits = vi.fn().mockResolvedValue(3);
|
||||||
export const mockGit = jest.fn().mockImplementation(() => {});
|
export const mockGit = vi.fn().mockImplementation(() => {});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
projectPath: mockProjectPath,
|
projectPath: mockProjectPath,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`Versioning determineBuildVersion throws for invalid strategy somethingRandom 1`] = `"Versioning strategy should be one of None, Semantic, Tag, Custom."`;
|
exports[`Versioning > determineBuildVersion > throws for invalid strategy somethingRandom 1`] = `[ValidationError: Versioning strategy should be one of None, Semantic, Tag, Custom.]`;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import Action from './action';
|
import Action from './action';
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||||
import AndroidVersioning from './android-versioning';
|
import AndroidVersioning from './android-versioning';
|
||||||
|
|
||||||
describe('Android Versioning', () => {
|
describe('Android Versioning', () => {
|
||||||
@@ -35,7 +36,9 @@ describe('Android Versioning', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('uses the specified api level', () => {
|
it('uses the specified api level', () => {
|
||||||
expect(AndroidVersioning.determineSdkManagerParameters('AndroidApiLevel30')).toBe('platforms;android-30');
|
expect(AndroidVersioning.determineSdkManagerParameters('AndroidApiLevel30')).toBe(
|
||||||
|
'platforms;android-30',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ export default class AndroidVersioning {
|
|||||||
|
|
||||||
static versionToVersionCode(version: string): string {
|
static versionToVersionCode(version: string): string {
|
||||||
if (version === 'none') {
|
if (version === 'none') {
|
||||||
core.info(`Versioning strategy is set to ${version}, so android version code should not be applied.`);
|
core.info(
|
||||||
|
`Versioning strategy is set to ${version}, so android version code should not be applied.`,
|
||||||
|
);
|
||||||
|
|
||||||
return '0';
|
return '0';
|
||||||
}
|
}
|
||||||
@@ -27,7 +29,8 @@ export default class AndroidVersioning {
|
|||||||
|
|
||||||
// The greatest value Google Plays allows is 2100000000.
|
// The greatest value Google Plays allows is 2100000000.
|
||||||
// Allow for 3 patch digits, 3 minor digits and 3 major digits.
|
// Allow for 3 patch digits, 3 minor digits and 3 major digits.
|
||||||
const versionCode = parsedVersion.major * 1000000 + parsedVersion.minor * 1000 + parsedVersion.patch;
|
const versionCode =
|
||||||
|
parsedVersion.major * 1000000 + parsedVersion.minor * 1000 + parsedVersion.patch;
|
||||||
|
|
||||||
if (versionCode >= 2050000000) {
|
if (versionCode >= 2050000000) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, test, vi } from 'vitest';
|
||||||
import Versioning from './versioning';
|
import Versioning from './versioning';
|
||||||
import UnityVersioning from './unity-versioning';
|
import UnityVersioning from './unity-versioning';
|
||||||
import AndroidVersioning from './android-versioning';
|
import AndroidVersioning from './android-versioning';
|
||||||
@@ -9,12 +10,12 @@ const testLicense =
|
|||||||
'<?xml version="1.0" encoding="UTF-8"?><root>\n <License id="Terms">\n <MachineBindings>\n <Binding Key="1" Value="576562626572264761624c65526f7578"/>\n <Binding Key="2" Value="576562626572264761624c65526f7578"/>\n </MachineBindings>\n <MachineID Value="D7nTUnjNAmtsUMcnoyrqkgIbYdM="/>\n <SerialHash Value="2033b8ac3e6faa3742ca9f0bfae44d18f2a96b80"/>\n <Features>\n <Feature Value="33"/>\n <Feature Value="1"/>\n <Feature Value="12"/>\n <Feature Value="2"/>\n <Feature Value="24"/>\n <Feature Value="3"/>\n <Feature Value="36"/>\n <Feature Value="17"/>\n <Feature Value="19"/>\n <Feature Value="62"/>\n </Features>\n <DeveloperData Value="AQAAAEY0LUJHUlgtWEQ0RS1aQ1dWLUM1SlctR0RIQg=="/>\n <SerialMasked Value="F4-BGRX-XD4E-ZCWV-C5JW-XXXX"/>\n <StartDate Value="2021-02-08T00:00:00"/>\n <UpdateDate Value="2021-02-09T00:34:57"/>\n <InitialActivationDate Value="2021-02-08T00:34:56"/>\n <LicenseVersion Value="6.x"/>\n <ClientProvidedVersion Value="2018.4.30f1"/>\n <AlwaysOnline Value="false"/>\n <Entitlements>\n <Entitlement Ns="unity_editor" Tag="UnityPersonal" Type="EDITOR" ValidTo="9999-12-31T00:00:00"/>\n <Entitlement Ns="unity_editor" Tag="DarkSkin" Type="EDITOR_FEATURE" ValidTo="9999-12-31T00:00:00"/>\n </Entitlements>\n </License>\n<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#Terms"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>m0Db8UK+ktnOLJBtHybkfetpcKo=</DigestValue></Reference></SignedInfo><SignatureValue>o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw==</SignatureValue></Signature></root>';
|
'<?xml version="1.0" encoding="UTF-8"?><root>\n <License id="Terms">\n <MachineBindings>\n <Binding Key="1" Value="576562626572264761624c65526f7578"/>\n <Binding Key="2" Value="576562626572264761624c65526f7578"/>\n </MachineBindings>\n <MachineID Value="D7nTUnjNAmtsUMcnoyrqkgIbYdM="/>\n <SerialHash Value="2033b8ac3e6faa3742ca9f0bfae44d18f2a96b80"/>\n <Features>\n <Feature Value="33"/>\n <Feature Value="1"/>\n <Feature Value="12"/>\n <Feature Value="2"/>\n <Feature Value="24"/>\n <Feature Value="3"/>\n <Feature Value="36"/>\n <Feature Value="17"/>\n <Feature Value="19"/>\n <Feature Value="62"/>\n </Features>\n <DeveloperData Value="AQAAAEY0LUJHUlgtWEQ0RS1aQ1dWLUM1SlctR0RIQg=="/>\n <SerialMasked Value="F4-BGRX-XD4E-ZCWV-C5JW-XXXX"/>\n <StartDate Value="2021-02-08T00:00:00"/>\n <UpdateDate Value="2021-02-09T00:34:57"/>\n <InitialActivationDate Value="2021-02-08T00:34:56"/>\n <LicenseVersion Value="6.x"/>\n <ClientProvidedVersion Value="2018.4.30f1"/>\n <AlwaysOnline Value="false"/>\n <Entitlements>\n <Entitlement Ns="unity_editor" Tag="UnityPersonal" Type="EDITOR" ValidTo="9999-12-31T00:00:00"/>\n <Entitlement Ns="unity_editor" Tag="DarkSkin" Type="EDITOR_FEATURE" ValidTo="9999-12-31T00:00:00"/>\n </Entitlements>\n </License>\n<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#Terms"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>m0Db8UK+ktnOLJBtHybkfetpcKo=</DigestValue></Reference></SignedInfo><SignatureValue>o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw==</SignatureValue></Signature></root>';
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
jest.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(Versioning, 'determineBuildVersion').mockImplementation(async () => '1.3.37');
|
vi.spyOn(Versioning, 'determineBuildVersion').mockImplementation(async () => '1.3.37');
|
||||||
process.env.UNITY_LICENSE = testLicense; // Todo - Don't use process.env directly, that's what the input model class is for.
|
process.env.UNITY_LICENSE = testLicense; // Todo - Don't use process.env directly, that's what the input model class is for.
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -25,20 +26,20 @@ describe('BuildParameters', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('determines the version only once', async () => {
|
it('determines the version only once', async () => {
|
||||||
jest.spyOn(Versioning, 'determineBuildVersion').mockImplementation(async () => '1.3.37');
|
vi.spyOn(Versioning, 'determineBuildVersion').mockImplementation(async () => '1.3.37');
|
||||||
await BuildParameters.create();
|
await BuildParameters.create();
|
||||||
await expect(Versioning.determineBuildVersion).toHaveBeenCalledTimes(1);
|
await expect(Versioning.determineBuildVersion).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('determines the unity version only once', async () => {
|
it('determines the unity version only once', async () => {
|
||||||
jest.spyOn(UnityVersioning, 'determineUnityVersion').mockImplementation(() => '2019.2.11f1');
|
vi.spyOn(UnityVersioning, 'determineUnityVersion').mockImplementation(() => '2019.2.11f1');
|
||||||
await BuildParameters.create();
|
await BuildParameters.create();
|
||||||
expect(UnityVersioning.determineUnityVersion).toHaveBeenCalledTimes(1);
|
expect(UnityVersioning.determineUnityVersion).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the android version code with provided input', async () => {
|
it('returns the android version code with provided input', async () => {
|
||||||
const mockValue = '42';
|
const mockValue = '42';
|
||||||
jest.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue);
|
vi.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
expect.objectContaining({ androidVersionCode: mockValue }),
|
expect.objectContaining({ androidVersionCode: mockValue }),
|
||||||
);
|
);
|
||||||
@@ -46,49 +47,59 @@ describe('BuildParameters', () => {
|
|||||||
|
|
||||||
it('returns the android version code from version by default', async () => {
|
it('returns the android version code from version by default', async () => {
|
||||||
const mockValue = '';
|
const mockValue = '';
|
||||||
jest.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue);
|
vi.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
expect.objectContaining({ androidVersionCode: '1003037' }),
|
expect.objectContaining({ androidVersionCode: '1003037' }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('determines the android sdk manager parameters only once', async () => {
|
it('determines the android sdk manager parameters only once', async () => {
|
||||||
jest.spyOn(AndroidVersioning, 'determineSdkManagerParameters').mockImplementation(() => 'platforms;android-30');
|
vi.spyOn(AndroidVersioning, 'determineSdkManagerParameters').mockImplementation(
|
||||||
|
() => 'platforms;android-30',
|
||||||
|
);
|
||||||
await BuildParameters.create();
|
await BuildParameters.create();
|
||||||
expect(AndroidVersioning.determineSdkManagerParameters).toHaveBeenCalledTimes(1);
|
expect(AndroidVersioning.determineSdkManagerParameters).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the targetPlatform', async () => {
|
it('returns the targetPlatform', async () => {
|
||||||
const mockValue = 'somePlatform';
|
const mockValue = 'somePlatform';
|
||||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockValue);
|
vi.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockValue);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ targetPlatform: mockValue }));
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
|
expect.objectContaining({ targetPlatform: mockValue }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the project path', async () => {
|
it('returns the project path', async () => {
|
||||||
const mockValue = 'path/to/project';
|
const mockValue = 'path/to/project';
|
||||||
jest.spyOn(UnityVersioning, 'determineUnityVersion').mockImplementation(() => '2019.2.11f1');
|
vi.spyOn(UnityVersioning, 'determineUnityVersion').mockImplementation(() => '2019.2.11f1');
|
||||||
jest.spyOn(Input, 'projectPath', 'get').mockReturnValue(mockValue);
|
vi.spyOn(Input, 'projectPath', 'get').mockReturnValue(mockValue);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ projectPath: mockValue }));
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
|
expect.objectContaining({ projectPath: mockValue }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the build profile', async () => {
|
it('returns the build profile', async () => {
|
||||||
const mockValue = 'path/to/build_profile.asset';
|
const mockValue = 'path/to/build_profile.asset';
|
||||||
jest.spyOn(Input, 'buildProfile', 'get').mockReturnValue(mockValue);
|
vi.spyOn(Input, 'buildProfile', 'get').mockReturnValue(mockValue);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildProfile: mockValue }));
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
|
expect.objectContaining({ buildProfile: mockValue }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the build name', async () => {
|
it('returns the build name', async () => {
|
||||||
const mockValue = 'someBuildName';
|
const mockValue = 'someBuildName';
|
||||||
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
|
vi.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildName: mockValue }));
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
|
expect.objectContaining({ buildName: mockValue }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the build path', async () => {
|
it('returns the build path', async () => {
|
||||||
const mockPath = 'somePath';
|
const mockPath = 'somePath';
|
||||||
const mockPlatform = 'somePlatform';
|
const mockPlatform = 'somePlatform';
|
||||||
const expectedBuildPath = `${mockPath}/${mockPlatform}`;
|
const expectedBuildPath = `${mockPath}/${mockPlatform}`;
|
||||||
jest.spyOn(Input, 'buildsPath', 'get').mockReturnValue(mockPath);
|
vi.spyOn(Input, 'buildsPath', 'get').mockReturnValue(mockPath);
|
||||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform);
|
vi.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
expect.objectContaining({ buildPath: expectedBuildPath }),
|
expect.objectContaining({ buildPath: expectedBuildPath }),
|
||||||
);
|
);
|
||||||
@@ -98,24 +109,29 @@ describe('BuildParameters', () => {
|
|||||||
const mockValue = 'someBuildName';
|
const mockValue = 'someBuildName';
|
||||||
const mockPlatform = 'somePlatform';
|
const mockPlatform = 'somePlatform';
|
||||||
|
|
||||||
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
|
vi.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
|
||||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform);
|
vi.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildFile: mockValue }));
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
|
expect.objectContaining({ buildFile: mockValue }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each`
|
test.each`
|
||||||
targetPlatform | expectedExtension | androidExportType
|
targetPlatform | expectedExtension | androidExportType | linux64RemoveExecutableExtension
|
||||||
${Platform.types.Android} | ${'.apk'} | ${'androidPackage'}
|
${Platform.types.Android} | ${'.apk'} | ${'androidPackage'} | ${false}
|
||||||
${Platform.types.Android} | ${'.aab'} | ${'androidAppBundle'}
|
${Platform.types.Android} | ${'.aab'} | ${'androidAppBundle'} | ${true}
|
||||||
${Platform.types.Android} | ${''} | ${'androidStudioProject'}
|
${Platform.types.Android} | ${''} | ${'androidStudioProject'} | ${false}
|
||||||
${Platform.types.StandaloneWindows} | ${'.exe'} | ${'n/a'}
|
${Platform.types.StandaloneWindows} | ${'.exe'} | ${'n/a'} | ${true}
|
||||||
${Platform.types.StandaloneWindows64} | ${'.exe'} | ${'n/a'}
|
${Platform.types.StandaloneWindows64} | ${'.exe'} | ${'n/a'} | ${false}
|
||||||
|
${Platform.types.StandaloneLinux64} | ${'.x86_64'} | ${'n/a'} | ${false}
|
||||||
|
${Platform.types.StandaloneLinux64} | ${''} | ${'n/a'} | ${true}
|
||||||
`(
|
`(
|
||||||
'appends $expectedExtension for $targetPlatform with androidExportType $androidExportType',
|
'appends $expectedExtension for $targetPlatform with linux64RemoveExecutableExtension=$linux64RemoveExecutableExtension',
|
||||||
async ({ targetPlatform, expectedExtension, androidExportType }) => {
|
async ({ targetPlatform, expectedExtension, androidExportType, linux64RemoveExecutableExtension }) => {
|
||||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
|
vi.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
|
||||||
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
|
vi.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
|
||||||
jest.spyOn(Input, 'androidExportType', 'get').mockReturnValue(androidExportType);
|
vi.spyOn(Input, 'androidExportType', 'get').mockReturnValue(androidExportType);
|
||||||
|
vi.spyOn(Input, 'linux64RemoveExecutableExtension', 'get').mockReturnValue(linux64RemoveExecutableExtension);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
expect.objectContaining({ buildFile: `${targetPlatform}${expectedExtension}` }),
|
expect.objectContaining({ buildFile: `${targetPlatform}${expectedExtension}` }),
|
||||||
);
|
);
|
||||||
@@ -132,22 +148,26 @@ describe('BuildParameters', () => {
|
|||||||
`(
|
`(
|
||||||
'androidSymbolType is set to $androidSymbolType when targetPlatform is $targetPlatform and input targetSymbolType is $androidSymbolType',
|
'androidSymbolType is set to $androidSymbolType when targetPlatform is $targetPlatform and input targetSymbolType is $androidSymbolType',
|
||||||
async ({ targetPlatform, androidSymbolType }) => {
|
async ({ targetPlatform, androidSymbolType }) => {
|
||||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
|
vi.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
|
||||||
jest.spyOn(Input, 'androidSymbolType', 'get').mockReturnValue(androidSymbolType);
|
vi.spyOn(Input, 'androidSymbolType', 'get').mockReturnValue(androidSymbolType);
|
||||||
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
|
vi.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidSymbolType }));
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
|
expect.objectContaining({ androidSymbolType }),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it('returns the build method', async () => {
|
it('returns the build method', async () => {
|
||||||
const mockValue = 'Namespace.ClassName.BuildMethod';
|
const mockValue = 'Namespace.ClassName.BuildMethod';
|
||||||
jest.spyOn(Input, 'buildMethod', 'get').mockReturnValue(mockValue);
|
vi.spyOn(Input, 'buildMethod', 'get').mockReturnValue(mockValue);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildMethod: mockValue }));
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
|
expect.objectContaining({ buildMethod: mockValue }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the android keystore name', async () => {
|
it('returns the android keystore name', async () => {
|
||||||
const mockValue = 'keystore.keystore';
|
const mockValue = 'keystore.keystore';
|
||||||
jest.spyOn(Input, 'androidKeystoreName', 'get').mockReturnValue(mockValue);
|
vi.spyOn(Input, 'androidKeystoreName', 'get').mockReturnValue(mockValue);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
expect.objectContaining({ androidKeystoreName: mockValue }),
|
expect.objectContaining({ androidKeystoreName: mockValue }),
|
||||||
);
|
);
|
||||||
@@ -155,7 +175,7 @@ describe('BuildParameters', () => {
|
|||||||
|
|
||||||
it('returns the android keystore base64-encoded content', async () => {
|
it('returns the android keystore base64-encoded content', async () => {
|
||||||
const mockValue = 'secret';
|
const mockValue = 'secret';
|
||||||
jest.spyOn(Input, 'androidKeystoreBase64', 'get').mockReturnValue(mockValue);
|
vi.spyOn(Input, 'androidKeystoreBase64', 'get').mockReturnValue(mockValue);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
expect.objectContaining({ androidKeystoreBase64: mockValue }),
|
expect.objectContaining({ androidKeystoreBase64: mockValue }),
|
||||||
);
|
);
|
||||||
@@ -163,7 +183,7 @@ describe('BuildParameters', () => {
|
|||||||
|
|
||||||
it('returns the android keystore pass', async () => {
|
it('returns the android keystore pass', async () => {
|
||||||
const mockValue = 'secret';
|
const mockValue = 'secret';
|
||||||
jest.spyOn(Input, 'androidKeystorePass', 'get').mockReturnValue(mockValue);
|
vi.spyOn(Input, 'androidKeystorePass', 'get').mockReturnValue(mockValue);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
expect.objectContaining({ androidKeystorePass: mockValue }),
|
expect.objectContaining({ androidKeystorePass: mockValue }),
|
||||||
);
|
);
|
||||||
@@ -171,7 +191,7 @@ describe('BuildParameters', () => {
|
|||||||
|
|
||||||
it('returns the android keyalias name', async () => {
|
it('returns the android keyalias name', async () => {
|
||||||
const mockValue = 'secret';
|
const mockValue = 'secret';
|
||||||
jest.spyOn(Input, 'androidKeyaliasName', 'get').mockReturnValue(mockValue);
|
vi.spyOn(Input, 'androidKeyaliasName', 'get').mockReturnValue(mockValue);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
expect.objectContaining({ androidKeyaliasName: mockValue }),
|
expect.objectContaining({ androidKeyaliasName: mockValue }),
|
||||||
);
|
);
|
||||||
@@ -179,7 +199,7 @@ describe('BuildParameters', () => {
|
|||||||
|
|
||||||
it('returns the android keyalias pass', async () => {
|
it('returns the android keyalias pass', async () => {
|
||||||
const mockValue = 'secret';
|
const mockValue = 'secret';
|
||||||
jest.spyOn(Input, 'androidKeyaliasPass', 'get').mockReturnValue(mockValue);
|
vi.spyOn(Input, 'androidKeyaliasPass', 'get').mockReturnValue(mockValue);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
expect.objectContaining({ androidKeyaliasPass: mockValue }),
|
expect.objectContaining({ androidKeyaliasPass: mockValue }),
|
||||||
);
|
);
|
||||||
@@ -187,7 +207,7 @@ describe('BuildParameters', () => {
|
|||||||
|
|
||||||
it('returns the android target sdk version', async () => {
|
it('returns the android target sdk version', async () => {
|
||||||
const mockValue = 'AndroidApiLevelAuto';
|
const mockValue = 'AndroidApiLevelAuto';
|
||||||
jest.spyOn(Input, 'androidTargetSdkVersion', 'get').mockReturnValue(mockValue);
|
vi.spyOn(Input, 'androidTargetSdkVersion', 'get').mockReturnValue(mockValue);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
expect.objectContaining({ androidTargetSdkVersion: mockValue }),
|
expect.objectContaining({ androidTargetSdkVersion: mockValue }),
|
||||||
);
|
);
|
||||||
@@ -195,7 +215,7 @@ describe('BuildParameters', () => {
|
|||||||
|
|
||||||
it('returns the unity licensing server address', async () => {
|
it('returns the unity licensing server address', async () => {
|
||||||
const mockValue = 'http://example.com';
|
const mockValue = 'http://example.com';
|
||||||
jest.spyOn(Input, 'unityLicensingServer', 'get').mockReturnValue(mockValue);
|
vi.spyOn(Input, 'unityLicensingServer', 'get').mockReturnValue(mockValue);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
expect.objectContaining({ unityLicensingServer: mockValue }),
|
expect.objectContaining({ unityLicensingServer: mockValue }),
|
||||||
);
|
);
|
||||||
@@ -210,14 +230,25 @@ describe('BuildParameters', () => {
|
|||||||
const mockValue = '123';
|
const mockValue = '123';
|
||||||
delete process.env.UNITY_LICENSE; // Need to delete this as it is set for every test currently
|
delete process.env.UNITY_LICENSE; // Need to delete this as it is set for every test currently
|
||||||
process.env.UNITY_SERIAL = mockValue;
|
process.env.UNITY_SERIAL = mockValue;
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ unitySerial: mockValue }));
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
|
expect.objectContaining({ unitySerial: mockValue }),
|
||||||
|
);
|
||||||
delete process.env.UNITY_SERIAL;
|
delete process.env.UNITY_SERIAL;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the custom parameters', async () => {
|
it('returns the custom parameters', async () => {
|
||||||
const mockValue = '-profile SomeProfile -someBoolean -someValue exampleValue';
|
const mockValue = '-profile SomeProfile -someBoolean -someValue exampleValue';
|
||||||
jest.spyOn(Input, 'customParameters', 'get').mockReturnValue(mockValue);
|
vi.spyOn(Input, 'customParameters', 'get').mockReturnValue(mockValue);
|
||||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ customParameters: mockValue }));
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
|
expect.objectContaining({ customParameters: mockValue }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([true, false])('returns the flag for useHostNetwork when %s', async (mockValue) => {
|
||||||
|
vi.spyOn(Input, 'useHostNetwork', 'get').mockReturnValue(mockValue);
|
||||||
|
await expect(BuildParameters.create()).resolves.toEqual(
|
||||||
|
expect.objectContaining({ useHostNetwork: mockValue }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
import AndroidVersioning from './android-versioning';
|
import AndroidVersioning from './android-versioning';
|
||||||
import OrchestratorConstants from './orchestrator/options/orchestrator-constants';
|
|
||||||
import OrchestratorBuildGuid from './orchestrator/options/orchestrator-guid';
|
|
||||||
import Input from './input';
|
import Input from './input';
|
||||||
import Platform from './platform';
|
import Platform from './platform';
|
||||||
import UnityVersioning from './unity-versioning';
|
import UnityVersioning from './unity-versioning';
|
||||||
@@ -10,8 +8,6 @@ import { GitRepoReader } from './input-readers/git-repo';
|
|||||||
import { GithubCliReader } from './input-readers/github-cli';
|
import { GithubCliReader } from './input-readers/github-cli';
|
||||||
import { Cli } from './cli/cli';
|
import { Cli } from './cli/cli';
|
||||||
import GitHub from './github';
|
import GitHub from './github';
|
||||||
import OrchestratorOptions from './orchestrator/options/orchestrator-options';
|
|
||||||
import Orchestrator from './orchestrator/orchestrator';
|
|
||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
|
|
||||||
class BuildParameters {
|
class BuildParameters {
|
||||||
@@ -51,72 +47,49 @@ class BuildParameters {
|
|||||||
public containerRegistryImageVersion!: string;
|
public containerRegistryImageVersion!: string;
|
||||||
|
|
||||||
public customParameters!: string;
|
public customParameters!: string;
|
||||||
|
public useHostNetwork!: boolean;
|
||||||
public sshAgent!: string;
|
public sshAgent!: string;
|
||||||
public sshPublicKeysDirectoryPath!: string;
|
public sshPublicKeysDirectoryPath!: string;
|
||||||
public providerStrategy!: string;
|
public providerStrategy!: string;
|
||||||
public gitPrivateToken!: string;
|
public gitPrivateToken!: string;
|
||||||
public awsStackName!: string;
|
|
||||||
public awsEndpoint?: string;
|
|
||||||
public awsCloudFormationEndpoint?: string;
|
|
||||||
public awsEcsEndpoint?: string;
|
|
||||||
public awsKinesisEndpoint?: string;
|
|
||||||
public awsCloudWatchLogsEndpoint?: string;
|
|
||||||
public awsS3Endpoint?: string;
|
|
||||||
public storageProvider!: string;
|
|
||||||
public rcloneRemote!: string;
|
|
||||||
public kubeConfig!: string;
|
|
||||||
public containerMemory!: string;
|
|
||||||
public containerCpu!: string;
|
|
||||||
public containerNamespace!: string;
|
|
||||||
public kubeVolumeSize!: string;
|
|
||||||
public kubeVolume!: string;
|
|
||||||
public kubeStorageClass!: string;
|
|
||||||
public runAsHostUser!: string;
|
public runAsHostUser!: string;
|
||||||
public chownFilesTo!: string;
|
public chownFilesTo!: string;
|
||||||
public commandHooks!: string;
|
|
||||||
public pullInputList!: string[];
|
|
||||||
public inputPullCommand!: string;
|
|
||||||
public cacheKey!: string;
|
|
||||||
|
|
||||||
public postBuildContainerHooks!: string;
|
|
||||||
public preBuildContainerHooks!: string;
|
|
||||||
public customJob!: string;
|
|
||||||
public runNumber!: string;
|
public runNumber!: string;
|
||||||
public branch!: string;
|
public branch!: string;
|
||||||
public githubRepo!: string;
|
public githubRepo!: string;
|
||||||
public orchestratorRepoName!: string;
|
|
||||||
public cloneDepth!: number;
|
|
||||||
public gitSha!: string;
|
public gitSha!: string;
|
||||||
public logId!: string;
|
public logId!: string;
|
||||||
public buildGuid!: string;
|
public buildGuid!: string;
|
||||||
public orchestratorBranch!: string;
|
|
||||||
public orchestratorDebug!: boolean | undefined;
|
|
||||||
public buildPlatform!: string | undefined;
|
public buildPlatform!: string | undefined;
|
||||||
public isCliMode!: boolean;
|
public isCliMode!: boolean;
|
||||||
public maxRetainedWorkspaces!: number;
|
|
||||||
public useLargePackages!: boolean;
|
|
||||||
public useCompressionStrategy!: boolean;
|
|
||||||
public garbageMaxAge!: number;
|
|
||||||
public githubChecks!: boolean;
|
|
||||||
public asyncWorkflow!: boolean;
|
|
||||||
public githubCheckId!: string;
|
|
||||||
public finalHooks!: string[];
|
|
||||||
public skipLfs!: boolean;
|
|
||||||
public skipCache!: boolean;
|
|
||||||
public cacheUnityInstallationOnMac!: boolean;
|
public cacheUnityInstallationOnMac!: boolean;
|
||||||
public unityHubVersionOnMac!: string;
|
public unityHubVersionOnMac!: string;
|
||||||
public dockerWorkspacePath!: string;
|
public dockerWorkspacePath!: string;
|
||||||
|
|
||||||
public static shouldUseRetainedWorkspaceMode(buildParameters: BuildParameters) {
|
|
||||||
return buildParameters.maxRetainedWorkspaces > 0 && Orchestrator.lockedWorkspace !== ``;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async create(): Promise<BuildParameters> {
|
static async create(): Promise<BuildParameters> {
|
||||||
const buildFile = this.parseBuildFile(Input.buildName, Input.targetPlatform, Input.androidExportType);
|
const buildFile = this.parseBuildFile(
|
||||||
const editorVersion = UnityVersioning.determineUnityVersion(Input.projectPath, Input.unityVersion);
|
Input.buildName,
|
||||||
const buildVersion = await Versioning.determineBuildVersion(Input.versioningStrategy, Input.specifiedVersion);
|
Input.targetPlatform,
|
||||||
const androidVersionCode = AndroidVersioning.determineVersionCode(buildVersion, Input.androidVersionCode);
|
Input.androidExportType,
|
||||||
const androidSdkManagerParameters = AndroidVersioning.determineSdkManagerParameters(Input.androidTargetSdkVersion);
|
Input.linux64RemoveExecutableExtension,
|
||||||
|
);
|
||||||
|
const editorVersion = UnityVersioning.determineUnityVersion(
|
||||||
|
Input.projectPath,
|
||||||
|
Input.unityVersion,
|
||||||
|
);
|
||||||
|
const buildVersion = await Versioning.determineBuildVersion(
|
||||||
|
Input.versioningStrategy,
|
||||||
|
Input.specifiedVersion,
|
||||||
|
);
|
||||||
|
const androidVersionCode = AndroidVersioning.determineVersionCode(
|
||||||
|
buildVersion,
|
||||||
|
Input.androidVersionCode,
|
||||||
|
);
|
||||||
|
const androidSdkManagerParameters = AndroidVersioning.determineSdkManagerParameters(
|
||||||
|
Input.androidTargetSdkVersion,
|
||||||
|
);
|
||||||
|
|
||||||
const androidSymbolExportType = Input.androidSymbolType;
|
const androidSymbolExportType = Input.androidSymbolType;
|
||||||
if (Platform.isAndroid(Input.targetPlatform)) {
|
if (Platform.isAndroid(Input.targetPlatform)) {
|
||||||
@@ -155,6 +128,9 @@ class BuildParameters {
|
|||||||
core.setSecret(`${unitySerial.slice(0, -4)}XXXX`);
|
core.setSecret(`${unitySerial.slice(0, -4)}XXXX`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const providerStrategy =
|
||||||
|
Input.getInput('providerStrategy') || (Cli.isCliMode ? 'aws' : 'local');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
editorVersion,
|
editorVersion,
|
||||||
customImage: Input.customImage,
|
customImage: Input.customImage,
|
||||||
@@ -183,6 +159,7 @@ class BuildParameters {
|
|||||||
androidExportType: Input.androidExportType,
|
androidExportType: Input.androidExportType,
|
||||||
androidSymbolType: androidSymbolExportType,
|
androidSymbolType: androidSymbolExportType,
|
||||||
customParameters: Input.customParameters,
|
customParameters: Input.customParameters,
|
||||||
|
useHostNetwork: Input.useHostNetwork,
|
||||||
sshAgent: Input.sshAgent,
|
sshAgent: Input.sshAgent,
|
||||||
sshPublicKeysDirectoryPath: Input.sshPublicKeysDirectoryPath,
|
sshPublicKeysDirectoryPath: Input.sshPublicKeysDirectoryPath,
|
||||||
gitPrivateToken: Input.gitPrivateToken ?? (await GithubCliReader.GetGitHubAuthToken()),
|
gitPrivateToken: Input.gitPrivateToken ?? (await GithubCliReader.GetGitHubAuthToken()),
|
||||||
@@ -193,59 +170,31 @@ class BuildParameters {
|
|||||||
dockerIsolationMode: Input.dockerIsolationMode,
|
dockerIsolationMode: Input.dockerIsolationMode,
|
||||||
containerRegistryRepository: Input.containerRegistryRepository,
|
containerRegistryRepository: Input.containerRegistryRepository,
|
||||||
containerRegistryImageVersion: Input.containerRegistryImageVersion,
|
containerRegistryImageVersion: Input.containerRegistryImageVersion,
|
||||||
providerStrategy: OrchestratorOptions.providerStrategy,
|
providerStrategy,
|
||||||
buildPlatform: OrchestratorOptions.buildPlatform,
|
buildPlatform: providerStrategy !== 'local' ? 'linux' : process.platform,
|
||||||
kubeConfig: OrchestratorOptions.kubeConfig,
|
|
||||||
containerMemory: OrchestratorOptions.containerMemory,
|
|
||||||
containerCpu: OrchestratorOptions.containerCpu,
|
|
||||||
containerNamespace: OrchestratorOptions.containerNamespace,
|
|
||||||
kubeVolumeSize: OrchestratorOptions.kubeVolumeSize,
|
|
||||||
kubeVolume: OrchestratorOptions.kubeVolume,
|
|
||||||
postBuildContainerHooks: OrchestratorOptions.postBuildContainerHooks,
|
|
||||||
preBuildContainerHooks: OrchestratorOptions.preBuildContainerHooks,
|
|
||||||
customJob: OrchestratorOptions.customJob,
|
|
||||||
runNumber: Input.runNumber,
|
runNumber: Input.runNumber,
|
||||||
branch: Input.branch.replace('/head', '') || (await GitRepoReader.GetBranch()),
|
branch: Input.branch.replace('/head', '') || (await GitRepoReader.GetBranch()),
|
||||||
orchestratorBranch: OrchestratorOptions.orchestratorBranch.split('/').reverse()[0],
|
githubRepo:
|
||||||
orchestratorDebug: OrchestratorOptions.orchestratorDebug,
|
(Input.githubRepo ?? (await GitRepoReader.GetRemote())) || 'game-ci/unity-builder',
|
||||||
githubRepo: (Input.githubRepo ?? (await GitRepoReader.GetRemote())) || OrchestratorOptions.orchestratorRepoName,
|
|
||||||
orchestratorRepoName: OrchestratorOptions.orchestratorRepoName,
|
|
||||||
cloneDepth: Number.parseInt(OrchestratorOptions.cloneDepth),
|
|
||||||
isCliMode: Cli.isCliMode,
|
|
||||||
awsStackName: OrchestratorOptions.awsStackName,
|
|
||||||
awsEndpoint: OrchestratorOptions.awsEndpoint,
|
|
||||||
awsCloudFormationEndpoint: OrchestratorOptions.awsCloudFormationEndpoint,
|
|
||||||
awsEcsEndpoint: OrchestratorOptions.awsEcsEndpoint,
|
|
||||||
awsKinesisEndpoint: OrchestratorOptions.awsKinesisEndpoint,
|
|
||||||
awsCloudWatchLogsEndpoint: OrchestratorOptions.awsCloudWatchLogsEndpoint,
|
|
||||||
awsS3Endpoint: OrchestratorOptions.awsS3Endpoint,
|
|
||||||
storageProvider: OrchestratorOptions.storageProvider,
|
|
||||||
rcloneRemote: OrchestratorOptions.rcloneRemote,
|
|
||||||
gitSha: Input.gitSha,
|
gitSha: Input.gitSha,
|
||||||
logId: customAlphabet(OrchestratorConstants.alphabet, 9)(),
|
logId: customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 9)(),
|
||||||
buildGuid: OrchestratorBuildGuid.generateGuid(Input.runNumber, Input.targetPlatform),
|
buildGuid: `${Input.runNumber}-${Input.targetPlatform.toLowerCase().replace('standalone', '')}-${customAlphabet(
|
||||||
commandHooks: OrchestratorOptions.commandHooks,
|
'0123456789abcdefghijklmnopqrstuvwxyz',
|
||||||
inputPullCommand: OrchestratorOptions.inputPullCommand,
|
4,
|
||||||
pullInputList: OrchestratorOptions.pullInputList,
|
)()}`,
|
||||||
kubeStorageClass: OrchestratorOptions.kubeStorageClass,
|
isCliMode: Cli.isCliMode,
|
||||||
cacheKey: OrchestratorOptions.cacheKey,
|
|
||||||
maxRetainedWorkspaces: Number.parseInt(OrchestratorOptions.maxRetainedWorkspaces),
|
|
||||||
useLargePackages: OrchestratorOptions.useLargePackages,
|
|
||||||
useCompressionStrategy: OrchestratorOptions.useCompressionStrategy,
|
|
||||||
garbageMaxAge: OrchestratorOptions.garbageMaxAge,
|
|
||||||
githubChecks: OrchestratorOptions.githubChecks,
|
|
||||||
asyncWorkflow: OrchestratorOptions.asyncOrchestrator,
|
|
||||||
githubCheckId: OrchestratorOptions.githubCheckId,
|
|
||||||
finalHooks: OrchestratorOptions.finalHooks,
|
|
||||||
skipLfs: OrchestratorOptions.skipLfs,
|
|
||||||
skipCache: OrchestratorOptions.skipCache,
|
|
||||||
cacheUnityInstallationOnMac: Input.cacheUnityInstallationOnMac,
|
cacheUnityInstallationOnMac: Input.cacheUnityInstallationOnMac,
|
||||||
unityHubVersionOnMac: Input.unityHubVersionOnMac,
|
unityHubVersionOnMac: Input.unityHubVersionOnMac,
|
||||||
dockerWorkspacePath: Input.dockerWorkspacePath,
|
dockerWorkspacePath: Input.dockerWorkspacePath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static parseBuildFile(filename: string, platform: string, androidExportType: string): string {
|
static parseBuildFile(
|
||||||
|
filename: string,
|
||||||
|
platform: string,
|
||||||
|
androidExportType: string,
|
||||||
|
linux64RemoveExecutableExtension: boolean,
|
||||||
|
): string {
|
||||||
if (Platform.isWindows(platform)) {
|
if (Platform.isWindows(platform)) {
|
||||||
return `${filename}.exe`;
|
return `${filename}.exe`;
|
||||||
}
|
}
|
||||||
@@ -265,6 +214,10 @@ class BuildParameters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (platform === Platform.types.StandaloneLinux64 && !linux64RemoveExecutableExtension) {
|
||||||
|
return `${filename}.x86_64`;
|
||||||
|
}
|
||||||
|
|
||||||
return filename;
|
return filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, vi } from 'vitest';
|
||||||
import Cache from './cache';
|
import Cache from './cache';
|
||||||
|
|
||||||
jest.mock('./input');
|
vi.mock('./input');
|
||||||
|
|
||||||
describe('Cache', () => {
|
describe('Cache', () => {
|
||||||
describe('Verification', () => {
|
describe('Verification', () => {
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import { Command } from 'commander-ts';
|
import { Command } from 'commander-ts';
|
||||||
import { BuildParameters, Orchestrator, ImageTag, Input } from '..';
|
import { Input } from '..';
|
||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
import { ActionYamlReader } from '../input-readers/action-yaml';
|
import { ActionYamlReader } from '../input-readers/action-yaml';
|
||||||
import OrchestratorLogger from '../orchestrator/services/core/orchestrator-logger';
|
|
||||||
import OrchestratorQueryOverride from '../orchestrator/options/orchestrator-query-override';
|
|
||||||
import { CliFunction, CliFunctionsRepository } from './cli-functions-repository';
|
import { CliFunction, CliFunctionsRepository } from './cli-functions-repository';
|
||||||
import { Caching } from '../orchestrator/remote-client/caching';
|
|
||||||
import { LfsHashing } from '../orchestrator/services/utility/lfs-hashing';
|
|
||||||
import { RemoteClient } from '../orchestrator/remote-client';
|
|
||||||
import OrchestratorOptionsReader from '../orchestrator/options/orchestrator-options-reader';
|
|
||||||
import GitHub from '../github';
|
|
||||||
import { OptionValues } from 'commander';
|
import { OptionValues } from 'commander';
|
||||||
import { InputKey } from '../input';
|
import { InputKey } from '../input';
|
||||||
|
|
||||||
@@ -30,14 +23,13 @@ export class Cli {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static InitCliMode() {
|
public static InitCliMode() {
|
||||||
CliFunctionsRepository.PushCliFunctionSource(RemoteClient);
|
|
||||||
CliFunctionsRepository.PushCliFunctionSource(Caching);
|
|
||||||
CliFunctionsRepository.PushCliFunctionSource(LfsHashing);
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
program.version('0.0.1');
|
program.version('0.0.1');
|
||||||
|
|
||||||
const properties = OrchestratorOptionsReader.GetProperties();
|
|
||||||
const actionYamlReader: ActionYamlReader = new ActionYamlReader();
|
const actionYamlReader: ActionYamlReader = new ActionYamlReader();
|
||||||
|
const properties = Object.getOwnPropertyNames(Input).filter(
|
||||||
|
(p) => p !== 'length' && p !== 'prototype' && p !== 'name',
|
||||||
|
);
|
||||||
for (const element of properties) {
|
for (const element of properties) {
|
||||||
program.option(`--${element} <${element}>`, actionYamlReader.GetActionYamlValue(element));
|
program.option(`--${element} <${element}>`, actionYamlReader.GetActionYamlValue(element));
|
||||||
}
|
}
|
||||||
@@ -47,12 +39,23 @@ export class Cli {
|
|||||||
.map((x) => `${x.key} (${x.description})`)
|
.map((x) => `${x.key} (${x.description})`)
|
||||||
.join(` | `),
|
.join(` | `),
|
||||||
);
|
);
|
||||||
program.option('--populateOverride <populateOverride>', 'should use override query to pull input false by default');
|
program.option(
|
||||||
|
'--populateOverride <populateOverride>',
|
||||||
|
'should use override query to pull input false by default',
|
||||||
|
);
|
||||||
program.option('--cachePushFrom <cachePushFrom>', 'cache push from source folder');
|
program.option('--cachePushFrom <cachePushFrom>', 'cache push from source folder');
|
||||||
program.option('--cachePushTo <cachePushTo>', 'cache push to caching folder');
|
program.option('--cachePushTo <cachePushTo>', 'cache push to caching folder');
|
||||||
program.option('--artifactName <artifactName>', 'caching artifact name');
|
program.option('--artifactName <artifactName>', 'caching artifact name');
|
||||||
program.option('--select <select>', 'select a particular resource');
|
program.option('--select <select>', 'select a particular resource');
|
||||||
program.option('--logFile <logFile>', 'output to log file (log stream only)');
|
program.option('--logFile <logFile>', 'output to log file (log stream only)');
|
||||||
|
program.option('--profilePath <profilePath>', 'path to submodule profile YAML');
|
||||||
|
program.option('--variantPath <variantPath>', 'path to submodule variant YAML');
|
||||||
|
program.option('--agentPath <agentPath>', 'path to custom LFS transfer agent');
|
||||||
|
program.option('--agentArgs <agentArgs>', 'arguments for custom LFS transfer agent');
|
||||||
|
program.option(
|
||||||
|
'--storagePaths <storagePaths>',
|
||||||
|
'semicolon-separated storage paths for LFS agent',
|
||||||
|
);
|
||||||
program.parse(process.argv);
|
program.parse(process.argv);
|
||||||
Cli.options = program.opts();
|
Cli.options = program.opts();
|
||||||
|
|
||||||
@@ -60,26 +63,15 @@ export class Cli {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async RunCli(): Promise<void> {
|
static async RunCli(): Promise<void> {
|
||||||
GitHub.githubInputEnabled = false;
|
|
||||||
if (Cli.options!['populateOverride'] === `true`) {
|
|
||||||
await OrchestratorQueryOverride.PopulateQueryOverrideInput();
|
|
||||||
}
|
|
||||||
if (Cli.options!['logInput']) {
|
|
||||||
Cli.logInput();
|
|
||||||
}
|
|
||||||
const results = CliFunctionsRepository.GetCliFunctions(Cli.options?.mode);
|
const results = CliFunctionsRepository.GetCliFunctions(Cli.options?.mode);
|
||||||
OrchestratorLogger.log(`Entrypoint: ${results.key}`);
|
if (!results) {
|
||||||
|
throw new Error(
|
||||||
|
`Unknown CLI mode: ${Cli.options?.mode}. Orchestrator CLI features require @game-ci/orchestrator.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
core.info(`Entrypoint: ${results.key}`);
|
||||||
Cli.options!.versioning = 'None';
|
Cli.options!.versioning = 'None';
|
||||||
|
|
||||||
Orchestrator.buildParameters = await BuildParameters.create();
|
|
||||||
Orchestrator.buildParameters.buildGuid = process.env.BUILD_GUID || ``;
|
|
||||||
OrchestratorLogger.log(`Build Params:
|
|
||||||
${JSON.stringify(Orchestrator.buildParameters, undefined, 4)}
|
|
||||||
`);
|
|
||||||
Orchestrator.lockedWorkspace = process.env.LOCKED_WORKSPACE || ``;
|
|
||||||
OrchestratorLogger.log(`Locked Workspace: ${Orchestrator.lockedWorkspace}`);
|
|
||||||
await Orchestrator.setup(Orchestrator.buildParameters);
|
|
||||||
|
|
||||||
return await results.target[results.propertyKey](Cli.options);
|
return await results.target[results.propertyKey](Cli.options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +79,9 @@ export class Cli {
|
|||||||
private static logInput() {
|
private static logInput() {
|
||||||
core.info(`\n`);
|
core.info(`\n`);
|
||||||
core.info(`INPUT:`);
|
core.info(`INPUT:`);
|
||||||
const properties = OrchestratorOptionsReader.GetProperties();
|
const properties = Object.getOwnPropertyNames(Input).filter(
|
||||||
|
(p) => p !== 'length' && p !== 'prototype' && p !== 'name',
|
||||||
|
);
|
||||||
for (const element of properties) {
|
for (const element of properties) {
|
||||||
if (
|
if (
|
||||||
element in Input &&
|
element in Input &&
|
||||||
@@ -103,73 +97,4 @@ export class Cli {
|
|||||||
}
|
}
|
||||||
core.info(`\n`);
|
core.info(`\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@CliFunction(`cli-build`, `runs a orchestrator build`)
|
|
||||||
public static async CLIBuild(): Promise<string> {
|
|
||||||
const buildParameter = await BuildParameters.create();
|
|
||||||
const baseImage = new ImageTag(buildParameter);
|
|
||||||
|
|
||||||
return (await Orchestrator.run(buildParameter, baseImage.toString())).BuildResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
@CliFunction(`async-workflow`, `runs a orchestrator build`)
|
|
||||||
public static async asyncronousWorkflow(): Promise<string> {
|
|
||||||
const buildParameter = await BuildParameters.create();
|
|
||||||
const baseImage = new ImageTag(buildParameter);
|
|
||||||
await Orchestrator.setup(buildParameter);
|
|
||||||
|
|
||||||
return (await Orchestrator.run(buildParameter, baseImage.toString())).BuildResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
@CliFunction(`checks-update`, `runs a orchestrator build`)
|
|
||||||
public static async checksUpdate() {
|
|
||||||
const buildParameter = await BuildParameters.create();
|
|
||||||
|
|
||||||
await Orchestrator.setup(buildParameter);
|
|
||||||
const input = JSON.parse(process.env.CHECKS_UPDATE || ``);
|
|
||||||
core.info(`Checks Update ${process.env.CHECKS_UPDATE}`);
|
|
||||||
if (input.mode === `create`) {
|
|
||||||
throw new Error(`Not supported: only use update`);
|
|
||||||
} else if (input.mode === `update`) {
|
|
||||||
await GitHub.updateGitHubCheckRequest(input.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@CliFunction(`garbage-collect`, `runs garbage collection`)
|
|
||||||
public static async GarbageCollect(): Promise<string> {
|
|
||||||
const buildParameter = await BuildParameters.create();
|
|
||||||
|
|
||||||
await Orchestrator.setup(buildParameter);
|
|
||||||
|
|
||||||
return await Orchestrator.Provider.garbageCollect(``, false, 0, false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@CliFunction(`list-resources`, `lists active resources`)
|
|
||||||
public static async ListResources(): Promise<string[]> {
|
|
||||||
const buildParameter = await BuildParameters.create();
|
|
||||||
|
|
||||||
await Orchestrator.setup(buildParameter);
|
|
||||||
const result = await Orchestrator.Provider.listResources();
|
|
||||||
OrchestratorLogger.log(JSON.stringify(result, undefined, 4));
|
|
||||||
|
|
||||||
return result.map((x) => x.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@CliFunction(`list-workflow`, `lists running workflows`)
|
|
||||||
public static async ListWorkflow(): Promise<string[]> {
|
|
||||||
const buildParameter = await BuildParameters.create();
|
|
||||||
|
|
||||||
await Orchestrator.setup(buildParameter);
|
|
||||||
|
|
||||||
return (await Orchestrator.Provider.listWorkflow()).map((x) => x.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@CliFunction(`watch`, `follows logs of a running workflow`)
|
|
||||||
public static async Watch(): Promise<string> {
|
|
||||||
const buildParameter = await BuildParameters.create();
|
|
||||||
|
|
||||||
await Orchestrator.setup(buildParameter);
|
|
||||||
|
|
||||||
return await Orchestrator.Provider.watchWorkflow();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||||
import Action from './action';
|
import Action from './action';
|
||||||
import Docker from './docker';
|
import Docker from './docker';
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,13 @@ class Docker {
|
|||||||
let runCommand = '';
|
let runCommand = '';
|
||||||
switch (process.platform) {
|
switch (process.platform) {
|
||||||
case 'linux':
|
case 'linux':
|
||||||
runCommand = this.getLinuxCommand(image, parameters, overrideCommands, additionalVariables, entrypointBash);
|
runCommand = this.getLinuxCommand(
|
||||||
|
image,
|
||||||
|
parameters,
|
||||||
|
overrideCommands,
|
||||||
|
additionalVariables,
|
||||||
|
entrypointBash,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'win32':
|
case 'win32':
|
||||||
runCommand = this.getWindowsCommand(image, parameters);
|
runCommand = this.getWindowsCommand(image, parameters);
|
||||||
@@ -42,6 +48,7 @@ class Docker {
|
|||||||
const {
|
const {
|
||||||
workspace,
|
workspace,
|
||||||
actionFolder,
|
actionFolder,
|
||||||
|
useHostNetwork,
|
||||||
runnerTempPath,
|
runnerTempPath,
|
||||||
sshAgent,
|
sshAgent,
|
||||||
sshPublicKeysDirectoryPath,
|
sshPublicKeysDirectoryPath,
|
||||||
@@ -85,6 +92,7 @@ class Docker {
|
|||||||
: ''
|
: ''
|
||||||
} \
|
} \
|
||||||
${sshPublicKeysDirectoryPath ? `--volume ${sshPublicKeysDirectoryPath}:/root/.ssh:ro` : ''} \
|
${sshPublicKeysDirectoryPath ? `--volume ${sshPublicKeysDirectoryPath}:/root/.ssh:ro` : ''} \
|
||||||
|
${useHostNetwork ? '--net=host' : ''} \
|
||||||
${entrypointBash ? `--entrypoint ${commandPrefix}` : ``} \
|
${entrypointBash ? `--entrypoint ${commandPrefix}` : ``} \
|
||||||
${image} \
|
${image} \
|
||||||
${entrypointBash ? `-c` : `${commandPrefix} -c`} \
|
${entrypointBash ? `-c` : `${commandPrefix} -c`} \
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||||
import CommandExecutionError from './command-execution-error';
|
import CommandExecutionError from './command-execution-error';
|
||||||
|
|
||||||
describe('CommandExecutionError', () => {
|
describe('CommandExecutionError', () => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||||
import NotImplementedException from './not-implemented-exception';
|
import NotImplementedException from './not-implemented-exception';
|
||||||
|
|
||||||
describe('NotImplementedException', () => {
|
describe('NotImplementedException', () => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||||
import ValidationError from './validation-error';
|
import ValidationError from './validation-error';
|
||||||
|
|
||||||
describe('ValidationError', () => {
|
describe('ValidationError', () => {
|
||||||
|
|||||||
@@ -1,222 +1,5 @@
|
|||||||
import OrchestratorLogger from './orchestrator/services/core/orchestrator-logger';
|
|
||||||
import Orchestrator from './orchestrator/orchestrator';
|
|
||||||
import OrchestratorOptions from './orchestrator/options/orchestrator-options';
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
import { Octokit } from '@octokit/core';
|
|
||||||
|
|
||||||
class GitHub {
|
class GitHub {
|
||||||
private static readonly asyncChecksApiWorkflowName = `Async Checks API`;
|
|
||||||
public static githubInputEnabled: boolean = true;
|
public static githubInputEnabled: boolean = true;
|
||||||
private static longDescriptionContent: string = ``;
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
private static get octokitPAT() {
|
|
||||||
return new Octokit({
|
|
||||||
auth: Orchestrator.buildParameters.gitPrivateToken,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
private static get sha() {
|
|
||||||
return Orchestrator.buildParameters.gitSha;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static get checkName() {
|
|
||||||
return `Orchestrator (${Orchestrator.buildParameters.buildGuid})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static get nameReadable() {
|
|
||||||
return GitHub.checkName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static get checkRunId() {
|
|
||||||
return Orchestrator.buildParameters.githubCheckId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static get owner() {
|
|
||||||
return OrchestratorOptions.githubOwner;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static get repo() {
|
|
||||||
return OrchestratorOptions.githubRepoName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async createGitHubCheck(summary: string) {
|
|
||||||
if (!Orchestrator.buildParameters.githubChecks) {
|
|
||||||
return ``;
|
|
||||||
}
|
|
||||||
GitHub.startedDate = new Date().toISOString();
|
|
||||||
|
|
||||||
OrchestratorLogger.log(`Creating github check`);
|
|
||||||
const data = {
|
|
||||||
owner: GitHub.owner,
|
|
||||||
repo: GitHub.repo,
|
|
||||||
name: GitHub.checkName,
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
head_sha: GitHub.sha,
|
|
||||||
status: 'queued',
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
external_id: Orchestrator.buildParameters.buildGuid,
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
started_at: GitHub.startedDate,
|
|
||||||
output: {
|
|
||||||
title: GitHub.nameReadable,
|
|
||||||
summary,
|
|
||||||
text: '',
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
alt: 'Game-CI',
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
image_url: 'https://game.ci/assets/images/game-ci-brand-logo-wordmark.svg',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const result = await GitHub.createGitHubCheckRequest(data);
|
|
||||||
|
|
||||||
OrchestratorLogger.log(`Creating github check ${result.status}`);
|
|
||||||
|
|
||||||
return result.data.id.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async updateGitHubCheck(
|
|
||||||
longDescription: string,
|
|
||||||
summary: string,
|
|
||||||
result = `neutral`,
|
|
||||||
status = `in_progress`,
|
|
||||||
) {
|
|
||||||
if (`${Orchestrator.buildParameters.githubChecks}` !== `true`) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
OrchestratorLogger.log(
|
|
||||||
`githubChecks: ${Orchestrator.buildParameters.githubChecks} checkRunId: ${GitHub.checkRunId} sha: ${GitHub.sha} async: ${Orchestrator.isOrchestratorAsyncEnvironment}`,
|
|
||||||
);
|
|
||||||
GitHub.longDescriptionContent += `\n${longDescription}`;
|
|
||||||
if (GitHub.result !== `success` && GitHub.result !== `failure`) {
|
|
||||||
GitHub.result = result;
|
|
||||||
} else {
|
|
||||||
result = GitHub.result;
|
|
||||||
}
|
|
||||||
const data: any = {
|
|
||||||
owner: GitHub.owner,
|
|
||||||
repo: GitHub.repo,
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
check_run_id: GitHub.checkRunId,
|
|
||||||
name: GitHub.checkName,
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
head_sha: GitHub.sha,
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
started_at: GitHub.startedDate,
|
|
||||||
status,
|
|
||||||
output: {
|
|
||||||
title: GitHub.nameReadable,
|
|
||||||
summary,
|
|
||||||
text: GitHub.longDescriptionContent,
|
|
||||||
annotations: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (status === `completed`) {
|
|
||||||
if (GitHub.endedDate !== undefined) {
|
|
||||||
GitHub.endedDate = new Date().toISOString();
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
data.completed_at = GitHub.endedDate || GitHub.startedDate;
|
|
||||||
data.conclusion = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
await (Orchestrator.isOrchestratorAsyncEnvironment || GitHub.forceAsyncTest
|
|
||||||
? GitHub.runUpdateAsyncChecksWorkflow(data, `update`)
|
|
||||||
: GitHub.updateGitHubCheckRequest(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async updateGitHubCheckRequest(data: any) {
|
|
||||||
return await GitHub.octokitDefaultToken.request(`PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async createGitHubCheckRequest(data: any) {
|
|
||||||
return await GitHub.octokitDefaultToken.request(`POST /repos/{owner}/{repo}/check-runs`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async runUpdateAsyncChecksWorkflow(data: any, mode: string) {
|
|
||||||
if (mode === `create`) {
|
|
||||||
throw new Error(`Not supported: only use update`);
|
|
||||||
}
|
|
||||||
const workflowsResult = await GitHub.octokitPAT.request(`GET /repos/{owner}/{repo}/actions/workflows`, {
|
|
||||||
owner: GitHub.owner,
|
|
||||||
repo: GitHub.repo,
|
|
||||||
});
|
|
||||||
const workflows = workflowsResult.data.workflows;
|
|
||||||
OrchestratorLogger.log(`Got ${workflows.length} workflows`);
|
|
||||||
let selectedId = ``;
|
|
||||||
for (let index = 0; index < workflowsResult.data.total_count; index++) {
|
|
||||||
if (workflows[index].name === GitHub.asyncChecksApiWorkflowName) {
|
|
||||||
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: OrchestratorOptions.branch,
|
|
||||||
inputs: {
|
|
||||||
checksObject: JSON.stringify({ data, mode }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async triggerWorkflowOnComplete(triggerWorkflowOnComplete: string[]) {
|
|
||||||
const isLocalAsync = Orchestrator.buildParameters.asyncWorkflow && !Orchestrator.isOrchestratorAsyncEnvironment;
|
|
||||||
if (isLocalAsync || triggerWorkflowOnComplete === undefined || triggerWorkflowOnComplete.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const workflowsResult = await GitHub.octokitPAT.request(`GET /repos/{owner}/{repo}/actions/workflows`, {
|
|
||||||
owner: GitHub.owner,
|
|
||||||
repo: GitHub.repo,
|
|
||||||
});
|
|
||||||
const workflows = workflowsResult.data.workflows;
|
|
||||||
OrchestratorLogger.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: OrchestratorOptions.branch,
|
|
||||||
inputs: {
|
|
||||||
buildGuid: Orchestrator.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;
|
export default GitHub;
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { DockerParameters, StringKeyValuePair } from './shared-types';
|
import { DockerParameters, StringKeyValuePair } from './shared-types';
|
||||||
|
|
||||||
class ImageEnvironmentFactory {
|
class ImageEnvironmentFactory {
|
||||||
public static getEnvVarString(parameters: DockerParameters, additionalVariables: StringKeyValuePair[] = []) {
|
public static getEnvVarString(
|
||||||
const environmentVariables = ImageEnvironmentFactory.getEnvironmentVariables(parameters, additionalVariables);
|
parameters: DockerParameters,
|
||||||
|
additionalVariables: StringKeyValuePair[] = [],
|
||||||
|
) {
|
||||||
|
const environmentVariables = ImageEnvironmentFactory.getEnvironmentVariables(
|
||||||
|
parameters,
|
||||||
|
additionalVariables,
|
||||||
|
);
|
||||||
let string = '';
|
let string = '';
|
||||||
for (const p of environmentVariables) {
|
for (const p of environmentVariables) {
|
||||||
if (p.value === '' || p.value === undefined || p.value === null) {
|
if (p.value === '' || p.value === undefined || p.value === null) {
|
||||||
@@ -21,7 +27,10 @@ class ImageEnvironmentFactory {
|
|||||||
return string;
|
return string;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getEnvironmentVariables(parameters: DockerParameters, additionalVariables: StringKeyValuePair[] = []) {
|
public static getEnvironmentVariables(
|
||||||
|
parameters: DockerParameters,
|
||||||
|
additionalVariables: StringKeyValuePair[] = [],
|
||||||
|
) {
|
||||||
let environmentVariables: StringKeyValuePair[] = [
|
let environmentVariables: StringKeyValuePair[] = [
|
||||||
{ name: 'UNITY_EMAIL', value: process.env.UNITY_EMAIL },
|
{ name: 'UNITY_EMAIL', value: process.env.UNITY_EMAIL },
|
||||||
{ name: 'UNITY_PASSWORD', value: process.env.UNITY_PASSWORD },
|
{ name: 'UNITY_PASSWORD', value: process.env.UNITY_PASSWORD },
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||||
import ImageTag from './image-tag';
|
import ImageTag from './image-tag';
|
||||||
|
|
||||||
describe('ImageTag', () => {
|
describe('ImageTag', () => {
|
||||||
@@ -27,7 +28,9 @@ describe('ImageTag', () => {
|
|||||||
expect(image.builderPlatform).toStrictEqual(testImageParameters.builderPlatform);
|
expect(image.builderPlatform).toStrictEqual(testImageParameters.builderPlatform);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each(['2000.0.0f0', '2011.1.11f1', '6000.0.0f1'])('accepts %p version format', (version) => {
|
test.each(['2000.0.0f0', '2011.1.11f1', '6000.0.0f1'])(
|
||||||
|
'accepts %p version format',
|
||||||
|
(version) => {
|
||||||
expect(
|
expect(
|
||||||
() =>
|
() =>
|
||||||
new ImageTag({
|
new ImageTag({
|
||||||
@@ -35,7 +38,8 @@ describe('ImageTag', () => {
|
|||||||
targetPlatform: testImageParameters.targetPlatform,
|
targetPlatform: testImageParameters.targetPlatform,
|
||||||
}),
|
}),
|
||||||
).not.toThrow();
|
).not.toThrow();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test.each(['some version', ''])('throws for incorrect version %p', (editorVersion) => {
|
test.each(['some version', ''])('throws for incorrect version %p', (editorVersion) => {
|
||||||
const { targetPlatform } = testImageParameters;
|
const { targetPlatform } = testImageParameters;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||||
import * as Index from '.';
|
import * as Index from '.';
|
||||||
|
|
||||||
interface ExportedModules {
|
interface ExportedModules {
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import Platform from './platform';
|
|||||||
import Project from './project';
|
import Project from './project';
|
||||||
import Unity from './unity';
|
import Unity from './unity';
|
||||||
import Versioning from './versioning';
|
import Versioning from './versioning';
|
||||||
import Orchestrator from './orchestrator/orchestrator';
|
|
||||||
import loadProvider, { ProviderLoader } from './orchestrator/providers/provider-loader';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Action,
|
Action,
|
||||||
@@ -24,7 +22,4 @@ export {
|
|||||||
Project,
|
Project,
|
||||||
Unity,
|
Unity,
|
||||||
Versioning,
|
Versioning,
|
||||||
Orchestrator as Orchestrator,
|
|
||||||
loadProvider,
|
|
||||||
ProviderLoader,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
import { OrchestratorSystem } from '../orchestrator/services/core/orchestrator-system';
|
import { exec } from 'node:child_process';
|
||||||
import OrchestratorOptions from '../orchestrator/options/orchestrator-options';
|
import Input from '../input';
|
||||||
|
|
||||||
export class GenericInputReader {
|
export class GenericInputReader {
|
||||||
public static async Run(command: string) {
|
public static async Run(command: string) {
|
||||||
if (OrchestratorOptions.providerStrategy === 'local') {
|
if ((Input.getInput('providerStrategy') || 'local') === 'local') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return await OrchestratorSystem.Run(command, false, true);
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
exec(command, { maxBuffer: 1024 * 10000 }, (error, stdout) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(stdout.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, vi } from 'vitest';
|
||||||
import { GitRepoReader } from './git-repo';
|
import { GitRepoReader } from './git-repo';
|
||||||
import { OrchestratorSystem } from '../orchestrator/services/core/orchestrator-system';
|
import Input from '../input';
|
||||||
import OrchestratorOptions from '../orchestrator/options/orchestrator-options';
|
|
||||||
|
|
||||||
describe(`git repo tests`, () => {
|
describe(`git repo tests`, () => {
|
||||||
it(`Branch value parsed from CLI to not contain illegal characters`, async () => {
|
it(`Branch value parsed from CLI to not contain illegal characters`, async () => {
|
||||||
@@ -10,15 +10,15 @@ describe(`git repo tests`, () => {
|
|||||||
|
|
||||||
it(`returns valid branch name when using https`, async () => {
|
it(`returns valid branch name when using https`, async () => {
|
||||||
const mockValue = 'https://github.com/example/example.git';
|
const mockValue = 'https://github.com/example/example.git';
|
||||||
await jest.spyOn(OrchestratorSystem, 'Run').mockReturnValue(Promise.resolve(mockValue));
|
vi.spyOn(GitRepoReader as any, 'runCommand').mockResolvedValue(mockValue);
|
||||||
await jest.spyOn(OrchestratorOptions, 'providerStrategy', 'get').mockReturnValue('not-local');
|
vi.spyOn(Input, 'getInput').mockReturnValue('not-local');
|
||||||
expect(await GitRepoReader.GetRemote()).toEqual(`example/example`);
|
expect(await GitRepoReader.GetRemote()).toEqual(`example/example`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`returns valid branch name when using ssh`, async () => {
|
it(`returns valid branch name when using ssh`, async () => {
|
||||||
const mockValue = 'git@github.com:example/example.git';
|
const mockValue = 'git@github.com:example/example.git';
|
||||||
await jest.spyOn(OrchestratorSystem, 'Run').mockReturnValue(Promise.resolve(mockValue));
|
vi.spyOn(GitRepoReader as any, 'runCommand').mockResolvedValue(mockValue);
|
||||||
await jest.spyOn(OrchestratorOptions, 'providerStrategy', 'get').mockReturnValue('not-local');
|
vi.spyOn(Input, 'getInput').mockReturnValue('not-local');
|
||||||
expect(await GitRepoReader.GetRemote()).toEqual(`example/example`);
|
expect(await GitRepoReader.GetRemote()).toEqual(`example/example`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,33 +1,44 @@
|
|||||||
import { assert } from 'node:console';
|
import { assert } from 'node:console';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { OrchestratorSystem } from '../orchestrator/services/core/orchestrator-system';
|
import { exec } from 'node:child_process';
|
||||||
import OrchestratorLogger from '../orchestrator/services/core/orchestrator-logger';
|
import * as core from '@actions/core';
|
||||||
import OrchestratorOptions from '../orchestrator/options/orchestrator-options';
|
|
||||||
import Input from '../input';
|
import Input from '../input';
|
||||||
|
|
||||||
export class GitRepoReader {
|
export class GitRepoReader {
|
||||||
|
private static async runCommand(command: string): Promise<string> {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
exec(command, { maxBuffer: 1024 * 10000 }, (error, stdout) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(stdout.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public static async GetRemote() {
|
public static async GetRemote() {
|
||||||
if (OrchestratorOptions.providerStrategy === 'local') {
|
if ((Input.getInput('providerStrategy') || 'local') === 'local') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
assert(fs.existsSync(`.git`));
|
assert(fs.existsSync(`.git`));
|
||||||
const value = (await OrchestratorSystem.Run(`cd ${Input.projectPath} && git remote -v`, false, true)).replace(
|
const value = (
|
||||||
/ /g,
|
await GitRepoReader.runCommand(`cd ${Input.projectPath} && git remote -v`)
|
||||||
``,
|
).replace(/ /g, ``);
|
||||||
);
|
core.info(`value ${value}`);
|
||||||
OrchestratorLogger.log(`value ${value}`);
|
|
||||||
assert(value.includes('github.com'));
|
assert(value.includes('github.com'));
|
||||||
|
|
||||||
return value.split('github.com')[1].split('.git')[0].slice(1);
|
return value.split('github.com')[1].split('.git')[0].slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async GetBranch() {
|
public static async GetBranch() {
|
||||||
if (OrchestratorOptions.providerStrategy === 'local') {
|
if ((Input.getInput('providerStrategy') || 'local') === 'local') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
assert(fs.existsSync(`.git`));
|
assert(fs.existsSync(`.git`));
|
||||||
|
|
||||||
return (await OrchestratorSystem.Run(`cd ${Input.projectPath} && git branch --show-current`, false, true))
|
return (await GitRepoReader.runCommand(`cd ${Input.projectPath} && git branch --show-current`))
|
||||||
.split('\n')[0]
|
.split('\n')[0]
|
||||||
.replace(/ /g, ``)
|
.replace(/ /g, ``)
|
||||||
.replace('/head', '');
|
.replace('/head', '');
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||||
import { GithubCliReader } from './github-cli';
|
import { GithubCliReader } from './github-cli';
|
||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,32 @@
|
|||||||
import { OrchestratorSystem } from '../orchestrator/services/core/orchestrator-system';
|
import { exec } from 'node:child_process';
|
||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
import OrchestratorOptions from '../orchestrator/options/orchestrator-options';
|
import Input from '../input';
|
||||||
|
|
||||||
export class GithubCliReader {
|
export class GithubCliReader {
|
||||||
|
private static async runCommand(command: string, suppressError = false): Promise<string> {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
exec(command, { maxBuffer: 1024 * 10000 }, (error, stdout, stderr) => {
|
||||||
|
if (error && !suppressError) {
|
||||||
|
reject(error);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve((stdout || '').toString() + (stderr || '').toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static async GetGitHubAuthToken() {
|
static async GetGitHubAuthToken() {
|
||||||
if (OrchestratorOptions.providerStrategy === 'local') {
|
if ((Input.getInput('providerStrategy') || 'local') === 'local') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const authStatus = await OrchestratorSystem.Run(`gh auth status`, true, true);
|
const authStatus = await GithubCliReader.runCommand(`gh auth status`, true);
|
||||||
if (authStatus.includes('You are not logged') || authStatus === '') {
|
if (authStatus.includes('You are not logged') || authStatus === '') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (await OrchestratorSystem.Run(`gh auth status -t`, false, true))
|
return (await GithubCliReader.runCommand(`gh auth status -t`))
|
||||||
.split(`Token: `)[1]
|
.split(`Token: `)[1]
|
||||||
.replace(/ /g, '')
|
.replace(/ /g, '')
|
||||||
.replace(/\n/g, '');
|
.replace(/\n/g, '');
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import YAML from 'yaml';
|
import YAML from 'yaml';
|
||||||
import OrchestratorOptions from '../orchestrator/options/orchestrator-options';
|
import Input from '../input';
|
||||||
|
|
||||||
export function ReadLicense(): string {
|
export function ReadLicense(): string {
|
||||||
if (OrchestratorOptions.providerStrategy === 'local') {
|
if ((Input.getInput('providerStrategy') || 'local') === 'local') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const pipelineFile = path.join(__dirname, `.github`, `workflows`, `orchestrator-k8s-pipeline.yml`);
|
const pipelineFile = path.join(
|
||||||
|
__dirname,
|
||||||
|
`.github`,
|
||||||
|
`workflows`,
|
||||||
|
`orchestrator-k8s-pipeline.yml`,
|
||||||
|
);
|
||||||
|
|
||||||
return fs.existsSync(pipelineFile) ? YAML.parse(fs.readFileSync(pipelineFile, 'utf8')).env.UNITY_LICENSE : '';
|
return fs.existsSync(pipelineFile)
|
||||||
|
? YAML.parse(fs.readFileSync(pipelineFile, 'utf8')).env.UNITY_LICENSE
|
||||||
|
: '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, test, vi } from 'vitest';
|
||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
|
|
||||||
import Input from './input';
|
import Input from './input';
|
||||||
import Platform from './platform';
|
import Platform from './platform';
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Input', () => {
|
describe('Input', () => {
|
||||||
@@ -15,7 +16,7 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = '2020.4.99f9';
|
const mockValue = '2020.4.99f9';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.unityVersion).toStrictEqual(mockValue);
|
expect(Input.unityVersion).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -27,7 +28,7 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = '2020.4.99f9';
|
const mockValue = '2020.4.99f9';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.customImage).toStrictEqual(mockValue);
|
expect(Input.customImage).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -40,7 +41,7 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = 'Android';
|
const mockValue = 'Android';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.targetPlatform).toStrictEqual(mockValue);
|
expect(Input.targetPlatform).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -53,7 +54,7 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = 'customProjectPath';
|
const mockValue = 'customProjectPath';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.projectPath).toStrictEqual(mockValue);
|
expect(Input.projectPath).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -66,7 +67,7 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = 'path/to/build_profile.asset';
|
const mockValue = 'path/to/build_profile.asset';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.buildProfile).toStrictEqual(mockValue);
|
expect(Input.buildProfile).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -79,14 +80,14 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = 'Build';
|
const mockValue = 'Build';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.buildName).toStrictEqual(mockValue);
|
expect(Input.buildName).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('takes special characters as input', () => {
|
it('takes special characters as input', () => {
|
||||||
const mockValue = '1ßúëld2';
|
const mockValue = '1ßúëld2';
|
||||||
jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.buildName).toStrictEqual(mockValue);
|
expect(Input.buildName).toStrictEqual(mockValue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -98,7 +99,7 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = 'customBuildsPath';
|
const mockValue = 'customBuildsPath';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.buildsPath).toStrictEqual(mockValue);
|
expect(Input.buildsPath).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -111,7 +112,7 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = 'Namespace.ClassName.Method';
|
const mockValue = 'Namespace.ClassName.Method';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.buildMethod).toStrictEqual(mockValue);
|
expect(Input.buildMethod).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -123,13 +124,13 @@ describe('Input', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns true when string true is passed', () => {
|
it('returns true when string true is passed', () => {
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue('true');
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue('true');
|
||||||
expect(Input.manualExit).toStrictEqual(true);
|
expect(Input.manualExit).toStrictEqual(true);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false when string false is passed', () => {
|
it('returns false when string false is passed', () => {
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue('false');
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue('false');
|
||||||
expect(Input.manualExit).toStrictEqual(false);
|
expect(Input.manualExit).toStrictEqual(false);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -141,13 +142,13 @@ describe('Input', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns true when string true is passed', () => {
|
it('returns true when string true is passed', () => {
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue('true');
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue('true');
|
||||||
expect(Input.enableGpu).toStrictEqual(true);
|
expect(Input.enableGpu).toStrictEqual(true);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false when string false is passed', () => {
|
it('returns false when string false is passed', () => {
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue('false');
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue('false');
|
||||||
expect(Input.enableGpu).toStrictEqual(false);
|
expect(Input.enableGpu).toStrictEqual(false);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -160,7 +161,7 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = 'Anything';
|
const mockValue = 'Anything';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.versioningStrategy).toStrictEqual(mockValue);
|
expect(Input.versioningStrategy).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -173,7 +174,7 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = '1.33.7';
|
const mockValue = '1.33.7';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.specifiedVersion).toStrictEqual(mockValue);
|
expect(Input.specifiedVersion).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -186,7 +187,7 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = '42';
|
const mockValue = '42';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.androidVersionCode).toStrictEqual(mockValue);
|
expect(Input.androidVersionCode).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -203,7 +204,7 @@ describe('Input', () => {
|
|||||||
${'androidAppBundle'} | ${'androidAppBundle'}
|
${'androidAppBundle'} | ${'androidAppBundle'}
|
||||||
${'androidStudioProject'} | ${'androidStudioProject'}
|
${'androidStudioProject'} | ${'androidStudioProject'}
|
||||||
`('returns $expected when $input is passed', ({ input, expected }) => {
|
`('returns $expected when $input is passed', ({ input, expected }) => {
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(input);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(input);
|
||||||
expect(Input.androidExportType).toStrictEqual(expected);
|
expect(Input.androidExportType).toStrictEqual(expected);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -220,7 +221,7 @@ describe('Input', () => {
|
|||||||
${'public'} | ${'public'}
|
${'public'} | ${'public'}
|
||||||
${'debugging'} | ${'debugging'}
|
${'debugging'} | ${'debugging'}
|
||||||
`('returns $expected when $input is passed', ({ input, expected }) => {
|
`('returns $expected when $input is passed', ({ input, expected }) => {
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(input);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(input);
|
||||||
expect(Input.androidExportType).toStrictEqual(expected);
|
expect(Input.androidExportType).toStrictEqual(expected);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -233,7 +234,7 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = 'keystore.keystore';
|
const mockValue = 'keystore.keystore';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.androidKeystoreName).toStrictEqual(mockValue);
|
expect(Input.androidKeystoreName).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -246,7 +247,7 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = 'secret';
|
const mockValue = 'secret';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.androidKeystoreBase64).toStrictEqual(mockValue);
|
expect(Input.androidKeystoreBase64).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -259,7 +260,7 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = 'secret';
|
const mockValue = 'secret';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.androidKeystorePass).toStrictEqual(mockValue);
|
expect(Input.androidKeystorePass).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -272,7 +273,7 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = 'secret';
|
const mockValue = 'secret';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.androidKeyaliasName).toStrictEqual(mockValue);
|
expect(Input.androidKeyaliasName).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -285,7 +286,7 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = 'secret';
|
const mockValue = 'secret';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.androidKeyaliasPass).toStrictEqual(mockValue);
|
expect(Input.androidKeyaliasPass).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -298,7 +299,7 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = 'secret';
|
const mockValue = 'secret';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.androidTargetSdkVersion).toStrictEqual(mockValue);
|
expect(Input.androidTargetSdkVersion).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -310,13 +311,13 @@ describe('Input', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns true when string true is passed', () => {
|
it('returns true when string true is passed', () => {
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue('true');
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue('true');
|
||||||
expect(Input.allowDirtyBuild).toStrictEqual(true);
|
expect(Input.allowDirtyBuild).toStrictEqual(true);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false when string false is passed', () => {
|
it('returns false when string false is passed', () => {
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue('false');
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue('false');
|
||||||
expect(Input.allowDirtyBuild).toStrictEqual(false);
|
expect(Input.allowDirtyBuild).toStrictEqual(false);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -329,9 +330,45 @@ describe('Input', () => {
|
|||||||
|
|
||||||
it('takes input from the users workflow', () => {
|
it('takes input from the users workflow', () => {
|
||||||
const mockValue = '-imAFlag';
|
const mockValue = '-imAFlag';
|
||||||
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||||
expect(Input.customParameters).toStrictEqual(mockValue);
|
expect(Input.customParameters).toStrictEqual(mockValue);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('useHostNetwork', () => {
|
||||||
|
it('returns the default value', () => {
|
||||||
|
expect(Input.useHostNetwork).toStrictEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when string true is passed', () => {
|
||||||
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue('true');
|
||||||
|
expect(Input.useHostNetwork).toStrictEqual(true);
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when string false is passed', () => {
|
||||||
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue('false');
|
||||||
|
expect(Input.useHostNetwork).toStrictEqual(false);
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('linux64RemoveExecutableExtension', () => {
|
||||||
|
it('returns the default value', () => {
|
||||||
|
expect(Input.linux64RemoveExecutableExtension).toStrictEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when string true is passed', () => {
|
||||||
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue('true');
|
||||||
|
expect(Input.linux64RemoveExecutableExtension).toStrictEqual(true);
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when string false is passed', () => {
|
||||||
|
const spy = vi.spyOn(core, 'getInput').mockReturnValue('false');
|
||||||
|
expect(Input.linux64RemoveExecutableExtension).toStrictEqual(false);
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { Cli } from './cli/cli';
|
import { Cli } from './cli/cli';
|
||||||
import OrchestratorQueryOverride from './orchestrator/options/orchestrator-query-override';
|
|
||||||
import Platform from './platform';
|
import Platform from './platform';
|
||||||
import GitHub from './github';
|
import GitHub from './github';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
@@ -15,7 +14,8 @@ export type InputKey = keyof typeof Input;
|
|||||||
*
|
*
|
||||||
* Note that input is always passed as a string, even booleans.
|
* Note that input is always passed as a string, even booleans.
|
||||||
*
|
*
|
||||||
* Todo: rename to UserInput and remove anything that is not direct input from the user / ci workflow
|
* Only core build inputs belong here. Orchestrator/plugin inputs are read
|
||||||
|
* directly by the @game-ci/orchestrator plugin via core.getInput() / env vars.
|
||||||
*/
|
*/
|
||||||
class Input {
|
class Input {
|
||||||
public static getInput(query: string): string | undefined {
|
public static getInput(query: string): string | undefined {
|
||||||
@@ -32,10 +32,6 @@ class Input {
|
|||||||
return Cli.query(query, alternativeQuery);
|
return Cli.query(query, alternativeQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OrchestratorQueryOverride.query(query, alternativeQuery)) {
|
|
||||||
return OrchestratorQueryOverride.query(query, alternativeQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env[query] !== undefined) {
|
if (process.env[query] !== undefined) {
|
||||||
return process.env[query]!;
|
return process.env[query]!;
|
||||||
}
|
}
|
||||||
@@ -45,17 +41,16 @@ class Input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static get region(): string {
|
|
||||||
return Input.getInput('region') ?? 'eu-west-2';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get githubRepo(): string | undefined {
|
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 {
|
static get branch(): string {
|
||||||
if (Input.getInput(`GITHUB_REF`)) {
|
if (Input.getInput(`GITHUB_REF`)) {
|
||||||
return Input.getInput(`GITHUB_REF`)!.replace('refs/', '').replace(`head/`, '').replace(`heads/`, '');
|
return Input.getInput(`GITHUB_REF`)!
|
||||||
|
.replace('refs/', '')
|
||||||
|
.replace(`head/`, '')
|
||||||
|
.replace(`heads/`, '');
|
||||||
} else if (Input.getInput('branch')) {
|
} else if (Input.getInput('branch')) {
|
||||||
return Input.getInput('branch')!;
|
return Input.getInput('branch')!;
|
||||||
} else {
|
} else {
|
||||||
@@ -147,6 +142,12 @@ class Input {
|
|||||||
return Input.getInput('customParameters') ?? '';
|
return Input.getInput('customParameters') ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get useHostNetwork(): boolean {
|
||||||
|
const input = Input.getInput('useHostNetwork') ?? false;
|
||||||
|
|
||||||
|
return input === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
static get versioningStrategy(): string {
|
static get versioningStrategy(): string {
|
||||||
return Input.getInput('versioning') ?? 'Semantic';
|
return Input.getInput('versioning') ?? 'Semantic';
|
||||||
}
|
}
|
||||||
@@ -262,7 +263,8 @@ class Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Input.getInput('dockerMemoryLimit') ?? `${Math.floor((os.totalmem() / bytesInMegabyte) * memoryMultiplier)}m`
|
Input.getInput('dockerMemoryLimit') ??
|
||||||
|
`${Math.floor((os.totalmem() / bytesInMegabyte) * memoryMultiplier)}m`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +284,12 @@ class Input {
|
|||||||
return Input.getInput('skipActivation')?.toLowerCase() ?? 'false';
|
return Input.getInput('skipActivation')?.toLowerCase() ?? 'false';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get linux64RemoveExecutableExtension(): boolean {
|
||||||
|
const input = Input.getInput('linux64RemoveExecutableExtension') ?? 'false';
|
||||||
|
|
||||||
|
return input === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
public static ToEnvVarFormat(input: string) {
|
public static ToEnvVarFormat(input: string) {
|
||||||
if (input.toUpperCase() === input) {
|
if (input.toUpperCase() === input) {
|
||||||
return input;
|
return input;
|
||||||
|
|||||||
17
src/model/orchestrator-plugin.test.ts
Normal file
17
src/model/orchestrator-plugin.test.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compatibility tests for the legacy orchestrator-plugin module name.
|
||||||
|
*
|
||||||
|
* CI targets this file pattern directly, and consumers may still import this
|
||||||
|
* module while migrating to the generic plugin API.
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe('orchestrator-plugin compatibility exports', () => {
|
||||||
|
it('keeps loadOrchestratorPlugin as an alias for loadPlugin', async () => {
|
||||||
|
const plugin = await import('./plugin');
|
||||||
|
const compatibility = await import('./orchestrator-plugin');
|
||||||
|
|
||||||
|
expect(compatibility.loadOrchestratorPlugin).toBe(plugin.loadPlugin);
|
||||||
|
});
|
||||||
|
});
|
||||||
2
src/model/orchestrator-plugin.ts
Normal file
2
src/model/orchestrator-plugin.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { loadPlugin as loadOrchestratorPlugin } from './plugin';
|
||||||
|
export type { Plugin as OrchestratorPlugin } from './plugin';
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import OrchestratorLogger from '../services/core/orchestrator-logger';
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
import Orchestrator from '../orchestrator';
|
|
||||||
import OrchestratorSecret from '../options/orchestrator-secret';
|
|
||||||
import BuildParameters from '../../build-parameters';
|
|
||||||
|
|
||||||
export class OrchestratorError {
|
|
||||||
public static async handleException(error: unknown, buildParameters: BuildParameters, secrets: OrchestratorSecret[]) {
|
|
||||||
OrchestratorLogger.error(JSON.stringify(error, undefined, 4));
|
|
||||||
core.setFailed('Orchestrator failed');
|
|
||||||
if (Orchestrator.Provider !== undefined) {
|
|
||||||
await Orchestrator.Provider.cleanupWorkflow(buildParameters, buildParameters.branch, secrets);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
class OrchestratorConstants {
|
|
||||||
static alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
|
|
||||||
}
|
|
||||||
export default OrchestratorConstants;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
class OrchestratorEnvironmentVariable {
|
|
||||||
public name!: string;
|
|
||||||
public value!: string;
|
|
||||||
}
|
|
||||||
export default OrchestratorEnvironmentVariable;
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import path from 'node:path';
|
|
||||||
import OrchestratorOptions from './orchestrator-options';
|
|
||||||
import Orchestrator from '../orchestrator';
|
|
||||||
import BuildParameters from '../../build-parameters';
|
|
||||||
|
|
||||||
export class OrchestratorFolders {
|
|
||||||
public static readonly repositoryFolder = 'repo';
|
|
||||||
|
|
||||||
public static ToLinuxFolder(folder: string) {
|
|
||||||
return folder.replace(/\\/g, `/`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only the following paths that do not start a path.join with another "Full" suffixed property need to start with an absolute /
|
|
||||||
|
|
||||||
public static get uniqueOrchestratorJobFolderAbsolute(): string {
|
|
||||||
return Orchestrator.buildParameters && BuildParameters.shouldUseRetainedWorkspaceMode(Orchestrator.buildParameters)
|
|
||||||
? path.join(`/`, OrchestratorFolders.buildVolumeFolder, Orchestrator.lockedWorkspace)
|
|
||||||
: path.join(`/`, OrchestratorFolders.buildVolumeFolder, Orchestrator.buildParameters.buildGuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get cacheFolderForAllFull(): string {
|
|
||||||
return path.join('/', OrchestratorFolders.buildVolumeFolder, OrchestratorFolders.cacheFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get cacheFolderForCacheKeyFull(): string {
|
|
||||||
return path.join(
|
|
||||||
'/',
|
|
||||||
OrchestratorFolders.buildVolumeFolder,
|
|
||||||
OrchestratorFolders.cacheFolder,
|
|
||||||
Orchestrator.buildParameters.cacheKey,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get builderPathAbsolute(): string {
|
|
||||||
return path.join(
|
|
||||||
OrchestratorOptions.useSharedBuilder
|
|
||||||
? `/${OrchestratorFolders.buildVolumeFolder}`
|
|
||||||
: OrchestratorFolders.uniqueOrchestratorJobFolderAbsolute,
|
|
||||||
`builder`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get repoPathAbsolute(): string {
|
|
||||||
return path.join(OrchestratorFolders.uniqueOrchestratorJobFolderAbsolute, OrchestratorFolders.repositoryFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get projectPathAbsolute(): string {
|
|
||||||
return path.join(OrchestratorFolders.repoPathAbsolute, Orchestrator.buildParameters.projectPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get libraryFolderAbsolute(): string {
|
|
||||||
return path.join(OrchestratorFolders.projectPathAbsolute, `Library`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get projectBuildFolderAbsolute(): string {
|
|
||||||
return path.join(OrchestratorFolders.repoPathAbsolute, Orchestrator.buildParameters.buildPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get lfsFolderAbsolute(): string {
|
|
||||||
return path.join(OrchestratorFolders.repoPathAbsolute, `.git`, `lfs`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get purgeRemoteCaching(): boolean {
|
|
||||||
return process.env.PURGE_REMOTE_BUILDER_CACHE !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get lfsCacheFolderFull() {
|
|
||||||
return path.join(OrchestratorFolders.cacheFolderForCacheKeyFull, `lfs`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get libraryCacheFolderFull() {
|
|
||||||
return path.join(OrchestratorFolders.cacheFolderForCacheKeyFull, `Library`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get unityBuilderRepoUrl(): string {
|
|
||||||
return `https://${Orchestrator.buildParameters.gitPrivateToken}@github.com/${Orchestrator.buildParameters.orchestratorRepoName}.git`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get targetBuildRepoUrl(): string {
|
|
||||||
return `https://${Orchestrator.buildParameters.gitPrivateToken}@github.com/${Orchestrator.buildParameters.githubRepo}.git`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get buildVolumeFolder() {
|
|
||||||
return 'data';
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get cacheFolder() {
|
|
||||||
return 'cache';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { customAlphabet } from 'nanoid';
|
|
||||||
import OrchestratorConstants from './orchestrator-constants';
|
|
||||||
|
|
||||||
class OrchestratorNamespace {
|
|
||||||
static generateGuid(runNumber: string | number, platform: string) {
|
|
||||||
const nanoid = customAlphabet(OrchestratorConstants.alphabet, 4);
|
|
||||||
|
|
||||||
return `${runNumber}-${platform.toLowerCase().replace('standalone', '')}-${nanoid()}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default OrchestratorNamespace;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import Input from '../../input';
|
|
||||||
import OrchestratorOptions from './orchestrator-options';
|
|
||||||
|
|
||||||
class OrchestratorOptionsReader {
|
|
||||||
static GetProperties() {
|
|
||||||
return [...Object.getOwnPropertyNames(Input), ...Object.getOwnPropertyNames(OrchestratorOptions)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OrchestratorOptionsReader;
|
|
||||||
@@ -1,338 +0,0 @@
|
|||||||
import { Cli } from '../../cli/cli';
|
|
||||||
import OrchestratorQueryOverride from './orchestrator-query-override';
|
|
||||||
import GitHub from '../../github';
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
|
|
||||||
class OrchestratorOptions {
|
|
||||||
// ### ### ###
|
|
||||||
// Input Handling
|
|
||||||
// ### ### ###
|
|
||||||
public static getInput(query: string): string | undefined {
|
|
||||||
if (GitHub.githubInputEnabled) {
|
|
||||||
const coreInput = core.getInput(query);
|
|
||||||
if (coreInput && coreInput !== '') {
|
|
||||||
return coreInput;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const alternativeQuery = OrchestratorOptions.ToEnvVarFormat(query);
|
|
||||||
|
|
||||||
// Query input sources
|
|
||||||
if (Cli.query(query, alternativeQuery)) {
|
|
||||||
return Cli.query(query, alternativeQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OrchestratorQueryOverride.query(query, alternativeQuery)) {
|
|
||||||
return OrchestratorQueryOverride.query(query, alternativeQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env[query] !== undefined) {
|
|
||||||
return process.env[query];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alternativeQuery !== query && process.env[alternativeQuery] !== undefined) {
|
|
||||||
return process.env[alternativeQuery];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ToEnvVarFormat(input: string): string {
|
|
||||||
if (input.toUpperCase() === input) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
return input
|
|
||||||
.replace(/([A-Z])/g, ' $1')
|
|
||||||
.trim()
|
|
||||||
.toUpperCase()
|
|
||||||
.replace(/ /g, '_');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ### ### ###
|
|
||||||
// Provider parameters
|
|
||||||
// ### ### ###
|
|
||||||
|
|
||||||
static get region(): string {
|
|
||||||
return OrchestratorOptions.getInput('region') || 'eu-west-2';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ### ### ###
|
|
||||||
// GitHub parameters
|
|
||||||
// ### ### ###
|
|
||||||
static get githubChecks(): boolean {
|
|
||||||
const value = OrchestratorOptions.getInput('githubChecks');
|
|
||||||
|
|
||||||
return value === `true` || false;
|
|
||||||
}
|
|
||||||
static get githubCheckId(): string {
|
|
||||||
return OrchestratorOptions.getInput('githubCheckId') || ``;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get githubOwner(): string {
|
|
||||||
return OrchestratorOptions.getInput('githubOwner') || OrchestratorOptions.githubRepo?.split(`/`)[0] || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get githubRepoName(): string {
|
|
||||||
return OrchestratorOptions.getInput('githubRepoName') || OrchestratorOptions.githubRepo?.split(`/`)[1] || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get orchestratorRepoName(): string {
|
|
||||||
return OrchestratorOptions.getInput('orchestratorRepoName') || 'game-ci/unity-builder';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get cloneDepth(): string {
|
|
||||||
return OrchestratorOptions.getInput('cloneDepth') || '50';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get finalHooks(): string[] {
|
|
||||||
return OrchestratorOptions.getInput('finalHooks')?.split(',') || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ### ### ###
|
|
||||||
// Git syncronization parameters
|
|
||||||
// ### ### ###
|
|
||||||
|
|
||||||
static get githubRepo(): string | undefined {
|
|
||||||
return (
|
|
||||||
OrchestratorOptions.getInput('GITHUB_REPOSITORY') || OrchestratorOptions.getInput('GITHUB_REPO') || undefined
|
|
||||||
);
|
|
||||||
}
|
|
||||||
static get branch(): string {
|
|
||||||
if (OrchestratorOptions.getInput(`GITHUB_REF`)) {
|
|
||||||
return (
|
|
||||||
OrchestratorOptions.getInput(`GITHUB_REF`)?.replace('refs/', '').replace(`head/`, '').replace(`heads/`, '') ||
|
|
||||||
``
|
|
||||||
);
|
|
||||||
} else if (OrchestratorOptions.getInput('branch')) {
|
|
||||||
return OrchestratorOptions.getInput('branch') || ``;
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ### ### ###
|
|
||||||
// Orchestrator parameters
|
|
||||||
// ### ### ###
|
|
||||||
|
|
||||||
static get buildPlatform(): string {
|
|
||||||
const input = OrchestratorOptions.getInput('buildPlatform');
|
|
||||||
if (input && input !== '') {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
if (OrchestratorOptions.providerStrategy !== 'local') {
|
|
||||||
return 'linux';
|
|
||||||
}
|
|
||||||
|
|
||||||
return process.platform;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get orchestratorBranch(): string {
|
|
||||||
return OrchestratorOptions.getInput('orchestratorBranch') || 'main';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get providerStrategy(): string {
|
|
||||||
const provider =
|
|
||||||
OrchestratorOptions.getInput('orchestratorCluster') || OrchestratorOptions.getInput('providerStrategy');
|
|
||||||
if (Cli.isCliMode) {
|
|
||||||
return provider || 'aws';
|
|
||||||
}
|
|
||||||
|
|
||||||
return provider || 'local';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get containerCpu(): string {
|
|
||||||
return OrchestratorOptions.getInput('containerCpu') || `1024`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get containerMemory(): string {
|
|
||||||
return OrchestratorOptions.getInput('containerMemory') || `3072`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get containerNamespace(): string {
|
|
||||||
return OrchestratorOptions.getInput('containerNamespace') || `default`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get customJob(): string {
|
|
||||||
return OrchestratorOptions.getInput('customJob') || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ### ### ###
|
|
||||||
// Custom commands from files parameters
|
|
||||||
// ### ### ###
|
|
||||||
|
|
||||||
static get containerHookFiles(): string[] {
|
|
||||||
return OrchestratorOptions.getInput('containerHookFiles')?.split(`,`) || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
static get commandHookFiles(): string[] {
|
|
||||||
return OrchestratorOptions.getInput('commandHookFiles')?.split(`,`) || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ### ### ###
|
|
||||||
// Custom commands from yaml parameters
|
|
||||||
// ### ### ###
|
|
||||||
|
|
||||||
static get commandHooks(): string {
|
|
||||||
return OrchestratorOptions.getInput('commandHooks') || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get postBuildContainerHooks(): string {
|
|
||||||
return OrchestratorOptions.getInput('postBuildContainerHooks') || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get preBuildContainerHooks(): string {
|
|
||||||
return OrchestratorOptions.getInput('preBuildContainerHooks') || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ### ### ###
|
|
||||||
// Input override handling
|
|
||||||
// ### ### ###
|
|
||||||
|
|
||||||
static get pullInputList(): string[] {
|
|
||||||
return OrchestratorOptions.getInput('pullInputList')?.split(`,`) || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
static get inputPullCommand(): string {
|
|
||||||
const value = OrchestratorOptions.getInput('inputPullCommand');
|
|
||||||
|
|
||||||
if (value === 'gcp-secret-manager') {
|
|
||||||
return 'gcloud secrets versions access 1 --secret="{0}"';
|
|
||||||
} else if (value === 'aws-secret-manager') {
|
|
||||||
return 'aws secretsmanager get-secret-value --secret-id {0}';
|
|
||||||
}
|
|
||||||
|
|
||||||
return value || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ### ### ###
|
|
||||||
// Aws
|
|
||||||
// ### ### ###
|
|
||||||
|
|
||||||
static get awsStackName() {
|
|
||||||
return OrchestratorOptions.getInput('awsStackName') || 'game-ci';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get awsEndpoint(): string | undefined {
|
|
||||||
return OrchestratorOptions.getInput('awsEndpoint');
|
|
||||||
}
|
|
||||||
|
|
||||||
static get awsCloudFormationEndpoint(): string | undefined {
|
|
||||||
return OrchestratorOptions.getInput('awsCloudFormationEndpoint') || OrchestratorOptions.awsEndpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get awsEcsEndpoint(): string | undefined {
|
|
||||||
return OrchestratorOptions.getInput('awsEcsEndpoint') || OrchestratorOptions.awsEndpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get awsKinesisEndpoint(): string | undefined {
|
|
||||||
return OrchestratorOptions.getInput('awsKinesisEndpoint') || OrchestratorOptions.awsEndpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get awsCloudWatchLogsEndpoint(): string | undefined {
|
|
||||||
return OrchestratorOptions.getInput('awsCloudWatchLogsEndpoint') || OrchestratorOptions.awsEndpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get awsS3Endpoint(): string | undefined {
|
|
||||||
return OrchestratorOptions.getInput('awsS3Endpoint') || OrchestratorOptions.awsEndpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ### ### ###
|
|
||||||
// Storage
|
|
||||||
// ### ### ###
|
|
||||||
|
|
||||||
static get storageProvider(): string {
|
|
||||||
return OrchestratorOptions.getInput('storageProvider') || 's3';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get rcloneRemote(): string {
|
|
||||||
return OrchestratorOptions.getInput('rcloneRemote') || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ### ### ###
|
|
||||||
// K8s
|
|
||||||
// ### ### ###
|
|
||||||
|
|
||||||
static get kubeConfig(): string {
|
|
||||||
return OrchestratorOptions.getInput('kubeConfig') || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get kubeVolume(): string {
|
|
||||||
return OrchestratorOptions.getInput('kubeVolume') || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get kubeVolumeSize(): string {
|
|
||||||
return OrchestratorOptions.getInput('kubeVolumeSize') || '25Gi';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get kubeStorageClass(): string {
|
|
||||||
return OrchestratorOptions.getInput('kubeStorageClass') || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ### ### ###
|
|
||||||
// Caching
|
|
||||||
// ### ### ###
|
|
||||||
|
|
||||||
static get cacheKey(): string {
|
|
||||||
return OrchestratorOptions.getInput('cacheKey') || OrchestratorOptions.branch;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ### ### ###
|
|
||||||
// Utility Parameters
|
|
||||||
// ### ### ###
|
|
||||||
|
|
||||||
static get orchestratorDebug(): boolean {
|
|
||||||
return (
|
|
||||||
OrchestratorOptions.getInput(`orchestratorTests`) === `true` ||
|
|
||||||
OrchestratorOptions.getInput(`orchestratorDebug`) === `true` ||
|
|
||||||
OrchestratorOptions.getInput(`orchestratorDebugTree`) === `true` ||
|
|
||||||
OrchestratorOptions.getInput(`orchestratorDebugEnv`) === `true` ||
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
static get skipLfs(): boolean {
|
|
||||||
return OrchestratorOptions.getInput(`skipLfs`) === `true`;
|
|
||||||
}
|
|
||||||
static get skipCache(): boolean {
|
|
||||||
return OrchestratorOptions.getInput(`skipCache`) === `true`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get asyncOrchestrator(): boolean {
|
|
||||||
return OrchestratorOptions.getInput('asyncOrchestrator') === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get resourceTracking(): boolean {
|
|
||||||
return OrchestratorOptions.getInput('resourceTracking') === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get useLargePackages(): boolean {
|
|
||||||
return OrchestratorOptions.getInput(`useLargePackages`) === `true`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get useSharedBuilder(): boolean {
|
|
||||||
return OrchestratorOptions.getInput(`useSharedBuilder`) === `true`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get useCompressionStrategy(): boolean {
|
|
||||||
return OrchestratorOptions.getInput(`useCompressionStrategy`) === `true`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get useCleanupCron(): boolean {
|
|
||||||
return (OrchestratorOptions.getInput(`useCleanupCron`) || 'true') === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ### ### ###
|
|
||||||
// Retained Workspace
|
|
||||||
// ### ### ###
|
|
||||||
|
|
||||||
public static get maxRetainedWorkspaces(): string {
|
|
||||||
return OrchestratorOptions.getInput(`maxRetainedWorkspaces`) || `0`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ### ### ###
|
|
||||||
// Garbage Collection
|
|
||||||
// ### ### ###
|
|
||||||
|
|
||||||
static get garbageMaxAge(): number {
|
|
||||||
return Number(OrchestratorOptions.getInput(`garbageMaxAge`)) || 24;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OrchestratorOptions;
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import Input from '../../input';
|
|
||||||
import { GenericInputReader } from '../../input-readers/generic-input-reader';
|
|
||||||
import OrchestratorOptions from './orchestrator-options';
|
|
||||||
|
|
||||||
const formatFunction = (value: string, arguments_: any[]) => {
|
|
||||||
for (const element of arguments_) {
|
|
||||||
value = value.replace(`{${element.key}}`, element.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
class OrchestratorQueryOverride {
|
|
||||||
static queryOverrides: { [key: string]: string } | undefined;
|
|
||||||
|
|
||||||
// TODO accept premade secret sources or custom secret source definition yamls
|
|
||||||
|
|
||||||
public static query(key: string, alternativeKey: string) {
|
|
||||||
if (OrchestratorQueryOverride.queryOverrides && OrchestratorQueryOverride.queryOverrides[key] !== undefined) {
|
|
||||||
return OrchestratorQueryOverride.queryOverrides[key];
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
OrchestratorQueryOverride.queryOverrides &&
|
|
||||||
alternativeKey &&
|
|
||||||
OrchestratorQueryOverride.queryOverrides[alternativeKey] !== undefined
|
|
||||||
) {
|
|
||||||
return OrchestratorQueryOverride.queryOverrides[alternativeKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static shouldUseOverride(query: string) {
|
|
||||||
if (OrchestratorOptions.inputPullCommand !== '') {
|
|
||||||
if (OrchestratorOptions.pullInputList.length > 0) {
|
|
||||||
const doesInclude =
|
|
||||||
OrchestratorOptions.pullInputList.includes(query) ||
|
|
||||||
OrchestratorOptions.pullInputList.includes(Input.ToEnvVarFormat(query));
|
|
||||||
|
|
||||||
return doesInclude ? true : false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async queryOverride(query: string) {
|
|
||||||
if (!this.shouldUseOverride(query)) {
|
|
||||||
throw new Error(`Should not be trying to run override query on ${query}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await GenericInputReader.Run(
|
|
||||||
formatFunction(OrchestratorOptions.inputPullCommand, [{ key: 0, value: query }]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async PopulateQueryOverrideInput() {
|
|
||||||
const queries = OrchestratorOptions.pullInputList;
|
|
||||||
OrchestratorQueryOverride.queryOverrides = {};
|
|
||||||
for (const element of queries) {
|
|
||||||
if (OrchestratorQueryOverride.shouldUseOverride(element)) {
|
|
||||||
OrchestratorQueryOverride.queryOverrides[element] = await OrchestratorQueryOverride.queryOverride(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default OrchestratorQueryOverride;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
class OrchestratorSecret {
|
|
||||||
public ParameterKey!: string;
|
|
||||||
public EnvironmentVariable!: string;
|
|
||||||
public ParameterValue!: string;
|
|
||||||
}
|
|
||||||
export default OrchestratorSecret;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export class OrchestratorStatics {
|
|
||||||
public static readonly logPrefix = `Orchestrator`;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import OrchestratorEnvironmentVariable from './orchestrator-environment-variable';
|
|
||||||
import OrchestratorSecret from './orchestrator-secret';
|
|
||||||
|
|
||||||
export class OrchestratorStepParameters {
|
|
||||||
public image: string;
|
|
||||||
public environment: OrchestratorEnvironmentVariable[];
|
|
||||||
public secrets: OrchestratorSecret[];
|
|
||||||
constructor(image: string, environmentVariables: OrchestratorEnvironmentVariable[], secrets: OrchestratorSecret[]) {
|
|
||||||
this.image = image;
|
|
||||||
this.environment = environmentVariables;
|
|
||||||
this.secrets = secrets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
import AwsBuildPlatform from './providers/aws';
|
|
||||||
import { BuildParameters, Input } from '..';
|
|
||||||
import Kubernetes from './providers/k8s';
|
|
||||||
import OrchestratorLogger from './services/core/orchestrator-logger';
|
|
||||||
import { OrchestratorStepParameters } from './options/orchestrator-step-parameters';
|
|
||||||
import { WorkflowCompositionRoot } from './workflows/workflow-composition-root';
|
|
||||||
import { OrchestratorError } from './error/orchestrator-error';
|
|
||||||
import { TaskParameterSerializer } from './services/core/task-parameter-serializer';
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
import OrchestratorSecret from './options/orchestrator-secret';
|
|
||||||
import { ProviderInterface } from './providers/provider-interface';
|
|
||||||
import OrchestratorEnvironmentVariable from './options/orchestrator-environment-variable';
|
|
||||||
import TestOrchestrator from './providers/test';
|
|
||||||
import LocalOrchestrator from './providers/local';
|
|
||||||
import LocalDockerOrchestrator from './providers/docker';
|
|
||||||
import loadProvider from './providers/provider-loader';
|
|
||||||
import GitHub from '../github';
|
|
||||||
import SharedWorkspaceLocking from './services/core/shared-workspace-locking';
|
|
||||||
import { FollowLogStreamService } from './services/core/follow-log-stream-service';
|
|
||||||
import OrchestratorResult from './services/core/orchestrator-result';
|
|
||||||
import OrchestratorOptions from './options/orchestrator-options';
|
|
||||||
import ResourceTracking from './services/core/resource-tracking';
|
|
||||||
|
|
||||||
class Orchestrator {
|
|
||||||
public static Provider: ProviderInterface;
|
|
||||||
public static buildParameters: BuildParameters;
|
|
||||||
private static defaultSecrets: OrchestratorSecret[];
|
|
||||||
private static orchestratorEnvironmentVariables: OrchestratorEnvironmentVariable[];
|
|
||||||
static lockedWorkspace: string = ``;
|
|
||||||
public static readonly retainedWorkspacePrefix: string = `retained-workspace`;
|
|
||||||
|
|
||||||
// When true, validates AWS CloudFormation templates even when using local-docker execution
|
|
||||||
// This is set by AWS_FORCE_PROVIDER=aws-local mode
|
|
||||||
public static validateAwsTemplates: boolean = false;
|
|
||||||
public static get isOrchestratorEnvironment() {
|
|
||||||
return process.env[`GITHUB_ACTIONS`] !== `true`;
|
|
||||||
}
|
|
||||||
public static get isOrchestratorAsyncEnvironment() {
|
|
||||||
return process.env[`ASYNC_WORKFLOW`] === `true`;
|
|
||||||
}
|
|
||||||
public static async setup(buildParameters: BuildParameters) {
|
|
||||||
OrchestratorLogger.setup();
|
|
||||||
OrchestratorLogger.log(`Setting up orchestrator`);
|
|
||||||
Orchestrator.buildParameters = buildParameters;
|
|
||||||
ResourceTracking.logAllocationSummary('setup');
|
|
||||||
await ResourceTracking.logDiskUsageSnapshot('setup');
|
|
||||||
if (Orchestrator.buildParameters.githubCheckId === ``) {
|
|
||||||
Orchestrator.buildParameters.githubCheckId = await GitHub.createGitHubCheck(
|
|
||||||
Orchestrator.buildParameters.buildGuid,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await Orchestrator.setupSelectedBuildPlatform();
|
|
||||||
Orchestrator.defaultSecrets = TaskParameterSerializer.readDefaultSecrets();
|
|
||||||
Orchestrator.orchestratorEnvironmentVariables =
|
|
||||||
TaskParameterSerializer.createOrchestratorEnvironmentVariables(buildParameters);
|
|
||||||
if (GitHub.githubInputEnabled) {
|
|
||||||
const buildParameterPropertyNames = Object.getOwnPropertyNames(buildParameters);
|
|
||||||
for (const element of Orchestrator.orchestratorEnvironmentVariables) {
|
|
||||||
// OrchestratorLogger.log(`Orchestrator output ${Input.ToEnvVarFormat(element.name)} = ${element.value}`);
|
|
||||||
core.setOutput(Input.ToEnvVarFormat(element.name), element.value);
|
|
||||||
}
|
|
||||||
for (const element of buildParameterPropertyNames) {
|
|
||||||
// OrchestratorLogger.log(`Orchestrator output ${Input.ToEnvVarFormat(element)} = ${buildParameters[element]}`);
|
|
||||||
core.setOutput(Input.ToEnvVarFormat(element), buildParameters[element]);
|
|
||||||
}
|
|
||||||
core.setOutput(
|
|
||||||
Input.ToEnvVarFormat(`buildArtifact`),
|
|
||||||
`build-${Orchestrator.buildParameters.buildGuid}.tar${
|
|
||||||
Orchestrator.buildParameters.useCompressionStrategy ? '.lz4' : ''
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
FollowLogStreamService.Reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async setupSelectedBuildPlatform() {
|
|
||||||
OrchestratorLogger.log(`Orchestrator platform selected ${Orchestrator.buildParameters.providerStrategy}`);
|
|
||||||
|
|
||||||
// Detect LocalStack endpoints and handle AWS provider appropriately
|
|
||||||
// AWS_FORCE_PROVIDER options:
|
|
||||||
// - 'aws': Force AWS provider (requires LocalStack Pro with ECS support)
|
|
||||||
// - 'aws-local': Validate AWS templates/config but execute via local-docker (for CI without ECS)
|
|
||||||
// - unset/other: Auto-fallback to local-docker when LocalStack detected
|
|
||||||
const awsForceProvider = process.env.AWS_FORCE_PROVIDER || '';
|
|
||||||
const forceAwsProvider = awsForceProvider === 'aws' || awsForceProvider === 'true';
|
|
||||||
const useAwsLocalMode = awsForceProvider === 'aws-local';
|
|
||||||
const endpointsToCheck = [
|
|
||||||
process.env.AWS_ENDPOINT,
|
|
||||||
process.env.AWS_S3_ENDPOINT,
|
|
||||||
process.env.AWS_CLOUD_FORMATION_ENDPOINT,
|
|
||||||
process.env.AWS_ECS_ENDPOINT,
|
|
||||||
process.env.AWS_KINESIS_ENDPOINT,
|
|
||||||
process.env.AWS_CLOUD_WATCH_LOGS_ENDPOINT,
|
|
||||||
OrchestratorOptions.awsEndpoint,
|
|
||||||
OrchestratorOptions.awsS3Endpoint,
|
|
||||||
OrchestratorOptions.awsCloudFormationEndpoint,
|
|
||||||
OrchestratorOptions.awsEcsEndpoint,
|
|
||||||
OrchestratorOptions.awsKinesisEndpoint,
|
|
||||||
OrchestratorOptions.awsCloudWatchLogsEndpoint,
|
|
||||||
]
|
|
||||||
.filter((x) => typeof x === 'string')
|
|
||||||
.join(' ');
|
|
||||||
const isLocalStack = /localstack|localhost|127\.0\.0\.1/i.test(endpointsToCheck);
|
|
||||||
let provider = Orchestrator.buildParameters.providerStrategy;
|
|
||||||
let validateAwsTemplates = false;
|
|
||||||
|
|
||||||
if (provider === 'aws' && isLocalStack) {
|
|
||||||
if (useAwsLocalMode) {
|
|
||||||
// aws-local mode: Validate AWS templates but execute via local-docker
|
|
||||||
// This provides confidence in AWS CloudFormation without requiring LocalStack Pro
|
|
||||||
OrchestratorLogger.log('AWS_FORCE_PROVIDER=aws-local: Validating AWS templates, executing via local-docker');
|
|
||||||
validateAwsTemplates = true;
|
|
||||||
provider = 'local-docker';
|
|
||||||
} else if (forceAwsProvider) {
|
|
||||||
// Force full AWS provider (requires LocalStack Pro with ECS support)
|
|
||||||
OrchestratorLogger.log(
|
|
||||||
'LocalStack endpoints detected but AWS_FORCE_PROVIDER=aws; using full AWS provider (requires ECS support)',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Auto-fallback to local-docker
|
|
||||||
OrchestratorLogger.log('LocalStack endpoints detected; routing provider to local-docker for this run');
|
|
||||||
OrchestratorLogger.log(
|
|
||||||
'Note: Set AWS_FORCE_PROVIDER=aws-local to validate AWS templates with local-docker execution',
|
|
||||||
);
|
|
||||||
provider = 'local-docker';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store whether we should validate AWS templates (used by aws-local mode)
|
|
||||||
Orchestrator.validateAwsTemplates = validateAwsTemplates;
|
|
||||||
|
|
||||||
switch (provider) {
|
|
||||||
case 'k8s':
|
|
||||||
Orchestrator.Provider = new Kubernetes(Orchestrator.buildParameters);
|
|
||||||
break;
|
|
||||||
case 'aws':
|
|
||||||
Orchestrator.Provider = new AwsBuildPlatform(Orchestrator.buildParameters);
|
|
||||||
|
|
||||||
// Validate that AWS provider is actually being used when expected
|
|
||||||
if (isLocalStack && forceAwsProvider) {
|
|
||||||
OrchestratorLogger.log('✓ AWS provider initialized with LocalStack - AWS functionality will be validated');
|
|
||||||
} else if (isLocalStack && !forceAwsProvider) {
|
|
||||||
OrchestratorLogger.log(
|
|
||||||
'⚠ WARNING: AWS provider was requested but LocalStack detected without AWS_FORCE_PROVIDER',
|
|
||||||
);
|
|
||||||
OrchestratorLogger.log('⚠ This may cause AWS functionality tests to fail validation');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'test':
|
|
||||||
Orchestrator.Provider = new TestOrchestrator();
|
|
||||||
break;
|
|
||||||
case 'local-docker':
|
|
||||||
Orchestrator.Provider = new LocalDockerOrchestrator();
|
|
||||||
break;
|
|
||||||
case 'local-system':
|
|
||||||
Orchestrator.Provider = new LocalOrchestrator();
|
|
||||||
break;
|
|
||||||
case 'local':
|
|
||||||
Orchestrator.Provider = new LocalOrchestrator();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Try to load provider using the dynamic loader for unknown providers
|
|
||||||
try {
|
|
||||||
Orchestrator.Provider = await loadProvider(provider, Orchestrator.buildParameters);
|
|
||||||
} catch (error: any) {
|
|
||||||
OrchestratorLogger.log(`Failed to load provider '${provider}' using dynamic loader: ${error.message}`);
|
|
||||||
OrchestratorLogger.log('Falling back to local provider...');
|
|
||||||
Orchestrator.Provider = new LocalOrchestrator();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final validation: Ensure provider matches expectations
|
|
||||||
const finalProviderName = Orchestrator.Provider.constructor.name;
|
|
||||||
if (Orchestrator.buildParameters.providerStrategy === 'aws' && finalProviderName !== 'AWSBuildEnvironment') {
|
|
||||||
OrchestratorLogger.log(`⚠ WARNING: Expected AWS provider but got ${finalProviderName}`);
|
|
||||||
OrchestratorLogger.log('⚠ AWS functionality tests may not be validating AWS services correctly');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async run(buildParameters: BuildParameters, baseImage: string) {
|
|
||||||
if (baseImage.includes(`undefined`)) {
|
|
||||||
throw new Error(`baseImage is undefined`);
|
|
||||||
}
|
|
||||||
await Orchestrator.setup(buildParameters);
|
|
||||||
|
|
||||||
// When aws-local mode is enabled, validate AWS CloudFormation templates
|
|
||||||
// This ensures AWS templates are correct even when executing via local-docker
|
|
||||||
if (Orchestrator.validateAwsTemplates) {
|
|
||||||
await Orchestrator.validateAwsCloudFormationTemplates();
|
|
||||||
}
|
|
||||||
await Orchestrator.Provider.setupWorkflow(
|
|
||||||
Orchestrator.buildParameters.buildGuid,
|
|
||||||
Orchestrator.buildParameters,
|
|
||||||
Orchestrator.buildParameters.branch,
|
|
||||||
Orchestrator.defaultSecrets,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
if (buildParameters.maxRetainedWorkspaces > 0) {
|
|
||||||
Orchestrator.lockedWorkspace = SharedWorkspaceLocking.NewWorkspaceName();
|
|
||||||
|
|
||||||
const result = await SharedWorkspaceLocking.GetLockedWorkspace(
|
|
||||||
Orchestrator.lockedWorkspace,
|
|
||||||
Orchestrator.buildParameters.buildGuid,
|
|
||||||
Orchestrator.buildParameters,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
OrchestratorLogger.logLine(`Using retained workspace ${Orchestrator.lockedWorkspace}`);
|
|
||||||
Orchestrator.orchestratorEnvironmentVariables = [
|
|
||||||
...Orchestrator.orchestratorEnvironmentVariables,
|
|
||||||
{ name: `LOCKED_WORKSPACE`, value: Orchestrator.lockedWorkspace },
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
OrchestratorLogger.log(`Max retained workspaces reached ${buildParameters.maxRetainedWorkspaces}`);
|
|
||||||
buildParameters.maxRetainedWorkspaces = 0;
|
|
||||||
Orchestrator.lockedWorkspace = ``;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await Orchestrator.updateStatusWithBuildParameters();
|
|
||||||
const output = await new WorkflowCompositionRoot().run(
|
|
||||||
new OrchestratorStepParameters(
|
|
||||||
baseImage,
|
|
||||||
Orchestrator.orchestratorEnvironmentVariables,
|
|
||||||
Orchestrator.defaultSecrets,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await Orchestrator.Provider.cleanupWorkflow(
|
|
||||||
Orchestrator.buildParameters,
|
|
||||||
Orchestrator.buildParameters.branch,
|
|
||||||
Orchestrator.defaultSecrets,
|
|
||||||
);
|
|
||||||
if (!Orchestrator.buildParameters.isCliMode) core.endGroup();
|
|
||||||
if (buildParameters.asyncWorkflow && this.isOrchestratorEnvironment && this.isOrchestratorAsyncEnvironment) {
|
|
||||||
await GitHub.updateGitHubCheck(Orchestrator.buildParameters.buildGuid, `success`, `success`, `completed`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BuildParameters.shouldUseRetainedWorkspaceMode(buildParameters)) {
|
|
||||||
const workspace = Orchestrator.lockedWorkspace || ``;
|
|
||||||
await SharedWorkspaceLocking.ReleaseWorkspace(
|
|
||||||
workspace,
|
|
||||||
Orchestrator.buildParameters.buildGuid,
|
|
||||||
Orchestrator.buildParameters,
|
|
||||||
);
|
|
||||||
const isLocked = await SharedWorkspaceLocking.IsWorkspaceLocked(workspace, Orchestrator.buildParameters);
|
|
||||||
if (isLocked) {
|
|
||||||
throw new Error(
|
|
||||||
`still locked after releasing ${await SharedWorkspaceLocking.GetAllLocksForWorkspace(
|
|
||||||
workspace,
|
|
||||||
buildParameters,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Orchestrator.lockedWorkspace = ``;
|
|
||||||
}
|
|
||||||
|
|
||||||
await GitHub.triggerWorkflowOnComplete(Orchestrator.buildParameters.finalHooks);
|
|
||||||
|
|
||||||
if (buildParameters.constantGarbageCollection) {
|
|
||||||
Orchestrator.Provider.garbageCollect(``, true, buildParameters.garbageMaxAge, true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new OrchestratorResult(buildParameters, output, true, true, false);
|
|
||||||
} catch (error: any) {
|
|
||||||
OrchestratorLogger.log(JSON.stringify(error, undefined, 4));
|
|
||||||
await GitHub.updateGitHubCheck(
|
|
||||||
Orchestrator.buildParameters.buildGuid,
|
|
||||||
`Failed - Error ${error?.message || error}`,
|
|
||||||
`failure`,
|
|
||||||
`completed`,
|
|
||||||
);
|
|
||||||
if (!Orchestrator.buildParameters.isCliMode) core.endGroup();
|
|
||||||
await OrchestratorError.handleException(error, Orchestrator.buildParameters, Orchestrator.defaultSecrets);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async updateStatusWithBuildParameters() {
|
|
||||||
const content = { ...Orchestrator.buildParameters };
|
|
||||||
content.gitPrivateToken = ``;
|
|
||||||
content.unitySerial = ``;
|
|
||||||
content.unityEmail = ``;
|
|
||||||
content.unityPassword = ``;
|
|
||||||
const jsonContent = JSON.stringify(content, undefined, 4);
|
|
||||||
await GitHub.updateGitHubCheck(jsonContent, Orchestrator.buildParameters.buildGuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates AWS CloudFormation templates without deploying them.
|
|
||||||
* Used by aws-local mode to ensure AWS templates are correct when executing via local-docker.
|
|
||||||
* This provides confidence that AWS ECS deployments would work with the generated templates.
|
|
||||||
*/
|
|
||||||
private static async validateAwsCloudFormationTemplates() {
|
|
||||||
OrchestratorLogger.log('=== AWS CloudFormation Template Validation (aws-local mode) ===');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Import AWS template formations
|
|
||||||
const { BaseStackFormation } = await import('./providers/aws/cloud-formations/base-stack-formation');
|
|
||||||
const { TaskDefinitionFormation } = await import('./providers/aws/cloud-formations/task-definition-formation');
|
|
||||||
|
|
||||||
// Validate base stack template
|
|
||||||
const baseTemplate = BaseStackFormation.formation;
|
|
||||||
OrchestratorLogger.log(`✓ Base stack template generated (${baseTemplate.length} chars)`);
|
|
||||||
|
|
||||||
// Check for required resources in base stack
|
|
||||||
const requiredBaseResources = ['AWS::EC2::VPC', 'AWS::ECS::Cluster', 'AWS::S3::Bucket', 'AWS::IAM::Role'];
|
|
||||||
for (const resource of requiredBaseResources) {
|
|
||||||
if (baseTemplate.includes(resource)) {
|
|
||||||
OrchestratorLogger.log(` ✓ Contains ${resource}`);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Base stack template missing required resource: ${resource}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate task definition template
|
|
||||||
const taskTemplate = TaskDefinitionFormation.formation;
|
|
||||||
OrchestratorLogger.log(`✓ Task definition template generated (${taskTemplate.length} chars)`);
|
|
||||||
|
|
||||||
// Check for required resources in task definition
|
|
||||||
const requiredTaskResources = ['AWS::ECS::TaskDefinition', 'AWS::Logs::LogGroup'];
|
|
||||||
for (const resource of requiredTaskResources) {
|
|
||||||
if (taskTemplate.includes(resource)) {
|
|
||||||
OrchestratorLogger.log(` ✓ Contains ${resource}`);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Task definition template missing required resource: ${resource}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate YAML syntax by checking for common patterns
|
|
||||||
if (!baseTemplate.includes('AWSTemplateFormatVersion')) {
|
|
||||||
throw new Error('Base stack template missing AWSTemplateFormatVersion');
|
|
||||||
}
|
|
||||||
if (!taskTemplate.includes('AWSTemplateFormatVersion')) {
|
|
||||||
throw new Error('Task definition template missing AWSTemplateFormatVersion');
|
|
||||||
}
|
|
||||||
|
|
||||||
OrchestratorLogger.log('=== AWS CloudFormation templates validated successfully ===');
|
|
||||||
OrchestratorLogger.log('Note: Actual execution will use local-docker provider');
|
|
||||||
} catch (error: any) {
|
|
||||||
OrchestratorLogger.log(`AWS CloudFormation template validation failed: ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default Orchestrator;
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
# Provider Loader Dynamic Imports
|
|
||||||
|
|
||||||
## What is a Provider?
|
|
||||||
|
|
||||||
A **provider** is a pluggable backend that Orchestrator uses to run builds and workflows. Examples include **AWS**, **Kubernetes**, or local execution. Each provider implements the [ProviderInterface](https://github.com/game-ci/unity-builder/blob/main/src/model/orchestrator/providers/provider-interface.ts), which defines the common lifecycle methods (setup, run, cleanup, garbage collection, etc.).
|
|
||||||
|
|
||||||
This abstraction makes Orchestrator flexible: you can switch execution environments or add your own provider (via npm package, GitHub repo, or local path) without changing the rest of your pipeline.
|
|
||||||
|
|
||||||
## Dynamic Provider Loading
|
|
||||||
|
|
||||||
The provider loader now supports dynamic loading of providers from multiple sources including local file paths, GitHub repositories, and NPM packages.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Local File Paths**: Load providers from relative or absolute file paths
|
|
||||||
- **GitHub URLs**: Clone and load providers from GitHub repositories with automatic updates
|
|
||||||
- **NPM Packages**: Load providers from installed NPM packages
|
|
||||||
- **Automatic Updates**: GitHub repositories are automatically updated when changes are available
|
|
||||||
- **Caching**: Local caching of cloned repositories for improved performance
|
|
||||||
- **Fallback Support**: Graceful fallback to local provider if loading fails
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### Loading Built-in Providers
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { ProviderLoader } from './provider-loader';
|
|
||||||
|
|
||||||
// Load built-in providers
|
|
||||||
const awsProvider = await ProviderLoader.loadProvider('aws', buildParameters);
|
|
||||||
const k8sProvider = await ProviderLoader.loadProvider('k8s', buildParameters);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Loading Local Providers
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Load from relative path
|
|
||||||
const localProvider = await ProviderLoader.loadProvider('./my-local-provider', buildParameters);
|
|
||||||
|
|
||||||
// Load from absolute path
|
|
||||||
const absoluteProvider = await ProviderLoader.loadProvider('/path/to/provider', buildParameters);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Loading GitHub Providers
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Load from GitHub URL
|
|
||||||
const githubProvider = await ProviderLoader.loadProvider(
|
|
||||||
'https://github.com/user/my-provider',
|
|
||||||
buildParameters
|
|
||||||
);
|
|
||||||
|
|
||||||
// Load from specific branch
|
|
||||||
const branchProvider = await ProviderLoader.loadProvider(
|
|
||||||
'https://github.com/user/my-provider/tree/develop',
|
|
||||||
buildParameters
|
|
||||||
);
|
|
||||||
|
|
||||||
// Load from specific path in repository
|
|
||||||
const pathProvider = await ProviderLoader.loadProvider(
|
|
||||||
'https://github.com/user/my-provider/tree/main/src/providers',
|
|
||||||
buildParameters
|
|
||||||
);
|
|
||||||
|
|
||||||
// Shorthand notation
|
|
||||||
const shorthandProvider = await ProviderLoader.loadProvider('user/repo', buildParameters);
|
|
||||||
const branchShorthand = await ProviderLoader.loadProvider('user/repo@develop', buildParameters);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Loading NPM Packages
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Load from NPM package
|
|
||||||
const npmProvider = await ProviderLoader.loadProvider('my-provider-package', buildParameters);
|
|
||||||
|
|
||||||
// Load from scoped NPM package
|
|
||||||
const scopedProvider = await ProviderLoader.loadProvider('@scope/my-provider', buildParameters);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Provider Interface
|
|
||||||
|
|
||||||
All providers must implement the `ProviderInterface`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface ProviderInterface {
|
|
||||||
cleanupWorkflow(): Promise<void>;
|
|
||||||
setupWorkflow(buildGuid: string, buildParameters: BuildParameters, branchName: string, defaultSecretsArray: any[]): Promise<void>;
|
|
||||||
runTaskInWorkflow(buildGuid: string, task: string, workingDirectory: string, buildVolumeFolder: string, environmentVariables: any[], secrets: any[]): Promise<string>;
|
|
||||||
garbageCollect(): Promise<void>;
|
|
||||||
listResources(): Promise<ProviderResource[]>;
|
|
||||||
listWorkflow(): Promise<ProviderWorkflow[]>;
|
|
||||||
watchWorkflow(): Promise<void>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example Provider Implementation
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// my-provider.ts
|
|
||||||
import { ProviderInterface } from './provider-interface';
|
|
||||||
import BuildParameters from './build-parameters';
|
|
||||||
|
|
||||||
export default class MyProvider implements ProviderInterface {
|
|
||||||
constructor(private buildParameters: BuildParameters) {}
|
|
||||||
|
|
||||||
async cleanupWorkflow(): Promise<void> {
|
|
||||||
// Cleanup logic
|
|
||||||
}
|
|
||||||
|
|
||||||
async setupWorkflow(buildGuid: string, buildParameters: BuildParameters, branchName: string, defaultSecretsArray: any[]): Promise<void> {
|
|
||||||
// Setup logic
|
|
||||||
}
|
|
||||||
|
|
||||||
async runTaskInWorkflow(buildGuid: string, task: string, workingDirectory: string, buildVolumeFolder: string, environmentVariables: any[], secrets: any[]): Promise<string> {
|
|
||||||
// Task execution logic
|
|
||||||
return 'Task completed';
|
|
||||||
}
|
|
||||||
|
|
||||||
async garbageCollect(): Promise<void> {
|
|
||||||
// Garbage collection logic
|
|
||||||
}
|
|
||||||
|
|
||||||
async listResources(): Promise<ProviderResource[]> {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async listWorkflow(): Promise<ProviderWorkflow[]> {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async watchWorkflow(): Promise<void> {
|
|
||||||
// Watch logic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Utility Methods
|
|
||||||
|
|
||||||
### Analyze Provider Source
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Analyze a provider source without loading it
|
|
||||||
const sourceInfo = ProviderLoader.analyzeProviderSource('https://github.com/user/repo');
|
|
||||||
console.log(sourceInfo.type); // 'github'
|
|
||||||
console.log(sourceInfo.owner); // 'user'
|
|
||||||
console.log(sourceInfo.repo); // 'repo'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Clean Up Cache
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Clean up old cached repositories (older than 30 days)
|
|
||||||
await ProviderLoader.cleanupCache();
|
|
||||||
|
|
||||||
// Clean up repositories older than 7 days
|
|
||||||
await ProviderLoader.cleanupCache(7);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Get Available Providers
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Get list of built-in providers
|
|
||||||
const providers = ProviderLoader.getAvailableProviders();
|
|
||||||
console.log(providers); // ['aws', 'k8s', 'test', 'local-docker', 'local-system', 'local']
|
|
||||||
```
|
|
||||||
|
|
||||||
## Supported URL Formats
|
|
||||||
|
|
||||||
### GitHub URLs
|
|
||||||
- `https://github.com/user/repo`
|
|
||||||
- `https://github.com/user/repo.git`
|
|
||||||
- `https://github.com/user/repo/tree/branch`
|
|
||||||
- `https://github.com/user/repo/tree/branch/path/to/provider`
|
|
||||||
- `git@github.com:user/repo.git`
|
|
||||||
|
|
||||||
### Shorthand GitHub References
|
|
||||||
- `user/repo`
|
|
||||||
- `user/repo@branch`
|
|
||||||
- `user/repo@branch/path/to/provider`
|
|
||||||
|
|
||||||
### Local Paths
|
|
||||||
- `./relative/path`
|
|
||||||
- `../relative/path`
|
|
||||||
- `/absolute/path`
|
|
||||||
- `C:\\path\\to\\provider` (Windows)
|
|
||||||
|
|
||||||
### NPM Packages
|
|
||||||
- `package-name`
|
|
||||||
- `@scope/package-name`
|
|
||||||
|
|
||||||
## Caching
|
|
||||||
|
|
||||||
GitHub repositories are automatically cached in the `.provider-cache` directory. The cache key is generated based on the repository owner, name, and branch. This ensures that:
|
|
||||||
|
|
||||||
1. Repositories are only cloned once
|
|
||||||
2. Updates are checked and applied automatically
|
|
||||||
3. Performance is improved for repeated loads
|
|
||||||
4. Storage is managed efficiently
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
The provider loader includes comprehensive error handling:
|
|
||||||
|
|
||||||
- **Missing packages**: Clear error messages when providers cannot be found
|
|
||||||
- **Interface validation**: Ensures providers implement the required interface
|
|
||||||
- **Git operations**: Handles network issues and repository access problems
|
|
||||||
- **Fallback mechanism**: Falls back to local provider if loading fails
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The provider loader can be configured through environment variables:
|
|
||||||
|
|
||||||
- `PROVIDER_CACHE_DIR`: Custom cache directory (default: `.provider-cache`)
|
|
||||||
- `GIT_TIMEOUT`: Git operation timeout in milliseconds (default: 30000)
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Use specific branches or tags**: Always specify the branch or specific tag when loading from GitHub
|
|
||||||
2. **Implement proper error handling**: Wrap provider loading in try-catch blocks
|
|
||||||
3. **Clean up regularly**: Use the cleanup utility to manage cache size
|
|
||||||
4. **Test locally first**: Test providers locally before deploying
|
|
||||||
5. **Use semantic versioning**: Tag your provider repositories for stable versions
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import OrchestratorLogger from '../../services/core/orchestrator-logger';
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
import {
|
|
||||||
CloudFormation,
|
|
||||||
CreateStackCommand,
|
|
||||||
// eslint-disable-next-line import/named
|
|
||||||
CreateStackCommandInput,
|
|
||||||
DescribeStacksCommand,
|
|
||||||
// eslint-disable-next-line import/named
|
|
||||||
DescribeStacksCommandInput,
|
|
||||||
ListStacksCommand,
|
|
||||||
// eslint-disable-next-line import/named
|
|
||||||
Parameter,
|
|
||||||
UpdateStackCommand,
|
|
||||||
// eslint-disable-next-line import/named
|
|
||||||
UpdateStackCommandInput,
|
|
||||||
waitUntilStackCreateComplete,
|
|
||||||
waitUntilStackUpdateComplete,
|
|
||||||
} from '@aws-sdk/client-cloudformation';
|
|
||||||
import { BaseStackFormation } from './cloud-formations/base-stack-formation';
|
|
||||||
import crypto from 'node:crypto';
|
|
||||||
|
|
||||||
const DEFAULT_STACK_WAIT_TIME_SECONDS = 600;
|
|
||||||
|
|
||||||
function getStackWaitTime(): number {
|
|
||||||
const overrideValue = Number(process.env.ORCHESTRATOR_AWS_STACK_WAIT_TIME ?? '');
|
|
||||||
if (!Number.isNaN(overrideValue) && overrideValue > 0) {
|
|
||||||
return overrideValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DEFAULT_STACK_WAIT_TIME_SECONDS;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AWSBaseStack {
|
|
||||||
constructor(baseStackName: string) {
|
|
||||||
this.baseStackName = baseStackName;
|
|
||||||
}
|
|
||||||
private baseStackName: string;
|
|
||||||
|
|
||||||
async setupBaseStack(CF: CloudFormation) {
|
|
||||||
const baseStackName = this.baseStackName;
|
|
||||||
const stackWaitTimeSeconds = getStackWaitTime();
|
|
||||||
|
|
||||||
const baseStack = BaseStackFormation.formation;
|
|
||||||
|
|
||||||
// Cloud Formation Input
|
|
||||||
const describeStackInput: DescribeStacksCommandInput = {
|
|
||||||
StackName: baseStackName,
|
|
||||||
};
|
|
||||||
const parametersWithoutHash: Parameter[] = [{ ParameterKey: 'EnvironmentName', ParameterValue: baseStackName }];
|
|
||||||
const parametersHash = crypto
|
|
||||||
.createHash('md5')
|
|
||||||
.update(baseStack + JSON.stringify(parametersWithoutHash))
|
|
||||||
.digest('hex');
|
|
||||||
const parameters: Parameter[] = [
|
|
||||||
...parametersWithoutHash,
|
|
||||||
...[{ ParameterKey: 'Version', ParameterValue: parametersHash }],
|
|
||||||
];
|
|
||||||
const updateInput: UpdateStackCommandInput = {
|
|
||||||
StackName: baseStackName,
|
|
||||||
TemplateBody: baseStack,
|
|
||||||
Parameters: parameters,
|
|
||||||
Capabilities: ['CAPABILITY_IAM'],
|
|
||||||
};
|
|
||||||
const createStackInput: CreateStackCommandInput = {
|
|
||||||
StackName: baseStackName,
|
|
||||||
TemplateBody: baseStack,
|
|
||||||
Parameters: parameters,
|
|
||||||
Capabilities: ['CAPABILITY_IAM'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const stacks = await CF.send(
|
|
||||||
new ListStacksCommand({
|
|
||||||
StackStatusFilter: [
|
|
||||||
'CREATE_IN_PROGRESS',
|
|
||||||
'UPDATE_IN_PROGRESS',
|
|
||||||
'UPDATE_COMPLETE',
|
|
||||||
'CREATE_COMPLETE',
|
|
||||||
'ROLLBACK_COMPLETE',
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const stackNames = stacks.StackSummaries?.map((x) => x.StackName) || [];
|
|
||||||
const stackExists: boolean = stackNames.includes(baseStackName);
|
|
||||||
const describeStack = async () => {
|
|
||||||
return await CF.send(new DescribeStacksCommand(describeStackInput));
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
if (!stackExists) {
|
|
||||||
OrchestratorLogger.log(`${baseStackName} stack does not exist (${JSON.stringify(stackNames)})`);
|
|
||||||
let created = false;
|
|
||||||
try {
|
|
||||||
await CF.send(new CreateStackCommand(createStackInput));
|
|
||||||
created = true;
|
|
||||||
} catch (error: any) {
|
|
||||||
const message = `${error?.name ?? ''} ${error?.message ?? ''}`;
|
|
||||||
if (message.includes('AlreadyExistsException')) {
|
|
||||||
OrchestratorLogger.log(`Base stack already exists, continuing with describe`);
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (created) {
|
|
||||||
OrchestratorLogger.log(`created stack (version: ${parametersHash})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const CFState = await describeStack();
|
|
||||||
let stack = CFState.Stacks?.[0];
|
|
||||||
if (!stack) {
|
|
||||||
throw new Error(`Base stack doesn't exist, even after creation, stackExists check: ${stackExists}`);
|
|
||||||
}
|
|
||||||
const stackVersion = stack.Parameters?.find((x) => x.ParameterKey === 'Version')?.ParameterValue;
|
|
||||||
|
|
||||||
if (stack.StackStatus === 'CREATE_IN_PROGRESS') {
|
|
||||||
OrchestratorLogger.log(
|
|
||||||
`Waiting up to ${stackWaitTimeSeconds}s for '${baseStackName}' CloudFormation creation to finish`,
|
|
||||||
);
|
|
||||||
await waitUntilStackCreateComplete(
|
|
||||||
{
|
|
||||||
client: CF,
|
|
||||||
maxWaitTime: stackWaitTimeSeconds,
|
|
||||||
},
|
|
||||||
describeStackInput,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stackExists) {
|
|
||||||
OrchestratorLogger.log(`Base stack exists (version: ${stackVersion}, local version: ${parametersHash})`);
|
|
||||||
if (parametersHash !== stackVersion) {
|
|
||||||
OrchestratorLogger.log(`Attempting update of base stack`);
|
|
||||||
try {
|
|
||||||
await CF.send(new UpdateStackCommand(updateInput));
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error['message'].includes('No updates are to be performed')) {
|
|
||||||
OrchestratorLogger.log(`No updates are to be performed`);
|
|
||||||
} else {
|
|
||||||
OrchestratorLogger.log(`Update Failed (Stack name: ${baseStackName})`);
|
|
||||||
OrchestratorLogger.log(error['message']);
|
|
||||||
}
|
|
||||||
OrchestratorLogger.log(`Continuing...`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
OrchestratorLogger.log(`No update required`);
|
|
||||||
}
|
|
||||||
stack = (await describeStack()).Stacks?.[0];
|
|
||||||
if (!stack) {
|
|
||||||
throw new Error(
|
|
||||||
`Base stack doesn't exist, even after updating and creation, stackExists check: ${stackExists}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (stack.StackStatus === 'UPDATE_IN_PROGRESS') {
|
|
||||||
OrchestratorLogger.log(
|
|
||||||
`Waiting up to ${stackWaitTimeSeconds}s for '${baseStackName}' CloudFormation update to finish`,
|
|
||||||
);
|
|
||||||
await waitUntilStackUpdateComplete(
|
|
||||||
{
|
|
||||||
client: CF,
|
|
||||||
maxWaitTime: stackWaitTimeSeconds,
|
|
||||||
},
|
|
||||||
describeStackInput,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OrchestratorLogger.log('base stack is now ready');
|
|
||||||
} catch (error) {
|
|
||||||
core.error(JSON.stringify(await describeStack(), undefined, 4));
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import { CloudFormation } from '@aws-sdk/client-cloudformation';
|
|
||||||
import { ECS } from '@aws-sdk/client-ecs';
|
|
||||||
import { Kinesis } from '@aws-sdk/client-kinesis';
|
|
||||||
import { CloudWatchLogs } from '@aws-sdk/client-cloudwatch-logs';
|
|
||||||
import { S3 } from '@aws-sdk/client-s3';
|
|
||||||
import { Input } from '../../..';
|
|
||||||
import OrchestratorOptions from '../../options/orchestrator-options';
|
|
||||||
|
|
||||||
export class AwsClientFactory {
|
|
||||||
private static cloudFormation: CloudFormation;
|
|
||||||
private static ecs: ECS;
|
|
||||||
private static kinesis: Kinesis;
|
|
||||||
private static cloudWatchLogs: CloudWatchLogs;
|
|
||||||
private static s3: S3;
|
|
||||||
|
|
||||||
private static getCredentials() {
|
|
||||||
// Explicitly provide credentials from environment variables for LocalStack compatibility
|
|
||||||
// LocalStack accepts any credentials, but the AWS SDK needs them to be explicitly set
|
|
||||||
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
|
||||||
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
||||||
|
|
||||||
if (accessKeyId && secretAccessKey) {
|
|
||||||
return {
|
|
||||||
accessKeyId,
|
|
||||||
secretAccessKey,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return undefined to let AWS SDK use default credential chain
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getCloudFormation(): CloudFormation {
|
|
||||||
if (!this.cloudFormation) {
|
|
||||||
this.cloudFormation = new CloudFormation({
|
|
||||||
region: Input.region,
|
|
||||||
endpoint: OrchestratorOptions.awsCloudFormationEndpoint,
|
|
||||||
credentials: AwsClientFactory.getCredentials(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.cloudFormation;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getECS(): ECS {
|
|
||||||
if (!this.ecs) {
|
|
||||||
this.ecs = new ECS({
|
|
||||||
region: Input.region,
|
|
||||||
endpoint: OrchestratorOptions.awsEcsEndpoint,
|
|
||||||
credentials: AwsClientFactory.getCredentials(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.ecs;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getKinesis(): Kinesis {
|
|
||||||
if (!this.kinesis) {
|
|
||||||
this.kinesis = new Kinesis({
|
|
||||||
region: Input.region,
|
|
||||||
endpoint: OrchestratorOptions.awsKinesisEndpoint,
|
|
||||||
credentials: AwsClientFactory.getCredentials(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.kinesis;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getCloudWatchLogs(): CloudWatchLogs {
|
|
||||||
if (!this.cloudWatchLogs) {
|
|
||||||
this.cloudWatchLogs = new CloudWatchLogs({
|
|
||||||
region: Input.region,
|
|
||||||
endpoint: OrchestratorOptions.awsCloudWatchLogsEndpoint,
|
|
||||||
credentials: AwsClientFactory.getCredentials(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.cloudWatchLogs;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getS3(): S3 {
|
|
||||||
if (!this.s3) {
|
|
||||||
this.s3 = new S3({
|
|
||||||
region: Input.region,
|
|
||||||
endpoint: OrchestratorOptions.awsS3Endpoint,
|
|
||||||
forcePathStyle: true,
|
|
||||||
credentials: AwsClientFactory.getCredentials(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.s3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { TaskDefinitionFormation } from './cloud-formations/task-definition-formation';
|
|
||||||
|
|
||||||
export class AWSCloudFormationTemplates {
|
|
||||||
public static getParameterTemplate(p1: string) {
|
|
||||||
return `
|
|
||||||
${p1}:
|
|
||||||
Type: String
|
|
||||||
Default: ''
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getSecretTemplate(p1: string) {
|
|
||||||
return `
|
|
||||||
${p1}Secret:
|
|
||||||
Type: AWS::SecretsManager::Secret
|
|
||||||
Properties:
|
|
||||||
Name: '${p1}'
|
|
||||||
SecretString: !Ref ${p1}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getSecretDefinitionTemplate(p1: string, p2: string) {
|
|
||||||
return `
|
|
||||||
Secrets:
|
|
||||||
- Name: '${p1}'
|
|
||||||
ValueFrom: !Ref ${p2}Secret
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static insertAtTemplate(template: string, insertionKey: string, insertion: string) {
|
|
||||||
const index = template.search(insertionKey) + insertionKey.length + '\n'.length;
|
|
||||||
template = [template.slice(0, index), insertion, template.slice(index)].join('');
|
|
||||||
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readTaskCloudFormationTemplate(): string {
|
|
||||||
return TaskDefinitionFormation.formation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import OrchestratorLogger from '../../services/core/orchestrator-logger';
|
|
||||||
import { CloudFormation, DescribeStackEventsCommand } from '@aws-sdk/client-cloudformation';
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
import Orchestrator from '../../orchestrator';
|
|
||||||
|
|
||||||
export class AWSError {
|
|
||||||
static async handleStackCreationFailure(error: any, CF: CloudFormation, taskDefStackName: string) {
|
|
||||||
OrchestratorLogger.log('aws error: ');
|
|
||||||
core.error(JSON.stringify(error, undefined, 4));
|
|
||||||
if (Orchestrator.buildParameters.orchestratorDebug) {
|
|
||||||
OrchestratorLogger.log('Getting events and resources for task stack');
|
|
||||||
const events = (await CF.send(new DescribeStackEventsCommand({ StackName: taskDefStackName }))).StackEvents;
|
|
||||||
OrchestratorLogger.log(JSON.stringify(events, undefined, 4));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user