mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-06-11 08:23:56 -07:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd95ad9604 | |||
| d829bfc901 | |||
| 2240bedf08 | |||
| 16c5c20793 | |||
| 821ba97789 | |||
| 365bdb5e12 | |||
| c7a43cde4d | |||
| d10fd10a95 | |||
| ef0555fb0d | |||
| 4a7fc08e63 | |||
| ce7ce7a416 |
@@ -1,5 +0,0 @@
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
||||
jest.config.js
|
||||
src/types/
|
||||
@@ -1,91 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -14,9 +14,7 @@ assignees: ''
|
||||
|
||||
<!--Steps to reproduce the behavior:-->
|
||||
|
||||
-
|
||||
|
||||
**Expected behavior**
|
||||
- **Expected behavior**
|
||||
|
||||
<!--A clear and concise description of what you expected to happen.-->
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ jobs:
|
||||
projectPath:
|
||||
- test-project
|
||||
unityVersion:
|
||||
- 2021.3.45f1
|
||||
- 2022.3.13f1
|
||||
- 2023.2.2f1
|
||||
- 2021.3.45f2
|
||||
- 2022.3.62f3
|
||||
- 2023.2.22f1
|
||||
targetPlatform:
|
||||
- StandaloneOSX # Build a MacOS executable
|
||||
- iOS # Build an iOS executable
|
||||
|
||||
@@ -9,8 +9,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
UNITY_LICENSE:
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n <License
|
||||
UNITY_LICENSE: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n <License
|
||||
id=\"Terms\">\n <MachineBindings>\n <Binding Key=\"1\"
|
||||
Value=\"576562626572264761624c65526f7578\"/>\n <Binding Key=\"2\"
|
||||
Value=\"576562626572264761624c65526f7578\"/>\n </MachineBindings>\n <MachineID
|
||||
@@ -36,8 +35,7 @@ env:
|
||||
|
||||
jobs:
|
||||
buildForAllPlatformsUbuntu:
|
||||
name:
|
||||
"${{ matrix.targetPlatform }} on ${{ matrix.unityVersion}}${{startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }}"
|
||||
name: "${{ matrix.targetPlatform }} on ${{ matrix.unityVersion}}${{startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }}"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -48,9 +46,9 @@ jobs:
|
||||
projectPath:
|
||||
- test-project
|
||||
unityVersion:
|
||||
- 2021.3.32f1
|
||||
- 2022.3.13f1
|
||||
- 2023.2.2f1
|
||||
- 2021.3.45f2
|
||||
- 2022.3.62f3
|
||||
- 2023.2.22f1
|
||||
targetPlatform:
|
||||
- StandaloneOSX # Build a macOS standalone (Intel 64-bit) with mono backend.
|
||||
- StandaloneWindows64 # Build a Windows 64-bit standalone with mono backend.
|
||||
@@ -200,7 +198,6 @@ jobs:
|
||||
###########################
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name:
|
||||
"Build ${{ matrix.targetPlatform }}${{ startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }} on Ubuntu (${{ matrix.unityVersion }}_il2cpp_${{ matrix.buildWithIl2cpp }}_params_${{ matrix.additionalParameters }})"
|
||||
name: "Build ${{ matrix.targetPlatform }}${{ startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }} on Ubuntu (${{ matrix.unityVersion }}_il2cpp_${{ matrix.buildWithIl2cpp }}_params_${{ matrix.additionalParameters }})"
|
||||
path: build
|
||||
retention-days: 14
|
||||
|
||||
@@ -18,9 +18,9 @@ jobs:
|
||||
projectPath:
|
||||
- test-project
|
||||
unityVersion:
|
||||
- 2021.3.32f1
|
||||
- 2022.3.13f1
|
||||
- 2023.2.2f1
|
||||
- 2021.3.45f2
|
||||
- 2022.3.62f3
|
||||
- 2023.2.22f1
|
||||
targetPlatform:
|
||||
- Android # Build an Android apk.
|
||||
- StandaloneWindows64 # Build a Windows 64-bit standalone.
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
- unityVersion: 6000.0.36f1
|
||||
targetPlatform: StandaloneWindows64
|
||||
buildProfile: 'Assets/Settings/Build Profiles/Sample Windows Build Profile.asset'
|
||||
|
||||
|
||||
steps:
|
||||
###########################
|
||||
# Checkout #
|
||||
@@ -66,6 +66,34 @@ jobs:
|
||||
run: |
|
||||
Move-Item -Path "./test-project/ProjectSettings/ProjectSettingsIl2cpp.asset" -Destination "./test-project/ProjectSettings/ProjectSettings.asset" -Force
|
||||
|
||||
###########################
|
||||
# Docker Readiness #
|
||||
###########################
|
||||
- name: Ensure Docker daemon is ready
|
||||
timeout-minutes: 2
|
||||
shell: powershell
|
||||
run: |
|
||||
$maxRetries = 10
|
||||
$retryDelay = 6
|
||||
for ($i = 0; $i -lt $maxRetries; $i++) {
|
||||
$svc = Get-Service docker -ErrorAction SilentlyContinue
|
||||
if ($svc -and $svc.Status -eq 'Running') {
|
||||
docker version 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "Docker is ready."
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
if ($svc -and $svc.Status -eq 'Stopped') {
|
||||
Write-Host "Docker service stopped, attempting to start..."
|
||||
Start-Service docker -ErrorAction SilentlyContinue
|
||||
}
|
||||
Write-Host "Waiting for Docker daemon (attempt $($i+1)/$maxRetries)..."
|
||||
Start-Sleep -Seconds $retryDelay
|
||||
}
|
||||
Write-Error "Docker daemon did not start within $($maxRetries * $retryDelay) seconds"
|
||||
exit 1
|
||||
|
||||
###########################
|
||||
# Build #
|
||||
###########################
|
||||
@@ -146,6 +174,8 @@ jobs:
|
||||
###########################
|
||||
- uses: actions/upload-artifact@v4
|
||||
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
|
||||
retention-days: 14
|
||||
|
||||
@@ -2,7 +2,8 @@ name: Integrity
|
||||
|
||||
on:
|
||||
push: { branches: [main] }
|
||||
pull_request: {}
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -22,10 +23,31 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install package manager (from package.json)
|
||||
run: |
|
||||
corepack enable
|
||||
corepack install
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
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 test:ci --coverage
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
@@ -34,6 +56,7 @@ jobs:
|
||||
|
||||
orchestrator-integration:
|
||||
name: Orchestrator Integration
|
||||
if: github.event_name == 'push'
|
||||
if: >-
|
||||
github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'run-integration')
|
||||
uses: ./.github/workflows/validate-orchestrator-integration.yml
|
||||
secrets: inherit
|
||||
|
||||
@@ -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
|
||||
@@ -1,41 +0,0 @@
|
||||
name: "TEMP: Push secrets to orchestrator repo"
|
||||
|
||||
# One-shot workflow — run manually, then delete this file and branch.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push-secrets:
|
||||
name: Push secrets to game-ci/orchestrator
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Push UNITY_EMAIL
|
||||
run: gh secret set UNITY_EMAIL --repo game-ci/orchestrator --body "$SECRET_VALUE"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
SECRET_VALUE: ${{ secrets.UNITY_EMAIL }}
|
||||
|
||||
- name: Push UNITY_PASSWORD
|
||||
run: gh secret set UNITY_PASSWORD --repo game-ci/orchestrator --body "$SECRET_VALUE"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
SECRET_VALUE: ${{ secrets.UNITY_PASSWORD }}
|
||||
|
||||
- name: Push UNITY_SERIAL
|
||||
run: gh secret set UNITY_SERIAL --repo game-ci/orchestrator --body "$SECRET_VALUE"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
SECRET_VALUE: ${{ secrets.UNITY_SERIAL }}
|
||||
|
||||
- name: Push GIT_PRIVATE_TOKEN
|
||||
run: gh secret set GIT_PRIVATE_TOKEN --repo game-ci/orchestrator --body "$SECRET_VALUE"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
SECRET_VALUE: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
|
||||
- name: Confirm
|
||||
run: |
|
||||
echo "Secrets pushed to game-ci/orchestrator:"
|
||||
gh secret list --repo game-ci/orchestrator
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
@@ -98,12 +98,12 @@ jobs:
|
||||
|
||||
# Add git package via manifest
|
||||
cd test-project
|
||||
cat Packages/manifest.json | python3 -c "
|
||||
python3 -c "
|
||||
import sys, json
|
||||
manifest = json.load(sys.stdin)
|
||||
manifest['dependencies']['${{ matrix.name }}'] = '${{ matrix.package }}'
|
||||
json.dump(manifest, sys.stdout, indent=2)
|
||||
" > Packages/manifest.tmp && mv Packages/manifest.tmp Packages/manifest.json
|
||||
" < Packages/manifest.json > Packages/manifest.tmp && mv Packages/manifest.tmp Packages/manifest.json
|
||||
cd ..
|
||||
fi
|
||||
|
||||
@@ -125,18 +125,20 @@ jobs:
|
||||
if: always()
|
||||
run: |
|
||||
STATUS="${{ steps.build.outcome }}"
|
||||
echo "## ${{ matrix.name }} — ${{ matrix.platform }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "$STATUS" = "success" ]; then
|
||||
echo "✅ **PASSED** — Compiled and built successfully" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "❌ **FAILED** — Build or compilation failed" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Unity: ${{ matrix.unity }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Platform: ${{ matrix.platform }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Source: ${{ matrix.source }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Package: \`${{ matrix.package }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
{
|
||||
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
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Orchestrator Integration Tests
|
||||
|
||||
# ==============================================================================
|
||||
# Exhaustive integration tests — runs on a daily cron and manual dispatch.
|
||||
# Slow (~1-2h wall-clock): k8s, AWS, local-docker, rclone via LocalStack + k3d.
|
||||
# Slow (~1-2h wall-clock): k8s, AWS, local-docker, rclone via MiniStack + k3d.
|
||||
#
|
||||
# Mirrors the full orchestrator-integrity.yml test suite from the orchestrator
|
||||
# repo, run from unity-builder's perspective to catch cross-repo regressions.
|
||||
@@ -21,9 +21,9 @@ permissions:
|
||||
checks: write
|
||||
statuses: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
# Note: no concurrency block here — when invoked via workflow_call, the caller
|
||||
# (integrity-check.yml) manages concurrency. For direct dispatch/cron, runs are
|
||||
# naturally serialized by GitHub's single-concurrency-per-ref default.
|
||||
|
||||
env:
|
||||
AWS_STACK_NAME: game-ci-team-pipelines
|
||||
@@ -47,20 +47,44 @@ jobs:
|
||||
- name: Checkout unity-builder
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout orchestrator
|
||||
- name: Checkout orchestrator (matching branch)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: game-ci/orchestrator
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
path: orchestrator-standalone
|
||||
continue-on-error: true
|
||||
id: orchestrator-branch
|
||||
|
||||
- name: Fallback to orchestrator main branch
|
||||
if: steps.orchestrator-branch.outcome == 'failure'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: game-ci/orchestrator
|
||||
path: orchestrator-standalone
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Install package manager (from package.json)
|
||||
run: |
|
||||
corepack enable
|
||||
corepack install
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 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-20-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
yarn-v2-${{ runner.os }}-node-20-
|
||||
|
||||
- name: Install unity-builder dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Build unity-builder
|
||||
run: |
|
||||
@@ -71,12 +95,12 @@ jobs:
|
||||
- name: Run plugin interface unit tests
|
||||
run: |
|
||||
echo "Running orchestrator-plugin unit tests..."
|
||||
npx jest orchestrator-plugin --verbose --detectOpenHandles --forceExit
|
||||
npx vitest run orchestrator-plugin --reporter=verbose
|
||||
|
||||
- name: Build and pack orchestrator
|
||||
working-directory: orchestrator-standalone
|
||||
run: |
|
||||
yarn install --frozen-lockfile
|
||||
yarn install --immutable
|
||||
echo "Building orchestrator..."
|
||||
npx tsc
|
||||
echo "✓ orchestrator compiles successfully"
|
||||
@@ -88,32 +112,27 @@ jobs:
|
||||
echo "Installing orchestrator into unity-builder workspace..."
|
||||
npm install ./orchestrator-standalone/game-ci-orchestrator-*.tgz --no-save --legacy-peer-deps
|
||||
|
||||
- name: Verify all services and lazy loaders
|
||||
- name: Verify plugin loader returns exports with orchestrator installed
|
||||
run: |
|
||||
node -e "
|
||||
const { loadOrchestrator, loadPluginServices } = require('./lib/model/orchestrator-plugin');
|
||||
const { loadOrchestratorPlugin } = require('./lib/model/orchestrator-plugin');
|
||||
(async () => {
|
||||
const orch = await loadOrchestrator();
|
||||
if (!orch || typeof orch.run !== 'function') {
|
||||
console.error('ERROR: loadOrchestrator failed');
|
||||
const plugin = await loadOrchestratorPlugin();
|
||||
if (plugin === undefined) {
|
||||
console.error('ERROR: loadOrchestratorPlugin should return defined plugin when package is installed');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✓ loadOrchestrator().run is a function');
|
||||
|
||||
const services = await loadPluginServices();
|
||||
if (!services) { console.error('ERROR: loadPluginServices failed'); process.exit(1); }
|
||||
|
||||
const eager = ['BuildReliabilityService','TestWorkflowService','HotRunnerService','OutputService','OutputTypeRegistry','ArtifactUploadHandler','IncrementalSyncService'];
|
||||
for (const s of eager) { if (!services[s]) { console.error('Missing: ' + s); process.exit(1); } }
|
||||
console.log('✓ All ' + eager.length + ' eager services loaded');
|
||||
|
||||
const lazy = ['loadChildWorkspaceService','loadLocalCacheService','loadSubmoduleProfileService','loadLfsAgentService','loadGitHooksService'];
|
||||
for (const l of lazy) {
|
||||
if (typeof services[l] !== 'function') { console.error('Missing loader: ' + l); process.exit(1); }
|
||||
const svc = await services[l]();
|
||||
if (!svc) { console.error(l + '() returned undefined'); 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('✓ All ' + lazy.length + ' lazy loaders return defined services');
|
||||
console.log('✓ loadOrchestratorPlugin() returns plugin with all ' + lifecycleMethods.length + ' lifecycle methods');
|
||||
})();
|
||||
"
|
||||
|
||||
@@ -128,7 +147,7 @@ jobs:
|
||||
"
|
||||
|
||||
# ============================================================================
|
||||
# K8S INTEGRATION TESTS (k3d + LocalStack)
|
||||
# K8S INTEGRATION TESTS (k3d + MiniStack)
|
||||
# ============================================================================
|
||||
k8s-integration:
|
||||
name: K8s Integration Tests
|
||||
@@ -139,17 +158,45 @@ jobs:
|
||||
RESOURCE_TRACKING: 'true'
|
||||
K8S_LOCALSTACK_HOST: localstack-main
|
||||
steps:
|
||||
- name: Checkout orchestrator
|
||||
- name: Checkout orchestrator (matching branch)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: game-ci/orchestrator
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
lfs: false
|
||||
continue-on-error: true
|
||||
id: orch-branch
|
||||
|
||||
- name: Clean corrupted checkout
|
||||
if: steps.orch-branch.outcome == 'failure'
|
||||
run: rm -rf .git || true
|
||||
|
||||
- name: Fallback to orchestrator main branch
|
||||
if: steps.orch-branch.outcome == 'failure'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: game-ci/orchestrator
|
||||
lfs: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Install package manager (from package.json)
|
||||
run: |
|
||||
corepack enable
|
||||
corepack install
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 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-20-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
yarn-v2-${{ runner.os }}-node-20-
|
||||
|
||||
- name: Set up kubectl
|
||||
uses: azure/setup-kubectl@v4
|
||||
@@ -216,19 +263,16 @@ jobs:
|
||||
echo "Disk usage after cleanup:"
|
||||
df -h
|
||||
|
||||
- name: Start LocalStack
|
||||
- name: Start MiniStack
|
||||
run: |
|
||||
echo "Starting LocalStack..."
|
||||
echo "Starting MiniStack..."
|
||||
docker run -d \
|
||||
--name localstack-main \
|
||||
--network orchestrator-net \
|
||||
--add-host=host.docker.internal:host-gateway \
|
||||
-p 4566:4566 \
|
||||
-e SERVICES=s3,cloudformation,ecs,kinesis,cloudwatch,logs,efs,ec2,iam,elasticfilesystem,secretsmanager,lambda,events,sts \
|
||||
-e DEBUG=0 \
|
||||
-e HOSTNAME_EXTERNAL=localstack-main \
|
||||
localstack/localstack:latest || true
|
||||
echo "Waiting for LocalStack to be ready..."
|
||||
ministackorg/ministack:latest || true
|
||||
echo "Waiting for MiniStack to be ready..."
|
||||
MAX_ATTEMPTS=60
|
||||
READY=false
|
||||
for i in $(seq 1 $MAX_ATTEMPTS); do
|
||||
@@ -236,14 +280,14 @@ jobs:
|
||||
HEALTH=$(curl -s http://localhost:4566/_localstack/health 2>/dev/null || echo "")
|
||||
if [ -z "$HEALTH" ] || ! echo "$HEALTH" | grep -q "services"; then sleep 2; continue; fi
|
||||
if echo "$HEALTH" | grep -q '"s3"'; then
|
||||
echo "LocalStack is ready (attempt $i/$MAX_ATTEMPTS)"
|
||||
echo "MiniStack is ready (attempt $i/$MAX_ATTEMPTS)"
|
||||
READY=true
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [ "$READY" != "true" ]; then
|
||||
echo "ERROR: LocalStack did not become ready"
|
||||
echo "ERROR: MiniStack did not become ready"
|
||||
docker logs localstack-main --tail 100 || true
|
||||
exit 1
|
||||
fi
|
||||
@@ -255,20 +299,20 @@ jobs:
|
||||
|
||||
- name: Create S3 bucket for tests
|
||||
run: |
|
||||
for i in {1..10}; do
|
||||
for _ in {1..10}; do
|
||||
if curl -s http://localhost:4566/_localstack/health > /dev/null 2>&1; then break; fi
|
||||
sleep 1
|
||||
done
|
||||
for i in {1..5}; do
|
||||
for _ in {1..5}; do
|
||||
if command -v awslocal > /dev/null 2>&1; then
|
||||
awslocal s3 mb s3://$AWS_STACK_NAME 2>&1 && break
|
||||
awslocal s3 mb s3://"$AWS_STACK_NAME" 2>&1 && break
|
||||
else
|
||||
aws --endpoint-url=http://localhost:4566 s3 mb s3://$AWS_STACK_NAME 2>&1 && break
|
||||
aws --endpoint-url=http://localhost:4566 s3 mb s3://"$AWS_STACK_NAME" 2>&1 && break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn install --immutable
|
||||
|
||||
- name: Build orchestrator
|
||||
run: |
|
||||
@@ -280,9 +324,20 @@ jobs:
|
||||
- name: Run orchestrator unit tests (fast, no infra)
|
||||
timeout-minutes: 2
|
||||
run: >-
|
||||
yarn run test
|
||||
--testPathPattern="orchestrator-guid|orchestrator-folders|task-parameter-serializer|follow-log-stream-service|runner-availability-service|provider-url-parser|provider-loader|provider-git-manager|orchestrator-image|orchestrator-hooks|orchestrator-github-checks|middleware-service"
|
||||
--verbose --detectOpenHandles --forceExit --runInBand
|
||||
yarn vitest run
|
||||
"orchestrator-guid"
|
||||
"orchestrator-folders"
|
||||
"task-parameter-serializer"
|
||||
"follow-log-stream-service"
|
||||
"runner-availability-service"
|
||||
"provider-url-parser"
|
||||
"provider-loader"
|
||||
"provider-git-manager"
|
||||
"orchestrator-image"
|
||||
"orchestrator-hooks"
|
||||
"orchestrator-github-checks"
|
||||
"middleware-service"
|
||||
--reporter=verbose --no-file-parallelism
|
||||
|
||||
# --- K8s cluster setup ---
|
||||
- name: Clean up disk space before K8s tests
|
||||
@@ -296,15 +351,15 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
LOCALSTACK_IP=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' localstack-main 2>/dev/null || echo "")
|
||||
echo "LocalStack container IP: $LOCALSTACK_IP"
|
||||
echo "MiniStack container IP: $LOCALSTACK_IP"
|
||||
k3d cluster create unity-builder \
|
||||
--agents 1 \
|
||||
--network orchestrator-net \
|
||||
--wait
|
||||
kubectl config current-context | cat
|
||||
echo "LOCALSTACK_IP=$LOCALSTACK_IP" >> $GITHUB_ENV
|
||||
echo "LOCALSTACK_IP=$LOCALSTACK_IP" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Verify cluster readiness and LocalStack connectivity
|
||||
- name: Verify cluster readiness and MiniStack connectivity
|
||||
timeout-minutes: 2
|
||||
run: |
|
||||
for i in {1..60}; do
|
||||
@@ -315,15 +370,15 @@ jobs:
|
||||
kubectl get nodes
|
||||
LOCALSTACK_IP=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' localstack-main 2>/dev/null || echo "")
|
||||
kubectl run test-localstack --image=curlimages/curl --rm -i --restart=Never --timeout=30s -- \
|
||||
curl -v --max-time 10 http://${LOCALSTACK_IP}:4566/_localstack/health 2>&1 | head -30 || \
|
||||
echo "Cluster connectivity test - LocalStack may not be accessible from k3d"
|
||||
curl -v --max-time 10 http://"${LOCALSTACK_IP}":4566/_localstack/health 2>&1 | head -30 || \
|
||||
echo "Cluster connectivity test - MiniStack may not be accessible from k3d"
|
||||
|
||||
- name: Clean up K8s resources before tests
|
||||
run: |
|
||||
source /tmp/cleanup-functions.sh
|
||||
k8s_resource_cleanup
|
||||
for i in {1..30}; do
|
||||
PVC_COUNT=$(kubectl get pvc -n default 2>/dev/null | grep "unity-builder-pvc-" | wc -l || echo "0")
|
||||
for _ in {1..30}; do
|
||||
PVC_COUNT=$(kubectl get pvc -n default 2>/dev/null | grep -c "unity-builder-pvc-" || echo "0")
|
||||
if [ "$PVC_COUNT" -eq 0 ]; then echo "All PVCs deleted"; break; fi
|
||||
sleep 1
|
||||
done
|
||||
@@ -332,7 +387,7 @@ jobs:
|
||||
# --- K8s Test 1: orchestrator-image ---
|
||||
- name: Run orchestrator-image test (K8s)
|
||||
timeout-minutes: 10
|
||||
run: yarn run test "orchestrator-image" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-image" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -356,7 +411,7 @@ jobs:
|
||||
# --- K8s Test 2: orchestrator-kubernetes ---
|
||||
- name: Run orchestrator-kubernetes test
|
||||
timeout-minutes: 30
|
||||
run: yarn run test "orchestrator-kubernetes" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-kubernetes" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -388,7 +443,7 @@ jobs:
|
||||
# --- K8s Test 3: orchestrator-s3-steps ---
|
||||
- name: Run orchestrator-s3-steps test (K8s)
|
||||
timeout-minutes: 30
|
||||
run: yarn run test "orchestrator-s3-steps" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-s3-steps" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -421,7 +476,7 @@ jobs:
|
||||
- name: Run orchestrator-end2end-caching test (K8s)
|
||||
timeout-minutes: 60
|
||||
continue-on-error: true
|
||||
run: yarn run test "orchestrator-end2end-caching" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-end2end-caching" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -460,7 +515,7 @@ jobs:
|
||||
- name: Run orchestrator-end2end-retaining test (K8s)
|
||||
timeout-minutes: 60
|
||||
continue-on-error: true
|
||||
run: yarn run test "orchestrator-end2end-retaining" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-end2end-retaining" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -511,17 +566,45 @@ jobs:
|
||||
AWS_ENDPOINT: http://localhost:4566
|
||||
AWS_ENDPOINT_URL: http://localhost:4566
|
||||
steps:
|
||||
- name: Checkout orchestrator
|
||||
- name: Checkout orchestrator (matching branch)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: game-ci/orchestrator
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
lfs: false
|
||||
continue-on-error: true
|
||||
id: orch-branch
|
||||
|
||||
- name: Clean corrupted checkout
|
||||
if: steps.orch-branch.outcome == 'failure'
|
||||
run: rm -rf .git || true
|
||||
|
||||
- name: Fallback to orchestrator main branch
|
||||
if: steps.orch-branch.outcome == 'failure'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: game-ci/orchestrator
|
||||
lfs: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Install package manager (from package.json)
|
||||
run: |
|
||||
corepack enable
|
||||
corepack install
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 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-20-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
yarn-v2-${{ runner.os }}-node-20-
|
||||
|
||||
- name: Define cleanup functions
|
||||
run: |
|
||||
@@ -546,18 +629,16 @@ jobs:
|
||||
docker system prune -af --volumes || true
|
||||
df -h
|
||||
|
||||
- name: Start LocalStack
|
||||
- name: Start MiniStack
|
||||
run: |
|
||||
docker run -d \
|
||||
--name localstack-main \
|
||||
-p 4566:4566 \
|
||||
-e SERVICES=s3,cloudformation,ecs,kinesis,cloudwatch,logs,efs,ec2,iam,elasticfilesystem,secretsmanager,lambda,events,sts \
|
||||
-e DEBUG=0 \
|
||||
localstack/localstack:latest || true
|
||||
ministackorg/ministack:latest || true
|
||||
MAX_ATTEMPTS=60
|
||||
for i in $(seq 1 $MAX_ATTEMPTS); do
|
||||
HEALTH=$(curl -s http://localhost:4566/_localstack/health 2>/dev/null || echo "")
|
||||
if echo "$HEALTH" | grep -q '"s3"'; then echo "LocalStack ready ($i/$MAX_ATTEMPTS)"; break; fi
|
||||
if echo "$HEALTH" | grep -q '"s3"'; then echo "MiniStack ready ($i/$MAX_ATTEMPTS)"; break; fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
@@ -568,16 +649,16 @@ jobs:
|
||||
|
||||
- name: Create S3 bucket for tests
|
||||
run: |
|
||||
for i in {1..5}; do
|
||||
for _ in {1..5}; do
|
||||
if command -v awslocal > /dev/null 2>&1; then
|
||||
awslocal s3 mb s3://$AWS_STACK_NAME 2>&1 && break
|
||||
awslocal s3 mb s3://"$AWS_STACK_NAME" 2>&1 && break
|
||||
else
|
||||
aws --endpoint-url=http://localhost:4566 s3 mb s3://$AWS_STACK_NAME 2>&1 && break
|
||||
aws --endpoint-url=http://localhost:4566 s3 mb s3://"$AWS_STACK_NAME" 2>&1 && break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn install --immutable
|
||||
|
||||
- name: Build orchestrator
|
||||
run: |
|
||||
@@ -588,7 +669,7 @@ jobs:
|
||||
# --- AWS Test 1: orchestrator-image ---
|
||||
- name: Run orchestrator-image test (AWS)
|
||||
timeout-minutes: 10
|
||||
run: yarn run test "orchestrator-image" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-image" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -606,7 +687,7 @@ jobs:
|
||||
# --- AWS Test 2: orchestrator-environment ---
|
||||
- name: Run orchestrator-environment test (AWS)
|
||||
timeout-minutes: 30
|
||||
run: yarn run test "orchestrator-environment" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-environment" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -624,7 +705,7 @@ jobs:
|
||||
# --- AWS Test 3: orchestrator-s3-steps ---
|
||||
- name: Run orchestrator-s3-steps test (AWS)
|
||||
timeout-minutes: 30
|
||||
run: yarn run test "orchestrator-s3-steps" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-s3-steps" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -642,7 +723,7 @@ jobs:
|
||||
# --- AWS Test 4: orchestrator-hooks ---
|
||||
- name: Run orchestrator-hooks test (AWS)
|
||||
timeout-minutes: 30
|
||||
run: yarn run test "orchestrator-hooks" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-hooks" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -657,48 +738,10 @@ jobs:
|
||||
if: always()
|
||||
run: source /tmp/cleanup-functions.sh && light_cleanup
|
||||
|
||||
# --- AWS Test 5: orchestrator-end2end-caching ---
|
||||
- name: Run orchestrator-end2end-caching test (AWS)
|
||||
timeout-minutes: 60
|
||||
continue-on-error: true
|
||||
run: yarn run test "orchestrator-end2end-caching" --detectOpenHandles --forceExit --runInBand
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
TARGET_PLATFORM: StandaloneWindows64
|
||||
orchestratorTests: true
|
||||
versioning: None
|
||||
PROVIDER_STRATEGY: aws
|
||||
GIT_PRIVATE_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
- name: Cleanup after orchestrator-end2end-caching (AWS)
|
||||
if: always()
|
||||
run: source /tmp/cleanup-functions.sh && heavy_cleanup
|
||||
|
||||
# --- AWS Test 6: orchestrator-end2end-retaining ---
|
||||
- name: Run orchestrator-end2end-retaining test (AWS)
|
||||
timeout-minutes: 60
|
||||
continue-on-error: true
|
||||
run: yarn run test "orchestrator-end2end-retaining" --detectOpenHandles --forceExit --runInBand
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
TARGET_PLATFORM: StandaloneWindows64
|
||||
orchestratorTests: true
|
||||
versioning: None
|
||||
PROVIDER_STRATEGY: aws
|
||||
GIT_PRIVATE_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
- name: Cleanup after orchestrator-end2end-retaining (AWS)
|
||||
if: always()
|
||||
run: source /tmp/cleanup-functions.sh && heavy_cleanup
|
||||
|
||||
# --- AWS Test 7: orchestrator-caching ---
|
||||
# --- AWS Test 5: orchestrator-caching ---
|
||||
- name: Run orchestrator-caching test (AWS)
|
||||
timeout-minutes: 60
|
||||
run: yarn run test "orchestrator-caching" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-caching" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -713,10 +756,10 @@ jobs:
|
||||
if: always()
|
||||
run: source /tmp/cleanup-functions.sh && heavy_cleanup
|
||||
|
||||
# --- AWS Test 8: orchestrator-locking-core ---
|
||||
# --- AWS Test 6: orchestrator-locking-core ---
|
||||
- name: Run orchestrator-locking-core test (AWS)
|
||||
timeout-minutes: 60
|
||||
run: yarn run test "orchestrator-locking-core" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-locking-core" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -731,10 +774,10 @@ jobs:
|
||||
if: always()
|
||||
run: source /tmp/cleanup-functions.sh && light_cleanup
|
||||
|
||||
# --- AWS Test 9: orchestrator-locking-get-locked ---
|
||||
# --- AWS Test 7: orchestrator-locking-get-locked ---
|
||||
- name: Run orchestrator-locking-get-locked test (AWS)
|
||||
timeout-minutes: 60
|
||||
run: yarn run test "orchestrator-locking-get-locked" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-locking-get-locked" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -749,11 +792,54 @@ jobs:
|
||||
if: always()
|
||||
run: source /tmp/cleanup-functions.sh && light_cleanup
|
||||
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
# End-to-end tests (continue-on-error) — run LAST to prevent
|
||||
# workspace corruption from affecting mandatory tests above.
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
# --- AWS Test 8: orchestrator-end2end-caching ---
|
||||
- name: Run orchestrator-end2end-caching test (AWS)
|
||||
timeout-minutes: 60
|
||||
continue-on-error: true
|
||||
run: yarn run test "orchestrator-end2end-caching" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
TARGET_PLATFORM: StandaloneWindows64
|
||||
orchestratorTests: true
|
||||
versioning: None
|
||||
PROVIDER_STRATEGY: aws
|
||||
GIT_PRIVATE_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
- name: Cleanup after orchestrator-end2end-caching (AWS)
|
||||
if: always()
|
||||
run: source /tmp/cleanup-functions.sh && heavy_cleanup
|
||||
|
||||
# --- AWS Test 9: orchestrator-end2end-retaining ---
|
||||
- name: Run orchestrator-end2end-retaining test (AWS)
|
||||
timeout-minutes: 60
|
||||
continue-on-error: true
|
||||
run: yarn run test "orchestrator-end2end-retaining" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
TARGET_PLATFORM: StandaloneWindows64
|
||||
orchestratorTests: true
|
||||
versioning: None
|
||||
PROVIDER_STRATEGY: aws
|
||||
GIT_PRIVATE_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
||||
- name: Cleanup after orchestrator-end2end-retaining (AWS)
|
||||
if: always()
|
||||
run: source /tmp/cleanup-functions.sh && heavy_cleanup
|
||||
|
||||
# --- AWS Test 10: orchestrator-end2end-locking ---
|
||||
- name: Run orchestrator-end2end-locking test (AWS)
|
||||
timeout-minutes: 60
|
||||
continue-on-error: true
|
||||
run: yarn run test "orchestrator-end2end-locking" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-end2end-locking" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -789,17 +875,45 @@ jobs:
|
||||
AWS_ENDPOINT: http://localhost:4566
|
||||
AWS_ENDPOINT_URL: http://localhost:4566
|
||||
steps:
|
||||
- name: Checkout orchestrator
|
||||
- name: Checkout orchestrator (matching branch)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: game-ci/orchestrator
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
lfs: false
|
||||
continue-on-error: true
|
||||
id: orch-branch
|
||||
|
||||
- name: Clean corrupted checkout
|
||||
if: steps.orch-branch.outcome == 'failure'
|
||||
run: rm -rf .git || true
|
||||
|
||||
- name: Fallback to orchestrator main branch
|
||||
if: steps.orch-branch.outcome == 'failure'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: game-ci/orchestrator
|
||||
lfs: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Install package manager (from package.json)
|
||||
run: |
|
||||
corepack enable
|
||||
corepack install
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 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-20-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
yarn-v2-${{ runner.os }}-node-20-
|
||||
|
||||
- name: Define cleanup functions
|
||||
run: |
|
||||
@@ -824,18 +938,16 @@ jobs:
|
||||
docker system prune -af --volumes || true
|
||||
df -h
|
||||
|
||||
- name: Start LocalStack (for S3-dependent tests)
|
||||
- name: Start MiniStack (for S3-dependent tests)
|
||||
run: |
|
||||
docker run -d \
|
||||
--name localstack-main \
|
||||
-p 4566:4566 \
|
||||
-e SERVICES=s3,cloudformation,ecs,kinesis,cloudwatch,logs \
|
||||
-e DEBUG=0 \
|
||||
localstack/localstack:latest || true
|
||||
ministackorg/ministack:latest || true
|
||||
MAX_ATTEMPTS=60
|
||||
for i in $(seq 1 $MAX_ATTEMPTS); do
|
||||
HEALTH=$(curl -s http://localhost:4566/_localstack/health 2>/dev/null || echo "")
|
||||
if echo "$HEALTH" | grep -q '"s3"'; then echo "LocalStack ready ($i/$MAX_ATTEMPTS)"; break; fi
|
||||
if echo "$HEALTH" | grep -q '"s3"'; then echo "MiniStack ready ($i/$MAX_ATTEMPTS)"; break; fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
@@ -846,16 +958,16 @@ jobs:
|
||||
|
||||
- name: Create S3 bucket for tests
|
||||
run: |
|
||||
for i in {1..5}; do
|
||||
for _ in {1..5}; do
|
||||
if command -v awslocal > /dev/null 2>&1; then
|
||||
awslocal s3 mb s3://$AWS_STACK_NAME 2>&1 && break
|
||||
awslocal s3 mb s3://"$AWS_STACK_NAME" 2>&1 && break
|
||||
else
|
||||
aws --endpoint-url=http://localhost:4566 s3 mb s3://$AWS_STACK_NAME 2>&1 && break
|
||||
aws --endpoint-url=http://localhost:4566 s3 mb s3://"$AWS_STACK_NAME" 2>&1 && break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn install --immutable
|
||||
|
||||
- name: Build orchestrator
|
||||
run: |
|
||||
@@ -865,7 +977,7 @@ jobs:
|
||||
# --- Local Docker Test 1: orchestrator-image ---
|
||||
- name: Run orchestrator-image test (local-docker)
|
||||
timeout-minutes: 10
|
||||
run: yarn run test "orchestrator-image" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-image" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -883,7 +995,7 @@ jobs:
|
||||
# --- Local Docker Test 2: orchestrator-hooks ---
|
||||
- name: Run orchestrator-hooks test (local-docker)
|
||||
timeout-minutes: 30
|
||||
run: yarn run test "orchestrator-hooks" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-hooks" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -901,7 +1013,7 @@ jobs:
|
||||
# --- Local Docker Test 3: orchestrator-local-persistence ---
|
||||
- name: Run orchestrator-local-persistence test (local-docker)
|
||||
timeout-minutes: 30
|
||||
run: yarn run test "orchestrator-local-persistence" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-local-persistence" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -919,7 +1031,7 @@ jobs:
|
||||
# --- Local Docker Test 4: orchestrator-caching ---
|
||||
- name: Run orchestrator-caching test (local-docker)
|
||||
timeout-minutes: 30
|
||||
run: yarn run test "orchestrator-caching" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-caching" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -937,7 +1049,7 @@ jobs:
|
||||
# --- Local Docker Test 5: orchestrator-github-checks ---
|
||||
- name: Run orchestrator-github-checks test (local-docker)
|
||||
timeout-minutes: 30
|
||||
run: yarn run test "orchestrator-github-checks" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-github-checks" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -955,7 +1067,7 @@ jobs:
|
||||
# --- Local Docker Test 6: orchestrator-locking-core (with S3) ---
|
||||
- name: Run orchestrator-locking-core test (local-docker + S3)
|
||||
timeout-minutes: 30
|
||||
run: yarn run test "orchestrator-locking-core" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-locking-core" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -973,7 +1085,7 @@ jobs:
|
||||
# --- Local Docker Test 7: orchestrator-locking-get-locked (with S3) ---
|
||||
- name: Run orchestrator-locking-get-locked test (local-docker + S3)
|
||||
timeout-minutes: 30
|
||||
run: yarn run test "orchestrator-locking-get-locked" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-locking-get-locked" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -991,7 +1103,7 @@ jobs:
|
||||
# --- Local Docker Test 8: orchestrator-s3-steps (with S3) ---
|
||||
- name: Run orchestrator-s3-steps test (local-docker + S3)
|
||||
timeout-minutes: 30
|
||||
run: yarn run test "orchestrator-s3-steps" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-s3-steps" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -1010,7 +1122,7 @@ jobs:
|
||||
- name: Run orchestrator-end2end-caching test (local-docker + S3)
|
||||
timeout-minutes: 60
|
||||
continue-on-error: true
|
||||
run: yarn run test "orchestrator-end2end-caching" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-end2end-caching" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
@@ -1046,35 +1158,61 @@ jobs:
|
||||
AWS_ENDPOINT: http://localhost:4566
|
||||
AWS_ENDPOINT_URL: http://localhost:4566
|
||||
steps:
|
||||
- name: Checkout orchestrator
|
||||
- name: Checkout orchestrator (matching branch)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: game-ci/orchestrator
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
lfs: false
|
||||
continue-on-error: true
|
||||
id: orch-branch
|
||||
|
||||
- name: Clean corrupted checkout
|
||||
if: steps.orch-branch.outcome == 'failure'
|
||||
run: rm -rf .git || true
|
||||
|
||||
- name: Fallback to orchestrator main branch
|
||||
if: steps.orch-branch.outcome == 'failure'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: game-ci/orchestrator
|
||||
lfs: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Install package manager (from package.json)
|
||||
run: |
|
||||
corepack enable
|
||||
corepack install
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 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-20-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
yarn-v2-${{ runner.os }}-node-20-
|
||||
|
||||
- name: Initial disk space cleanup
|
||||
run: |
|
||||
docker system prune -af --volumes || true
|
||||
df -h
|
||||
|
||||
- name: Start LocalStack
|
||||
- name: Start MiniStack
|
||||
run: |
|
||||
docker run -d \
|
||||
--name localstack-main \
|
||||
-p 4566:4566 \
|
||||
-e SERVICES=s3 \
|
||||
-e DEBUG=0 \
|
||||
localstack/localstack:latest || true
|
||||
ministackorg/ministack:latest || true
|
||||
MAX_ATTEMPTS=60
|
||||
for i in $(seq 1 $MAX_ATTEMPTS); do
|
||||
HEALTH=$(curl -s http://localhost:4566/_localstack/health 2>/dev/null || echo "")
|
||||
if echo "$HEALTH" | grep -q '"s3"'; then echo "LocalStack ready ($i/$MAX_ATTEMPTS)"; break; fi
|
||||
if echo "$HEALTH" | grep -q '"s3"'; then echo "MiniStack ready ($i/$MAX_ATTEMPTS)"; break; fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
@@ -1090,16 +1228,16 @@ jobs:
|
||||
|
||||
- name: Create S3 bucket for tests
|
||||
run: |
|
||||
for i in {1..5}; do
|
||||
for _ in {1..5}; do
|
||||
if command -v awslocal > /dev/null 2>&1; then
|
||||
awslocal s3 mb s3://$AWS_STACK_NAME 2>&1 && break
|
||||
awslocal s3 mb s3://"$AWS_STACK_NAME" 2>&1 && break
|
||||
else
|
||||
aws --endpoint-url=http://localhost:4566 s3 mb s3://$AWS_STACK_NAME 2>&1 && break
|
||||
aws --endpoint-url=http://localhost:4566 s3 mb s3://"$AWS_STACK_NAME" 2>&1 && break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn install --immutable
|
||||
|
||||
- name: Build orchestrator
|
||||
run: |
|
||||
@@ -1109,7 +1247,7 @@ jobs:
|
||||
# --- Rclone Test ---
|
||||
- name: Run orchestrator-rclone-steps test
|
||||
timeout-minutes: 30
|
||||
run: yarn run test "orchestrator-rclone-steps" --detectOpenHandles --forceExit --runInBand
|
||||
run: yarn run test "orchestrator-rclone-steps" --no-file-parallelism
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
|
||||
@@ -68,20 +68,48 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout orchestrator
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: game-ci/orchestrator
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
path: orchestrator-standalone
|
||||
continue-on-error: true
|
||||
id: orchestrator-branch
|
||||
|
||||
- name: Fallback to orchestrator main branch
|
||||
if: steps.orchestrator-branch.outcome == 'failure'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: game-ci/orchestrator
|
||||
path: orchestrator-standalone
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Install package manager (from package.json)
|
||||
run: |
|
||||
corepack enable
|
||||
corepack install
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 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-20-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
yarn-v2-${{ runner.os }}-node-20-
|
||||
|
||||
# --- unity-builder compilation and tests ---
|
||||
- name: Install unity-builder dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
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: |
|
||||
@@ -92,28 +120,21 @@ jobs:
|
||||
- name: Run orchestrator-plugin unit tests
|
||||
run: |
|
||||
echo "Running orchestrator-plugin unit tests..."
|
||||
npx jest orchestrator-plugin --verbose --detectOpenHandles --forceExit
|
||||
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 { loadOrchestrator, loadPluginServices } = require('./lib/model/orchestrator-plugin');
|
||||
const { loadOrchestratorPlugin } = require('./lib/model/orchestrator-plugin');
|
||||
(async () => {
|
||||
const orch = await loadOrchestrator();
|
||||
if (orch !== undefined) {
|
||||
console.error('ERROR: loadOrchestrator should return undefined when package not installed');
|
||||
const plugin = await loadOrchestratorPlugin();
|
||||
if (plugin !== undefined) {
|
||||
console.error('ERROR: loadOrchestratorPlugin should return undefined when package not installed');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✓ loadOrchestrator() returns undefined when package not installed');
|
||||
|
||||
const services = await loadPluginServices();
|
||||
if (services !== undefined) {
|
||||
console.error('ERROR: loadPluginServices should return undefined when package not installed');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✓ loadPluginServices() returns undefined when package not installed');
|
||||
console.log('✓ loadOrchestratorPlugin() returns undefined when package not installed');
|
||||
})();
|
||||
"
|
||||
|
||||
@@ -130,7 +151,7 @@ jobs:
|
||||
- name: Build and pack orchestrator
|
||||
working-directory: orchestrator-standalone
|
||||
run: |
|
||||
yarn install --frozen-lockfile
|
||||
yarn install --immutable
|
||||
echo "Building orchestrator..."
|
||||
npx tsc
|
||||
echo "✓ orchestrator compiles successfully"
|
||||
@@ -141,7 +162,7 @@ jobs:
|
||||
working-directory: orchestrator-standalone
|
||||
run: |
|
||||
echo "Running orchestrator unit tests..."
|
||||
npx jest --no-cache 2>&1 | tail -20
|
||||
yarn vitest run 2>&1 | tail -30
|
||||
|
||||
# --- Plugin loader with orchestrator installed ---
|
||||
- name: Install orchestrator into unity-builder
|
||||
@@ -153,53 +174,24 @@ jobs:
|
||||
run: |
|
||||
echo "Checking plugin loader returns defined exports..."
|
||||
node -e "
|
||||
const { loadOrchestrator, loadPluginServices } = require('./lib/model/orchestrator-plugin');
|
||||
const { loadOrchestratorPlugin } = require('./lib/model/orchestrator-plugin');
|
||||
(async () => {
|
||||
const orch = await loadOrchestrator();
|
||||
if (orch === undefined) {
|
||||
console.error('ERROR: loadOrchestrator should return defined exports when package is installed');
|
||||
const plugin = await loadOrchestratorPlugin();
|
||||
if (plugin === undefined) {
|
||||
console.error('ERROR: loadOrchestratorPlugin should return defined plugin when package is installed');
|
||||
process.exit(1);
|
||||
}
|
||||
if (typeof orch.run !== 'function') {
|
||||
console.error('ERROR: loadOrchestrator().run should be a function');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✓ loadOrchestrator() returns defined exports with orchestrator installed');
|
||||
|
||||
const services = await loadPluginServices();
|
||||
if (services === undefined) {
|
||||
console.error('ERROR: loadPluginServices should return defined exports when package is installed');
|
||||
process.exit(1);
|
||||
}
|
||||
const expectedServices = [
|
||||
'BuildReliabilityService', 'TestWorkflowService', 'HotRunnerService',
|
||||
'OutputService', 'OutputTypeRegistry', 'ArtifactUploadHandler',
|
||||
'IncrementalSyncService',
|
||||
const lifecycleMethods = [
|
||||
'initialize', 'canHandleBuild', 'handleBuild',
|
||||
'beforeLocalBuild', 'afterLocalBuild', 'handlePostBuild',
|
||||
];
|
||||
for (const svc of expectedServices) {
|
||||
if (services[svc] === undefined) {
|
||||
console.error('ERROR: ' + svc + ' should be defined');
|
||||
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('✓ loadPluginServices() returns all ' + expectedServices.length + ' services');
|
||||
|
||||
const lazyLoaders = [
|
||||
'loadChildWorkspaceService', 'loadLocalCacheService',
|
||||
'loadSubmoduleProfileService', 'loadLfsAgentService', 'loadGitHooksService',
|
||||
];
|
||||
for (const loader of lazyLoaders) {
|
||||
if (typeof services[loader] !== 'function') {
|
||||
console.error('ERROR: ' + loader + ' should be a function');
|
||||
process.exit(1);
|
||||
}
|
||||
const loaded = await services[loader]();
|
||||
if (loaded === undefined) {
|
||||
console.error('ERROR: ' + loader + '() should return defined service');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
console.log('✓ All ' + lazyLoaders.length + ' lazy loaders return defined services');
|
||||
console.log('✓ loadOrchestratorPlugin() returns plugin with all ' + lifecycleMethods.length + ' lifecycle methods');
|
||||
})();
|
||||
"
|
||||
|
||||
@@ -227,62 +219,37 @@ jobs:
|
||||
run: |
|
||||
echo "Verifying orchestrator build wiring end-to-end..."
|
||||
node -e "
|
||||
const { loadOrchestrator, loadPluginServices } = require('./lib/model/orchestrator-plugin');
|
||||
const { Orchestrator, BuildParameters } = require('@game-ci/orchestrator');
|
||||
const { loadOrchestratorPlugin } = require('./lib/model/orchestrator-plugin');
|
||||
|
||||
(async () => {
|
||||
// Verify Orchestrator has the expected static API
|
||||
const requiredMethods = ['setup', 'run'];
|
||||
for (const m of requiredMethods) {
|
||||
if (typeof Orchestrator[m] !== 'function') {
|
||||
console.error('ERROR: Orchestrator.' + m + ' should be a function, got ' + typeof Orchestrator[m]);
|
||||
process.exit(1);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
console.log('✓ Orchestrator has setup() and run() methods');
|
||||
|
||||
// Verify BuildParameters.create() produces a valid config object
|
||||
const params = await BuildParameters.create();
|
||||
const requiredFields = [
|
||||
'targetPlatform', 'projectPath', 'providerStrategy',
|
||||
'editorVersion', 'buildPath', 'buildName',
|
||||
// Verify all lifecycle methods are callable
|
||||
const lifecycleMethods = [
|
||||
'initialize', 'canHandleBuild', 'handleBuild',
|
||||
'beforeLocalBuild', 'afterLocalBuild', 'handlePostBuild',
|
||||
];
|
||||
for (const f of requiredFields) {
|
||||
if (params[f] === undefined) {
|
||||
console.error('ERROR: BuildParameters.' + f + ' is undefined');
|
||||
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('✓ BuildParameters.create() produces valid config with ' + requiredFields.length + ' required fields');
|
||||
console.log('✓ Plugin has all ' + lifecycleMethods.length + ' lifecycle methods');
|
||||
|
||||
// Verify loadOrchestrator().run is wired to Orchestrator.run
|
||||
const orch = await loadOrchestrator();
|
||||
if (typeof orch.run !== 'function') {
|
||||
console.error('ERROR: loadOrchestrator().run is not a function');
|
||||
// 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');
|
||||
|
||||
// Call run with a bad baseImage to confirm it reaches Orchestrator.run
|
||||
// (Orchestrator.run throws 'baseImage is undefined' for images containing 'undefined')
|
||||
try {
|
||||
await orch.run(params, 'undefined-image');
|
||||
console.error('ERROR: Expected Orchestrator.run to throw for undefined baseImage');
|
||||
process.exit(1);
|
||||
} catch (e) {
|
||||
if (e.message.includes('baseImage is undefined')) {
|
||||
console.log('✓ loadOrchestrator().run() correctly wired to Orchestrator.run()');
|
||||
} else {
|
||||
console.error('ERROR: Unexpected error from Orchestrator.run:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify plugin services wire through to real service instances
|
||||
const services = await loadPluginServices();
|
||||
if (typeof services.BuildReliabilityService.configureGitEnvironment !== 'function') {
|
||||
console.error('ERROR: BuildReliabilityService.configureGitEnvironment should be a function');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✓ Plugin services wire through to real orchestrator implementations');
|
||||
console.log('✓ Plugin architecture wiring verified');
|
||||
})();
|
||||
"
|
||||
|
||||
@@ -7,3 +7,12 @@ yarn-error.log
|
||||
.orig
|
||||
$LOG_FILE
|
||||
temp/
|
||||
|
||||
# yarn 4 (berry)
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
Executable
+7
@@ -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
|
||||
@@ -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/**"
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -0,0 +1,10 @@
|
||||
approvedGitRepositories:
|
||||
- '**'
|
||||
|
||||
compressionLevel: mixed
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
enableHardenedMode: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
+23
-557
@@ -9,8 +9,7 @@ inputs:
|
||||
unityVersion:
|
||||
required: false
|
||||
default: 'auto'
|
||||
description:
|
||||
'Version of unity to use for building the project. Use "auto" to get from your ProjectSettings/ProjectVersion.txt'
|
||||
description: 'Version of unity to use for building the project. Use "auto" to get from your ProjectSettings/ProjectVersion.txt'
|
||||
customImage:
|
||||
required: false
|
||||
default: ''
|
||||
@@ -47,6 +46,10 @@ inputs:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'Custom parameters to configure the build.'
|
||||
useHostNetwork:
|
||||
required: false
|
||||
default: false
|
||||
description: 'Initialises Docker using the host network. (Linux only)'
|
||||
versioning:
|
||||
required: false
|
||||
default: 'Semantic'
|
||||
@@ -104,17 +107,13 @@ inputs:
|
||||
gitPrivateToken:
|
||||
required: false
|
||||
default: ''
|
||||
description: '[Orchestrator] Github private token to pull from github'
|
||||
gitAuthMode:
|
||||
description: 'Github private token to pull from github'
|
||||
providerStrategy:
|
||||
default: 'local'
|
||||
required: false
|
||||
default: 'header'
|
||||
description:
|
||||
'[Orchestrator] How git authentication is configured. "header" (default) uses http.extraHeader so the token
|
||||
never appears in clone URLs or git config. "url" embeds the token in clone URLs (legacy behavior).'
|
||||
githubOwner:
|
||||
required: false
|
||||
default: ''
|
||||
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:
|
||||
required: false
|
||||
default: 'false'
|
||||
@@ -124,8 +123,7 @@ inputs:
|
||||
chownFilesTo:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'User and optionally group (user or user:group or uid:gid) to give ownership of the resulting build artifacts'
|
||||
description: 'User and optionally group (user or user:group or uid:gid) to give ownership of the resulting build artifacts'
|
||||
dockerCpuLimit:
|
||||
required: false
|
||||
default: ''
|
||||
@@ -155,147 +153,7 @@ inputs:
|
||||
allowDirtyBuild:
|
||||
required: false
|
||||
default: ''
|
||||
description: '[Orchestrator] 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.'
|
||||
fallbackProviderStrategy:
|
||||
default: ''
|
||||
required: false
|
||||
description:
|
||||
'[Orchestrator] Fallback provider when the primary is unavailable. Used with runnerCheckEnabled for automatic
|
||||
failover, or as a catch-all if the primary provider fails to initialize.'
|
||||
runnerCheckEnabled:
|
||||
default: 'false'
|
||||
required: false
|
||||
description:
|
||||
'[Orchestrator] Check GitHub Actions runner availability before starting a build. When no suitable runners are
|
||||
available and fallbackProviderStrategy is set, automatically routes to the fallback provider.'
|
||||
runnerCheckLabels:
|
||||
default: ''
|
||||
required: false
|
||||
description:
|
||||
'[Orchestrator] Comma-separated runner labels to filter when checking availability (e.g. self-hosted,linux).
|
||||
When empty, checks all runners in the repository.'
|
||||
runnerCheckMinAvailable:
|
||||
default: '1'
|
||||
required: false
|
||||
description:
|
||||
'[Orchestrator] Minimum number of idle runners required for the primary provider. If fewer are available,
|
||||
routes to fallbackProviderStrategy.'
|
||||
retryOnFallback:
|
||||
default: 'false'
|
||||
required: false
|
||||
description:
|
||||
'[Orchestrator] When true and fallbackProviderStrategy is set, automatically retry the build on the fallback
|
||||
provider if the primary provider fails. Useful for long builds where transient cloud failures are common.'
|
||||
providerInitTimeout:
|
||||
default: '0'
|
||||
required: false
|
||||
description:
|
||||
'[Orchestrator] Maximum seconds to wait for the primary provider to initialize (setupWorkflow). If exceeded
|
||||
and fallbackProviderStrategy is set, switches to the fallback. Set to 0 to disable (default).'
|
||||
secretSource:
|
||||
default: ''
|
||||
required: false
|
||||
description:
|
||||
'[Orchestrator] Premade secret source for pulling build secrets. Supported values: aws-secrets-manager,
|
||||
aws-parameter-store, gcp-secret-manager, azure-key-vault, hashicorp-vault, hashicorp-vault-kv1,
|
||||
vault (alias for hashicorp-vault), env. Can also be a custom shell command with {0} placeholder
|
||||
for the key, or a path to a YAML file defining custom sources. Takes precedence over
|
||||
inputPullCommand when set.'
|
||||
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.'
|
||||
description: 'Allows the branch of the build to be dirty, and still generate the build.'
|
||||
cacheUnityInstallationOnMac:
|
||||
default: 'false'
|
||||
required: false
|
||||
@@ -310,6 +168,14 @@ inputs:
|
||||
default: ''
|
||||
required: false
|
||||
description: 'The Unity licensing server address to use for activating Unity.'
|
||||
unityLicensingToolset:
|
||||
default: ''
|
||||
required: false
|
||||
description:
|
||||
'Optional toolset identifier for Unity floating-license servers that host multiple toolsets (e.g.
|
||||
"LicenseServer_1234567890_3"). When set, written to services-config.json so the Licensing Client
|
||||
requests entitlements from the named toolset instead of relying on the server-side default. Leave
|
||||
empty to preserve previous behavior.'
|
||||
dockerWorkspacePath:
|
||||
default: '/github/workspace'
|
||||
required: false
|
||||
@@ -320,408 +186,10 @@ inputs:
|
||||
default: 'false'
|
||||
required: false
|
||||
description: 'Skip the activation/deactivation of Unity. This assumes Unity is already activated.'
|
||||
artifactOutputTypes:
|
||||
description: 'Comma-separated list of output types to collect (build, logs, test-results, coverage, images, metrics, data-export, server-build, custom)'
|
||||
required: false
|
||||
default: 'build,logs,test-results'
|
||||
artifactUploadTarget:
|
||||
description: 'Where to upload artifacts: github-artifacts, storage, local, none'
|
||||
required: false
|
||||
default: 'github-artifacts'
|
||||
artifactUploadPath:
|
||||
description: 'Destination path for artifact upload (storage URI or local path)'
|
||||
required: false
|
||||
artifactCompression:
|
||||
description: 'Compression for artifacts: none, gzip, lz4'
|
||||
required: false
|
||||
default: 'gzip'
|
||||
artifactRetentionDays:
|
||||
description: 'Retention period for uploaded artifacts in days'
|
||||
required: false
|
||||
default: '30'
|
||||
artifactCustomTypes:
|
||||
description: 'JSON string defining custom output types [{name, defaultPath, description}]'
|
||||
required: false
|
||||
cloneDepth:
|
||||
default: '50'
|
||||
required: false
|
||||
description: '[Orchestrator] Specifies the depth of the git clone for the repository. Use 0 for full clone.'
|
||||
orchestratorRepoName:
|
||||
default: 'game-ci/unity-builder'
|
||||
required: false
|
||||
description:
|
||||
'[Orchestrator] Specifies the repo for the unity builder. Useful if you forked the repo for testing, features, or
|
||||
fixes.'
|
||||
submoduleProfilePath:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'Path to a YAML submodule profile file (relative to repo root). Defines which submodules to initialize (branch:
|
||||
main) or skip (branch: empty). See docs for format.'
|
||||
submoduleVariantPath:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'Path to a YAML variant overlay file that modifies the base submodule profile. Used for server or debug build
|
||||
variants.'
|
||||
submoduleToken:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'Git token for authenticating submodule clones. Falls back to gitPrivateToken or GITHUB_TOKEN if empty.'
|
||||
localCacheEnabled:
|
||||
required: false
|
||||
linux64RemoveExecutableExtension:
|
||||
default: 'false'
|
||||
description:
|
||||
'Enable filesystem-based caching for local builds. Caches the Unity Library folder and optionally LFS objects
|
||||
between builds without requiring actions/cache.'
|
||||
localCacheRoot:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'Root directory for local build cache. Defaults to $RUNNER_TEMP/game-ci-cache or .game-ci/cache if RUNNER_TEMP is
|
||||
not set.'
|
||||
localCacheLibrary:
|
||||
required: false
|
||||
default: 'true'
|
||||
description: 'Cache the Unity Library folder for local builds. Only effective when localCacheEnabled is true.'
|
||||
localCacheLfs:
|
||||
required: false
|
||||
default: 'false'
|
||||
description: 'Cache Git LFS objects for local builds. Only effective when localCacheEnabled is true.'
|
||||
childWorkspacesEnabled:
|
||||
required: false
|
||||
default: 'false'
|
||||
description:
|
||||
'Enable child workspace isolation for multi-product builds. Uses atomic filesystem moves for O(1) workspace
|
||||
restore instead of tar/download/extract. Ideal for 50GB+ workspaces on self-hosted runners.'
|
||||
childWorkspaceName:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'Name for this child workspace (e.g., product name like "TurnOfWar"). Used as the cache key for workspace
|
||||
isolation. Required when childWorkspacesEnabled is true.'
|
||||
childWorkspaceCacheRoot:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'Parent directory for cached child workspaces. Should be on the same NTFS volume as the build directory for O(1)
|
||||
atomic restore via filesystem rename. Defaults to $RUNNER_TEMP/game-ci-workspaces.'
|
||||
childWorkspacePreserveGit:
|
||||
required: false
|
||||
default: 'true'
|
||||
description:
|
||||
'Preserve .git directory in cached child workspace. Enables delta operations on restore but increases cache size.
|
||||
Set to false to save disk space at the cost of full re-clone on restore.'
|
||||
childWorkspaceSeparateLibrary:
|
||||
required: false
|
||||
default: 'true'
|
||||
description:
|
||||
'Cache Unity Library folder separately from the child workspace. Allows independent Library restore even when
|
||||
workspace cache is invalidated. Recommended for large projects.'
|
||||
lfsTransferAgent:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'Custom Git LFS transfer agent. Set to "elastic-git-storage" for built-in support (auto-installs from GitHub
|
||||
releases). Append @version for a specific release (e.g. "elastic-git-storage@v1.0.0"). Or provide a path to any
|
||||
custom transfer agent executable. When set, the agent is registered via git config before LFS operations.'
|
||||
lfsTransferAgentArgs:
|
||||
required: false
|
||||
default: ''
|
||||
description: 'Additional arguments to pass to the custom LFS transfer agent.'
|
||||
lfsStoragePaths:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'Semicolon-separated list of storage paths for the custom LFS transfer agent. Interpretation depends on the agent
|
||||
(e.g. local paths, WebDAV URLs, rclone remotes).'
|
||||
gitHooksEnabled:
|
||||
required: false
|
||||
default: 'false'
|
||||
description:
|
||||
'Install and run git hooks (lefthook, husky, or native) during builds. When false (default), hooks are disabled
|
||||
for build performance.'
|
||||
gitHooksSkipList:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'Comma-separated list of hook names to skip even when gitHooksEnabled is true. Example: pre-push,post-merge'
|
||||
gitHooksRunBeforeBuild:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'Comma-separated list of lefthook hook groups to run before the Unity build. Allows CI to trigger checks that
|
||||
normally only run on git events. Example: pre-commit,pre-push. Requires lefthook. Works with Unity Git Hooks
|
||||
(com.frostebite.unitygithooks) when installed as a UPM package — the init script runs automatically.'
|
||||
providerExecutable:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'Path to an external CLI executable that implements the provider protocol. Enables providers written in any
|
||||
language (Go, Python, Rust, shell). Uses JSON-over-stdin/stdout communication.'
|
||||
gcpProject:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'[Orchestrator] [Experimental] Google Cloud project ID for Cloud Run Jobs provider. Falls back to
|
||||
GOOGLE_CLOUD_PROJECT env var.'
|
||||
gcpRegion:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'[Orchestrator] [Experimental] Google Cloud region for Cloud Run Jobs (e.g. us-central1). Defaults to the region
|
||||
input if empty.'
|
||||
gcpStorageType:
|
||||
required: false
|
||||
default: 'gcs-fuse'
|
||||
description:
|
||||
'[Orchestrator] [Experimental] Storage type for Cloud Run Jobs. Options: gcs-fuse (mount GCS bucket as filesystem,
|
||||
unlimited size, best for large sequential I/O), gcs-copy (copy artifacts in/out via gsutil, simpler, no FUSE
|
||||
overhead), nfs (Filestore NFS mount, true POSIX, good random I/O, up to 100 TiB), in-memory (tmpfs, fastest but
|
||||
volatile, up to 32 GiB).'
|
||||
gcpBucket:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'[Orchestrator] [Experimental] GCS bucket name for build artifact storage. Used by gcs-fuse and gcs-copy storage
|
||||
types.'
|
||||
gcpFilestoreIp:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'[Orchestrator] [Experimental] Filestore instance IP address for NFS storage type. Required when gcpStorageType is
|
||||
nfs.'
|
||||
gcpFilestoreShare:
|
||||
required: false
|
||||
default: '/share1'
|
||||
description:
|
||||
'[Orchestrator] [Experimental] Filestore share name for NFS storage type. Defaults to /share1 (the Filestore
|
||||
default).'
|
||||
gcpMachineType:
|
||||
required: false
|
||||
default: 'e2-standard-4'
|
||||
description: '[Orchestrator] [Experimental] Machine type for Cloud Run Jobs (e.g. e2-standard-4, e2-highmem-8).'
|
||||
gcpDiskSizeGb:
|
||||
required: false
|
||||
default: '100'
|
||||
description:
|
||||
'[Orchestrator] [Experimental] Disk size in GB for Cloud Run Jobs in-memory volumes. Only applies to in-memory
|
||||
storage type (max 32).'
|
||||
gcpServiceAccount:
|
||||
required: false
|
||||
default: ''
|
||||
description: '[Orchestrator] [Experimental] Google Cloud service account email for Cloud Run Jobs execution.'
|
||||
gcpVpcConnector:
|
||||
required: false
|
||||
default: ''
|
||||
description: '[Orchestrator] [Experimental] VPC connector name for Cloud Run Jobs private networking.'
|
||||
azureResourceGroup:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'[Orchestrator] [Experimental] Azure resource group for Container Instances provider. Falls back to
|
||||
AZURE_RESOURCE_GROUP env var.'
|
||||
azureLocation:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'[Orchestrator] [Experimental] Azure region for Container Instances (e.g. eastus, westeurope). Defaults to the
|
||||
region input if empty.'
|
||||
azureStorageType:
|
||||
required: false
|
||||
default: 'azure-files'
|
||||
description:
|
||||
'[Orchestrator] [Experimental] Storage type for Azure Container Instances. Options: azure-files (SMB file share
|
||||
mount, up to 100 TiB, premium throughput), blob-copy (copy artifacts in/out via az storage blob, no mount
|
||||
overhead), azure-files-nfs (NFS 4.1 file share mount, true POSIX, no SMB lock overhead), in-memory (emptyDir
|
||||
tmpfs, fastest but volatile, size limited by container memory).'
|
||||
azureStorageAccount:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'[Orchestrator] [Experimental] Azure Storage Account name. Used by azure-files, azure-files-nfs, and blob-copy
|
||||
storage types.'
|
||||
azureFileShareName:
|
||||
required: false
|
||||
default: 'unity-builds'
|
||||
description:
|
||||
'[Orchestrator] [Experimental] Azure File Share name within the storage account. Used by azure-files and
|
||||
azure-files-nfs storage types. Supports up to 100 TiB per share.'
|
||||
azureBlobContainer:
|
||||
required: false
|
||||
default: 'unity-builds'
|
||||
description: '[Orchestrator] [Experimental] Azure Blob container name for blob-copy storage type.'
|
||||
azureSubscriptionId:
|
||||
required: false
|
||||
default: ''
|
||||
description: '[Orchestrator] [Experimental] Azure subscription ID. Falls back to AZURE_SUBSCRIPTION_ID env var.'
|
||||
azureCpu:
|
||||
required: false
|
||||
default: '4'
|
||||
description: '[Orchestrator] [Experimental] CPU cores for Azure Container Instances (1-16).'
|
||||
azureMemoryGb:
|
||||
required: false
|
||||
default: '16'
|
||||
description: '[Orchestrator] [Experimental] Memory in GB for Azure Container Instances (1-16).'
|
||||
azureDiskSizeGb:
|
||||
required: false
|
||||
default: '100'
|
||||
description:
|
||||
'[Orchestrator] [Experimental] File share quota in GB for Azure Container Instances. Premium shares support up to
|
||||
102400 GB (100 TiB).'
|
||||
azureSubnetId:
|
||||
required: false
|
||||
default: ''
|
||||
description: '[Orchestrator] [Experimental] Azure subnet resource ID for VNet-integrated Container Instances.'
|
||||
remotePowershellHost:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Remote PowerShell host (hostname or IP) for the remote-powershell provider'
|
||||
remotePowershellCredential:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Remote PowerShell credential (username:password or certificate path)'
|
||||
remotePowershellTransport:
|
||||
default: 'wsman'
|
||||
required: false
|
||||
description: '[Orchestrator] Remote PowerShell transport protocol (wsman or ssh)'
|
||||
githubActionsRepo:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Target repository (owner/repo) for the github-actions provider'
|
||||
githubActionsWorkflow:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Workflow filename or ID to dispatch for the github-actions provider'
|
||||
githubActionsToken:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] PAT with actions:write scope for the github-actions provider'
|
||||
githubActionsRef:
|
||||
default: 'main'
|
||||
required: false
|
||||
description: '[Orchestrator] Branch/ref to run the workflow on for the github-actions provider'
|
||||
gitlabProjectId:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] GitLab project ID or URL-encoded path for the gitlab-ci provider'
|
||||
gitlabTriggerToken:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Pipeline trigger token for the gitlab-ci provider'
|
||||
gitlabApiUrl:
|
||||
default: 'https://gitlab.com'
|
||||
required: false
|
||||
description: '[Orchestrator] GitLab API URL (for self-hosted instances) for the gitlab-ci provider'
|
||||
gitlabRef:
|
||||
default: 'main'
|
||||
required: false
|
||||
description: '[Orchestrator] Branch/ref to trigger the pipeline on for the gitlab-ci provider'
|
||||
ansibleInventory:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Path to Ansible inventory file or dynamic inventory script'
|
||||
ansiblePlaybook:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Path to Ansible playbook for Unity builds'
|
||||
ansibleExtraVars:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Additional Ansible variables as JSON'
|
||||
ansibleVaultPassword:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Path to Ansible vault password file'
|
||||
gitIntegrityCheck:
|
||||
description: 'Run git integrity checks before build (fsck, lock cleanup, submodule validation)'
|
||||
required: false
|
||||
default: 'false'
|
||||
gitAutoRecover:
|
||||
description: 'Attempt automatic recovery if git corruption is detected'
|
||||
required: false
|
||||
default: 'false'
|
||||
cleanReservedFilenames:
|
||||
description: 'Remove Windows reserved filenames that cause Unity import loops'
|
||||
required: false
|
||||
default: 'false'
|
||||
buildArchiveEnabled:
|
||||
description: 'Archive build output after successful build'
|
||||
required: false
|
||||
default: 'false'
|
||||
buildArchivePath:
|
||||
description: 'Path to store build archives'
|
||||
required: false
|
||||
default: './build-archives'
|
||||
buildArchiveRetention:
|
||||
description: 'Days to retain build archives before cleanup'
|
||||
required: false
|
||||
default: '30'
|
||||
testSuitePath:
|
||||
description: 'Path to YAML test suite definition file'
|
||||
required: false
|
||||
testSuiteEvent:
|
||||
description: 'CI event name for suite selection (pr, push, release)'
|
||||
required: false
|
||||
testTaxonomyPath:
|
||||
description: 'Path to custom taxonomy definition YAML'
|
||||
required: false
|
||||
testResultFormat:
|
||||
description: 'Test result output format: junit, json, or both'
|
||||
required: false
|
||||
default: 'junit'
|
||||
testResultPath:
|
||||
description: 'Directory for structured test result output'
|
||||
required: false
|
||||
default: './test-results'
|
||||
|
||||
hotRunnerEnabled:
|
||||
description: '[HotRunner] Use persistent hot runner for builds (requires pre-registered runners)'
|
||||
required: false
|
||||
default: 'false'
|
||||
hotRunnerTransport:
|
||||
description: '[HotRunner] Transport protocol for hot runner communication: websocket, grpc, named-pipe'
|
||||
required: false
|
||||
default: 'websocket'
|
||||
hotRunnerHost:
|
||||
description: '[HotRunner] Hot runner host address'
|
||||
required: false
|
||||
default: 'localhost'
|
||||
hotRunnerPort:
|
||||
description: '[HotRunner] Hot runner port number'
|
||||
required: false
|
||||
default: '9090'
|
||||
hotRunnerHealthInterval:
|
||||
description: '[HotRunner] Health check interval in seconds'
|
||||
required: false
|
||||
default: '30'
|
||||
hotRunnerMaxIdle:
|
||||
description: '[HotRunner] Maximum idle time in seconds before recycling runner'
|
||||
required: false
|
||||
default: '3600'
|
||||
hotRunnerFallbackToCold:
|
||||
description: '[HotRunner] Fall back to cold build if no hot runner available'
|
||||
required: false
|
||||
default: 'true'
|
||||
syncStrategy:
|
||||
description: 'Workspace sync strategy: full, git-delta, direct-input, storage-pull'
|
||||
required: false
|
||||
default: 'full'
|
||||
syncInputRef:
|
||||
description: 'URI for direct-input or storage-pull content (storage://remote/path or file path)'
|
||||
required: false
|
||||
syncStorageRemote:
|
||||
description: 'rclone remote name for storage-backed inputs (defaults to rcloneRemote)'
|
||||
required: false
|
||||
syncRevertAfter:
|
||||
description: 'Revert overlaid changes after job completion'
|
||||
required: false
|
||||
default: 'true'
|
||||
syncStatePath:
|
||||
description: 'Path to sync state file for delta tracking'
|
||||
required: false
|
||||
default: '.game-ci/sync-state.json'
|
||||
description: 'When building for StandaloneLinux64, remove the default file extension of `.x86_64`. Set to true to restore the extensionless behavior from v4.'
|
||||
|
||||
outputs:
|
||||
volume:
|
||||
@@ -735,11 +203,9 @@ outputs:
|
||||
'Returns the exit code from the build scripts. This code is 0 if the build was successful. If there was an error
|
||||
during activation, the code is from the activation step. If activation is successful, the code is from the project
|
||||
build step.'
|
||||
artifactManifestPath:
|
||||
description: 'Path to the generated artifact manifest JSON file'
|
||||
branding:
|
||||
icon: 'box'
|
||||
color: 'gray-dark'
|
||||
runs:
|
||||
using: 'node20'
|
||||
using: 'node24'
|
||||
main: 'dist/index.js'
|
||||
|
||||
+69508
-65605
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+258
-559
File diff suppressed because it is too large
Load Diff
Vendored
+6
-2
@@ -71,8 +71,12 @@ if [[ "$RUN_AS_HOST_USER" == "true" ]]; then
|
||||
# Don't stop on error when running our scripts as error handling is baked in
|
||||
set +e
|
||||
|
||||
# Switch to the host user so we can create files with the correct ownership
|
||||
su $USERNAME -c "$SHELL -c 'source /steps/runsteps.sh'"
|
||||
# Switch to the host user so we can create files with the correct ownership.
|
||||
# Pass HOME/USER explicitly so the Unity Licensing Client (which writes to
|
||||
# ~/.config/unity3d) resolves a real, writable home directory rather than
|
||||
# falling back to /home/UNKNOWN when getpwuid() inside the container has no
|
||||
# entry for the host UID. -p preserves the rest of the env from root.
|
||||
su -p $USERNAME -c "HOME=/home/$USERNAME USER=$USERNAME LOGNAME=$USERNAME $SHELL -c 'source /steps/runsteps.sh'"
|
||||
else
|
||||
echo "Running as root"
|
||||
|
||||
|
||||
@@ -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'],
|
||||
};
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
[tools]
|
||||
node = "20.18.0"
|
||||
yarn = "4.14.1"
|
||||
actionlint = "latest"
|
||||
shellcheck = "latest"
|
||||
gitleaks = "latest"
|
||||
+39
-30
@@ -7,58 +7,67 @@
|
||||
"author": "Webber <webber@takken.io>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prepare": "lefthook install",
|
||||
"prepare": "husky",
|
||||
"build": "yarn && tsc && ncc build lib --source-map --license licenses.txt",
|
||||
"lint": "prettier --check \"src/**/*.{js,ts}\" && eslint src/**/*.ts",
|
||||
"format": "prettier --write \"src/**/*.{js,ts}\"",
|
||||
"test": "jest",
|
||||
"test:ci": "jest --config=jest.ci.config.js --runInBand"
|
||||
"test": "node scripts/ensure-husky.mjs && vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:ci": "vitest run",
|
||||
"coverage": "vitest run --coverage",
|
||||
"lint": "yarn oxlint --report-unused-disable-directives",
|
||||
"format": "oxfmt --write",
|
||||
"format:check": "oxfmt --check",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck:tsgo": "tsgo --noEmit",
|
||||
"setup:hooks": "node scripts/ensure-husky.mjs"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.@(ts|tsx|mts|js|jsx|mjs|cjs)": [
|
||||
"oxlint --fix --quiet",
|
||||
"oxfmt --write"
|
||||
],
|
||||
"*.@(json|jsonc|json5|md|mdx|yaml|yml|css|scss|sass|html|toml)": "oxfmt --write",
|
||||
".github/workflows/*.@(yml|yaml)": "actionlint"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/cache": "^4.0.0",
|
||||
"@actions/cache": "^4.1.0",
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/github": "^6.0.0",
|
||||
"commander": "^9.0.0",
|
||||
"commander-ts": "^0.2.0",
|
||||
"@actions/github": "^6.0.1",
|
||||
"md5": "^2.3.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"nanoid": "^3.3.1",
|
||||
"semver": "^7.5.2",
|
||||
"nanoid": "^3.3.12",
|
||||
"semver": "^7.7.4",
|
||||
"ts-md5": "^1.3.1",
|
||||
"unity-changeset": "^3.1.0",
|
||||
"yaml": "^2.2.2"
|
||||
"yaml": "^2.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/node": "^17.0.23",
|
||||
"@types/semver": "^7.3.9",
|
||||
"@typescript-eslint/parser": "4.8.1",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260505.1",
|
||||
"@vercel/ncc": "^0.36.1",
|
||||
"@vitest/coverage-istanbul": "^4.1.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^7.23.0",
|
||||
"eslint-config-prettier": "8.1.0",
|
||||
"eslint-plugin-github": "^4.1.1",
|
||||
"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",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-plugin-unicorn": "^64.0.0",
|
||||
"husky": "9",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lefthook": "^1.6.1",
|
||||
"lint-staged": "^16.4.0",
|
||||
"node-fetch": "2",
|
||||
"prettier": "^2.5.1",
|
||||
"ts-jest": "^27.1.3",
|
||||
"oxfmt": "^0.48.0",
|
||||
"oxlint": "^1.63.0",
|
||||
"ts-node": "10.8.1",
|
||||
"typescript": "4.7.4",
|
||||
"vite": "^7",
|
||||
"vitest": "^4",
|
||||
"yarn-audit-fix": "^9.3.8"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.5.1",
|
||||
"yarn": "1.22.19"
|
||||
"packageManager": "yarn@4.14.1",
|
||||
"dependenciesMeta": {
|
||||
"lefthook": {
|
||||
"built": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+55
@@ -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);
|
||||
}
|
||||
+178
-554
@@ -1,195 +1,89 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi, type Mock } from 'vitest';
|
||||
/**
|
||||
* Integration wiring tests for plugin features in index.ts
|
||||
* Integration wiring tests for the plugin lifecycle in index.ts
|
||||
*
|
||||
* These tests verify the conditional gating logic in runMain():
|
||||
* - Each plugin feature is only invoked when its gate condition is met
|
||||
* - Services are NOT called when their feature is disabled (the default)
|
||||
* - The order of operations is correct (restore before build, save after build)
|
||||
* 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
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { BuildParameters } from './model';
|
||||
import { BuildParameters, Docker } from './model';
|
||||
import * as core from '@actions/core';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Service mocks — must be declared before importing index.ts (jest hoists them)
|
||||
// Mock plugin
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockChildWorkspaceService = {
|
||||
buildConfig: jest.fn().mockReturnValue({ enabled: true, workspaceName: 'Test' }),
|
||||
initializeWorkspace: jest.fn().mockReturnValue(false),
|
||||
getWorkspaceSize: jest.fn().mockReturnValue('0 B'),
|
||||
saveWorkspace: jest.fn(),
|
||||
};
|
||||
// `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),
|
||||
};
|
||||
});
|
||||
|
||||
const mockSubmoduleProfileService = {
|
||||
createInitPlan: jest.fn().mockResolvedValue([]),
|
||||
execute: jest.fn().mockResolvedValue(''),
|
||||
};
|
||||
|
||||
const mockLfsAgentService = {
|
||||
configure: jest.fn().mockResolvedValue(''),
|
||||
};
|
||||
|
||||
const mockLocalCacheService = {
|
||||
resolveCacheRoot: jest.fn().mockReturnValue('/cache'),
|
||||
generateCacheKey: jest.fn().mockReturnValue('key-1'),
|
||||
restoreLfsCache: jest.fn().mockResolvedValue(true),
|
||||
restoreLibraryCache: jest.fn().mockResolvedValue(true),
|
||||
saveLibraryCache: jest.fn().mockResolvedValue(''),
|
||||
saveLfsCache: jest.fn().mockResolvedValue(''),
|
||||
};
|
||||
|
||||
const mockGitHooksService = {
|
||||
installHooks: jest.fn().mockResolvedValue(''),
|
||||
configureSkipList: jest.fn().mockReturnValue({ LEFTHOOK_EXCLUDE: 'pre-commit' }),
|
||||
};
|
||||
|
||||
const mockBuildReliabilityService = {
|
||||
configureGitEnvironment: jest.fn(),
|
||||
checkGitIntegrity: jest.fn().mockReturnValue(true),
|
||||
cleanStaleLockFiles: jest.fn(),
|
||||
validateSubmoduleBackingStores: jest.fn(),
|
||||
cleanReservedFilenames: jest.fn(),
|
||||
recoverCorruptedRepo: jest.fn().mockReturnValue(true),
|
||||
archiveBuildOutput: jest.fn(),
|
||||
enforceRetention: jest.fn(),
|
||||
};
|
||||
|
||||
const mockTestWorkflowService = {
|
||||
executeTestSuite: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
const mockHotRunnerService = jest.fn();
|
||||
|
||||
const mockIncrementalSyncService = {
|
||||
resolveStrategy: jest.fn().mockReturnValue('full'),
|
||||
syncGitDelta: jest.fn().mockResolvedValue(0),
|
||||
applyDirectInput: jest.fn().mockResolvedValue([]),
|
||||
syncStoragePull: jest.fn().mockResolvedValue([]),
|
||||
revertOverlays: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
const mockOutputService = {
|
||||
collectOutputs: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
const mockOutputTypeRegistry = {
|
||||
registerType: jest.fn(),
|
||||
};
|
||||
|
||||
const mockArtifactUploadHandler = {
|
||||
parseConfig: jest.fn().mockImplementation(() => {
|
||||
/* no config */
|
||||
}),
|
||||
uploadArtifacts: jest.fn().mockResolvedValue({ success: true, entries: [] }),
|
||||
};
|
||||
|
||||
const mockOrchestrator = {
|
||||
run: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
// Mock the orchestrator-plugin module to directly return our mock services.
|
||||
// This avoids any issues with dynamic imports inside loadPluginServices().
|
||||
jest.mock('./model/orchestrator-plugin', () => ({
|
||||
loadOrchestrator: jest.fn().mockResolvedValue({
|
||||
run: mockOrchestrator.run,
|
||||
}),
|
||||
loadPluginServices: jest.fn().mockResolvedValue({
|
||||
BuildReliabilityService: mockBuildReliabilityService,
|
||||
TestWorkflowService: mockTestWorkflowService,
|
||||
HotRunnerService: mockHotRunnerService,
|
||||
OutputService: mockOutputService,
|
||||
OutputTypeRegistry: mockOutputTypeRegistry,
|
||||
ArtifactUploadHandler: mockArtifactUploadHandler,
|
||||
IncrementalSyncService: mockIncrementalSyncService,
|
||||
|
||||
// Lazy-loaded services (matching the plugin loader API)
|
||||
loadChildWorkspaceService: jest.fn().mockResolvedValue(mockChildWorkspaceService),
|
||||
loadLocalCacheService: jest.fn().mockResolvedValue(mockLocalCacheService),
|
||||
loadSubmoduleProfileService: jest.fn().mockResolvedValue(mockSubmoduleProfileService),
|
||||
loadLfsAgentService: jest.fn().mockResolvedValue(mockLfsAgentService),
|
||||
loadGitHooksService: jest.fn().mockResolvedValue(mockGitHooksService),
|
||||
}),
|
||||
vi.mock('./model/plugin', () => ({
|
||||
loadPlugin: mockLoadPlugin,
|
||||
}));
|
||||
|
||||
// Mock all non-plugin dependencies to isolate the wiring logic
|
||||
jest.mock('@actions/core');
|
||||
jest.mock('./model', () => ({
|
||||
vi.mock('@actions/core');
|
||||
vi.mock('./model', () => ({
|
||||
Action: {
|
||||
checkCompatibility: jest.fn(),
|
||||
checkCompatibility: vi.fn(),
|
||||
workspace: '/workspace',
|
||||
actionFolder: '/action',
|
||||
},
|
||||
BuildParameters: {
|
||||
create: jest.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
Cache: {
|
||||
verify: jest.fn(),
|
||||
verify: vi.fn(),
|
||||
},
|
||||
Docker: {
|
||||
run: jest.fn().mockResolvedValue(0),
|
||||
run: vi.fn().mockResolvedValue(0),
|
||||
},
|
||||
ImageTag: jest.fn().mockImplementation(() => ({
|
||||
toString: () => 'mock-image:latest',
|
||||
})),
|
||||
// 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: jest.fn().mockResolvedValue(''),
|
||||
setAndroidVersionCode: jest.fn().mockResolvedValue(''),
|
||||
setEngineExitCode: jest.fn().mockResolvedValue(''),
|
||||
setBuildVersion: vi.fn().mockResolvedValue(''),
|
||||
setAndroidVersionCode: vi.fn().mockResolvedValue(''),
|
||||
setEngineExitCode: vi.fn().mockResolvedValue(''),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./model/cli/cli', () => ({
|
||||
Cli: {
|
||||
InitCliMode: jest.fn().mockReturnValue(false),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./model/mac-builder', () => ({
|
||||
vi.mock('./model/mac-builder', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
run: jest.fn().mockResolvedValue(0),
|
||||
run: vi.fn().mockResolvedValue(0),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./model/platform-setup', () => ({
|
||||
vi.mock('./model/platform-setup', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
setup: jest.fn().mockResolvedValue(''),
|
||||
setup: vi.fn().mockResolvedValue(''),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockedBuildParametersCreate = BuildParameters.create as jest.Mock;
|
||||
const mockedBuildParametersCreate = BuildParameters.create as Mock;
|
||||
|
||||
interface PluginBuildParametersOverrides {
|
||||
providerStrategy?: string;
|
||||
childWorkspacesEnabled?: boolean;
|
||||
childWorkspaceName?: string;
|
||||
childWorkspaceCacheRoot?: string;
|
||||
childWorkspacePreserveGit?: boolean;
|
||||
childWorkspaceSeparateLibrary?: boolean;
|
||||
submoduleProfilePath?: string;
|
||||
submoduleVariantPath?: string;
|
||||
submoduleToken?: string;
|
||||
gitPrivateToken?: string;
|
||||
lfsTransferAgent?: string;
|
||||
lfsTransferAgentArgs?: string;
|
||||
lfsStoragePaths?: string;
|
||||
localCacheEnabled?: boolean;
|
||||
localCacheRoot?: string;
|
||||
localCacheLibrary?: boolean;
|
||||
localCacheLfs?: boolean;
|
||||
gitHooksEnabled?: boolean;
|
||||
gitHooksSkipList?: string;
|
||||
gitHooksRunBeforeBuild?: string;
|
||||
}
|
||||
|
||||
function createMockBuildParameters(overrides: PluginBuildParametersOverrides = {}) {
|
||||
function createMockBuildParameters(overrides: Record<string, any> = {}) {
|
||||
return {
|
||||
// Required base properties
|
||||
providerStrategy: 'local',
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
editorVersion: '2021.3.1f1',
|
||||
@@ -198,67 +92,38 @@ function createMockBuildParameters(overrides: PluginBuildParametersOverrides = {
|
||||
projectPath: '.',
|
||||
branch: 'main',
|
||||
runnerTempPath: '/tmp',
|
||||
|
||||
// Plugin features - all disabled by default
|
||||
childWorkspacesEnabled: false,
|
||||
childWorkspaceName: '',
|
||||
childWorkspaceCacheRoot: '',
|
||||
childWorkspacePreserveGit: true,
|
||||
childWorkspaceSeparateLibrary: true,
|
||||
submoduleProfilePath: '',
|
||||
submoduleVariantPath: '',
|
||||
submoduleToken: '',
|
||||
gitPrivateToken: '',
|
||||
lfsTransferAgent: '',
|
||||
lfsTransferAgentArgs: '',
|
||||
lfsStoragePaths: '',
|
||||
localCacheEnabled: false,
|
||||
localCacheRoot: '',
|
||||
localCacheLibrary: true,
|
||||
localCacheLfs: false,
|
||||
gitHooksEnabled: false,
|
||||
gitHooksSkipList: '',
|
||||
gitHooksRunBeforeBuild: '',
|
||||
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The entry point (runMain) is invoked by importing index.ts.
|
||||
* Since it calls `runMain()` at module scope, we need to re-import it
|
||||
* for each test. jest.isolateModules() handles this.
|
||||
*/
|
||||
async function runIndex(overrides: PluginBuildParametersOverrides = {}): Promise<void> {
|
||||
async function runIndex(overrides: Record<string, any> = {}): Promise<void> {
|
||||
mockedBuildParametersCreate.mockResolvedValue(createMockBuildParameters(overrides));
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
jest.isolateModules(() => {
|
||||
require('./index');
|
||||
|
||||
// runMain() is async; give it a tick to complete
|
||||
// We use setImmediate to ensure all microtasks from the dynamic imports resolve
|
||||
});
|
||||
|
||||
// Allow all promises and microtasks to settle
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
// 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 feature wiring', () => {
|
||||
describe('index.ts plugin lifecycle wiring', () => {
|
||||
const originalPlatform = process.platform;
|
||||
const originalEnvironment = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
process.env.GITHUB_WORKSPACE = '/workspace';
|
||||
|
||||
// Force linux platform so Docker.run is used (not MacBuilder)
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
|
||||
// Reset plugin to default behavior
|
||||
mockPlugin.canHandleBuild.mockReturnValue(false);
|
||||
mockPlugin.handleBuild.mockResolvedValue({ exitCode: 0 });
|
||||
mockLoadPlugin.mockResolvedValue(mockPlugin);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -267,382 +132,141 @@ describe('index.ts plugin feature wiring', () => {
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// GitHooksService gating
|
||||
// Local build with plugin
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('GitHooksService gating', () => {
|
||||
it('should NOT call GitHooksService when gitHooksEnabled is false (default)', async () => {
|
||||
await runIndex({ gitHooksEnabled: false });
|
||||
|
||||
expect(mockGitHooksService.installHooks).not.toHaveBeenCalled();
|
||||
expect(mockGitHooksService.configureSkipList).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call installHooks when gitHooksEnabled is true', async () => {
|
||||
await runIndex({ gitHooksEnabled: true });
|
||||
|
||||
expect(mockGitHooksService.installHooks).toHaveBeenCalledWith('/workspace');
|
||||
});
|
||||
|
||||
it('should call configureSkipList when gitHooksEnabled and gitHooksSkipList is set', async () => {
|
||||
await runIndex({
|
||||
gitHooksEnabled: true,
|
||||
gitHooksSkipList: 'pre-commit,pre-push',
|
||||
});
|
||||
|
||||
expect(mockGitHooksService.configureSkipList).toHaveBeenCalledWith(['pre-commit', 'pre-push']);
|
||||
});
|
||||
|
||||
it('should NOT call configureSkipList when gitHooksSkipList is empty', async () => {
|
||||
await runIndex({
|
||||
gitHooksEnabled: true,
|
||||
gitHooksSkipList: '',
|
||||
});
|
||||
|
||||
expect(mockGitHooksService.installHooks).toHaveBeenCalled();
|
||||
expect(mockGitHooksService.configureSkipList).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// LocalCacheService gating
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('LocalCacheService gating', () => {
|
||||
it('should NOT call LocalCacheService when localCacheEnabled is false (default)', async () => {
|
||||
await runIndex({ localCacheEnabled: false });
|
||||
|
||||
expect(mockLocalCacheService.resolveCacheRoot).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.generateCacheKey).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.restoreLibraryCache).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.restoreLfsCache).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.saveLibraryCache).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.saveLfsCache).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call restore and save operations when localCacheEnabled is true', async () => {
|
||||
await runIndex({
|
||||
localCacheEnabled: true,
|
||||
localCacheLibrary: true,
|
||||
localCacheLfs: true,
|
||||
});
|
||||
|
||||
expect(mockLocalCacheService.resolveCacheRoot).toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.generateCacheKey).toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.restoreLibraryCache).toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.restoreLfsCache).toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.saveLibraryCache).toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.saveLfsCache).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should only cache Library when localCacheLibrary is true and localCacheLfs is false', async () => {
|
||||
await runIndex({
|
||||
localCacheEnabled: true,
|
||||
localCacheLibrary: true,
|
||||
localCacheLfs: false,
|
||||
});
|
||||
|
||||
expect(mockLocalCacheService.restoreLibraryCache).toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.restoreLfsCache).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.saveLibraryCache).toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.saveLfsCache).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should only cache LFS when localCacheLfs is true and localCacheLibrary is false', async () => {
|
||||
await runIndex({
|
||||
localCacheEnabled: true,
|
||||
localCacheLibrary: false,
|
||||
localCacheLfs: true,
|
||||
});
|
||||
|
||||
expect(mockLocalCacheService.restoreLibraryCache).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.restoreLfsCache).toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.saveLibraryCache).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.saveLfsCache).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ChildWorkspaceService gating
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('ChildWorkspaceService gating', () => {
|
||||
it('should NOT call ChildWorkspaceService when childWorkspacesEnabled is false (default)', async () => {
|
||||
await runIndex({ childWorkspacesEnabled: false });
|
||||
|
||||
expect(mockChildWorkspaceService.buildConfig).not.toHaveBeenCalled();
|
||||
expect(mockChildWorkspaceService.initializeWorkspace).not.toHaveBeenCalled();
|
||||
expect(mockChildWorkspaceService.saveWorkspace).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should NOT call ChildWorkspaceService when childWorkspacesEnabled is true but childWorkspaceName is empty', async () => {
|
||||
await runIndex({
|
||||
childWorkspacesEnabled: true,
|
||||
childWorkspaceName: '',
|
||||
});
|
||||
|
||||
expect(mockChildWorkspaceService.buildConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call buildConfig, initializeWorkspace, and saveWorkspace when enabled with a name', async () => {
|
||||
mockChildWorkspaceService.buildConfig.mockReturnValue({ enabled: true, workspaceName: 'TurnOfWar' });
|
||||
|
||||
await runIndex({
|
||||
childWorkspacesEnabled: true,
|
||||
childWorkspaceName: 'TurnOfWar',
|
||||
childWorkspaceCacheRoot: '/cache/workspaces',
|
||||
});
|
||||
|
||||
expect(mockChildWorkspaceService.buildConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
childWorkspacesEnabled: true,
|
||||
childWorkspaceName: 'TurnOfWar',
|
||||
}),
|
||||
);
|
||||
expect(mockChildWorkspaceService.initializeWorkspace).toHaveBeenCalled();
|
||||
expect(mockChildWorkspaceService.getWorkspaceSize).toHaveBeenCalled();
|
||||
expect(mockChildWorkspaceService.saveWorkspace).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// SubmoduleProfileService gating
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('SubmoduleProfileService gating', () => {
|
||||
it('should NOT call SubmoduleProfileService when submoduleProfilePath is empty (default)', async () => {
|
||||
await runIndex({ submoduleProfilePath: '' });
|
||||
|
||||
expect(mockSubmoduleProfileService.createInitPlan).not.toHaveBeenCalled();
|
||||
expect(mockSubmoduleProfileService.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call createInitPlan and execute when submoduleProfilePath is set', async () => {
|
||||
await runIndex({
|
||||
submoduleProfilePath: '/path/to/profile.yml',
|
||||
submoduleVariantPath: '',
|
||||
submoduleToken: 'my-token',
|
||||
});
|
||||
|
||||
expect(mockSubmoduleProfileService.createInitPlan).toHaveBeenCalledWith('/path/to/profile.yml', '', '/workspace');
|
||||
expect(mockSubmoduleProfileService.execute).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pass variant path when provided', async () => {
|
||||
await runIndex({
|
||||
submoduleProfilePath: '/path/to/profile.yml',
|
||||
submoduleVariantPath: '/path/to/variant.yml',
|
||||
});
|
||||
|
||||
expect(mockSubmoduleProfileService.createInitPlan).toHaveBeenCalledWith(
|
||||
'/path/to/profile.yml',
|
||||
'/path/to/variant.yml',
|
||||
'/workspace',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use submoduleToken for auth, falling back to gitPrivateToken', async () => {
|
||||
await runIndex({
|
||||
submoduleProfilePath: '/path/to/profile.yml',
|
||||
submoduleToken: '',
|
||||
gitPrivateToken: 'fallback-token',
|
||||
});
|
||||
|
||||
expect(mockSubmoduleProfileService.execute).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'/workspace',
|
||||
'fallback-token',
|
||||
);
|
||||
});
|
||||
|
||||
it('should prefer submoduleToken over gitPrivateToken', async () => {
|
||||
await runIndex({
|
||||
submoduleProfilePath: '/path/to/profile.yml',
|
||||
submoduleToken: 'specific-token',
|
||||
gitPrivateToken: 'fallback-token',
|
||||
});
|
||||
|
||||
expect(mockSubmoduleProfileService.execute).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'/workspace',
|
||||
'specific-token',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// LfsAgentService gating
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('LfsAgentService gating', () => {
|
||||
it('should NOT call LfsAgentService when lfsTransferAgent is empty (default)', async () => {
|
||||
await runIndex({ lfsTransferAgent: '' });
|
||||
|
||||
expect(mockLfsAgentService.configure).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call configure when lfsTransferAgent is set', async () => {
|
||||
await runIndex({
|
||||
lfsTransferAgent: '/tools/elastic-git-storage',
|
||||
lfsTransferAgentArgs: '--verbose',
|
||||
lfsStoragePaths: '/path/a;/path/b',
|
||||
});
|
||||
|
||||
expect(mockLfsAgentService.configure).toHaveBeenCalledWith(
|
||||
'/tools/elastic-git-storage',
|
||||
'--verbose',
|
||||
['/path/a', '/path/b'],
|
||||
'/workspace',
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass empty array when lfsStoragePaths is empty', async () => {
|
||||
await runIndex({
|
||||
lfsTransferAgent: '/tools/agent',
|
||||
lfsStoragePaths: '',
|
||||
});
|
||||
|
||||
expect(mockLfsAgentService.configure).toHaveBeenCalledWith('/tools/agent', '', [], '/workspace');
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Order of operations (restore before build, save after build)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('order of operations', () => {
|
||||
it('should execute restore operations before build and save operations after build', async () => {
|
||||
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'));
|
||||
|
||||
// Track call order for each relevant operation
|
||||
mockChildWorkspaceService.buildConfig.mockReturnValue({ enabled: true, workspaceName: 'Test' });
|
||||
mockChildWorkspaceService.initializeWorkspace.mockImplementation(() => {
|
||||
callOrder.push('child-workspace-restore');
|
||||
await runIndex();
|
||||
|
||||
return false;
|
||||
});
|
||||
mockChildWorkspaceService.getWorkspaceSize.mockImplementation(() => {
|
||||
callOrder.push('child-workspace-size');
|
||||
expect(callOrder).toEqual([
|
||||
'initialize',
|
||||
'beforeLocalBuild',
|
||||
'afterLocalBuild',
|
||||
'handlePostBuild',
|
||||
]);
|
||||
});
|
||||
|
||||
return '0 B';
|
||||
});
|
||||
mockSubmoduleProfileService.createInitPlan.mockImplementation(async () => {
|
||||
callOrder.push('submodule-profile-plan');
|
||||
it('should pass buildParameters and workspace to initialize', async () => {
|
||||
await runIndex({ targetPlatform: 'WebGL' });
|
||||
|
||||
return [];
|
||||
});
|
||||
mockSubmoduleProfileService.execute.mockImplementation(async () => {
|
||||
callOrder.push('submodule-profile-execute');
|
||||
});
|
||||
mockLfsAgentService.configure.mockImplementation(async () => {
|
||||
callOrder.push('lfs-agent-configure');
|
||||
});
|
||||
mockLocalCacheService.resolveCacheRoot.mockImplementation(() => {
|
||||
callOrder.push('local-cache-resolve');
|
||||
expect(mockPlugin.initialize).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ targetPlatform: 'WebGL' }),
|
||||
'/workspace',
|
||||
);
|
||||
});
|
||||
|
||||
return '/cache';
|
||||
});
|
||||
mockLocalCacheService.generateCacheKey.mockImplementation(() => {
|
||||
callOrder.push('local-cache-keygen');
|
||||
it('should pass workspace to beforeLocalBuild', async () => {
|
||||
await runIndex();
|
||||
|
||||
return 'key-1';
|
||||
});
|
||||
mockLocalCacheService.restoreLfsCache.mockImplementation(async () => {
|
||||
callOrder.push('local-cache-restore-lfs');
|
||||
expect(mockPlugin.beforeLocalBuild).toHaveBeenCalledWith('/workspace');
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
mockLocalCacheService.restoreLibraryCache.mockImplementation(async () => {
|
||||
callOrder.push('local-cache-restore-library');
|
||||
it('should pass workspace and exit code to afterLocalBuild', async () => {
|
||||
await runIndex();
|
||||
|
||||
return true;
|
||||
});
|
||||
mockGitHooksService.installHooks.mockImplementation(async () => {
|
||||
callOrder.push('git-hooks-install');
|
||||
});
|
||||
mockLocalCacheService.saveLibraryCache.mockImplementation(async () => {
|
||||
callOrder.push('local-cache-save-library');
|
||||
});
|
||||
mockLocalCacheService.saveLfsCache.mockImplementation(async () => {
|
||||
callOrder.push('local-cache-save-lfs');
|
||||
});
|
||||
mockChildWorkspaceService.saveWorkspace.mockImplementation(() => {
|
||||
callOrder.push('child-workspace-save');
|
||||
});
|
||||
expect(mockPlugin.afterLocalBuild).toHaveBeenCalledWith('/workspace', 0);
|
||||
});
|
||||
|
||||
await runIndex({
|
||||
childWorkspacesEnabled: true,
|
||||
childWorkspaceName: 'TurnOfWar',
|
||||
submoduleProfilePath: '/profile.yml',
|
||||
lfsTransferAgent: '/tools/agent',
|
||||
localCacheEnabled: true,
|
||||
localCacheLfs: true,
|
||||
localCacheLibrary: true,
|
||||
gitHooksEnabled: true,
|
||||
});
|
||||
it('should pass exit code to handlePostBuild', async () => {
|
||||
await runIndex();
|
||||
|
||||
// Verify restore operations happen before save operations.
|
||||
// The expected order from index.ts is:
|
||||
// 1. Child workspace restore
|
||||
// 2. Submodule profile init
|
||||
// 3. LFS agent configure
|
||||
// 4. Local cache restore (LFS then Library)
|
||||
// 5. Git hooks install
|
||||
// 6. [BUILD happens here - Docker.run or MacBuilder.run]
|
||||
// 7. Local cache save (Library then LFS)
|
||||
// 8. Child workspace save
|
||||
|
||||
const restoreOps = [
|
||||
'child-workspace-restore',
|
||||
'submodule-profile-plan',
|
||||
'submodule-profile-execute',
|
||||
'lfs-agent-configure',
|
||||
'local-cache-restore-lfs',
|
||||
'local-cache-restore-library',
|
||||
'git-hooks-install',
|
||||
];
|
||||
|
||||
const saveOps = ['local-cache-save-library', 'local-cache-save-lfs', 'child-workspace-save'];
|
||||
|
||||
// All restore ops should appear before all save ops
|
||||
for (const restoreOp of restoreOps) {
|
||||
if (!callOrder.includes(restoreOp)) continue; // Skip if the operation wasn't called
|
||||
for (const saveOp of saveOps) {
|
||||
if (!callOrder.includes(saveOp)) continue;
|
||||
expect(callOrder.indexOf(restoreOp)).toBeLessThan(callOrder.indexOf(saveOp));
|
||||
}
|
||||
}
|
||||
|
||||
// Child workspace save should be last
|
||||
if (callOrder.includes('child-workspace-save') && callOrder.includes('local-cache-save-lfs')) {
|
||||
expect(callOrder.indexOf('local-cache-save-lfs')).toBeLessThan(callOrder.indexOf('child-workspace-save'));
|
||||
}
|
||||
expect(mockPlugin.handlePostBuild).toHaveBeenCalledWith(0);
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Non-local provider strategy
|
||||
// Plugin handles build entirely
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('non-local provider strategy', () => {
|
||||
it('should skip all plugin features when providerStrategy is not local', async () => {
|
||||
await runIndex({
|
||||
providerStrategy: 'aws',
|
||||
childWorkspacesEnabled: true,
|
||||
childWorkspaceName: 'Test',
|
||||
submoduleProfilePath: '/profile.yml',
|
||||
lfsTransferAgent: '/tools/agent',
|
||||
localCacheEnabled: true,
|
||||
gitHooksEnabled: true,
|
||||
});
|
||||
describe('plugin handles build (canHandleBuild = true)', () => {
|
||||
it('should call handleBuild instead of Docker.run', async () => {
|
||||
mockPlugin.canHandleBuild.mockReturnValue(true);
|
||||
mockPlugin.handleBuild.mockResolvedValue({ exitCode: 0 });
|
||||
|
||||
// None of the plugin services should be called because
|
||||
// they are inside the `if (providerStrategy === 'local')` block
|
||||
expect(mockChildWorkspaceService.buildConfig).not.toHaveBeenCalled();
|
||||
expect(mockSubmoduleProfileService.createInitPlan).not.toHaveBeenCalled();
|
||||
expect(mockLfsAgentService.configure).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.resolveCacheRoot).not.toHaveBeenCalled();
|
||||
expect(mockGitHooksService.installHooks).not.toHaveBeenCalled();
|
||||
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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+34
-407
@@ -1,356 +1,49 @@
|
||||
import * as core from '@actions/core';
|
||||
import path from 'node:path';
|
||||
import { Action, BuildParameters, Cache, Docker, ImageTag, Output } from './model';
|
||||
import { Cli } from './model/cli/cli';
|
||||
import MacBuilder from './model/mac-builder';
|
||||
import PlatformSetup from './model/platform-setup';
|
||||
import { loadOrchestrator, loadPluginServices } from './model/orchestrator-plugin';
|
||||
type SyncStrategy = 'full' | 'git-delta' | 'direct-input' | 'storage-pull';
|
||||
import { Plugin, loadPlugin } from './model/plugin';
|
||||
|
||||
type PluginServices = Exclude<ReturnType<typeof loadPluginServices> extends Promise<infer T> ? T : never, undefined>;
|
||||
|
||||
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 {
|
||||
if (Cli.InitCliMode()) {
|
||||
await Cli.RunCli();
|
||||
|
||||
return;
|
||||
}
|
||||
Action.checkCompatibility();
|
||||
Cache.verify();
|
||||
|
||||
const plugin = await loadPluginServices();
|
||||
|
||||
// Always configure git environment for CI reliability
|
||||
plugin?.BuildReliabilityService.configureGitEnvironment();
|
||||
|
||||
const { workspace, actionFolder } = Action;
|
||||
|
||||
const buildParameters = await BuildParameters.create();
|
||||
|
||||
// If a test suite path is provided, use the test workflow engine
|
||||
// instead of the standard build execution path
|
||||
if (buildParameters.testSuitePath) {
|
||||
core.info('[TestWorkflow] Test suite path detected, using test workflow engine');
|
||||
const results = await plugin?.TestWorkflowService.executeTestSuite(
|
||||
buildParameters.testSuitePath,
|
||||
buildParameters,
|
||||
);
|
||||
|
||||
let totalFailed = 0;
|
||||
for (const result of results || []) {
|
||||
totalFailed += result.failed;
|
||||
}
|
||||
|
||||
if (totalFailed > 0) {
|
||||
core.setFailed(`Test workflow completed with ${totalFailed} failure(s)`);
|
||||
} else {
|
||||
core.info('[TestWorkflow] All test runs passed');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const baseImage = new ImageTag(buildParameters);
|
||||
|
||||
// Pre-build reliability checks
|
||||
if (buildParameters.gitIntegrityCheck) {
|
||||
core.info('Running git integrity checks...');
|
||||
|
||||
const isHealthy = plugin?.BuildReliabilityService.checkGitIntegrity(workspace);
|
||||
plugin?.BuildReliabilityService.cleanStaleLockFiles(workspace);
|
||||
plugin?.BuildReliabilityService.validateSubmoduleBackingStores(workspace);
|
||||
|
||||
if (buildParameters.cleanReservedFilenames) {
|
||||
plugin?.BuildReliabilityService.cleanReservedFilenames(buildParameters.projectPath);
|
||||
}
|
||||
|
||||
if (!isHealthy && buildParameters.gitAutoRecover) {
|
||||
core.info('Git corruption detected, attempting automatic recovery...');
|
||||
const recovered = plugin?.BuildReliabilityService.recoverCorruptedRepo(workspace);
|
||||
if (!recovered) {
|
||||
core.warning('Automatic recovery failed. Build may encounter issues.');
|
||||
}
|
||||
}
|
||||
} else if (buildParameters.cleanReservedFilenames) {
|
||||
// cleanReservedFilenames can run independently of gitIntegrityCheck
|
||||
plugin?.BuildReliabilityService.cleanReservedFilenames(buildParameters.projectPath);
|
||||
}
|
||||
// Load optional plugin. The default implementation is @game-ci/orchestrator.
|
||||
const plugin = await loadPlugin();
|
||||
await plugin?.initialize(buildParameters, workspace);
|
||||
|
||||
let exitCode = -1;
|
||||
|
||||
// Hot runner path: attempt to use a persistent Unity editor instance
|
||||
if (buildParameters.hotRunnerEnabled) {
|
||||
core.info('[HotRunner] Hot runner mode enabled, attempting hot build...');
|
||||
if (plugin?.canHandleBuild()) {
|
||||
// Plugin handles the build entirely (remote providers, hot runner, test workflows)
|
||||
const result = await plugin.handleBuild(baseImage.toString());
|
||||
|
||||
const hotRunnerConfig = {
|
||||
enabled: true,
|
||||
transport: buildParameters.hotRunnerTransport,
|
||||
host: buildParameters.hotRunnerHost,
|
||||
port: buildParameters.hotRunnerPort,
|
||||
healthCheckInterval: buildParameters.hotRunnerHealthInterval,
|
||||
maxIdleTime: buildParameters.hotRunnerMaxIdle,
|
||||
maxJobsBeforeRecycle: 0, // no automatic recycle by job count
|
||||
};
|
||||
|
||||
if (!plugin?.HotRunnerService) {
|
||||
throw new Error('[HotRunner] Orchestrator plugin required for hot runner mode');
|
||||
}
|
||||
|
||||
const hotRunnerService = new plugin.HotRunnerService();
|
||||
|
||||
try {
|
||||
await hotRunnerService.initialize(hotRunnerConfig);
|
||||
const result = await hotRunnerService.submitBuild(buildParameters, (output: string) => {
|
||||
core.info(output);
|
||||
});
|
||||
|
||||
exitCode = result.exitCode;
|
||||
core.info(`[HotRunner] Build completed with exit code ${exitCode}`);
|
||||
await hotRunnerService.shutdown();
|
||||
} catch (hotRunnerError) {
|
||||
await hotRunnerService.shutdown();
|
||||
|
||||
if (buildParameters.hotRunnerFallbackToCold) {
|
||||
core.warning(
|
||||
`[HotRunner] Hot runner failed: ${(hotRunnerError as Error).message}. Falling back to cold build.`,
|
||||
);
|
||||
exitCode = await runColdBuild(buildParameters, baseImage, workspace, actionFolder);
|
||||
} else {
|
||||
throw hotRunnerError;
|
||||
}
|
||||
}
|
||||
exitCode = result.fallbackToLocal
|
||||
? await runLocalBuild(buildParameters, baseImage, workspace, actionFolder, plugin)
|
||||
: result.exitCode;
|
||||
} else if (buildParameters.providerStrategy === 'local') {
|
||||
core.info('Building locally');
|
||||
|
||||
// Child workspace isolation - restore cached workspace before any other setup
|
||||
let childWorkspaceConfig: any;
|
||||
if (buildParameters.childWorkspacesEnabled && buildParameters.childWorkspaceName) {
|
||||
const ChildWorkspaceService = await plugin?.loadChildWorkspaceService();
|
||||
const cacheRoot =
|
||||
buildParameters.childWorkspaceCacheRoot ||
|
||||
path.join(buildParameters.runnerTempPath || process.env.RUNNER_TEMP || '', 'game-ci-workspaces');
|
||||
childWorkspaceConfig = ChildWorkspaceService?.buildConfig({
|
||||
childWorkspacesEnabled: buildParameters.childWorkspacesEnabled,
|
||||
childWorkspaceName: buildParameters.childWorkspaceName,
|
||||
childWorkspaceCacheRoot: cacheRoot,
|
||||
childWorkspacePreserveGit: buildParameters.childWorkspacePreserveGit,
|
||||
childWorkspaceSeparateLibrary: buildParameters.childWorkspaceSeparateLibrary,
|
||||
});
|
||||
const projectFullPath = path.join(workspace, buildParameters.projectPath);
|
||||
const restored = ChildWorkspaceService?.initializeWorkspace(projectFullPath, childWorkspaceConfig);
|
||||
core.info(
|
||||
`Child workspace "${buildParameters.childWorkspaceName}": ${
|
||||
restored ? 'restored from cache' : 'starting fresh'
|
||||
}`,
|
||||
);
|
||||
|
||||
// Log workspace size for resource tracking
|
||||
const size = ChildWorkspaceService?.getWorkspaceSize(projectFullPath);
|
||||
core.info(`Child workspace size after restore: ${size}`);
|
||||
}
|
||||
|
||||
// Submodule profile initialization
|
||||
if (buildParameters.submoduleProfilePath) {
|
||||
core.info('Initializing submodules from profile...');
|
||||
const SubmoduleProfileService = await plugin?.loadSubmoduleProfileService();
|
||||
const plan = await SubmoduleProfileService?.createInitPlan(
|
||||
buildParameters.submoduleProfilePath,
|
||||
buildParameters.submoduleVariantPath,
|
||||
workspace,
|
||||
);
|
||||
|
||||
if (plan) {
|
||||
await SubmoduleProfileService?.execute(
|
||||
plan,
|
||||
workspace,
|
||||
buildParameters.submoduleToken || buildParameters.gitPrivateToken,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Configure custom LFS transfer agent
|
||||
if (buildParameters.lfsTransferAgent) {
|
||||
core.info('Configuring custom LFS transfer agent...');
|
||||
const LfsAgentService = await plugin?.loadLfsAgentService();
|
||||
await LfsAgentService?.configure(
|
||||
buildParameters.lfsTransferAgent,
|
||||
buildParameters.lfsTransferAgentArgs,
|
||||
buildParameters.lfsStoragePaths ? buildParameters.lfsStoragePaths.split(';') : [],
|
||||
workspace,
|
||||
);
|
||||
}
|
||||
|
||||
// Local build caching - restore
|
||||
let cacheRoot = '';
|
||||
let cacheKey = '';
|
||||
// eslint-disable-next-line no-undef
|
||||
let LocalCacheService: Awaited<ReturnType<NonNullable<typeof plugin>['loadLocalCacheService']>> | undefined;
|
||||
if (buildParameters.localCacheEnabled) {
|
||||
LocalCacheService = await plugin?.loadLocalCacheService();
|
||||
cacheRoot = LocalCacheService?.resolveCacheRoot(buildParameters) || '';
|
||||
cacheKey =
|
||||
LocalCacheService?.generateCacheKey(
|
||||
buildParameters.targetPlatform,
|
||||
buildParameters.editorVersion,
|
||||
buildParameters.branch || '',
|
||||
) || '';
|
||||
if (buildParameters.localCacheLfs) {
|
||||
await LocalCacheService?.restoreLfsCache(workspace, cacheRoot, cacheKey);
|
||||
}
|
||||
if (buildParameters.localCacheLibrary) {
|
||||
const projectFullPath = path.join(workspace, buildParameters.projectPath);
|
||||
await LocalCacheService?.restoreLibraryCache(projectFullPath, cacheRoot, cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Git hooks — opt-in only. When disabled (default), do not touch hooks at all.
|
||||
if (buildParameters.gitHooksEnabled) {
|
||||
const GitHooksService = await plugin?.loadGitHooksService();
|
||||
await GitHooksService?.installHooks(workspace);
|
||||
if (buildParameters.gitHooksSkipList) {
|
||||
const environment = GitHooksService?.configureSkipList(buildParameters.gitHooksSkipList.split(','));
|
||||
if (environment) {
|
||||
Object.assign(process.env, environment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply incremental sync strategy before build
|
||||
const syncStrategy = buildParameters.syncStrategy as SyncStrategy;
|
||||
if (syncStrategy !== 'full') {
|
||||
core.info(`[Sync] Applying sync strategy: ${syncStrategy}`);
|
||||
await applySyncStrategy(buildParameters, workspace, plugin);
|
||||
}
|
||||
|
||||
await PlatformSetup.setup(buildParameters, actionFolder);
|
||||
exitCode =
|
||||
process.platform === 'darwin'
|
||||
? await MacBuilder.run(actionFolder)
|
||||
: await Docker.run(baseImage.toString(), {
|
||||
workspace,
|
||||
actionFolder,
|
||||
...buildParameters,
|
||||
});
|
||||
|
||||
// Local build caching - save
|
||||
if (buildParameters.localCacheEnabled && LocalCacheService) {
|
||||
if (buildParameters.localCacheLibrary) {
|
||||
const projectFullPath = path.join(workspace, buildParameters.projectPath);
|
||||
await LocalCacheService.saveLibraryCache(projectFullPath, cacheRoot, cacheKey);
|
||||
}
|
||||
if (buildParameters.localCacheLfs) {
|
||||
await LocalCacheService.saveLfsCache(workspace, cacheRoot, cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Child workspace isolation - save workspace for next run
|
||||
if (childWorkspaceConfig && childWorkspaceConfig.enabled) {
|
||||
const ChildWorkspaceService = await plugin?.loadChildWorkspaceService();
|
||||
const projectFullPath = path.join(workspace, buildParameters.projectPath);
|
||||
const preSaveSize = ChildWorkspaceService?.getWorkspaceSize(projectFullPath);
|
||||
core.info(`Child workspace size before save: ${preSaveSize}`);
|
||||
|
||||
ChildWorkspaceService?.saveWorkspace(projectFullPath, childWorkspaceConfig);
|
||||
core.info(`Child workspace "${buildParameters.childWorkspaceName}" saved to cache`);
|
||||
}
|
||||
|
||||
// Revert overlays after job completion if configured
|
||||
if (buildParameters.syncRevertAfter && syncStrategy !== 'full') {
|
||||
core.info('[Sync] Reverting overlay changes after job completion');
|
||||
try {
|
||||
await plugin?.IncrementalSyncService.revertOverlays(workspace, buildParameters.syncStatePath);
|
||||
} catch (revertError) {
|
||||
core.warning(`[Sync] Overlay revert failed: ${(revertError as Error).message}`);
|
||||
}
|
||||
}
|
||||
exitCode = await runLocalBuild(buildParameters, baseImage, workspace, actionFolder, plugin);
|
||||
} else {
|
||||
const orchestrator = await loadOrchestrator();
|
||||
if (!orchestrator) {
|
||||
throw new Error(
|
||||
'Orchestrator package not available. Install @game-ci/orchestrator or use providerStrategy=local.',
|
||||
);
|
||||
}
|
||||
await orchestrator.run(buildParameters, baseImage.toString());
|
||||
exitCode = 0;
|
||||
}
|
||||
|
||||
// Post-build: archive and enforce retention
|
||||
if (buildParameters.buildArchiveEnabled && exitCode === 0) {
|
||||
core.info('Archiving build output...');
|
||||
plugin?.BuildReliabilityService.archiveBuildOutput(buildParameters.buildPath, buildParameters.buildArchivePath);
|
||||
plugin?.BuildReliabilityService.enforceRetention(
|
||||
buildParameters.buildArchivePath,
|
||||
buildParameters.buildArchiveRetention,
|
||||
throw new Error(
|
||||
`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.setAndroidVersionCode(buildParameters.androidVersionCode);
|
||||
await Output.setEngineExitCode(exitCode);
|
||||
|
||||
// Artifact collection and upload (runs on both success and failure)
|
||||
try {
|
||||
// Register custom output types if provided
|
||||
if (buildParameters.artifactCustomTypes) {
|
||||
try {
|
||||
const customTypes = JSON.parse(buildParameters.artifactCustomTypes);
|
||||
if (Array.isArray(customTypes)) {
|
||||
for (const ct of customTypes) {
|
||||
plugin?.OutputTypeRegistry.registerType({
|
||||
name: ct.name,
|
||||
defaultPath: ct.defaultPath || ct.pattern || `./${ct.name}/`,
|
||||
description: ct.description || `Custom output type: ${ct.name}`,
|
||||
builtIn: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
core.warning(`Failed to parse artifactCustomTypes: ${(parseError as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect outputs and generate manifest
|
||||
const manifestPath = path.join(buildParameters.projectPath, 'output-manifest.json');
|
||||
const manifest = await plugin?.OutputService.collectOutputs(
|
||||
buildParameters.projectPath,
|
||||
buildParameters.buildGuid,
|
||||
buildParameters.artifactOutputTypes,
|
||||
manifestPath,
|
||||
);
|
||||
|
||||
core.setOutput('artifactManifestPath', manifestPath);
|
||||
|
||||
if (manifest) {
|
||||
// Upload artifacts
|
||||
const uploadConfig = plugin?.ArtifactUploadHandler.parseConfig(
|
||||
buildParameters.artifactUploadTarget,
|
||||
buildParameters.artifactUploadPath || undefined,
|
||||
buildParameters.artifactCompression,
|
||||
buildParameters.artifactRetentionDays,
|
||||
);
|
||||
|
||||
if (uploadConfig) {
|
||||
const uploadResult = await plugin?.ArtifactUploadHandler.uploadArtifacts(
|
||||
manifest,
|
||||
uploadConfig,
|
||||
buildParameters.projectPath,
|
||||
);
|
||||
|
||||
if (uploadResult && !uploadResult.success) {
|
||||
core.warning(
|
||||
`Artifact upload completed with errors: ${uploadResult.entries
|
||||
.filter((entry: any) => !entry.success)
|
||||
.map((entry: any) => `${entry.type}: ${entry.error}`)
|
||||
.join('; ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (artifactError) {
|
||||
core.warning(`Artifact collection/upload failed: ${(artifactError as Error).message}`);
|
||||
}
|
||||
// Plugin handles post-build (artifacts, archiving, retention)
|
||||
await plugin?.handlePostBuild(exitCode);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
core.setFailed(`Build failed with exit code ${exitCode}`);
|
||||
@@ -360,99 +53,33 @@ async function runMain() {
|
||||
}
|
||||
}
|
||||
|
||||
async function runColdBuild(
|
||||
async function runLocalBuild(
|
||||
buildParameters: BuildParameters,
|
||||
baseImage: ImageTag,
|
||||
workspace: string,
|
||||
actionFolder: string,
|
||||
plugin?: Plugin,
|
||||
): Promise<number> {
|
||||
if (buildParameters.providerStrategy === 'local') {
|
||||
core.info('Building locally');
|
||||
await PlatformSetup.setup(buildParameters, actionFolder);
|
||||
await plugin?.beforeLocalBuild(workspace);
|
||||
|
||||
return process.platform === 'darwin'
|
||||
await PlatformSetup.setup(buildParameters, actionFolder);
|
||||
const exitCode =
|
||||
process.platform === 'darwin'
|
||||
? await MacBuilder.run(actionFolder)
|
||||
: await Docker.run(baseImage.toString(), {
|
||||
workspace,
|
||||
actionFolder,
|
||||
...buildParameters,
|
||||
});
|
||||
} else {
|
||||
const orchestrator = await loadOrchestrator();
|
||||
if (!orchestrator) {
|
||||
throw new Error(
|
||||
'Orchestrator package not available. Install @game-ci/orchestrator or use providerStrategy=local.',
|
||||
);
|
||||
}
|
||||
await orchestrator.run(buildParameters, baseImage.toString());
|
||||
|
||||
return 0;
|
||||
}
|
||||
await plugin?.afterLocalBuild(workspace, exitCode);
|
||||
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the configured sync strategy to the workspace before build.
|
||||
*/
|
||||
async function applySyncStrategy(
|
||||
buildParameters: BuildParameters,
|
||||
workspace: string,
|
||||
plugin?: PluginServices | undefined,
|
||||
): Promise<void> {
|
||||
if (!plugin?.IncrementalSyncService) {
|
||||
core.warning('[Sync] Orchestrator plugin not available, skipping sync strategy');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { IncrementalSyncService } = plugin;
|
||||
const strategy = buildParameters.syncStrategy as SyncStrategy;
|
||||
const resolvedStrategy = IncrementalSyncService.resolveStrategy(strategy, workspace, buildParameters.syncStatePath);
|
||||
|
||||
if (resolvedStrategy === 'full') {
|
||||
core.info('[Sync] Resolved to full sync (no incremental state available)');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch (resolvedStrategy) {
|
||||
case 'git-delta': {
|
||||
const targetReference = buildParameters.gitSha || buildParameters.branch;
|
||||
const changedFiles = await IncrementalSyncService.syncGitDelta(
|
||||
workspace,
|
||||
targetReference,
|
||||
buildParameters.syncStatePath,
|
||||
);
|
||||
core.info(`[Sync] Git delta sync applied: ${changedFiles} file(s) changed`);
|
||||
break;
|
||||
}
|
||||
case 'direct-input': {
|
||||
if (!buildParameters.syncInputRef) {
|
||||
throw new Error('[Sync] direct-input strategy requires syncInputRef to be set');
|
||||
}
|
||||
const overlays = await IncrementalSyncService.applyDirectInput(
|
||||
workspace,
|
||||
buildParameters.syncInputRef,
|
||||
buildParameters.syncStorageRemote || undefined,
|
||||
buildParameters.syncStatePath,
|
||||
);
|
||||
core.info(`[Sync] Direct input applied: ${overlays.length} overlay(s)`);
|
||||
break;
|
||||
}
|
||||
case 'storage-pull': {
|
||||
if (!buildParameters.syncInputRef) {
|
||||
throw new Error('[Sync] storage-pull strategy requires syncInputRef to be set');
|
||||
}
|
||||
const pulledFiles = await IncrementalSyncService.syncStoragePull(workspace, buildParameters.syncInputRef, {
|
||||
rcloneRemote: buildParameters.syncStorageRemote || undefined,
|
||||
syncRevertAfter: buildParameters.syncRevertAfter,
|
||||
statePath: buildParameters.syncStatePath,
|
||||
});
|
||||
core.info(`[Sync] Storage pull complete: ${pulledFiles.length} file(s)`);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
core.warning(`[Sync] Unknown sync strategy: ${resolvedStrategy}`);
|
||||
}
|
||||
// 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,3 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||
import { stat } from 'node:fs/promises';
|
||||
|
||||
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 Platform from '../platform';
|
||||
|
||||
export const mockGetFromUser = jest.fn().mockResolvedValue({
|
||||
export const mockGetFromUser = vi.fn().mockResolvedValue({
|
||||
editorVersion: '',
|
||||
targetPlatform: Platform.types.Test,
|
||||
projectPath: '.',
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { vi } from 'vitest';
|
||||
/* eslint unicorn/prevent-abbreviations: "off" */
|
||||
|
||||
// Import these named export into your test file:
|
||||
export const mockProjectPath = jest.fn().mockResolvedValue('mockProjectPath');
|
||||
export const mockIsDirtyAllowed = jest.fn().mockResolvedValue(false);
|
||||
export const mockBranch = jest.fn().mockResolvedValue('mockBranch');
|
||||
export const mockHeadRef = jest.fn().mockResolvedValue('mockHeadRef');
|
||||
export const mockRef = jest.fn().mockResolvedValue('mockRef');
|
||||
export const mockDetermineVersion = jest.fn().mockResolvedValue('1.2.3');
|
||||
export const mockGenerateSemanticVersion = jest.fn().mockResolvedValue('2.3.4');
|
||||
export const mockGenerateTagVersion = jest.fn().mockResolvedValue('1.0');
|
||||
export const mockParseSemanticVersion = jest.fn().mockResolvedValue({});
|
||||
export const mockFetch = jest.fn().mockImplementation(() => {});
|
||||
export const mockGetVersionDescription = jest.fn().mockResolvedValue('1.2-3-g12345678-dirty');
|
||||
export const mockIsDirty = jest.fn().mockResolvedValue(false);
|
||||
export const mockGetTag = jest.fn().mockResolvedValue('v1.0');
|
||||
export const mockHasAnyVersionTags = jest.fn().mockResolvedValue(true);
|
||||
export const mockGetTotalNumberOfCommits = jest.fn().mockResolvedValue(3);
|
||||
export const mockGit = jest.fn().mockImplementation(() => {});
|
||||
export const mockProjectPath = vi.fn().mockResolvedValue('mockProjectPath');
|
||||
export const mockIsDirtyAllowed = vi.fn().mockResolvedValue(false);
|
||||
export const mockBranch = vi.fn().mockResolvedValue('mockBranch');
|
||||
export const mockHeadRef = vi.fn().mockResolvedValue('mockHeadRef');
|
||||
export const mockRef = vi.fn().mockResolvedValue('mockRef');
|
||||
export const mockDetermineVersion = vi.fn().mockResolvedValue('1.2.3');
|
||||
export const mockGenerateSemanticVersion = vi.fn().mockResolvedValue('2.3.4');
|
||||
export const mockGenerateTagVersion = vi.fn().mockResolvedValue('1.0');
|
||||
export const mockParseSemanticVersion = vi.fn().mockResolvedValue({});
|
||||
export const mockFetch = vi.fn().mockImplementation(() => {});
|
||||
export const mockGetVersionDescription = vi.fn().mockResolvedValue('1.2-3-g12345678-dirty');
|
||||
export const mockIsDirty = vi.fn().mockResolvedValue(false);
|
||||
export const mockGetTag = vi.fn().mockResolvedValue('v1.0');
|
||||
export const mockHasAnyVersionTags = vi.fn().mockResolvedValue(true);
|
||||
export const mockGetTotalNumberOfCommits = vi.fn().mockResolvedValue(3);
|
||||
export const mockGit = vi.fn().mockImplementation(() => {});
|
||||
|
||||
export default {
|
||||
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 fs from 'node:fs';
|
||||
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';
|
||||
|
||||
describe('Android Versioning', () => {
|
||||
@@ -35,7 +36,9 @@ describe('Android Versioning', () => {
|
||||
});
|
||||
|
||||
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 {
|
||||
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';
|
||||
}
|
||||
@@ -27,7 +29,8 @@ export default class AndroidVersioning {
|
||||
|
||||
// The greatest value Google Plays allows is 2100000000.
|
||||
// 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) {
|
||||
throw new Error(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, test, vi } from 'vitest';
|
||||
import Versioning from './versioning';
|
||||
import UnityVersioning from './unity-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>';
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
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.
|
||||
});
|
||||
|
||||
@@ -25,20 +26,20 @@ describe('BuildParameters', () => {
|
||||
});
|
||||
|
||||
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 expect(Versioning.determineBuildVersion).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
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();
|
||||
expect(UnityVersioning.determineUnityVersion).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('returns the android version code with provided input', async () => {
|
||||
const mockValue = '42';
|
||||
jest.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue);
|
||||
vi.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidVersionCode: mockValue }),
|
||||
);
|
||||
@@ -46,49 +47,59 @@ describe('BuildParameters', () => {
|
||||
|
||||
it('returns the android version code from version by default', async () => {
|
||||
const mockValue = '';
|
||||
jest.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue);
|
||||
vi.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidVersionCode: '1003037' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('determines the android sdk manager parameters only once', async () => {
|
||||
jest.spyOn(AndroidVersioning, 'determineSdkManagerParameters').mockImplementation(() => 'platforms;android-30');
|
||||
vi.spyOn(AndroidVersioning, 'determineSdkManagerParameters').mockImplementation(
|
||||
() => 'platforms;android-30',
|
||||
);
|
||||
await BuildParameters.create();
|
||||
expect(AndroidVersioning.determineSdkManagerParameters).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('returns the targetPlatform', async () => {
|
||||
const mockValue = 'somePlatform';
|
||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ targetPlatform: mockValue }));
|
||||
vi.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ targetPlatform: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the project path', async () => {
|
||||
const mockValue = 'path/to/project';
|
||||
jest.spyOn(UnityVersioning, 'determineUnityVersion').mockImplementation(() => '2019.2.11f1');
|
||||
jest.spyOn(Input, 'projectPath', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ projectPath: mockValue }));
|
||||
vi.spyOn(UnityVersioning, 'determineUnityVersion').mockImplementation(() => '2019.2.11f1');
|
||||
vi.spyOn(Input, 'projectPath', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ projectPath: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the build profile', async () => {
|
||||
const mockValue = 'path/to/build_profile.asset';
|
||||
jest.spyOn(Input, 'buildProfile', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildProfile: mockValue }));
|
||||
vi.spyOn(Input, 'buildProfile', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ buildProfile: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the build name', async () => {
|
||||
const mockValue = 'someBuildName';
|
||||
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildName: mockValue }));
|
||||
vi.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ buildName: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the build path', async () => {
|
||||
const mockPath = 'somePath';
|
||||
const mockPlatform = 'somePlatform';
|
||||
const expectedBuildPath = `${mockPath}/${mockPlatform}`;
|
||||
jest.spyOn(Input, 'buildsPath', 'get').mockReturnValue(mockPath);
|
||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform);
|
||||
vi.spyOn(Input, 'buildsPath', 'get').mockReturnValue(mockPath);
|
||||
vi.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ buildPath: expectedBuildPath }),
|
||||
);
|
||||
@@ -98,24 +109,36 @@ describe('BuildParameters', () => {
|
||||
const mockValue = 'someBuildName';
|
||||
const mockPlatform = 'somePlatform';
|
||||
|
||||
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
|
||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildFile: mockValue }));
|
||||
vi.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
|
||||
vi.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ buildFile: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
test.each`
|
||||
targetPlatform | expectedExtension | androidExportType
|
||||
${Platform.types.Android} | ${'.apk'} | ${'androidPackage'}
|
||||
${Platform.types.Android} | ${'.aab'} | ${'androidAppBundle'}
|
||||
${Platform.types.Android} | ${''} | ${'androidStudioProject'}
|
||||
${Platform.types.StandaloneWindows} | ${'.exe'} | ${'n/a'}
|
||||
${Platform.types.StandaloneWindows64} | ${'.exe'} | ${'n/a'}
|
||||
targetPlatform | expectedExtension | androidExportType | linux64RemoveExecutableExtension
|
||||
${Platform.types.Android} | ${'.apk'} | ${'androidPackage'} | ${false}
|
||||
${Platform.types.Android} | ${'.aab'} | ${'androidAppBundle'} | ${true}
|
||||
${Platform.types.Android} | ${''} | ${'androidStudioProject'} | ${false}
|
||||
${Platform.types.StandaloneWindows} | ${'.exe'} | ${'n/a'} | ${true}
|
||||
${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',
|
||||
async ({ targetPlatform, expectedExtension, androidExportType }) => {
|
||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
|
||||
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
|
||||
jest.spyOn(Input, 'androidExportType', 'get').mockReturnValue(androidExportType);
|
||||
'appends $expectedExtension for $targetPlatform with linux64RemoveExecutableExtension=$linux64RemoveExecutableExtension',
|
||||
async ({
|
||||
targetPlatform,
|
||||
expectedExtension,
|
||||
androidExportType,
|
||||
linux64RemoveExecutableExtension,
|
||||
}) => {
|
||||
vi.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
|
||||
vi.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
|
||||
vi.spyOn(Input, 'androidExportType', 'get').mockReturnValue(androidExportType);
|
||||
vi.spyOn(Input, 'linux64RemoveExecutableExtension', 'get').mockReturnValue(
|
||||
linux64RemoveExecutableExtension,
|
||||
);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ buildFile: `${targetPlatform}${expectedExtension}` }),
|
||||
);
|
||||
@@ -132,22 +155,26 @@ describe('BuildParameters', () => {
|
||||
`(
|
||||
'androidSymbolType is set to $androidSymbolType when targetPlatform is $targetPlatform and input targetSymbolType is $androidSymbolType',
|
||||
async ({ targetPlatform, androidSymbolType }) => {
|
||||
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
|
||||
jest.spyOn(Input, 'androidSymbolType', 'get').mockReturnValue(androidSymbolType);
|
||||
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidSymbolType }));
|
||||
vi.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
|
||||
vi.spyOn(Input, 'androidSymbolType', 'get').mockReturnValue(androidSymbolType);
|
||||
vi.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidSymbolType }),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('returns the build method', async () => {
|
||||
const mockValue = 'Namespace.ClassName.BuildMethod';
|
||||
jest.spyOn(Input, 'buildMethod', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildMethod: mockValue }));
|
||||
vi.spyOn(Input, 'buildMethod', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ buildMethod: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the android keystore name', async () => {
|
||||
const mockValue = 'keystore.keystore';
|
||||
jest.spyOn(Input, 'androidKeystoreName', 'get').mockReturnValue(mockValue);
|
||||
vi.spyOn(Input, 'androidKeystoreName', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidKeystoreName: mockValue }),
|
||||
);
|
||||
@@ -155,7 +182,7 @@ describe('BuildParameters', () => {
|
||||
|
||||
it('returns the android keystore base64-encoded content', async () => {
|
||||
const mockValue = 'secret';
|
||||
jest.spyOn(Input, 'androidKeystoreBase64', 'get').mockReturnValue(mockValue);
|
||||
vi.spyOn(Input, 'androidKeystoreBase64', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidKeystoreBase64: mockValue }),
|
||||
);
|
||||
@@ -163,7 +190,7 @@ describe('BuildParameters', () => {
|
||||
|
||||
it('returns the android keystore pass', async () => {
|
||||
const mockValue = 'secret';
|
||||
jest.spyOn(Input, 'androidKeystorePass', 'get').mockReturnValue(mockValue);
|
||||
vi.spyOn(Input, 'androidKeystorePass', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidKeystorePass: mockValue }),
|
||||
);
|
||||
@@ -171,7 +198,7 @@ describe('BuildParameters', () => {
|
||||
|
||||
it('returns the android keyalias name', async () => {
|
||||
const mockValue = 'secret';
|
||||
jest.spyOn(Input, 'androidKeyaliasName', 'get').mockReturnValue(mockValue);
|
||||
vi.spyOn(Input, 'androidKeyaliasName', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidKeyaliasName: mockValue }),
|
||||
);
|
||||
@@ -179,7 +206,7 @@ describe('BuildParameters', () => {
|
||||
|
||||
it('returns the android keyalias pass', async () => {
|
||||
const mockValue = 'secret';
|
||||
jest.spyOn(Input, 'androidKeyaliasPass', 'get').mockReturnValue(mockValue);
|
||||
vi.spyOn(Input, 'androidKeyaliasPass', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidKeyaliasPass: mockValue }),
|
||||
);
|
||||
@@ -187,7 +214,7 @@ describe('BuildParameters', () => {
|
||||
|
||||
it('returns the android target sdk version', async () => {
|
||||
const mockValue = 'AndroidApiLevelAuto';
|
||||
jest.spyOn(Input, 'androidTargetSdkVersion', 'get').mockReturnValue(mockValue);
|
||||
vi.spyOn(Input, 'androidTargetSdkVersion', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ androidTargetSdkVersion: mockValue }),
|
||||
);
|
||||
@@ -195,12 +222,20 @@ describe('BuildParameters', () => {
|
||||
|
||||
it('returns the unity licensing server address', async () => {
|
||||
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(
|
||||
expect.objectContaining({ unityLicensingServer: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the unity licensing toolset', async () => {
|
||||
const mockValue = 'LicenseServer_1234567890_3';
|
||||
vi.spyOn(Input, 'unityLicensingToolset', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ unityLicensingToolset: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error when no unity license provider provided', async () => {
|
||||
delete process.env.UNITY_LICENSE; // Need to delete this as it is set for every test currently
|
||||
await expect(BuildParameters.create()).rejects.toThrowError();
|
||||
@@ -210,14 +245,25 @@ describe('BuildParameters', () => {
|
||||
const mockValue = '123';
|
||||
delete process.env.UNITY_LICENSE; // Need to delete this as it is set for every test currently
|
||||
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;
|
||||
});
|
||||
|
||||
it('returns the custom parameters', async () => {
|
||||
const mockValue = '-profile SomeProfile -someBoolean -someValue exampleValue';
|
||||
jest.spyOn(Input, 'customParameters', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ customParameters: mockValue }));
|
||||
vi.spyOn(Input, 'customParameters', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ customParameters: mockValue }),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([true, false])('returns the flag for useHostNetwork when %s', async (mockValue) => {
|
||||
vi.spyOn(Input, 'useHostNetwork', 'get').mockReturnValue(mockValue);
|
||||
await expect(BuildParameters.create()).resolves.toEqual(
|
||||
expect.objectContaining({ useHostNetwork: mockValue }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+44
-296
@@ -6,7 +6,7 @@ import UnityVersioning from './unity-versioning';
|
||||
import Versioning from './versioning';
|
||||
import { GitRepoReader } from './input-readers/git-repo';
|
||||
import { GithubCliReader } from './input-readers/github-cli';
|
||||
import { Cli } from './cli/cli';
|
||||
import { PluginOptions } from './plugin-options';
|
||||
import GitHub from './github';
|
||||
import * as core from '@actions/core';
|
||||
|
||||
@@ -18,6 +18,7 @@ class BuildParameters {
|
||||
public customImage!: string;
|
||||
public unitySerial!: string;
|
||||
public unityLicensingServer!: string;
|
||||
public unityLicensingToolset!: string;
|
||||
public skipActivation!: string;
|
||||
public runnerTempPath!: string;
|
||||
public targetPlatform!: string;
|
||||
@@ -47,172 +48,49 @@ class BuildParameters {
|
||||
public containerRegistryImageVersion!: string;
|
||||
|
||||
public customParameters!: string;
|
||||
public useHostNetwork!: boolean;
|
||||
public sshAgent!: string;
|
||||
public sshPublicKeysDirectoryPath!: string;
|
||||
public providerStrategy!: string;
|
||||
public fallbackProviderStrategy!: string;
|
||||
public runnerCheckEnabled!: boolean;
|
||||
public runnerCheckLabels!: string[];
|
||||
public runnerCheckMinAvailable!: number;
|
||||
public retryOnFallback!: boolean;
|
||||
public providerInitTimeout!: number;
|
||||
public gitAuthMode!: 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 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 branch!: string;
|
||||
public githubRepo!: string;
|
||||
public orchestratorRepoName!: string;
|
||||
public cloneDepth!: number;
|
||||
public gitSha!: string;
|
||||
public logId!: string;
|
||||
public buildGuid!: string;
|
||||
public orchestratorBranch!: string;
|
||||
public orchestratorDebug!: boolean | undefined;
|
||||
public buildPlatform!: string | undefined;
|
||||
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 unityHubVersionOnMac!: string;
|
||||
public dockerWorkspacePath!: string;
|
||||
public submoduleProfilePath!: string;
|
||||
public submoduleVariantPath!: string;
|
||||
public submoduleToken!: string;
|
||||
public localCacheEnabled!: boolean;
|
||||
public localCacheRoot!: string;
|
||||
public localCacheLibrary!: boolean;
|
||||
public localCacheLfs!: boolean;
|
||||
public childWorkspacesEnabled!: boolean;
|
||||
public childWorkspaceName!: string;
|
||||
public childWorkspaceCacheRoot!: string;
|
||||
public childWorkspacePreserveGit!: boolean;
|
||||
public childWorkspaceSeparateLibrary!: boolean;
|
||||
public lfsTransferAgent!: string;
|
||||
public lfsTransferAgentArgs!: string;
|
||||
public lfsStoragePaths!: string;
|
||||
public gitHooksEnabled!: boolean;
|
||||
public gitHooksSkipList!: string;
|
||||
public gitHooksRunBeforeBuild!: string;
|
||||
public providerExecutable!: string;
|
||||
|
||||
// GCP Cloud Run (Experimental)
|
||||
public gcpProject!: string;
|
||||
public gcpRegion!: string;
|
||||
public gcpStorageType!: string;
|
||||
public gcpBucket!: string;
|
||||
public gcpFilestoreIp!: string;
|
||||
public gcpFilestoreShare!: string;
|
||||
public gcpMachineType!: string;
|
||||
public gcpDiskSizeGb!: string;
|
||||
public gcpServiceAccount!: string;
|
||||
public gcpVpcConnector!: string;
|
||||
|
||||
// Azure Container Instances (Experimental)
|
||||
public azureResourceGroup!: string;
|
||||
public azureLocation!: string;
|
||||
public azureStorageType!: string;
|
||||
public azureStorageAccount!: string;
|
||||
public azureBlobContainer!: string;
|
||||
public azureFileShareName!: string;
|
||||
public azureSubscriptionId!: string;
|
||||
public azureCpu!: string;
|
||||
public azureMemoryGb!: string;
|
||||
public azureDiskSizeGb!: string;
|
||||
public azureSubnetId!: string;
|
||||
|
||||
// Remote PowerShell provider
|
||||
public remotePowershellHost!: string;
|
||||
public remotePowershellCredential!: string;
|
||||
public remotePowershellTransport!: string;
|
||||
|
||||
// GitHub Actions provider
|
||||
public githubActionsRepo!: string;
|
||||
public githubActionsWorkflow!: string;
|
||||
public githubActionsToken!: string;
|
||||
public githubActionsRef!: string;
|
||||
|
||||
// GitLab CI provider
|
||||
public gitlabProjectId!: string;
|
||||
public gitlabTriggerToken!: string;
|
||||
public gitlabApiUrl!: string;
|
||||
public gitlabRef!: string;
|
||||
|
||||
// Ansible provider
|
||||
public ansibleInventory!: string;
|
||||
public ansiblePlaybook!: string;
|
||||
public ansibleExtraVars!: string;
|
||||
public ansibleVaultPassword!: string;
|
||||
public gitIntegrityCheck!: boolean;
|
||||
public gitAutoRecover!: boolean;
|
||||
public cleanReservedFilenames!: boolean;
|
||||
public buildArchiveEnabled!: boolean;
|
||||
public buildArchivePath!: string;
|
||||
public buildArchiveRetention!: number;
|
||||
|
||||
public testSuitePath!: string;
|
||||
public testSuiteEvent!: string;
|
||||
public testTaxonomyPath!: string;
|
||||
public testResultFormat!: string;
|
||||
public testResultPath!: string;
|
||||
public hotRunnerEnabled!: boolean;
|
||||
public hotRunnerTransport!: 'websocket' | 'grpc' | 'named-pipe';
|
||||
public hotRunnerHost!: string;
|
||||
public hotRunnerPort!: number;
|
||||
public hotRunnerHealthInterval!: number;
|
||||
public hotRunnerMaxIdle!: number;
|
||||
public hotRunnerFallbackToCold!: boolean;
|
||||
public artifactOutputTypes!: string;
|
||||
public artifactUploadTarget!: string;
|
||||
public artifactUploadPath!: string;
|
||||
public artifactCompression!: string;
|
||||
public artifactRetentionDays!: string;
|
||||
public artifactCustomTypes!: string;
|
||||
public syncStrategy!: string;
|
||||
public syncInputRef!: string;
|
||||
public syncStorageRemote!: string;
|
||||
public syncRevertAfter!: boolean;
|
||||
public syncStatePath!: string;
|
||||
|
||||
static async create(): Promise<BuildParameters> {
|
||||
const buildFile = this.parseBuildFile(Input.buildName, Input.targetPlatform, Input.androidExportType);
|
||||
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 buildFile = this.parseBuildFile(
|
||||
Input.buildName,
|
||||
Input.targetPlatform,
|
||||
Input.androidExportType,
|
||||
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;
|
||||
if (Platform.isAndroid(Input.targetPlatform)) {
|
||||
@@ -251,11 +129,15 @@ class BuildParameters {
|
||||
core.setSecret(`${unitySerial.slice(0, -4)}XXXX`);
|
||||
}
|
||||
|
||||
const providerStrategy =
|
||||
Input.getInput('providerStrategy') || (PluginOptions.isPluginMode ? 'aws' : 'local');
|
||||
|
||||
return {
|
||||
editorVersion,
|
||||
customImage: Input.customImage,
|
||||
unitySerial,
|
||||
unityLicensingServer: Input.unityLicensingServer,
|
||||
unityLicensingToolset: Input.unityLicensingToolset,
|
||||
skipActivation: Input.skipActivation,
|
||||
runnerTempPath: Input.runnerTempPath,
|
||||
targetPlatform: Input.targetPlatform,
|
||||
@@ -279,6 +161,7 @@ class BuildParameters {
|
||||
androidExportType: Input.androidExportType,
|
||||
androidSymbolType: androidSymbolExportType,
|
||||
customParameters: Input.customParameters,
|
||||
useHostNetwork: Input.useHostNetwork,
|
||||
sshAgent: Input.sshAgent,
|
||||
sshPublicKeysDirectoryPath: Input.sshPublicKeysDirectoryPath,
|
||||
gitPrivateToken: Input.gitPrivateToken ?? (await GithubCliReader.GetGitHubAuthToken()),
|
||||
@@ -289,170 +172,31 @@ class BuildParameters {
|
||||
dockerIsolationMode: Input.dockerIsolationMode,
|
||||
containerRegistryRepository: Input.containerRegistryRepository,
|
||||
containerRegistryImageVersion: Input.containerRegistryImageVersion,
|
||||
providerStrategy: Input.getInput('providerStrategy') || (Cli.isCliMode ? 'aws' : 'local'),
|
||||
fallbackProviderStrategy: Input.getInput('fallbackProviderStrategy') || '',
|
||||
runnerCheckEnabled: Input.getInput('runnerCheckEnabled') === 'true',
|
||||
runnerCheckLabels: (Input.getInput('runnerCheckLabels') || '')
|
||||
.split(',')
|
||||
.map((l: string) => l.trim())
|
||||
.filter(Boolean),
|
||||
runnerCheckMinAvailable: Number(Input.getInput('runnerCheckMinAvailable')) || 1,
|
||||
retryOnFallback: Input.getInput('retryOnFallback') === 'true',
|
||||
providerInitTimeout: Number(Input.getInput('providerInitTimeout')) || 0,
|
||||
gitAuthMode: Input.getInput('gitAuthMode') || 'header',
|
||||
buildPlatform:
|
||||
Input.getInput('buildPlatform') ||
|
||||
((Input.getInput('providerStrategy') || 'local') !== 'local' ? 'linux' : process.platform),
|
||||
kubeConfig: Input.getInput('kubeConfig') || '',
|
||||
containerMemory: Input.getInput('containerMemory') || '3072',
|
||||
containerCpu: Input.getInput('containerCpu') || '1024',
|
||||
containerNamespace: Input.getInput('containerNamespace') || 'default',
|
||||
kubeVolumeSize: Input.getInput('kubeVolumeSize') || '25Gi',
|
||||
kubeVolume: Input.getInput('kubeVolume') || '',
|
||||
postBuildContainerHooks: Input.getInput('postBuildContainerHooks') || '',
|
||||
preBuildContainerHooks: Input.getInput('preBuildContainerHooks') || '',
|
||||
customJob: Input.getInput('customJob') || '',
|
||||
providerStrategy,
|
||||
buildPlatform: providerStrategy !== 'local' ? 'linux' : process.platform,
|
||||
runNumber: Input.runNumber,
|
||||
branch: Input.branch.replace('/head', '') || (await GitRepoReader.GetBranch()),
|
||||
orchestratorBranch: (Input.getInput('orchestratorBranch') || 'main').split('/').reverse()[0],
|
||||
orchestratorDebug:
|
||||
Input.getInput('orchestratorDebug') === 'true' || Input.getInput('orchestratorTests') === 'true',
|
||||
githubRepo:
|
||||
(Input.githubRepo ?? (await GitRepoReader.GetRemote())) ||
|
||||
Input.getInput('orchestratorRepoName') ||
|
||||
'game-ci/unity-builder',
|
||||
orchestratorRepoName: Input.getInput('orchestratorRepoName') || 'game-ci/unity-builder',
|
||||
cloneDepth: Number.parseInt(Input.getInput('cloneDepth') || '50'),
|
||||
isCliMode: Cli.isCliMode,
|
||||
awsStackName: Input.getInput('awsStackName') || 'game-ci',
|
||||
awsEndpoint: Input.getInput('awsEndpoint'),
|
||||
awsCloudFormationEndpoint: Input.getInput('awsCloudFormationEndpoint') || Input.getInput('awsEndpoint'),
|
||||
awsEcsEndpoint: Input.getInput('awsEcsEndpoint') || Input.getInput('awsEndpoint'),
|
||||
awsKinesisEndpoint: Input.getInput('awsKinesisEndpoint') || Input.getInput('awsEndpoint'),
|
||||
awsCloudWatchLogsEndpoint: Input.getInput('awsCloudWatchLogsEndpoint') || Input.getInput('awsEndpoint'),
|
||||
awsS3Endpoint: Input.getInput('awsS3Endpoint') || Input.getInput('awsEndpoint'),
|
||||
storageProvider: Input.getInput('storageProvider') || 's3',
|
||||
rcloneRemote: Input.getInput('rcloneRemote') || '',
|
||||
(Input.githubRepo ?? (await GitRepoReader.GetRemote())) || 'game-ci/unity-builder',
|
||||
gitSha: Input.gitSha,
|
||||
logId: customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 9)(),
|
||||
buildGuid: `${Input.runNumber}-${Input.targetPlatform.toLowerCase().replace('standalone', '')}-${customAlphabet(
|
||||
'0123456789abcdefghijklmnopqrstuvwxyz',
|
||||
4,
|
||||
)()}`,
|
||||
commandHooks: Input.getInput('commandHooks') || '',
|
||||
inputPullCommand: Input.getInput('inputPullCommand') || '',
|
||||
pullInputList: (Input.getInput('pullInputList') || '').split(',').filter(Boolean),
|
||||
kubeStorageClass: Input.getInput('kubeStorageClass') || '',
|
||||
gcpProject: Input.gcpProject,
|
||||
gcpRegion: Input.gcpRegion,
|
||||
gcpStorageType: Input.gcpStorageType,
|
||||
gcpBucket: Input.gcpBucket,
|
||||
gcpFilestoreIp: Input.gcpFilestoreIp,
|
||||
gcpFilestoreShare: Input.gcpFilestoreShare,
|
||||
gcpMachineType: Input.gcpMachineType,
|
||||
gcpDiskSizeGb: Input.gcpDiskSizeGb,
|
||||
gcpServiceAccount: Input.gcpServiceAccount,
|
||||
gcpVpcConnector: Input.gcpVpcConnector,
|
||||
azureResourceGroup: Input.azureResourceGroup,
|
||||
azureLocation: Input.azureLocation,
|
||||
azureStorageType: Input.azureStorageType,
|
||||
azureStorageAccount: Input.azureStorageAccount,
|
||||
azureBlobContainer: Input.azureBlobContainer,
|
||||
azureFileShareName: Input.azureFileShareName,
|
||||
azureSubscriptionId: Input.azureSubscriptionId,
|
||||
azureCpu: Input.azureCpu,
|
||||
azureMemoryGb: Input.azureMemoryGb,
|
||||
azureDiskSizeGb: Input.azureDiskSizeGb,
|
||||
azureSubnetId: Input.azureSubnetId,
|
||||
cacheKey: Input.getInput('cacheKey') || Input.branch,
|
||||
maxRetainedWorkspaces: Number.parseInt(Input.getInput('maxRetainedWorkspaces') || '0'),
|
||||
useLargePackages: Input.getInput('useLargePackages') === 'true',
|
||||
useCompressionStrategy: Input.getInput('useCompressionStrategy') === 'true',
|
||||
garbageMaxAge: Number(Input.getInput('garbageMaxAge')) || 24,
|
||||
githubChecks: Input.getInput('githubChecks') === 'true',
|
||||
asyncWorkflow: Input.getInput('asyncOrchestrator') === 'true',
|
||||
githubCheckId: Input.getInput('githubCheckId') || '',
|
||||
finalHooks: (Input.getInput('finalHooks') || '').split(',').filter(Boolean),
|
||||
skipLfs: Input.getInput('skipLfs') === 'true',
|
||||
skipCache: Input.getInput('skipCache') === 'true',
|
||||
isCliMode: PluginOptions.isPluginMode,
|
||||
cacheUnityInstallationOnMac: Input.cacheUnityInstallationOnMac,
|
||||
unityHubVersionOnMac: Input.unityHubVersionOnMac,
|
||||
dockerWorkspacePath: Input.dockerWorkspacePath,
|
||||
submoduleProfilePath: Input.submoduleProfilePath,
|
||||
submoduleVariantPath: Input.submoduleVariantPath,
|
||||
submoduleToken: Input.submoduleToken,
|
||||
localCacheEnabled: Input.localCacheEnabled,
|
||||
localCacheRoot: Input.localCacheRoot,
|
||||
localCacheLibrary: Input.localCacheLibrary,
|
||||
localCacheLfs: Input.localCacheLfs,
|
||||
childWorkspacesEnabled: Input.childWorkspacesEnabled,
|
||||
childWorkspaceName: Input.childWorkspaceName,
|
||||
childWorkspaceCacheRoot: Input.childWorkspaceCacheRoot,
|
||||
childWorkspacePreserveGit: Input.childWorkspacePreserveGit,
|
||||
childWorkspaceSeparateLibrary: Input.childWorkspaceSeparateLibrary,
|
||||
lfsTransferAgent: Input.lfsTransferAgent,
|
||||
lfsTransferAgentArgs: Input.lfsTransferAgentArgs,
|
||||
lfsStoragePaths: Input.lfsStoragePaths,
|
||||
gitHooksEnabled: Input.gitHooksEnabled,
|
||||
gitHooksSkipList: Input.gitHooksSkipList,
|
||||
gitHooksRunBeforeBuild: Input.gitHooksRunBeforeBuild,
|
||||
providerExecutable: Input.providerExecutable,
|
||||
|
||||
// Remote PowerShell provider
|
||||
remotePowershellHost: Input.remotePowershellHost,
|
||||
remotePowershellCredential: Input.remotePowershellCredential,
|
||||
remotePowershellTransport: Input.remotePowershellTransport,
|
||||
|
||||
// GitHub Actions provider
|
||||
githubActionsRepo: Input.githubActionsRepo,
|
||||
githubActionsWorkflow: Input.githubActionsWorkflow,
|
||||
githubActionsToken: Input.githubActionsToken,
|
||||
githubActionsRef: Input.githubActionsRef,
|
||||
|
||||
// GitLab CI provider
|
||||
gitlabProjectId: Input.gitlabProjectId,
|
||||
gitlabTriggerToken: Input.gitlabTriggerToken,
|
||||
gitlabApiUrl: Input.gitlabApiUrl,
|
||||
gitlabRef: Input.gitlabRef,
|
||||
|
||||
// Ansible provider
|
||||
ansibleInventory: Input.ansibleInventory,
|
||||
ansiblePlaybook: Input.ansiblePlaybook,
|
||||
ansibleExtraVars: Input.ansibleExtraVars,
|
||||
ansibleVaultPassword: Input.ansibleVaultPassword,
|
||||
gitIntegrityCheck: Input.gitIntegrityCheck,
|
||||
gitAutoRecover: Input.gitAutoRecover,
|
||||
cleanReservedFilenames: Input.cleanReservedFilenames,
|
||||
buildArchiveEnabled: Input.buildArchiveEnabled,
|
||||
buildArchivePath: Input.buildArchivePath,
|
||||
buildArchiveRetention: Input.buildArchiveRetention,
|
||||
testSuitePath: Input.testSuitePath,
|
||||
testSuiteEvent: Input.testSuiteEvent,
|
||||
testTaxonomyPath: Input.testTaxonomyPath,
|
||||
testResultFormat: Input.testResultFormat,
|
||||
testResultPath: Input.testResultPath,
|
||||
hotRunnerEnabled: Input.hotRunnerEnabled,
|
||||
hotRunnerTransport: Input.hotRunnerTransport,
|
||||
hotRunnerHost: Input.hotRunnerHost,
|
||||
hotRunnerPort: Input.hotRunnerPort,
|
||||
hotRunnerHealthInterval: Input.hotRunnerHealthInterval,
|
||||
hotRunnerMaxIdle: Input.hotRunnerMaxIdle,
|
||||
hotRunnerFallbackToCold: Input.hotRunnerFallbackToCold,
|
||||
artifactOutputTypes: Input.artifactOutputTypes,
|
||||
artifactUploadTarget: Input.artifactUploadTarget,
|
||||
artifactUploadPath: Input.artifactUploadPath,
|
||||
artifactCompression: Input.artifactCompression,
|
||||
artifactRetentionDays: Input.artifactRetentionDays,
|
||||
artifactCustomTypes: Input.artifactCustomTypes,
|
||||
syncStrategy: Input.syncStrategy,
|
||||
syncInputRef: Input.syncInputRef,
|
||||
syncStorageRemote: Input.syncStorageRemote,
|
||||
syncRevertAfter: Input.syncRevertAfter,
|
||||
syncStatePath: Input.syncStatePath,
|
||||
};
|
||||
}
|
||||
|
||||
static parseBuildFile(filename: string, platform: string, androidExportType: string): string {
|
||||
static parseBuildFile(
|
||||
filename: string,
|
||||
platform: string,
|
||||
androidExportType: string,
|
||||
linux64RemoveExecutableExtension: boolean,
|
||||
): string {
|
||||
if (Platform.isWindows(platform)) {
|
||||
return `${filename}.exe`;
|
||||
}
|
||||
@@ -472,6 +216,10 @@ class BuildParameters {
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === Platform.types.StandaloneLinux64 && !linux64RemoveExecutableExtension) {
|
||||
return `${filename}.x86_64`;
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, vi } from 'vitest';
|
||||
import Cache from './cache';
|
||||
|
||||
jest.mock('./input');
|
||||
vi.mock('./input');
|
||||
|
||||
describe('Cache', () => {
|
||||
describe('Verification', () => {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
export class CliFunctionsRepository {
|
||||
private static targets: any[] = [];
|
||||
public static PushCliFunction(
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor,
|
||||
key: string,
|
||||
description: string,
|
||||
) {
|
||||
CliFunctionsRepository.targets.push({
|
||||
target,
|
||||
propertyKey,
|
||||
descriptor,
|
||||
key,
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
public static GetCliFunctions(key: any) {
|
||||
const results = CliFunctionsRepository.targets.find((x) => x.key === key);
|
||||
if (results === undefined || results.length === 0) {
|
||||
throw new Error(`no CLI mode found for ${key}`);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public static GetAllCliModes() {
|
||||
return CliFunctionsRepository.targets.map((x) => {
|
||||
return {
|
||||
key: x.key,
|
||||
description: x.description,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
public static PushCliFunctionSource(cliFunction: any) {}
|
||||
}
|
||||
|
||||
export function CliFunction(key: string, description: string) {
|
||||
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||
CliFunctionsRepository.PushCliFunction(target, propertyKey, descriptor, key, description);
|
||||
};
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { Command } from 'commander-ts';
|
||||
import { Input } from '..';
|
||||
import * as core from '@actions/core';
|
||||
import { ActionYamlReader } from '../input-readers/action-yaml';
|
||||
import { CliFunction, CliFunctionsRepository } from './cli-functions-repository';
|
||||
import { OptionValues } from 'commander';
|
||||
import { InputKey } from '../input';
|
||||
|
||||
export class Cli {
|
||||
public static options: OptionValues | undefined;
|
||||
static get isCliMode() {
|
||||
return Cli.options !== undefined && Cli.options.mode !== undefined && Cli.options.mode !== '';
|
||||
}
|
||||
public static query(key: string, alternativeKey: string) {
|
||||
if (Cli.options && Cli.options[key] !== undefined) {
|
||||
return Cli.options[key];
|
||||
}
|
||||
if (Cli.options && alternativeKey && Cli.options[alternativeKey] !== undefined) {
|
||||
return Cli.options[alternativeKey];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public static InitCliMode() {
|
||||
const program = new Command();
|
||||
program.version('0.0.1');
|
||||
|
||||
const actionYamlReader: ActionYamlReader = new ActionYamlReader();
|
||||
const properties = Object.getOwnPropertyNames(Input).filter(
|
||||
(p) => p !== 'length' && p !== 'prototype' && p !== 'name',
|
||||
);
|
||||
for (const element of properties) {
|
||||
program.option(`--${element} <${element}>`, actionYamlReader.GetActionYamlValue(element));
|
||||
}
|
||||
program.option(
|
||||
'-m, --mode <mode>',
|
||||
CliFunctionsRepository.GetAllCliModes()
|
||||
.map((x) => `${x.key} (${x.description})`)
|
||||
.join(` | `),
|
||||
);
|
||||
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('--cachePushTo <cachePushTo>', 'cache push to caching folder');
|
||||
program.option('--artifactName <artifactName>', 'caching artifact name');
|
||||
program.option('--select <select>', 'select a particular resource');
|
||||
program.option('--logFile <logFile>', 'output to log file (log stream only)');
|
||||
program.option('--profilePath <profilePath>', 'path to submodule profile YAML');
|
||||
program.option('--variantPath <variantPath>', 'path to submodule variant YAML');
|
||||
program.option('--agentPath <agentPath>', 'path to custom LFS transfer agent');
|
||||
program.option('--agentArgs <agentArgs>', 'arguments for custom LFS transfer agent');
|
||||
program.option('--storagePaths <storagePaths>', 'semicolon-separated storage paths for LFS agent');
|
||||
program.parse(process.argv);
|
||||
Cli.options = program.opts();
|
||||
|
||||
return Cli.isCliMode;
|
||||
}
|
||||
|
||||
static async RunCli(): Promise<void> {
|
||||
const results = CliFunctionsRepository.GetCliFunctions(Cli.options?.mode);
|
||||
if (!results) {
|
||||
throw new Error(
|
||||
`Unknown CLI mode: ${Cli.options?.mode}. Orchestrator CLI features require @game-ci/orchestrator.`,
|
||||
);
|
||||
}
|
||||
core.info(`Entrypoint: ${results.key}`);
|
||||
Cli.options!.versioning = 'None';
|
||||
|
||||
return await results.target[results.propertyKey](Cli.options);
|
||||
}
|
||||
|
||||
@CliFunction(`print-input`, `prints all input`)
|
||||
private static logInput() {
|
||||
core.info(`\n`);
|
||||
core.info(`INPUT:`);
|
||||
const properties = Object.getOwnPropertyNames(Input).filter(
|
||||
(p) => p !== 'length' && p !== 'prototype' && p !== 'name',
|
||||
);
|
||||
for (const element of properties) {
|
||||
if (
|
||||
element in Input &&
|
||||
Input[element as InputKey] !== undefined &&
|
||||
Input[element as InputKey] !== '' &&
|
||||
typeof Input[element as InputKey] !== `function` &&
|
||||
element !== 'length' &&
|
||||
element !== 'cliOptions' &&
|
||||
element !== 'prototype'
|
||||
) {
|
||||
core.info(`${element} ${Input[element as InputKey]}`);
|
||||
}
|
||||
}
|
||||
core.info(`\n`);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||
import Action from './action';
|
||||
import Docker from './docker';
|
||||
|
||||
|
||||
+9
-1
@@ -17,7 +17,13 @@ class Docker {
|
||||
let runCommand = '';
|
||||
switch (process.platform) {
|
||||
case 'linux':
|
||||
runCommand = this.getLinuxCommand(image, parameters, overrideCommands, additionalVariables, entrypointBash);
|
||||
runCommand = this.getLinuxCommand(
|
||||
image,
|
||||
parameters,
|
||||
overrideCommands,
|
||||
additionalVariables,
|
||||
entrypointBash,
|
||||
);
|
||||
break;
|
||||
case 'win32':
|
||||
runCommand = this.getWindowsCommand(image, parameters);
|
||||
@@ -42,6 +48,7 @@ class Docker {
|
||||
const {
|
||||
workspace,
|
||||
actionFolder,
|
||||
useHostNetwork,
|
||||
runnerTempPath,
|
||||
sshAgent,
|
||||
sshPublicKeysDirectoryPath,
|
||||
@@ -85,6 +92,7 @@ class Docker {
|
||||
: ''
|
||||
} \
|
||||
${sshPublicKeysDirectoryPath ? `--volume ${sshPublicKeysDirectoryPath}:/root/.ssh:ro` : ''} \
|
||||
${useHostNetwork ? '--net=host' : ''} \
|
||||
${entrypointBash ? `--entrypoint ${commandPrefix}` : ``} \
|
||||
${image} \
|
||||
${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';
|
||||
|
||||
describe('CommandExecutionError', () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||
import NotImplementedException from './not-implemented-exception';
|
||||
|
||||
describe('NotImplementedException', () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||
import ValidationError from './validation-error';
|
||||
|
||||
describe('ValidationError', () => {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { DockerParameters, StringKeyValuePair } from './shared-types';
|
||||
|
||||
class ImageEnvironmentFactory {
|
||||
public static getEnvVarString(parameters: DockerParameters, additionalVariables: StringKeyValuePair[] = []) {
|
||||
const environmentVariables = ImageEnvironmentFactory.getEnvironmentVariables(parameters, additionalVariables);
|
||||
public static getEnvVarString(
|
||||
parameters: DockerParameters,
|
||||
additionalVariables: StringKeyValuePair[] = [],
|
||||
) {
|
||||
const environmentVariables = ImageEnvironmentFactory.getEnvironmentVariables(
|
||||
parameters,
|
||||
additionalVariables,
|
||||
);
|
||||
let string = '';
|
||||
for (const p of environmentVariables) {
|
||||
if (p.value === '' || p.value === undefined || p.value === null) {
|
||||
@@ -21,7 +27,10 @@ class ImageEnvironmentFactory {
|
||||
return string;
|
||||
}
|
||||
|
||||
public static getEnvironmentVariables(parameters: DockerParameters, additionalVariables: StringKeyValuePair[] = []) {
|
||||
public static getEnvironmentVariables(
|
||||
parameters: DockerParameters,
|
||||
additionalVariables: StringKeyValuePair[] = [],
|
||||
) {
|
||||
let environmentVariables: StringKeyValuePair[] = [
|
||||
{ name: 'UNITY_EMAIL', value: process.env.UNITY_EMAIL },
|
||||
{ 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';
|
||||
|
||||
describe('ImageTag', () => {
|
||||
@@ -27,15 +28,18 @@ describe('ImageTag', () => {
|
||||
expect(image.builderPlatform).toStrictEqual(testImageParameters.builderPlatform);
|
||||
});
|
||||
|
||||
test.each(['2000.0.0f0', '2011.1.11f1', '6000.0.0f1'])('accepts %p version format', (version) => {
|
||||
expect(
|
||||
() =>
|
||||
new ImageTag({
|
||||
editorVersion: version,
|
||||
targetPlatform: testImageParameters.targetPlatform,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
test.each(['2000.0.0f0', '2011.1.11f1', '6000.0.0f1'])(
|
||||
'accepts %p version format',
|
||||
(version) => {
|
||||
expect(
|
||||
() =>
|
||||
new ImageTag({
|
||||
editorVersion: version,
|
||||
targetPlatform: testImageParameters.targetPlatform,
|
||||
}),
|
||||
).not.toThrow();
|
||||
},
|
||||
);
|
||||
|
||||
test.each(['some version', ''])('throws for incorrect version %p', (editorVersion) => {
|
||||
const { targetPlatform } = testImageParameters;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||
import * as Index from '.';
|
||||
|
||||
interface ExportedModules {
|
||||
|
||||
+13
-1
@@ -10,4 +10,16 @@ import Project from './project';
|
||||
import Unity from './unity';
|
||||
import Versioning from './versioning';
|
||||
|
||||
export { Action, BuildParameters, Cache, Docker, Input, ImageTag, Output, Platform, Project, Unity, Versioning };
|
||||
export {
|
||||
Action,
|
||||
BuildParameters,
|
||||
Cache,
|
||||
Docker,
|
||||
Input,
|
||||
ImageTag,
|
||||
Output,
|
||||
Platform,
|
||||
Project,
|
||||
Unity,
|
||||
Versioning,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, vi } from 'vitest';
|
||||
import { GitRepoReader } from './git-repo';
|
||||
import Input from '../input';
|
||||
|
||||
@@ -9,15 +10,15 @@ describe(`git repo tests`, () => {
|
||||
|
||||
it(`returns valid branch name when using https`, async () => {
|
||||
const mockValue = 'https://github.com/example/example.git';
|
||||
jest.spyOn(GitRepoReader as any, 'runCommand').mockResolvedValue(mockValue);
|
||||
jest.spyOn(Input, 'getInput').mockReturnValue('not-local');
|
||||
vi.spyOn(GitRepoReader as any, 'runCommand').mockResolvedValue(mockValue);
|
||||
vi.spyOn(Input, 'getInput').mockReturnValue('not-local');
|
||||
expect(await GitRepoReader.GetRemote()).toEqual(`example/example`);
|
||||
});
|
||||
|
||||
it(`returns valid branch name when using ssh`, async () => {
|
||||
const mockValue = 'git@github.com:example/example.git';
|
||||
jest.spyOn(GitRepoReader as any, 'runCommand').mockResolvedValue(mockValue);
|
||||
jest.spyOn(Input, 'getInput').mockReturnValue('not-local');
|
||||
vi.spyOn(GitRepoReader as any, 'runCommand').mockResolvedValue(mockValue);
|
||||
vi.spyOn(Input, 'getInput').mockReturnValue('not-local');
|
||||
expect(await GitRepoReader.GetRemote()).toEqual(`example/example`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,9 @@ export class GitRepoReader {
|
||||
return '';
|
||||
}
|
||||
assert(fs.existsSync(`.git`));
|
||||
const value = (await GitRepoReader.runCommand(`cd ${Input.projectPath} && git remote -v`)).replace(/ /g, ``);
|
||||
const value = (
|
||||
await GitRepoReader.runCommand(`cd ${Input.projectPath} && git remote -v`)
|
||||
).replace(/ /g, ``);
|
||||
core.info(`value ${value}`);
|
||||
assert(value.includes('github.com'));
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||
import { GithubCliReader } from './github-cli';
|
||||
import * as core from '@actions/core';
|
||||
|
||||
|
||||
@@ -7,7 +7,14 @@ export function ReadLicense(): string {
|
||||
if ((Input.getInput('providerStrategy') || 'local') === 'local') {
|
||||
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
|
||||
: '';
|
||||
}
|
||||
|
||||
+65
-28
@@ -1,10 +1,11 @@
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, test, vi } from 'vitest';
|
||||
import * as core from '@actions/core';
|
||||
|
||||
import Input from './input';
|
||||
import Platform from './platform';
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Input', () => {
|
||||
@@ -15,7 +16,7 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -27,7 +28,7 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -40,7 +41,7 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -53,7 +54,7 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -66,7 +67,7 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -79,14 +80,14 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('takes special characters as input', () => {
|
||||
const mockValue = '1ßúëld2';
|
||||
jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||
vi.spyOn(core, 'getInput').mockReturnValue(mockValue);
|
||||
expect(Input.buildName).toStrictEqual(mockValue);
|
||||
});
|
||||
});
|
||||
@@ -98,7 +99,7 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -111,7 +112,7 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -123,13 +124,13 @@ describe('Input', () => {
|
||||
});
|
||||
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -141,13 +142,13 @@ describe('Input', () => {
|
||||
});
|
||||
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -160,7 +161,7 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -173,7 +174,7 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -186,7 +187,7 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -203,7 +204,7 @@ describe('Input', () => {
|
||||
${'androidAppBundle'} | ${'androidAppBundle'}
|
||||
${'androidStudioProject'} | ${'androidStudioProject'}
|
||||
`('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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -220,7 +221,7 @@ describe('Input', () => {
|
||||
${'public'} | ${'public'}
|
||||
${'debugging'} | ${'debugging'}
|
||||
`('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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -233,7 +234,7 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -246,7 +247,7 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -259,7 +260,7 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -272,7 +273,7 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -285,7 +286,7 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -298,7 +299,7 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -310,13 +311,13 @@ describe('Input', () => {
|
||||
});
|
||||
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
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(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -329,9 +330,45 @@ describe('Input', () => {
|
||||
|
||||
it('takes input from the users workflow', () => {
|
||||
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(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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+23
-374
@@ -1,6 +1,6 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { Cli } from './cli/cli';
|
||||
import { PluginOptions } from './plugin-options';
|
||||
import Platform from './platform';
|
||||
import GitHub from './github';
|
||||
import os from 'node:os';
|
||||
@@ -14,7 +14,8 @@ export type InputKey = keyof typeof Input;
|
||||
*
|
||||
* 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 {
|
||||
public static getInput(query: string): string | undefined {
|
||||
@@ -27,8 +28,8 @@ class Input {
|
||||
const alternativeQuery = Input.ToEnvVarFormat(query);
|
||||
|
||||
// Query input sources
|
||||
if (Cli.query(query, alternativeQuery)) {
|
||||
return Cli.query(query, alternativeQuery);
|
||||
if (PluginOptions.query(query, alternativeQuery)) {
|
||||
return PluginOptions.query(query, alternativeQuery);
|
||||
}
|
||||
|
||||
if (process.env[query] !== undefined) {
|
||||
@@ -40,17 +41,16 @@ class Input {
|
||||
}
|
||||
}
|
||||
|
||||
static get region(): string {
|
||||
return Input.getInput('region') ?? 'eu-west-2';
|
||||
}
|
||||
|
||||
static get githubRepo(): string | undefined {
|
||||
return Input.getInput('GITHUB_REPOSITORY') ?? Input.getInput('GITHUB_REPO') ?? undefined;
|
||||
}
|
||||
|
||||
static get branch(): string {
|
||||
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')) {
|
||||
return Input.getInput('branch')!;
|
||||
} else {
|
||||
@@ -122,6 +122,10 @@ class Input {
|
||||
return Input.getInput('unityLicensingServer') ?? '';
|
||||
}
|
||||
|
||||
static get unityLicensingToolset(): string {
|
||||
return Input.getInput('unityLicensingToolset') ?? '';
|
||||
}
|
||||
|
||||
static get buildMethod(): string {
|
||||
return Input.getInput('buildMethod') ?? ''; // Processed in docker file
|
||||
}
|
||||
@@ -142,6 +146,12 @@ class Input {
|
||||
return Input.getInput('customParameters') ?? '';
|
||||
}
|
||||
|
||||
static get useHostNetwork(): boolean {
|
||||
const input = Input.getInput('useHostNetwork') ?? false;
|
||||
|
||||
return input === 'true';
|
||||
}
|
||||
|
||||
static get versioningStrategy(): string {
|
||||
return Input.getInput('versioning') ?? 'Semantic';
|
||||
}
|
||||
@@ -236,28 +246,6 @@ class Input {
|
||||
return Input.getInput('dockerWorkspacePath') ?? '/github/workspace';
|
||||
}
|
||||
|
||||
static get syncStrategy(): string {
|
||||
return Input.getInput('syncStrategy') ?? 'full';
|
||||
}
|
||||
|
||||
static get syncInputRef(): string {
|
||||
return Input.getInput('syncInputRef') ?? '';
|
||||
}
|
||||
|
||||
static get syncStorageRemote(): string {
|
||||
return Input.getInput('syncStorageRemote') ?? '';
|
||||
}
|
||||
|
||||
static get syncRevertAfter(): boolean {
|
||||
const input = Input.getInput('syncRevertAfter') ?? 'true';
|
||||
|
||||
return input === 'true';
|
||||
}
|
||||
|
||||
static get syncStatePath(): string {
|
||||
return Input.getInput('syncStatePath') ?? '.game-ci/sync-state.json';
|
||||
}
|
||||
|
||||
static get dockerCpuLimit(): string {
|
||||
return Input.getInput('dockerCpuLimit') ?? os.cpus().length.toString();
|
||||
}
|
||||
@@ -279,7 +267,8 @@ class Input {
|
||||
}
|
||||
|
||||
return (
|
||||
Input.getInput('dockerMemoryLimit') ?? `${Math.floor((os.totalmem() / bytesInMegabyte) * memoryMultiplier)}m`
|
||||
Input.getInput('dockerMemoryLimit') ??
|
||||
`${Math.floor((os.totalmem() / bytesInMegabyte) * memoryMultiplier)}m`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -295,356 +284,16 @@ class Input {
|
||||
return Input.getInput('containerRegistryImageVersion') ?? '3';
|
||||
}
|
||||
|
||||
static get artifactOutputTypes(): string {
|
||||
return Input.getInput('artifactOutputTypes') ?? 'build,logs,test-results';
|
||||
}
|
||||
|
||||
static get artifactUploadTarget(): string {
|
||||
return Input.getInput('artifactUploadTarget') ?? 'github-artifacts';
|
||||
}
|
||||
|
||||
static get artifactUploadPath(): string {
|
||||
return Input.getInput('artifactUploadPath') ?? '';
|
||||
}
|
||||
|
||||
static get artifactCompression(): string {
|
||||
return Input.getInput('artifactCompression') ?? 'gzip';
|
||||
}
|
||||
|
||||
static get artifactRetentionDays(): string {
|
||||
return Input.getInput('artifactRetentionDays') ?? '30';
|
||||
}
|
||||
|
||||
static get artifactCustomTypes(): string {
|
||||
return Input.getInput('artifactCustomTypes') ?? '';
|
||||
}
|
||||
|
||||
static get skipActivation(): string {
|
||||
return Input.getInput('skipActivation')?.toLowerCase() ?? 'false';
|
||||
}
|
||||
|
||||
static get submoduleProfilePath(): string {
|
||||
return Input.getInput('submoduleProfilePath') ?? '';
|
||||
}
|
||||
|
||||
static get submoduleVariantPath(): string {
|
||||
return Input.getInput('submoduleVariantPath') ?? '';
|
||||
}
|
||||
|
||||
static get submoduleToken(): string {
|
||||
return Input.getInput('submoduleToken') ?? '';
|
||||
}
|
||||
|
||||
static get localCacheEnabled(): boolean {
|
||||
return (Input.getInput('localCacheEnabled') ?? 'false') === 'true';
|
||||
}
|
||||
|
||||
static get localCacheRoot(): string {
|
||||
return Input.getInput('localCacheRoot') ?? '';
|
||||
}
|
||||
|
||||
static get localCacheLibrary(): boolean {
|
||||
return (Input.getInput('localCacheLibrary') ?? 'true') === 'true';
|
||||
}
|
||||
|
||||
static get localCacheLfs(): boolean {
|
||||
return (Input.getInput('localCacheLfs') ?? 'false') === 'true';
|
||||
}
|
||||
|
||||
static get childWorkspacesEnabled(): boolean {
|
||||
return (Input.getInput('childWorkspacesEnabled') ?? 'false') === 'true';
|
||||
}
|
||||
|
||||
static get childWorkspaceName(): string {
|
||||
return Input.getInput('childWorkspaceName') ?? '';
|
||||
}
|
||||
|
||||
static get childWorkspaceCacheRoot(): string {
|
||||
return Input.getInput('childWorkspaceCacheRoot') ?? '';
|
||||
}
|
||||
|
||||
static get childWorkspacePreserveGit(): boolean {
|
||||
return (Input.getInput('childWorkspacePreserveGit') ?? 'true') === 'true';
|
||||
}
|
||||
|
||||
static get childWorkspaceSeparateLibrary(): boolean {
|
||||
return (Input.getInput('childWorkspaceSeparateLibrary') ?? 'true') === 'true';
|
||||
}
|
||||
|
||||
static get lfsTransferAgent(): string {
|
||||
return Input.getInput('lfsTransferAgent') ?? '';
|
||||
}
|
||||
|
||||
static get lfsTransferAgentArgs(): string {
|
||||
return Input.getInput('lfsTransferAgentArgs') ?? '';
|
||||
}
|
||||
|
||||
static get lfsStoragePaths(): string {
|
||||
return Input.getInput('lfsStoragePaths') ?? '';
|
||||
}
|
||||
|
||||
static get gitHooksEnabled(): boolean {
|
||||
return (Input.getInput('gitHooksEnabled') ?? 'false') === 'true';
|
||||
}
|
||||
|
||||
static get gitHooksSkipList(): string {
|
||||
return Input.getInput('gitHooksSkipList') ?? '';
|
||||
}
|
||||
|
||||
static get gitHooksRunBeforeBuild(): string {
|
||||
return Input.getInput('gitHooksRunBeforeBuild') ?? '';
|
||||
}
|
||||
|
||||
static get providerExecutable(): string {
|
||||
return Input.getInput('providerExecutable') ?? '';
|
||||
}
|
||||
|
||||
// GCP Cloud Run (Experimental)
|
||||
static get gcpProject(): string {
|
||||
return Input.getInput('gcpProject') ?? '';
|
||||
}
|
||||
|
||||
static get gcpRegion(): string {
|
||||
return Input.getInput('gcpRegion') ?? '';
|
||||
}
|
||||
|
||||
static get gcpStorageType(): string {
|
||||
return Input.getInput('gcpStorageType') ?? 'gcs-fuse';
|
||||
}
|
||||
|
||||
static get gcpBucket(): string {
|
||||
return Input.getInput('gcpBucket') ?? '';
|
||||
}
|
||||
|
||||
static get gcpFilestoreIp(): string {
|
||||
return Input.getInput('gcpFilestoreIp') ?? '';
|
||||
}
|
||||
|
||||
static get gcpFilestoreShare(): string {
|
||||
return Input.getInput('gcpFilestoreShare') ?? '/share1';
|
||||
}
|
||||
|
||||
static get gcpMachineType(): string {
|
||||
return Input.getInput('gcpMachineType') ?? 'e2-standard-4';
|
||||
}
|
||||
|
||||
static get gcpDiskSizeGb(): string {
|
||||
return Input.getInput('gcpDiskSizeGb') ?? '100';
|
||||
}
|
||||
|
||||
static get gcpServiceAccount(): string {
|
||||
return Input.getInput('gcpServiceAccount') ?? '';
|
||||
}
|
||||
|
||||
static get gcpVpcConnector(): string {
|
||||
return Input.getInput('gcpVpcConnector') ?? '';
|
||||
}
|
||||
|
||||
// Azure Container Instances (Experimental)
|
||||
static get azureResourceGroup(): string {
|
||||
return Input.getInput('azureResourceGroup') ?? '';
|
||||
}
|
||||
|
||||
static get azureLocation(): string {
|
||||
return Input.getInput('azureLocation') ?? '';
|
||||
}
|
||||
|
||||
static get azureStorageType(): string {
|
||||
return Input.getInput('azureStorageType') ?? 'azure-files';
|
||||
}
|
||||
|
||||
static get azureStorageAccount(): string {
|
||||
return Input.getInput('azureStorageAccount') ?? '';
|
||||
}
|
||||
|
||||
static get azureBlobContainer(): string {
|
||||
return Input.getInput('azureBlobContainer') ?? 'unity-builds';
|
||||
}
|
||||
|
||||
static get azureFileShareName(): string {
|
||||
return Input.getInput('azureFileShareName') ?? 'unity-builds';
|
||||
}
|
||||
|
||||
static get azureSubscriptionId(): string {
|
||||
return Input.getInput('azureSubscriptionId') ?? '';
|
||||
}
|
||||
|
||||
static get azureCpu(): string {
|
||||
return Input.getInput('azureCpu') ?? '4';
|
||||
}
|
||||
|
||||
static get azureMemoryGb(): string {
|
||||
return Input.getInput('azureMemoryGb') ?? '16';
|
||||
}
|
||||
|
||||
static get azureDiskSizeGb(): string {
|
||||
return Input.getInput('azureDiskSizeGb') ?? '100';
|
||||
}
|
||||
|
||||
static get azureSubnetId(): string {
|
||||
return Input.getInput('azureSubnetId') ?? '';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Remote PowerShell provider
|
||||
// ### ### ###
|
||||
|
||||
static get remotePowershellHost(): string {
|
||||
return Input.getInput('remotePowershellHost') ?? '';
|
||||
}
|
||||
|
||||
static get remotePowershellCredential(): string {
|
||||
return Input.getInput('remotePowershellCredential') ?? '';
|
||||
}
|
||||
|
||||
static get remotePowershellTransport(): string {
|
||||
return Input.getInput('remotePowershellTransport') ?? 'wsman';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// GitHub Actions provider
|
||||
// ### ### ###
|
||||
|
||||
static get githubActionsRepo(): string {
|
||||
return Input.getInput('githubActionsRepo') ?? '';
|
||||
}
|
||||
|
||||
static get githubActionsWorkflow(): string {
|
||||
return Input.getInput('githubActionsWorkflow') ?? '';
|
||||
}
|
||||
|
||||
static get githubActionsToken(): string {
|
||||
return Input.getInput('githubActionsToken') ?? '';
|
||||
}
|
||||
|
||||
static get githubActionsRef(): string {
|
||||
return Input.getInput('githubActionsRef') ?? 'main';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// GitLab CI provider
|
||||
// ### ### ###
|
||||
|
||||
static get gitlabProjectId(): string {
|
||||
return Input.getInput('gitlabProjectId') ?? '';
|
||||
}
|
||||
|
||||
static get gitlabTriggerToken(): string {
|
||||
return Input.getInput('gitlabTriggerToken') ?? '';
|
||||
}
|
||||
|
||||
static get gitlabApiUrl(): string {
|
||||
return Input.getInput('gitlabApiUrl') ?? 'https://gitlab.com';
|
||||
}
|
||||
|
||||
static get gitlabRef(): string {
|
||||
return Input.getInput('gitlabRef') ?? 'main';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Ansible provider
|
||||
// ### ### ###
|
||||
|
||||
static get ansibleInventory(): string {
|
||||
return Input.getInput('ansibleInventory') ?? '';
|
||||
}
|
||||
|
||||
static get ansiblePlaybook(): string {
|
||||
return Input.getInput('ansiblePlaybook') ?? '';
|
||||
}
|
||||
|
||||
static get ansibleExtraVars(): string {
|
||||
return Input.getInput('ansibleExtraVars') ?? '';
|
||||
}
|
||||
|
||||
static get ansibleVaultPassword(): string {
|
||||
return Input.getInput('ansibleVaultPassword') ?? '';
|
||||
}
|
||||
|
||||
static get gitIntegrityCheck(): boolean {
|
||||
const input = Input.getInput('gitIntegrityCheck') ?? 'false';
|
||||
static get linux64RemoveExecutableExtension(): boolean {
|
||||
const input = Input.getInput('linux64RemoveExecutableExtension') ?? 'false';
|
||||
|
||||
return input === 'true';
|
||||
}
|
||||
|
||||
static get hotRunnerEnabled(): boolean {
|
||||
const input = Input.getInput('hotRunnerEnabled') ?? false;
|
||||
|
||||
return input === 'true';
|
||||
}
|
||||
|
||||
static get gitAutoRecover(): boolean {
|
||||
const input = Input.getInput('gitAutoRecover') ?? 'false';
|
||||
|
||||
return input === 'true';
|
||||
}
|
||||
|
||||
static get hotRunnerTransport(): 'websocket' | 'grpc' | 'named-pipe' {
|
||||
return (Input.getInput('hotRunnerTransport') ?? 'websocket') as 'websocket' | 'grpc' | 'named-pipe';
|
||||
}
|
||||
|
||||
static get hotRunnerHost(): string {
|
||||
return Input.getInput('hotRunnerHost') ?? 'localhost';
|
||||
}
|
||||
|
||||
static get hotRunnerPort(): number {
|
||||
return Number.parseInt(Input.getInput('hotRunnerPort') ?? '9090', 10);
|
||||
}
|
||||
|
||||
static get hotRunnerHealthInterval(): number {
|
||||
return Number.parseInt(Input.getInput('hotRunnerHealthInterval') ?? '30', 10);
|
||||
}
|
||||
|
||||
static get hotRunnerMaxIdle(): number {
|
||||
return Number.parseInt(Input.getInput('hotRunnerMaxIdle') ?? '3600', 10);
|
||||
}
|
||||
|
||||
static get hotRunnerFallbackToCold(): boolean {
|
||||
const input = Input.getInput('hotRunnerFallbackToCold') ?? 'true';
|
||||
|
||||
return input === 'true';
|
||||
}
|
||||
|
||||
static get cleanReservedFilenames(): boolean {
|
||||
const input = Input.getInput('cleanReservedFilenames') ?? 'false';
|
||||
|
||||
return input === 'true';
|
||||
}
|
||||
|
||||
static get buildArchiveEnabled(): boolean {
|
||||
const input = Input.getInput('buildArchiveEnabled') ?? 'false';
|
||||
|
||||
return input === 'true';
|
||||
}
|
||||
|
||||
static get buildArchivePath(): string {
|
||||
return Input.getInput('buildArchivePath') ?? './build-archives';
|
||||
}
|
||||
|
||||
static get buildArchiveRetention(): number {
|
||||
return Number.parseInt(Input.getInput('buildArchiveRetention') ?? '30', 10);
|
||||
}
|
||||
|
||||
static get testSuitePath(): string {
|
||||
return Input.getInput('testSuitePath') ?? '';
|
||||
}
|
||||
|
||||
static get testSuiteEvent(): string {
|
||||
return Input.getInput('testSuiteEvent') ?? '';
|
||||
}
|
||||
|
||||
static get testTaxonomyPath(): string {
|
||||
return Input.getInput('testTaxonomyPath') ?? '';
|
||||
}
|
||||
|
||||
static get testResultFormat(): string {
|
||||
return Input.getInput('testResultFormat') ?? 'junit';
|
||||
}
|
||||
|
||||
static get testResultPath(): string {
|
||||
return Input.getInput('testResultPath') ?? './test-results';
|
||||
}
|
||||
|
||||
public static ToEnvVarFormat(input: string) {
|
||||
if (input.toUpperCase() === input) {
|
||||
return input;
|
||||
|
||||
@@ -1,285 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* Tests for the orchestrator plugin interface (orchestrator-plugin.ts).
|
||||
* Compatibility tests for the legacy orchestrator-plugin module name.
|
||||
*
|
||||
* The plugin acts as a dynamic bridge to @game-ci/orchestrator, which is an
|
||||
* optional dependency. Two scenarios exist:
|
||||
*
|
||||
* 1. Package NOT installed (the natural state in unity-builder) -- both
|
||||
* loadOrchestrator() and loadPluginServices() must degrade gracefully.
|
||||
*
|
||||
* 2. Package IS installed (mocked) -- the returned wrappers must faithfully
|
||||
* forward calls and map results.
|
||||
* CI targets this file pattern directly, and consumers may still import this
|
||||
* module while migrating to the generic plugin API.
|
||||
*/
|
||||
|
||||
// Mock @actions/core so we can inspect core.warning calls even after
|
||||
// jest.resetModules() re-imports orchestrator-plugin (which statically
|
||||
// imports @actions/core at the top level).
|
||||
const mockWarning = jest.fn();
|
||||
jest.mock('@actions/core', () => ({
|
||||
warning: mockWarning,
|
||||
}));
|
||||
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');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
mockWarning.mockClear();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Part 1: Package NOT installed (natural state)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('orchestrator-plugin (package not installed)', () => {
|
||||
it('loadOrchestrator() returns undefined', async () => {
|
||||
const { loadOrchestrator } = await import('./orchestrator-plugin');
|
||||
|
||||
const result = await loadOrchestrator();
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('loadPluginServices() returns undefined and logs a warning', async () => {
|
||||
const { loadPluginServices } = await import('./orchestrator-plugin');
|
||||
|
||||
const result = await loadPluginServices();
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockWarning).toHaveBeenCalledTimes(1);
|
||||
expect(mockWarning).toHaveBeenCalledWith(expect.stringContaining('Orchestrator plugin not available'));
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Part 2: Package IS installed (mocked)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('orchestrator-plugin (package installed)', () => {
|
||||
// Fake service sentinels -- unique objects so we can assert identity.
|
||||
const fakeBuildReliabilityService = { _id: 'BuildReliabilityService' };
|
||||
const fakeTestWorkflowService = { _id: 'TestWorkflowService' };
|
||||
const fakeHotRunnerService = { _id: 'HotRunnerService' };
|
||||
const fakeOutputService = { _id: 'OutputService' };
|
||||
const fakeOutputTypeRegistry = { _id: 'OutputTypeRegistry' };
|
||||
const fakeArtifactUploadHandler = { _id: 'ArtifactUploadHandler' };
|
||||
const fakeIncrementalSyncService = { _id: 'IncrementalSyncService' };
|
||||
const fakeChildWorkspaceService = { _id: 'ChildWorkspaceService' };
|
||||
const fakeLocalCacheService = { _id: 'LocalCacheService' };
|
||||
const fakeSubmoduleProfileService = { _id: 'SubmoduleProfileService' };
|
||||
const fakeLfsAgentService = { _id: 'LfsAgentService' };
|
||||
const fakeGitHooksService = { _id: 'GitHooksService' };
|
||||
|
||||
const mockOrchestratorRun = jest.fn();
|
||||
|
||||
/**
|
||||
* Install the mock BEFORE importing orchestrator-plugin so that the dynamic
|
||||
* import('@game-ci/orchestrator') inside loadOrchestrator / loadPluginServices
|
||||
* resolves to our fake module.
|
||||
*
|
||||
* The { virtual: true } flag is required because @game-ci/orchestrator is
|
||||
* not physically installed in unity-builder's node_modules.
|
||||
*/
|
||||
function installOrchestratorMock(overrides: Record<string, unknown> = {}) {
|
||||
jest.doMock(
|
||||
'@game-ci/orchestrator',
|
||||
() => ({
|
||||
Orchestrator: { run: mockOrchestratorRun },
|
||||
BuildReliabilityService: fakeBuildReliabilityService,
|
||||
TestWorkflowService: fakeTestWorkflowService,
|
||||
HotRunnerService: fakeHotRunnerService,
|
||||
OutputService: fakeOutputService,
|
||||
OutputTypeRegistry: fakeOutputTypeRegistry,
|
||||
ArtifactUploadHandler: fakeArtifactUploadHandler,
|
||||
IncrementalSyncService: fakeIncrementalSyncService,
|
||||
ChildWorkspaceService: fakeChildWorkspaceService,
|
||||
LocalCacheService: fakeLocalCacheService,
|
||||
SubmoduleProfileService: fakeSubmoduleProfileService,
|
||||
LfsAgentService: fakeLfsAgentService,
|
||||
GitHooksService: fakeGitHooksService,
|
||||
...overrides,
|
||||
}),
|
||||
{ virtual: true },
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockOrchestratorRun.mockReset();
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// loadOrchestrator()
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('loadOrchestrator()', () => {
|
||||
it('returns an object with a run function', async () => {
|
||||
installOrchestratorMock();
|
||||
const { loadOrchestrator } = await import('./orchestrator-plugin');
|
||||
|
||||
const orchestrator = await loadOrchestrator();
|
||||
|
||||
expect(orchestrator).toBeDefined();
|
||||
expect(typeof orchestrator!.run).toBe('function');
|
||||
});
|
||||
|
||||
it('run() maps BuildSucceeded=true to exitCode=0', async () => {
|
||||
mockOrchestratorRun.mockResolvedValue({ BuildSucceeded: true, BuildResults: 'ok' });
|
||||
installOrchestratorMock();
|
||||
const { loadOrchestrator } = await import('./orchestrator-plugin');
|
||||
|
||||
const orchestrator = await loadOrchestrator();
|
||||
const result = await orchestrator!.run({}, 'ubuntu:latest');
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.BuildSucceeded).toBe(true);
|
||||
});
|
||||
|
||||
it('run() maps BuildSucceeded=false to exitCode=1', async () => {
|
||||
mockOrchestratorRun.mockResolvedValue({ BuildSucceeded: false, BuildResults: 'fail' });
|
||||
installOrchestratorMock();
|
||||
const { loadOrchestrator } = await import('./orchestrator-plugin');
|
||||
|
||||
const orchestrator = await loadOrchestrator();
|
||||
const result = await orchestrator!.run({}, 'ubuntu:latest');
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.BuildSucceeded).toBe(false);
|
||||
});
|
||||
|
||||
it('run() passes buildParameters and baseImage to Orchestrator.run', async () => {
|
||||
const buildParameters = { targetPlatform: 'StandaloneLinux64', editorVersion: '2021.3.1f1' };
|
||||
const baseImage = 'unityci/editor:2021.3.1f1-linux-il2cpp-1';
|
||||
|
||||
mockOrchestratorRun.mockResolvedValue({ BuildSucceeded: true, BuildResults: '' });
|
||||
installOrchestratorMock();
|
||||
const { loadOrchestrator } = await import('./orchestrator-plugin');
|
||||
|
||||
const orchestrator = await loadOrchestrator();
|
||||
await orchestrator!.run(buildParameters, baseImage);
|
||||
|
||||
expect(mockOrchestratorRun).toHaveBeenCalledTimes(1);
|
||||
expect(mockOrchestratorRun).toHaveBeenCalledWith(buildParameters, baseImage);
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// loadPluginServices()
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('loadPluginServices()', () => {
|
||||
it('returns all 7 eager services', async () => {
|
||||
installOrchestratorMock();
|
||||
const { loadPluginServices } = await import('./orchestrator-plugin');
|
||||
|
||||
const services = await loadPluginServices();
|
||||
|
||||
expect(services).toBeDefined();
|
||||
expect(services!.BuildReliabilityService).toBe(fakeBuildReliabilityService);
|
||||
expect(services!.TestWorkflowService).toBe(fakeTestWorkflowService);
|
||||
expect(services!.HotRunnerService).toBe(fakeHotRunnerService);
|
||||
expect(services!.OutputService).toBe(fakeOutputService);
|
||||
expect(services!.OutputTypeRegistry).toBe(fakeOutputTypeRegistry);
|
||||
expect(services!.ArtifactUploadHandler).toBe(fakeArtifactUploadHandler);
|
||||
expect(services!.IncrementalSyncService).toBe(fakeIncrementalSyncService);
|
||||
});
|
||||
|
||||
it('returns all 5 lazy loader functions', async () => {
|
||||
installOrchestratorMock();
|
||||
const { loadPluginServices } = await import('./orchestrator-plugin');
|
||||
|
||||
const services = await loadPluginServices();
|
||||
|
||||
expect(services).toBeDefined();
|
||||
expect(typeof services!.loadChildWorkspaceService).toBe('function');
|
||||
expect(typeof services!.loadLocalCacheService).toBe('function');
|
||||
expect(typeof services!.loadSubmoduleProfileService).toBe('function');
|
||||
expect(typeof services!.loadLfsAgentService).toBe('function');
|
||||
expect(typeof services!.loadGitHooksService).toBe('function');
|
||||
});
|
||||
|
||||
it('loadChildWorkspaceService() returns the correct service', async () => {
|
||||
installOrchestratorMock();
|
||||
const { loadPluginServices } = await import('./orchestrator-plugin');
|
||||
|
||||
const services = await loadPluginServices();
|
||||
const service = await services!.loadChildWorkspaceService();
|
||||
|
||||
expect(service).toBe(fakeChildWorkspaceService);
|
||||
});
|
||||
|
||||
it('loadLocalCacheService() returns the correct service', async () => {
|
||||
installOrchestratorMock();
|
||||
const { loadPluginServices } = await import('./orchestrator-plugin');
|
||||
|
||||
const services = await loadPluginServices();
|
||||
const service = await services!.loadLocalCacheService();
|
||||
|
||||
expect(service).toBe(fakeLocalCacheService);
|
||||
});
|
||||
|
||||
it('loadSubmoduleProfileService() returns the correct service', async () => {
|
||||
installOrchestratorMock();
|
||||
const { loadPluginServices } = await import('./orchestrator-plugin');
|
||||
|
||||
const services = await loadPluginServices();
|
||||
const service = await services!.loadSubmoduleProfileService();
|
||||
|
||||
expect(service).toBe(fakeSubmoduleProfileService);
|
||||
});
|
||||
|
||||
it('loadLfsAgentService() returns the correct service', async () => {
|
||||
installOrchestratorMock();
|
||||
const { loadPluginServices } = await import('./orchestrator-plugin');
|
||||
|
||||
const services = await loadPluginServices();
|
||||
const service = await services!.loadLfsAgentService();
|
||||
|
||||
expect(service).toBe(fakeLfsAgentService);
|
||||
});
|
||||
|
||||
it('loadGitHooksService() returns the correct service', async () => {
|
||||
installOrchestratorMock();
|
||||
const { loadPluginServices } = await import('./orchestrator-plugin');
|
||||
|
||||
const services = await loadPluginServices();
|
||||
const service = await services!.loadGitHooksService();
|
||||
|
||||
expect(service).toBe(fakeGitHooksService);
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Error handling
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('error handling', () => {
|
||||
it('propagates errors thrown by Orchestrator.run()', async () => {
|
||||
const orchestratorError = new Error('Build infrastructure failure');
|
||||
mockOrchestratorRun.mockRejectedValue(orchestratorError);
|
||||
installOrchestratorMock();
|
||||
const { loadOrchestrator } = await import('./orchestrator-plugin');
|
||||
|
||||
const orchestrator = await loadOrchestrator();
|
||||
|
||||
await expect(orchestrator!.run({}, 'ubuntu:latest')).rejects.toThrow('Build infrastructure failure');
|
||||
});
|
||||
|
||||
it('returns undefined services as-is when a service export is undefined', async () => {
|
||||
installOrchestratorMock({
|
||||
BuildReliabilityService: undefined,
|
||||
ChildWorkspaceService: undefined,
|
||||
});
|
||||
const { loadPluginServices } = await import('./orchestrator-plugin');
|
||||
|
||||
const services = await loadPluginServices();
|
||||
|
||||
expect(services).toBeDefined();
|
||||
expect(services!.BuildReliabilityService).toBeUndefined();
|
||||
|
||||
// The lazy loader still works -- it just returns undefined
|
||||
const childService = await services!.loadChildWorkspaceService();
|
||||
expect(childService).toBeUndefined();
|
||||
});
|
||||
expect(compatibility.loadOrchestratorPlugin).toBe(plugin.loadPlugin);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,97 +1,2 @@
|
||||
import * as core from '@actions/core';
|
||||
|
||||
export interface OrchestratorPluginResult {
|
||||
exitCode: number;
|
||||
BuildSucceeded: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the orchestrator for remote builds.
|
||||
* Returns undefined if orchestrator is not available.
|
||||
*/
|
||||
export async function loadOrchestrator(): Promise<
|
||||
| {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
run: (buildParameters: any, baseImage: string) => Promise<OrchestratorPluginResult>;
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
try {
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
const { Orchestrator } = await import('@game-ci/orchestrator');
|
||||
|
||||
return {
|
||||
run: async (buildParameters: any, baseImage: string): Promise<OrchestratorPluginResult> => {
|
||||
const result = await Orchestrator.run(buildParameters, baseImage);
|
||||
|
||||
return {
|
||||
exitCode: result.BuildSucceeded ? 0 : 1,
|
||||
BuildSucceeded: result.BuildSucceeded,
|
||||
};
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (!isModuleNotFoundError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load orchestrator plugin services for local builds.
|
||||
* These services are part of the orchestrator but also used in local builds
|
||||
* (child workspaces, local cache, git hooks, LFS agents, etc.).
|
||||
*/
|
||||
export async function loadPluginServices() {
|
||||
try {
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
const orchestrator = await import('@game-ci/orchestrator');
|
||||
|
||||
return {
|
||||
BuildReliabilityService: orchestrator.BuildReliabilityService,
|
||||
TestWorkflowService: orchestrator.TestWorkflowService,
|
||||
HotRunnerService: orchestrator.HotRunnerService,
|
||||
OutputService: orchestrator.OutputService,
|
||||
OutputTypeRegistry: orchestrator.OutputTypeRegistry,
|
||||
ArtifactUploadHandler: orchestrator.ArtifactUploadHandler,
|
||||
IncrementalSyncService: orchestrator.IncrementalSyncService,
|
||||
|
||||
// Lazy-loaded services (only imported when needed)
|
||||
async loadChildWorkspaceService() {
|
||||
return orchestrator.ChildWorkspaceService;
|
||||
},
|
||||
|
||||
async loadLocalCacheService() {
|
||||
return orchestrator.LocalCacheService;
|
||||
},
|
||||
|
||||
async loadSubmoduleProfileService() {
|
||||
return orchestrator.SubmoduleProfileService;
|
||||
},
|
||||
|
||||
async loadLfsAgentService() {
|
||||
return orchestrator.LfsAgentService;
|
||||
},
|
||||
|
||||
async loadGitHooksService() {
|
||||
return orchestrator.GitHooksService;
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (!isModuleNotFoundError(error)) {
|
||||
throw error;
|
||||
}
|
||||
core.warning(`Orchestrator plugin not available: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function isModuleNotFoundError(error: unknown): boolean {
|
||||
if (error && typeof error === 'object' && 'code' in error) {
|
||||
const code = (error as { code: string }).code;
|
||||
if (code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return typeof (error as Error)?.message === 'string' && /cannot find module/i.test((error as Error).message);
|
||||
}
|
||||
export { loadPlugin as loadOrchestratorPlugin } from './plugin';
|
||||
export type { Plugin as OrchestratorPlugin } from './plugin';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||
import Output from './output';
|
||||
|
||||
describe('Output', () => {
|
||||
|
||||
@@ -32,6 +32,13 @@ class PlatformSetup {
|
||||
|
||||
let servicesConfig = fs.readFileSync(servicesConfigPathTemplate).toString();
|
||||
servicesConfig = servicesConfig.replace('%URL%', buildParameters.unityLicensingServer);
|
||||
|
||||
if (buildParameters.unityLicensingToolset) {
|
||||
const parsed = JSON.parse(servicesConfig);
|
||||
parsed.toolset = buildParameters.unityLicensingToolset;
|
||||
servicesConfig = JSON.stringify(parsed, undefined, 2);
|
||||
}
|
||||
|
||||
fs.writeFileSync(servicesConfigPath, servicesConfig);
|
||||
|
||||
SetupAndroid.setup(buildParameters);
|
||||
|
||||
@@ -4,17 +4,30 @@ import { BuildParameters } from '..';
|
||||
|
||||
class SetupAndroid {
|
||||
public static async setup(buildParameters: BuildParameters) {
|
||||
const { targetPlatform, androidKeystoreBase64, androidKeystoreName, projectPath } = buildParameters;
|
||||
const { targetPlatform, androidKeystoreBase64, androidKeystoreName, projectPath } =
|
||||
buildParameters;
|
||||
|
||||
if (targetPlatform === 'Android' && androidKeystoreBase64 !== '' && androidKeystoreName !== '') {
|
||||
if (
|
||||
targetPlatform === 'Android' &&
|
||||
androidKeystoreBase64 !== '' &&
|
||||
androidKeystoreName !== ''
|
||||
) {
|
||||
SetupAndroid.setupAndroidRun(androidKeystoreBase64, androidKeystoreName, projectPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static setupAndroidRun(androidKeystoreBase64: string, androidKeystoreName: string, projectPath: string) {
|
||||
private static setupAndroidRun(
|
||||
androidKeystoreBase64: string,
|
||||
androidKeystoreName: string,
|
||||
projectPath: string,
|
||||
) {
|
||||
const decodedKeystore = Buffer.from(androidKeystoreBase64, 'base64').toString('binary');
|
||||
const githubWorkspace = process.env.GITHUB_WORKSPACE || '';
|
||||
fs.writeFileSync(path.join(githubWorkspace, projectPath, androidKeystoreName), decodedKeystore, 'binary');
|
||||
fs.writeFileSync(
|
||||
path.join(githubWorkspace, projectPath, androidKeystoreName),
|
||||
decodedKeystore,
|
||||
'binary',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,8 @@ class SetupMac {
|
||||
}
|
||||
}
|
||||
|
||||
const commandSuffix = buildParameters.unityHubVersionOnMac !== '' ? `@${buildParameters.unityHubVersionOnMac}` : '';
|
||||
const commandSuffix =
|
||||
buildParameters.unityHubVersionOnMac !== '' ? `@${buildParameters.unityHubVersionOnMac}` : '';
|
||||
const command = `brew install unity-hub${commandSuffix}`;
|
||||
|
||||
// Ignoring return code because the log seems to overflow the internal buffer which triggers
|
||||
@@ -52,7 +53,9 @@ class SetupMac {
|
||||
ignoreReturnCode: true,
|
||||
});
|
||||
if (errorCode) {
|
||||
throw new Error(`There was an error installing the Unity Editor. See logs above for details.`);
|
||||
throw new Error(
|
||||
`There was an error installing the Unity Editor. See logs above for details.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (buildParameters.cacheUnityInstallationOnMac) {
|
||||
@@ -135,7 +138,9 @@ class SetupMac {
|
||||
}
|
||||
|
||||
const unityChangeset = await getUnityChangeset(buildParameters.editorVersion);
|
||||
const moduleArguments = SetupMac.getModuleParametersForTargetPlatform(buildParameters.targetPlatform);
|
||||
const moduleArguments = SetupMac.getModuleParametersForTargetPlatform(
|
||||
buildParameters.targetPlatform,
|
||||
);
|
||||
const architectureArguments = SetupMac.getArchitectureParameters();
|
||||
|
||||
const execArguments: string[] = [
|
||||
@@ -156,7 +161,9 @@ class SetupMac {
|
||||
ignoreReturnCode: true,
|
||||
});
|
||||
if (errorCode) {
|
||||
throw new Error(`There was an error installing the Unity Editor. See logs above for details.`);
|
||||
throw new Error(
|
||||
`There was an error installing the Unity Editor. See logs above for details.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (buildParameters.cacheUnityInstallationOnMac) {
|
||||
@@ -164,7 +171,10 @@ class SetupMac {
|
||||
}
|
||||
}
|
||||
|
||||
private static async setEnvironmentVariables(buildParameters: BuildParameters, actionFolder: string) {
|
||||
private static async setEnvironmentVariables(
|
||||
buildParameters: BuildParameters,
|
||||
actionFolder: string,
|
||||
) {
|
||||
// Need to set environment variables from here because we execute
|
||||
// the scripts on the host for mac
|
||||
process.env.ACTION_FOLDER = actionFolder;
|
||||
|
||||
@@ -42,7 +42,9 @@ class ValidateWindows {
|
||||
|
||||
private static checkForVisualStudio() {
|
||||
// Note: When upgrading to Server 2022, we will need to move to just "program files" since VS will be 64-bit
|
||||
const visualStudioInstallPathExists = fs.existsSync('C:/Program Files (x86)/Microsoft Visual Studio');
|
||||
const visualStudioInstallPathExists = fs.existsSync(
|
||||
'C:/Program Files (x86)/Microsoft Visual Studio',
|
||||
);
|
||||
const visualStudioDataPathExists = fs.existsSync('C:/ProgramData/Microsoft/VisualStudio');
|
||||
|
||||
if (!visualStudioInstallPathExists || !visualStudioDataPathExists) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||
import Platform from './platform';
|
||||
|
||||
describe('Platform', () => {
|
||||
|
||||
@@ -1,509 +0,0 @@
|
||||
/**
|
||||
* Tests for plugin input properties and their wiring into BuildParameters.
|
||||
*
|
||||
* Covers all 20 new input properties added for plugin features:
|
||||
* - Boolean inputs: localCacheEnabled, childWorkspacesEnabled, gitHooksEnabled,
|
||||
* localCacheLibrary, localCacheLfs, childWorkspacePreserveGit, childWorkspaceSeparateLibrary
|
||||
* - String inputs: submoduleProfilePath, submoduleVariantPath, submoduleToken,
|
||||
* localCacheRoot, childWorkspaceName, childWorkspaceCacheRoot, lfsTransferAgent,
|
||||
* lfsTransferAgentArgs, lfsStoragePaths, providerExecutable, gitHooksSkipList,
|
||||
* gitHooksRunBeforeBuild
|
||||
*
|
||||
* Special attention to boolean inputs: GitHub Actions always passes inputs as strings,
|
||||
* so 'false' must NOT evaluate as truthy (the #1 source of bugs).
|
||||
*/
|
||||
|
||||
import * as core from '@actions/core';
|
||||
import Input from './input';
|
||||
import Versioning from './versioning';
|
||||
import BuildParameters from './build-parameters';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Part 1: Input getters — defaults and explicit values
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Plugin Input properties', () => {
|
||||
// -----------------------------------------------------------------------
|
||||
// Boolean inputs — default and string parsing
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('localCacheEnabled', () => {
|
||||
it('returns false by default', () => {
|
||||
expect(Input.localCacheEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when string "true" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.localCacheEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when string "false" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.localCacheEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when empty string is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('');
|
||||
expect(Input.localCacheEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('localCacheLibrary', () => {
|
||||
it('returns true by default (library caching on by default when cache enabled)', () => {
|
||||
expect(Input.localCacheLibrary).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when string "true" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.localCacheLibrary).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when string "false" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.localCacheLibrary).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('localCacheLfs', () => {
|
||||
it('returns false by default', () => {
|
||||
expect(Input.localCacheLfs).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when string "true" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.localCacheLfs).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when string "false" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.localCacheLfs).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('childWorkspacesEnabled', () => {
|
||||
it('returns false by default', () => {
|
||||
expect(Input.childWorkspacesEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when string "true" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.childWorkspacesEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when string "false" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.childWorkspacesEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when empty string is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('');
|
||||
expect(Input.childWorkspacesEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('childWorkspacePreserveGit', () => {
|
||||
it('returns true by default', () => {
|
||||
expect(Input.childWorkspacePreserveGit).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when string "false" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.childWorkspacePreserveGit).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when string "true" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.childWorkspacePreserveGit).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('childWorkspaceSeparateLibrary', () => {
|
||||
it('returns true by default', () => {
|
||||
expect(Input.childWorkspaceSeparateLibrary).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when string "false" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.childWorkspaceSeparateLibrary).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when string "true" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.childWorkspaceSeparateLibrary).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gitHooksEnabled', () => {
|
||||
it('returns false by default', () => {
|
||||
expect(Input.gitHooksEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when string "true" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.gitHooksEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when string "false" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.gitHooksEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when empty string is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('');
|
||||
expect(Input.gitHooksEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Boolean truthiness edge cases — the #1 source of bugs
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('boolean input string handling (edge cases)', () => {
|
||||
// These tests verify that the === 'true' comparison is correct.
|
||||
// In JavaScript, 'false' is truthy when used in a boolean context,
|
||||
// but the Input class correctly uses === 'true' comparison.
|
||||
|
||||
const booleanInputs: Array<{
|
||||
name: string;
|
||||
getter: () => boolean;
|
||||
defaultValue: boolean;
|
||||
}> = [
|
||||
{ name: 'localCacheEnabled', getter: () => Input.localCacheEnabled, defaultValue: false },
|
||||
{ name: 'localCacheLfs', getter: () => Input.localCacheLfs, defaultValue: false },
|
||||
{ name: 'childWorkspacesEnabled', getter: () => Input.childWorkspacesEnabled, defaultValue: false },
|
||||
{ name: 'gitHooksEnabled', getter: () => Input.gitHooksEnabled, defaultValue: false },
|
||||
|
||||
// These default to true:
|
||||
{ name: 'localCacheLibrary', getter: () => Input.localCacheLibrary, defaultValue: true },
|
||||
{ name: 'childWorkspacePreserveGit', getter: () => Input.childWorkspacePreserveGit, defaultValue: true },
|
||||
{ name: 'childWorkspaceSeparateLibrary', getter: () => Input.childWorkspaceSeparateLibrary, defaultValue: true },
|
||||
];
|
||||
|
||||
test.each(booleanInputs)('$name: "false" string does NOT evaluate as truthy', ({ getter }) => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(getter()).toBe(false);
|
||||
});
|
||||
|
||||
test.each(booleanInputs)('$name: "true" string evaluates as truthy', ({ getter }) => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(getter()).toBe(true);
|
||||
});
|
||||
|
||||
test.each(booleanInputs)('$name: "TRUE" (uppercase) does NOT evaluate as true (case sensitive)', ({ getter }) => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('TRUE');
|
||||
expect(getter()).toBe(false);
|
||||
});
|
||||
|
||||
test.each(booleanInputs)('$name: "1" does NOT evaluate as true', ({ getter }) => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('1');
|
||||
expect(getter()).toBe(false);
|
||||
});
|
||||
|
||||
test.each(booleanInputs)('$name: "yes" does NOT evaluate as true', ({ getter }) => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('yes');
|
||||
expect(getter()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// String inputs — defaults and explicit values
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('submoduleProfilePath', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.submoduleProfilePath).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('config/submodule-profiles/tow/ec/profile.yml');
|
||||
expect(Input.submoduleProfilePath).toBe('config/submodule-profiles/tow/ec/profile.yml');
|
||||
});
|
||||
});
|
||||
|
||||
describe('submoduleVariantPath', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.submoduleVariantPath).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('config/submodule-profiles/tow/ec/server.yml');
|
||||
expect(Input.submoduleVariantPath).toBe('config/submodule-profiles/tow/ec/server.yml');
|
||||
});
|
||||
});
|
||||
|
||||
describe('submoduleToken', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.submoduleToken).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('ghp_abc123');
|
||||
expect(Input.submoduleToken).toBe('ghp_abc123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('localCacheRoot', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.localCacheRoot).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('/d/cache/game-ci');
|
||||
expect(Input.localCacheRoot).toBe('/d/cache/game-ci');
|
||||
});
|
||||
});
|
||||
|
||||
describe('childWorkspaceName', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.childWorkspaceName).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('TurnOfWarEndlessCrusade');
|
||||
expect(Input.childWorkspaceName).toBe('TurnOfWarEndlessCrusade');
|
||||
});
|
||||
});
|
||||
|
||||
describe('childWorkspaceCacheRoot', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.childWorkspaceCacheRoot).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('/d/workspaces');
|
||||
expect(Input.childWorkspaceCacheRoot).toBe('/d/workspaces');
|
||||
});
|
||||
});
|
||||
|
||||
describe('lfsTransferAgent', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.lfsTransferAgent).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('/tools/elastic-git-storage');
|
||||
expect(Input.lfsTransferAgent).toBe('/tools/elastic-git-storage');
|
||||
});
|
||||
});
|
||||
|
||||
describe('lfsTransferAgentArgs', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.lfsTransferAgentArgs).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('--verbose --timeout=60');
|
||||
expect(Input.lfsTransferAgentArgs).toBe('--verbose --timeout=60');
|
||||
});
|
||||
});
|
||||
|
||||
describe('lfsStoragePaths', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.lfsStoragePaths).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('/storage/primary;/storage/secondary');
|
||||
expect(Input.lfsStoragePaths).toBe('/storage/primary;/storage/secondary');
|
||||
});
|
||||
});
|
||||
|
||||
describe('providerExecutable', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.providerExecutable).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('/usr/local/bin/custom-provider');
|
||||
expect(Input.providerExecutable).toBe('/usr/local/bin/custom-provider');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gitHooksSkipList', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.gitHooksSkipList).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('pre-commit,pre-push');
|
||||
expect(Input.gitHooksSkipList).toBe('pre-commit,pre-push');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gitHooksRunBeforeBuild', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.gitHooksRunBeforeBuild).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('pre-commit');
|
||||
expect(Input.gitHooksRunBeforeBuild).toBe('pre-commit');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Part 2: BuildParameters.create() maps new inputs to properties
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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>';
|
||||
|
||||
describe('BuildParameters.create() plugin property mapping', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Versioning, 'determineBuildVersion').mockImplementation(async () => '1.3.37');
|
||||
process.env.UNITY_LICENSE = testLicense;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('maps submoduleProfilePath from Input', async () => {
|
||||
jest.spyOn(Input, 'submoduleProfilePath', 'get').mockReturnValue('/path/to/profile.yml');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.submoduleProfilePath).toBe('/path/to/profile.yml');
|
||||
});
|
||||
|
||||
it('maps submoduleVariantPath from Input', async () => {
|
||||
jest.spyOn(Input, 'submoduleVariantPath', 'get').mockReturnValue('/path/to/variant.yml');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.submoduleVariantPath).toBe('/path/to/variant.yml');
|
||||
});
|
||||
|
||||
it('maps submoduleToken from Input', async () => {
|
||||
jest.spyOn(Input, 'submoduleToken', 'get').mockReturnValue('ghp_token123');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.submoduleToken).toBe('ghp_token123');
|
||||
});
|
||||
|
||||
it('maps localCacheEnabled from Input', async () => {
|
||||
jest.spyOn(Input, 'localCacheEnabled', 'get').mockReturnValue(true);
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.localCacheEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('maps localCacheRoot from Input', async () => {
|
||||
jest.spyOn(Input, 'localCacheRoot', 'get').mockReturnValue('/d/cache');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.localCacheRoot).toBe('/d/cache');
|
||||
});
|
||||
|
||||
it('maps localCacheLibrary from Input', async () => {
|
||||
jest.spyOn(Input, 'localCacheLibrary', 'get').mockReturnValue(false);
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.localCacheLibrary).toBe(false);
|
||||
});
|
||||
|
||||
it('maps localCacheLfs from Input', async () => {
|
||||
jest.spyOn(Input, 'localCacheLfs', 'get').mockReturnValue(true);
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.localCacheLfs).toBe(true);
|
||||
});
|
||||
|
||||
it('maps childWorkspacesEnabled from Input', async () => {
|
||||
jest.spyOn(Input, 'childWorkspacesEnabled', 'get').mockReturnValue(true);
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.childWorkspacesEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('maps childWorkspaceName from Input', async () => {
|
||||
jest.spyOn(Input, 'childWorkspaceName', 'get').mockReturnValue('TurnOfWar');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.childWorkspaceName).toBe('TurnOfWar');
|
||||
});
|
||||
|
||||
it('maps childWorkspaceCacheRoot from Input', async () => {
|
||||
jest.spyOn(Input, 'childWorkspaceCacheRoot', 'get').mockReturnValue('/cache/workspaces');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.childWorkspaceCacheRoot).toBe('/cache/workspaces');
|
||||
});
|
||||
|
||||
it('maps childWorkspacePreserveGit from Input', async () => {
|
||||
jest.spyOn(Input, 'childWorkspacePreserveGit', 'get').mockReturnValue(false);
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.childWorkspacePreserveGit).toBe(false);
|
||||
});
|
||||
|
||||
it('maps childWorkspaceSeparateLibrary from Input', async () => {
|
||||
jest.spyOn(Input, 'childWorkspaceSeparateLibrary', 'get').mockReturnValue(false);
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.childWorkspaceSeparateLibrary).toBe(false);
|
||||
});
|
||||
|
||||
it('maps lfsTransferAgent from Input', async () => {
|
||||
jest.spyOn(Input, 'lfsTransferAgent', 'get').mockReturnValue('/tools/elastic-git-storage');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.lfsTransferAgent).toBe('/tools/elastic-git-storage');
|
||||
});
|
||||
|
||||
it('maps lfsTransferAgentArgs from Input', async () => {
|
||||
jest.spyOn(Input, 'lfsTransferAgentArgs', 'get').mockReturnValue('--verbose');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.lfsTransferAgentArgs).toBe('--verbose');
|
||||
});
|
||||
|
||||
it('maps lfsStoragePaths from Input', async () => {
|
||||
jest.spyOn(Input, 'lfsStoragePaths', 'get').mockReturnValue('/path/a;/path/b');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.lfsStoragePaths).toBe('/path/a;/path/b');
|
||||
});
|
||||
|
||||
it('maps gitHooksEnabled from Input', async () => {
|
||||
jest.spyOn(Input, 'gitHooksEnabled', 'get').mockReturnValue(true);
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.gitHooksEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('maps gitHooksSkipList from Input', async () => {
|
||||
jest.spyOn(Input, 'gitHooksSkipList', 'get').mockReturnValue('pre-commit,pre-push');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.gitHooksSkipList).toBe('pre-commit,pre-push');
|
||||
});
|
||||
|
||||
it('maps gitHooksRunBeforeBuild from Input', async () => {
|
||||
jest.spyOn(Input, 'gitHooksRunBeforeBuild', 'get').mockReturnValue('pre-commit');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.gitHooksRunBeforeBuild).toBe('pre-commit');
|
||||
});
|
||||
|
||||
it('maps providerExecutable from Input', async () => {
|
||||
jest.spyOn(Input, 'providerExecutable', 'get').mockReturnValue('/usr/local/bin/provider');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.providerExecutable).toBe('/usr/local/bin/provider');
|
||||
});
|
||||
|
||||
// Test that all plugin properties have correct defaults when not explicitly set
|
||||
it('has correct defaults for all plugin properties', async () => {
|
||||
const parameters = await BuildParameters.create();
|
||||
|
||||
expect(parameters.submoduleProfilePath).toBe('');
|
||||
expect(parameters.submoduleVariantPath).toBe('');
|
||||
expect(parameters.submoduleToken).toBe('');
|
||||
expect(parameters.localCacheEnabled).toBe(false);
|
||||
expect(parameters.localCacheRoot).toBe('');
|
||||
expect(parameters.localCacheLibrary).toBe(true);
|
||||
expect(parameters.localCacheLfs).toBe(false);
|
||||
expect(parameters.childWorkspacesEnabled).toBe(false);
|
||||
expect(parameters.childWorkspaceName).toBe('');
|
||||
expect(parameters.childWorkspaceCacheRoot).toBe('');
|
||||
expect(parameters.childWorkspacePreserveGit).toBe(true);
|
||||
expect(parameters.childWorkspaceSeparateLibrary).toBe(true);
|
||||
expect(parameters.lfsTransferAgent).toBe('');
|
||||
expect(parameters.lfsTransferAgentArgs).toBe('');
|
||||
expect(parameters.lfsStoragePaths).toBe('');
|
||||
expect(parameters.gitHooksEnabled).toBe(false);
|
||||
expect(parameters.gitHooksSkipList).toBe('');
|
||||
expect(parameters.gitHooksRunBeforeBuild).toBe('');
|
||||
expect(parameters.providerExecutable).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Shared options bridge between unity-builder and plugins (e.g. @game-ci/orchestrator).
|
||||
*
|
||||
* Plugins set PluginOptions.options to pass configuration into BuildParameters
|
||||
* and Input. When options are set, isPluginMode is true and query() reads
|
||||
* from the options map instead of @actions/core.getInput().
|
||||
*/
|
||||
export class PluginOptions {
|
||||
public static options: Record<string, any> | undefined;
|
||||
|
||||
static get isPluginMode() {
|
||||
return Boolean(PluginOptions.options?.mode);
|
||||
}
|
||||
|
||||
public static query(key: string, alternativeKey: string) {
|
||||
if (PluginOptions.options && PluginOptions.options[key] !== undefined) {
|
||||
return PluginOptions.options[key];
|
||||
}
|
||||
if (
|
||||
PluginOptions.options &&
|
||||
alternativeKey &&
|
||||
PluginOptions.options[alternativeKey] !== undefined
|
||||
) {
|
||||
return PluginOptions.options[alternativeKey];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Backwards-compatible alias — the orchestrator still imports { Cli }
|
||||
export { PluginOptions as Cli };
|
||||
@@ -0,0 +1,116 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
/**
|
||||
* Tests for the generic plugin loader (plugin.ts).
|
||||
*
|
||||
* The default plugin implementation is currently @game-ci/orchestrator, but
|
||||
* unity-builder depends on the generic Plugin lifecycle rather than an
|
||||
* orchestrator-specific type.
|
||||
*/
|
||||
|
||||
const mockWarning = vi.fn();
|
||||
const mockInfo = vi.fn();
|
||||
vi.mock('@actions/core', () => ({
|
||||
warning: mockWarning,
|
||||
info: mockInfo,
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
mockWarning.mockClear();
|
||||
mockInfo.mockClear();
|
||||
});
|
||||
|
||||
describe('plugin (default package not installed)', () => {
|
||||
it('loadPlugin() returns undefined', async () => {
|
||||
const { loadPlugin } = await import('./plugin');
|
||||
|
||||
const result = await loadPlugin();
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('plugin (default package installed)', () => {
|
||||
const fakePlugin = {
|
||||
initialize: vi.fn(),
|
||||
canHandleBuild: vi.fn().mockReturnValue(false),
|
||||
handleBuild: vi.fn().mockResolvedValue({ exitCode: 0 }),
|
||||
beforeLocalBuild: vi.fn(),
|
||||
afterLocalBuild: vi.fn(),
|
||||
handlePostBuild: vi.fn(),
|
||||
};
|
||||
|
||||
const mockCreatePlugin = vi.fn().mockReturnValue(fakePlugin);
|
||||
|
||||
function installDefaultPluginMock(overrides: Record<string, unknown> = {}) {
|
||||
// The `@game-ci/orchestrator` module is intentionally optional and may not
|
||||
// be installed. `vi.doMock` lets the dynamic import in the loader resolve
|
||||
// through this factory before vite tries to load a real package.
|
||||
vi.doMock('@game-ci/orchestrator', () => ({
|
||||
createPlugin: mockCreatePlugin,
|
||||
...overrides,
|
||||
}));
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockCreatePlugin.mockClear();
|
||||
fakePlugin.initialize.mockClear();
|
||||
fakePlugin.canHandleBuild.mockClear();
|
||||
fakePlugin.handleBuild.mockClear();
|
||||
fakePlugin.beforeLocalBuild.mockClear();
|
||||
fakePlugin.afterLocalBuild.mockClear();
|
||||
fakePlugin.handlePostBuild.mockClear();
|
||||
});
|
||||
|
||||
it('returns the plugin from createPlugin()', async () => {
|
||||
installDefaultPluginMock();
|
||||
const { loadPlugin } = await import('./plugin');
|
||||
|
||||
const plugin = await loadPlugin();
|
||||
|
||||
expect(plugin).toBeDefined();
|
||||
expect(mockCreatePlugin).toHaveBeenCalledTimes(1);
|
||||
expect(plugin).toBe(fakePlugin);
|
||||
});
|
||||
|
||||
it('returns a plugin with all lifecycle methods', async () => {
|
||||
installDefaultPluginMock();
|
||||
const { loadPlugin } = await import('./plugin');
|
||||
|
||||
const plugin = await loadPlugin();
|
||||
|
||||
expect(typeof plugin!.initialize).toBe('function');
|
||||
expect(typeof plugin!.canHandleBuild).toBe('function');
|
||||
expect(typeof plugin!.handleBuild).toBe('function');
|
||||
expect(typeof plugin!.beforeLocalBuild).toBe('function');
|
||||
expect(typeof plugin!.afterLocalBuild).toBe('function');
|
||||
expect(typeof plugin!.handlePostBuild).toBe('function');
|
||||
});
|
||||
|
||||
it('returns undefined and warns when createPlugin is not a function', async () => {
|
||||
installDefaultPluginMock({ createPlugin: undefined });
|
||||
const { loadPlugin } = await import('./plugin');
|
||||
|
||||
const plugin = await loadPlugin();
|
||||
|
||||
expect(plugin).toBeUndefined();
|
||||
expect(mockWarning).toHaveBeenCalledWith(
|
||||
expect.stringContaining('does not export createPlugin'),
|
||||
);
|
||||
});
|
||||
|
||||
it('propagates non-MODULE_NOT_FOUND errors', async () => {
|
||||
// Throw lazily from `createPlugin` rather than from the mock factory
|
||||
// itself: vitest 4 wraps factory-time errors with its own message, which
|
||||
// masks the inner error at the assertion site.
|
||||
installDefaultPluginMock({
|
||||
createPlugin: () => {
|
||||
throw new Error('Syntax error in module');
|
||||
},
|
||||
});
|
||||
const { loadPlugin } = await import('./plugin');
|
||||
|
||||
await expect(loadPlugin()).rejects.toThrow('Syntax error in module');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as core from '@actions/core';
|
||||
|
||||
const DEFAULT_PLUGIN_MODULE = '@game-ci/orchestrator';
|
||||
|
||||
/**
|
||||
* Generic lifecycle contract for optional unity-builder plugins.
|
||||
*
|
||||
* Plugins read their own configuration from environment variables and GitHub
|
||||
* Actions inputs. Unity-builder only calls lifecycle hooks at the points where
|
||||
* an external implementation can extend or replace the local build flow.
|
||||
*/
|
||||
export interface Plugin {
|
||||
initialize(coreParameters: Record<string, any>, workspace: string): Promise<void>;
|
||||
|
||||
/** Whether the plugin wants to handle the entire build. */
|
||||
canHandleBuild(): boolean;
|
||||
|
||||
/**
|
||||
* Execute the build when canHandleBuild() returns true.
|
||||
* If the plugin needs to fall back to a local build, it returns
|
||||
* { exitCode: -1, fallbackToLocal: true }.
|
||||
*/
|
||||
handleBuild(baseImage: string): Promise<{ exitCode: number; fallbackToLocal?: boolean }>;
|
||||
|
||||
/** Pre-build hook for local builds. */
|
||||
beforeLocalBuild(workspace: string): Promise<void>;
|
||||
|
||||
/** Post-build hook for local builds. */
|
||||
afterLocalBuild(workspace: string, exitCode: number): Promise<void>;
|
||||
|
||||
/** Post-build hook for all build types. */
|
||||
handlePostBuild(exitCode: number): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to load the default optional plugin.
|
||||
*
|
||||
* Today the default implementation is @game-ci/orchestrator. The loader is
|
||||
* intentionally named after the generic plugin contract so additional plugin
|
||||
* implementations can be added without making orchestrator part of the core
|
||||
* abstraction.
|
||||
*/
|
||||
export async function loadPlugin(moduleName = DEFAULT_PLUGIN_MODULE): Promise<Plugin | undefined> {
|
||||
try {
|
||||
const pluginModule = await import(/* webpackIgnore: true */ moduleName);
|
||||
|
||||
if (typeof pluginModule.createPlugin !== 'function') {
|
||||
core.warning(
|
||||
`Plugin package "${moduleName}" found but does not export createPlugin(). ` +
|
||||
'Update the plugin package to the latest version.',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return pluginModule.createPlugin();
|
||||
} catch (error) {
|
||||
if (!isModuleNotFoundError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isModuleNotFoundError(error: unknown): boolean {
|
||||
if (error && typeof error === 'object' && 'code' in error) {
|
||||
const code = (error as { code: string }).code;
|
||||
if (code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
typeof (error as Error)?.message === 'string' &&
|
||||
/cannot find module/i.test((error as Error).message)
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, vi } from 'vitest';
|
||||
import Project from './project';
|
||||
|
||||
jest.mock('./input');
|
||||
vi.mock('./input');
|
||||
|
||||
describe('Platform', () => {
|
||||
describe('relativePath', () => {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, vi } from 'vitest';
|
||||
import * as core from '@actions/core';
|
||||
import System from './system';
|
||||
|
||||
jest.spyOn(core, 'debug').mockImplementation(() => {});
|
||||
jest.spyOn(core, 'info').mockImplementation(() => {});
|
||||
jest.spyOn(core, 'warning').mockImplementation(() => {});
|
||||
jest.spyOn(core, 'error').mockImplementation(() => {});
|
||||
vi.spyOn(core, 'debug').mockImplementation(() => {});
|
||||
vi.spyOn(core, 'info').mockImplementation(() => {});
|
||||
vi.spyOn(core, 'warning').mockImplementation(() => {});
|
||||
vi.spyOn(core, 'error').mockImplementation(() => {});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
afterEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('System', () => {
|
||||
describe('run', () => {
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, vi } from 'vitest';
|
||||
import * as core from '@actions/core';
|
||||
import * as exec from '@actions/exec';
|
||||
import System from './system';
|
||||
|
||||
jest.spyOn(core, 'debug').mockImplementation(() => {});
|
||||
const info = jest.spyOn(core, 'info').mockImplementation(() => {});
|
||||
jest.spyOn(core, 'warning').mockImplementation(() => {});
|
||||
jest.spyOn(core, 'error').mockImplementation(() => {});
|
||||
const execSpy = jest.spyOn(exec, 'exec').mockImplementation(async () => 0);
|
||||
vi.spyOn(core, 'debug').mockImplementation(() => {});
|
||||
const info = vi.spyOn(core, 'info').mockImplementation(() => {});
|
||||
vi.spyOn(core, 'warning').mockImplementation(() => {});
|
||||
vi.spyOn(core, 'error').mockImplementation(() => {});
|
||||
const execSpy = vi.spyOn(exec, 'exec').mockImplementation(async () => 0);
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
afterEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('System', () => {
|
||||
describe('run', () => {
|
||||
describe('units', () => {
|
||||
it('passes the command to command line', async () => {
|
||||
await expect(System.run('echo test')).resolves.not.toBeNull();
|
||||
await expect(execSpy).toHaveBeenLastCalledWith('echo test', expect.anything(), expect.anything());
|
||||
await expect(execSpy).toHaveBeenLastCalledWith(
|
||||
'echo test',
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws on when error code is not 0', async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll, test } from 'vitest';
|
||||
import UnityVersioning from './unity-versioning';
|
||||
|
||||
describe('Unity Versioning', () => {
|
||||
|
||||
@@ -13,7 +13,9 @@ export default class UnityVersioning {
|
||||
static read(projectPath: string) {
|
||||
const filePath = path.join(projectPath, 'ProjectSettings', 'ProjectVersion.txt');
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`Project settings file not found at "${filePath}". Have you correctly set the projectPath?`);
|
||||
throw new Error(
|
||||
`Project settings file not found at "${filePath}". Have you correctly set the projectPath?`,
|
||||
);
|
||||
}
|
||||
|
||||
return UnityVersioning.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import Unity from './unity';
|
||||
|
||||
describe('Unity', () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, test, vi } from 'vitest';
|
||||
import * as core from '@actions/core';
|
||||
import NotImplementedException from './error/not-implemented-exception';
|
||||
import System from './system';
|
||||
@@ -5,7 +6,7 @@ import Versioning from './versioning';
|
||||
import { validVersionTagInputs, invalidVersionTagInputs } from './__data__/versions';
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Versioning', () => {
|
||||
@@ -39,7 +40,9 @@ describe('Versioning', () => {
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
const matchInputUsingGrep = async (input: string) => {
|
||||
const output = await System.run('sh', undefined, {
|
||||
input: Buffer.from(`echo '${input}' | grep -E '${Versioning.grepCompatibleInputVersionRegex}'`),
|
||||
input: Buffer.from(
|
||||
`echo '${input}' | grep -E '${Versioning.grepCompatibleInputVersionRegex}'`,
|
||||
),
|
||||
silent: true,
|
||||
});
|
||||
|
||||
@@ -50,30 +53,39 @@ describe('Versioning', () => {
|
||||
expect(await matchInputUsingGrep(input)).toStrictEqual(input);
|
||||
});
|
||||
|
||||
it.concurrent.each(invalidVersionTagInputs)(`rejects non-version tag input '%s'`, async (input) => {
|
||||
await expect(async () => matchInputUsingGrep(input)).rejects.toThrowError(/^Failed to run/);
|
||||
});
|
||||
it.concurrent.each(invalidVersionTagInputs)(
|
||||
`rejects non-version tag input '%s'`,
|
||||
async (input) => {
|
||||
await expect(async () => matchInputUsingGrep(input)).rejects.toThrowError(/^Failed to run/);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('branch', () => {
|
||||
it('returns headRef when set', () => {
|
||||
const headReference = jest.spyOn(Versioning, 'headRef', 'get').mockReturnValue('feature-branch-1');
|
||||
const headReference = vi
|
||||
.spyOn(Versioning, 'headRef', 'get')
|
||||
.mockReturnValue('feature-branch-1');
|
||||
|
||||
expect(Versioning.branch).toStrictEqual('feature-branch-1');
|
||||
expect(headReference).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('returns part of Ref when set', () => {
|
||||
jest.spyOn(Versioning, 'headRef', 'get').mockImplementation();
|
||||
const reference = jest.spyOn(Versioning, 'ref', 'get').mockReturnValue('refs/heads/feature-branch-2');
|
||||
vi.spyOn(Versioning, 'headRef', 'get').mockImplementation(() => undefined);
|
||||
const reference = vi
|
||||
.spyOn(Versioning, 'ref', 'get')
|
||||
.mockReturnValue('refs/heads/feature-branch-2');
|
||||
|
||||
expect(Versioning.branch).toStrictEqual('feature-branch-2');
|
||||
expect(reference).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('prefers headRef over ref when set', () => {
|
||||
const headReference = jest.spyOn(Versioning, 'headRef', 'get').mockReturnValue('feature-branch-1');
|
||||
const reference = jest.spyOn(Versioning, 'ref', 'get').mockReturnValue('refs/heads/feature-2');
|
||||
const headReference = vi
|
||||
.spyOn(Versioning, 'headRef', 'get')
|
||||
.mockReturnValue('feature-branch-1');
|
||||
const reference = vi.spyOn(Versioning, 'ref', 'get').mockReturnValue('refs/heads/feature-2');
|
||||
|
||||
expect(Versioning.branch).toStrictEqual('feature-branch-1');
|
||||
expect(headReference).toHaveBeenCalledTimes(1);
|
||||
@@ -81,8 +93,10 @@ describe('Versioning', () => {
|
||||
});
|
||||
|
||||
it('returns undefined when headRef and ref are not set', () => {
|
||||
const headReference = jest.spyOn(Versioning, 'headRef', 'get').mockImplementation();
|
||||
const reference = jest.spyOn(Versioning, 'ref', 'get').mockImplementation();
|
||||
const headReference = vi
|
||||
.spyOn(Versioning, 'headRef', 'get')
|
||||
.mockImplementation(() => undefined);
|
||||
const reference = vi.spyOn(Versioning, 'ref', 'get').mockImplementation(() => undefined);
|
||||
|
||||
expect(Versioning.branch).not.toBeDefined();
|
||||
|
||||
@@ -106,16 +120,19 @@ describe('Versioning', () => {
|
||||
describe('logging git diff', () => {
|
||||
it('calls git diff', async () => {
|
||||
// allowDirtyBuild: true
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
jest.spyOn(Versioning, 'isShallow').mockResolvedValue(true);
|
||||
jest.spyOn(Versioning, 'isDirty').mockResolvedValue(false);
|
||||
jest.spyOn(Versioning, 'fetch').mockImplementation();
|
||||
jest.spyOn(Versioning, 'hasAnyVersionTags').mockResolvedValue(true);
|
||||
jest
|
||||
.spyOn(Versioning, 'parseSemanticVersion')
|
||||
.mockResolvedValue({ match: '', tag: 'mocktag', commits: 'abcdef', hash: '75822BCAF' });
|
||||
const logDiffSpy = jest.spyOn(Versioning, 'logDiff');
|
||||
const gitSpy = jest.spyOn(System, 'run').mockImplementation();
|
||||
vi.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
vi.spyOn(Versioning, 'isShallow').mockResolvedValue(true);
|
||||
vi.spyOn(Versioning, 'isDirty').mockResolvedValue(false);
|
||||
vi.spyOn(Versioning, 'fetch').mockResolvedValue();
|
||||
vi.spyOn(Versioning, 'hasAnyVersionTags').mockResolvedValue(true);
|
||||
vi.spyOn(Versioning, 'parseSemanticVersion').mockResolvedValue({
|
||||
match: '',
|
||||
tag: 'mocktag',
|
||||
commits: 'abcdef',
|
||||
hash: '75822BCAF',
|
||||
});
|
||||
const logDiffSpy = vi.spyOn(Versioning, 'logDiff');
|
||||
const gitSpy = vi.spyOn(System, 'run').mockResolvedValue('');
|
||||
|
||||
await Versioning.generateSemanticVersion();
|
||||
|
||||
@@ -157,12 +174,16 @@ describe('Versioning', () => {
|
||||
|
||||
describe('determineBuildVersion', () => {
|
||||
test.each(['somethingRandom'])('throws for invalid strategy %s', async (strategy) => {
|
||||
await expect(Versioning.determineBuildVersion(strategy, '')).rejects.toThrowErrorMatchingSnapshot();
|
||||
await expect(
|
||||
Versioning.determineBuildVersion(strategy, ''),
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
describe('opt out strategy', () => {
|
||||
it("returns 'none'", async () => {
|
||||
await expect(Versioning.determineBuildVersion('None', 'v1.0')).resolves.toMatchInlineSnapshot(`"none"`);
|
||||
await expect(
|
||||
Versioning.determineBuildVersion('None', 'v1.0'),
|
||||
).resolves.toMatchInlineSnapshot(`"none"`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -170,23 +191,31 @@ describe('Versioning', () => {
|
||||
test.each(['v0.1', '1', 'CamelCase', 'dashed-version'])(
|
||||
'returns the inputVersion for %s',
|
||||
async (inputVersion) => {
|
||||
await expect(Versioning.determineBuildVersion('Custom', inputVersion)).resolves.toStrictEqual(inputVersion);
|
||||
await expect(
|
||||
Versioning.determineBuildVersion('Custom', inputVersion),
|
||||
).resolves.toStrictEqual(inputVersion);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('semantic strategy', () => {
|
||||
it('refers to generateSemanticVersion', async () => {
|
||||
const generateSemanticVersion = jest.spyOn(Versioning, 'generateSemanticVersion').mockResolvedValue('1.3.37');
|
||||
const generateSemanticVersion = vi
|
||||
.spyOn(Versioning, 'generateSemanticVersion')
|
||||
.mockResolvedValue('1.3.37');
|
||||
|
||||
await expect(Versioning.determineBuildVersion('Semantic', '')).resolves.toStrictEqual('1.3.37');
|
||||
await expect(Versioning.determineBuildVersion('Semantic', '')).resolves.toStrictEqual(
|
||||
'1.3.37',
|
||||
);
|
||||
expect(generateSemanticVersion).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tag strategy', () => {
|
||||
it('refers to generateTagVersion', async () => {
|
||||
const generateTagVersion = jest.spyOn(Versioning, 'generateTagVersion').mockResolvedValue('0.1');
|
||||
const generateTagVersion = vi
|
||||
.spyOn(Versioning, 'generateTagVersion')
|
||||
.mockResolvedValue('0.1');
|
||||
|
||||
await expect(Versioning.determineBuildVersion('Tag', '')).resolves.toStrictEqual('0.1');
|
||||
expect(generateTagVersion).toHaveBeenCalledTimes(1);
|
||||
@@ -197,22 +226,24 @@ describe('Versioning', () => {
|
||||
it('throws a not implemented exception', async () => {
|
||||
const strategy = 'Test';
|
||||
// @ts-ignore
|
||||
jest.spyOn(Versioning, 'strategies', 'get').mockReturnValue({ [strategy]: strategy });
|
||||
await expect(Versioning.determineBuildVersion(strategy, '')).rejects.toThrowError(NotImplementedException);
|
||||
vi.spyOn(Versioning, 'strategies', 'get').mockReturnValue({ [strategy]: strategy });
|
||||
await expect(Versioning.determineBuildVersion(strategy, '')).rejects.toThrowError(
|
||||
NotImplementedException,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateTagVersion', () => {
|
||||
it('removes the v', async () => {
|
||||
jest.spyOn(Versioning, 'getTag').mockResolvedValue('v1.3.37');
|
||||
vi.spyOn(Versioning, 'getTag').mockResolvedValue('v1.3.37');
|
||||
await expect(Versioning.generateTagVersion()).resolves.toStrictEqual('1.3.37');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseSemanticVersion', () => {
|
||||
it('returns the named parts', async () => {
|
||||
jest.spyOn(Versioning, 'getVersionDescription').mockResolvedValue('v0.1-2-g12345678');
|
||||
vi.spyOn(Versioning, 'getVersionDescription').mockResolvedValue('v0.1-2-g12345678');
|
||||
|
||||
await expect(Versioning.parseSemanticVersion()).resolves.toMatchObject({
|
||||
tag: '0.1',
|
||||
@@ -222,7 +253,7 @@ describe('Versioning', () => {
|
||||
});
|
||||
|
||||
it('throws when no match could be made', async () => {
|
||||
jest.spyOn(Versioning, 'getVersionDescription').mockResolvedValue('no-match-can-be-made');
|
||||
vi.spyOn(Versioning, 'getVersionDescription').mockResolvedValue('no-match-can-be-made');
|
||||
|
||||
await expect(Versioning.parseSemanticVersion()).toMatchObject({});
|
||||
});
|
||||
@@ -231,7 +262,7 @@ describe('Versioning', () => {
|
||||
describe('getVersionDescription', () => {
|
||||
it('returns the commands output', async () => {
|
||||
const runOutput = 'someValue';
|
||||
jest.spyOn(System, 'run').mockResolvedValue(runOutput);
|
||||
vi.spyOn(System, 'run').mockResolvedValue(runOutput);
|
||||
await expect(Versioning.getVersionDescription()).resolves.toStrictEqual(runOutput);
|
||||
});
|
||||
});
|
||||
@@ -239,27 +270,27 @@ describe('Versioning', () => {
|
||||
describe('isShallow', () => {
|
||||
it('returns true when the repo is shallow', async () => {
|
||||
const runOutput = 'true\n';
|
||||
jest.spyOn(System, 'run').mockResolvedValue(runOutput);
|
||||
vi.spyOn(System, 'run').mockResolvedValue(runOutput);
|
||||
await expect(Versioning.isShallow()).resolves.toStrictEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when the repo is not shallow', async () => {
|
||||
const runOutput = 'false\n';
|
||||
jest.spyOn(System, 'run').mockResolvedValue(runOutput);
|
||||
vi.spyOn(System, 'run').mockResolvedValue(runOutput);
|
||||
await expect(Versioning.isShallow()).resolves.toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetch', () => {
|
||||
it('awaits the command', async () => {
|
||||
jest.spyOn(core, 'warning').mockImplementation(() => {});
|
||||
jest.spyOn(System, 'run').mockImplementation();
|
||||
vi.spyOn(core, 'warning').mockImplementation(() => {});
|
||||
vi.spyOn(System, 'run').mockResolvedValue('');
|
||||
await expect(Versioning.fetch()).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('falls back to the second strategy when the first fails', async () => {
|
||||
jest.spyOn(core, 'warning').mockImplementation(() => {});
|
||||
const gitFetch = jest.spyOn(System, 'run').mockImplementation();
|
||||
vi.spyOn(core, 'warning').mockImplementation(() => {});
|
||||
const gitFetch = vi.spyOn(System, 'run').mockResolvedValue('');
|
||||
|
||||
await expect(Versioning.fetch()).resolves.not.toThrow();
|
||||
expect(gitFetch).toHaveBeenCalledTimes(1);
|
||||
@@ -268,12 +299,12 @@ describe('Versioning', () => {
|
||||
|
||||
describe('generateSemanticVersion', () => {
|
||||
it('returns a proper version from description', async () => {
|
||||
jest.spyOn(System, 'run').mockImplementation();
|
||||
jest.spyOn(core, 'info').mockImplementation(() => {});
|
||||
jest.spyOn(Versioning, 'isDirty').mockResolvedValue(false);
|
||||
jest.spyOn(Versioning, 'hasAnyVersionTags').mockResolvedValue(true);
|
||||
jest.spyOn(Versioning, 'getTotalNumberOfCommits').mockResolvedValue(2);
|
||||
jest.spyOn(Versioning, 'parseSemanticVersion').mockResolvedValue({
|
||||
vi.spyOn(System, 'run').mockResolvedValue('');
|
||||
vi.spyOn(core, 'info').mockImplementation(() => {});
|
||||
vi.spyOn(Versioning, 'isDirty').mockResolvedValue(false);
|
||||
vi.spyOn(Versioning, 'hasAnyVersionTags').mockResolvedValue(true);
|
||||
vi.spyOn(Versioning, 'getTotalNumberOfCommits').mockResolvedValue(2);
|
||||
vi.spyOn(Versioning, 'parseSemanticVersion').mockResolvedValue({
|
||||
match: '0.1-2-g1b345678',
|
||||
tag: '0.1',
|
||||
commits: '2',
|
||||
@@ -284,19 +315,19 @@ describe('Versioning', () => {
|
||||
});
|
||||
|
||||
it('throws when dirty', async () => {
|
||||
jest.spyOn(System, 'run').mockImplementation();
|
||||
jest.spyOn(core, 'info').mockImplementation(() => {});
|
||||
jest.spyOn(Versioning, 'isDirty').mockResolvedValue(true);
|
||||
vi.spyOn(System, 'run').mockResolvedValue('');
|
||||
vi.spyOn(core, 'info').mockImplementation(() => {});
|
||||
vi.spyOn(Versioning, 'isDirty').mockResolvedValue(true);
|
||||
await expect(Versioning.generateSemanticVersion()).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it('falls back to commits only, when no tags are present', async () => {
|
||||
const commits = Math.round(Math.random() * 10);
|
||||
jest.spyOn(System, 'run').mockImplementation();
|
||||
jest.spyOn(core, 'info').mockImplementation(() => {});
|
||||
jest.spyOn(Versioning, 'isDirty').mockResolvedValue(false);
|
||||
jest.spyOn(Versioning, 'hasAnyVersionTags').mockResolvedValue(false);
|
||||
jest.spyOn(Versioning, 'getTotalNumberOfCommits').mockResolvedValue(commits);
|
||||
vi.spyOn(System, 'run').mockResolvedValue('');
|
||||
vi.spyOn(core, 'info').mockImplementation(() => {});
|
||||
vi.spyOn(Versioning, 'isDirty').mockResolvedValue(false);
|
||||
vi.spyOn(Versioning, 'hasAnyVersionTags').mockResolvedValue(false);
|
||||
vi.spyOn(Versioning, 'getTotalNumberOfCommits').mockResolvedValue(commits);
|
||||
|
||||
await expect(Versioning.generateSemanticVersion()).resolves.toStrictEqual(`0.0.${commits}`);
|
||||
});
|
||||
@@ -305,13 +336,13 @@ describe('Versioning', () => {
|
||||
describe('isDirty', () => {
|
||||
it('returns true when there are files listed', async () => {
|
||||
const runOutput = 'file.ext\nfile2.ext';
|
||||
jest.spyOn(System, 'run').mockResolvedValue(runOutput);
|
||||
vi.spyOn(System, 'run').mockResolvedValue(runOutput);
|
||||
await expect(Versioning.isDirty()).resolves.toStrictEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when there is no output', async () => {
|
||||
const runOutput = '';
|
||||
jest.spyOn(System, 'run').mockResolvedValue(runOutput);
|
||||
vi.spyOn(System, 'run').mockResolvedValue(runOutput);
|
||||
await expect(Versioning.isDirty()).resolves.toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
@@ -319,7 +350,7 @@ describe('Versioning', () => {
|
||||
describe('getTag', () => {
|
||||
it('returns the commands output', async () => {
|
||||
const runOutput = 'v1.0';
|
||||
jest.spyOn(System, 'run').mockResolvedValue(runOutput);
|
||||
vi.spyOn(System, 'run').mockResolvedValue(runOutput);
|
||||
await expect(Versioning.getTag()).resolves.toStrictEqual(runOutput);
|
||||
});
|
||||
});
|
||||
@@ -327,20 +358,20 @@ describe('Versioning', () => {
|
||||
describe('hasAnyVersionTags', () => {
|
||||
it('returns false when the command returns 0', async () => {
|
||||
const runOutput = '0';
|
||||
jest.spyOn(System, 'run').mockResolvedValue(runOutput);
|
||||
vi.spyOn(System, 'run').mockResolvedValue(runOutput);
|
||||
await expect(Versioning.hasAnyVersionTags()).resolves.toStrictEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when the command returns >= 0', async () => {
|
||||
const runOutput = '9';
|
||||
jest.spyOn(System, 'run').mockResolvedValue(runOutput);
|
||||
vi.spyOn(System, 'run').mockResolvedValue(runOutput);
|
||||
await expect(Versioning.hasAnyVersionTags()).resolves.toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTotalNumberOfCommits', () => {
|
||||
it('returns a number from the command', async () => {
|
||||
jest.spyOn(System, 'run').mockResolvedValue('9');
|
||||
vi.spyOn(System, 'run').mockResolvedValue('9');
|
||||
await expect(Versioning.getTotalNumberOfCommits()).resolves.toStrictEqual(9);
|
||||
});
|
||||
});
|
||||
|
||||
+18
-6
@@ -71,7 +71,9 @@ export default class Versioning {
|
||||
static async determineBuildVersion(strategy: string, inputVersion: string): Promise<string> {
|
||||
// Validate input
|
||||
if (!Object.hasOwnProperty.call(this.strategies, strategy)) {
|
||||
throw new ValidationError(`Versioning strategy should be one of ${Object.values(this.strategies).join(', ')}.`);
|
||||
throw new ValidationError(
|
||||
`Versioning strategy should be one of ${Object.values(this.strategies).join(', ')}.`,
|
||||
);
|
||||
}
|
||||
|
||||
switch (strategy) {
|
||||
@@ -122,7 +124,9 @@ export default class Versioning {
|
||||
|
||||
// Ensure 3 digits (commits should always be patch level)
|
||||
const [major, minor, patch] = `${tag}.${commits}`.split('.');
|
||||
const threeDigitVersion = /^\d+$/.test(patch) ? `${major}.${minor}.${patch}` : `${major}.0.${minor}`;
|
||||
const threeDigitVersion = /^\d+$/.test(patch)
|
||||
? `${major}.${minor}.${patch}`
|
||||
: `${major}.0.${minor}`;
|
||||
|
||||
core.info(`Found semantic version ${threeDigitVersion} for ${this.branch}@${hash}`);
|
||||
|
||||
@@ -168,7 +172,9 @@ export default class Versioning {
|
||||
}
|
||||
}
|
||||
|
||||
core.warning(`Failed to parse git describe output or version can not be determined through: "${description}".`);
|
||||
core.warning(
|
||||
`Failed to parse git describe output or version can not be determined through: "${description}".`,
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -207,7 +213,9 @@ export default class Versioning {
|
||||
* identifies the current commit.
|
||||
*/
|
||||
static async getVersionDescription() {
|
||||
const versionTags = (await this.git(['tag', '--list', '--merged', 'HEAD', '--sort=-creatordate']))
|
||||
const versionTags = (
|
||||
await this.git(['tag', '--list', '--merged', 'HEAD', '--sort=-creatordate'])
|
||||
)
|
||||
.split('\n')
|
||||
.filter((tag) => new RegExp(this.grepCompatibleInputVersionRegex).test(tag));
|
||||
|
||||
@@ -218,7 +226,9 @@ export default class Versioning {
|
||||
}
|
||||
|
||||
const latestVersionTag = versionTags[0];
|
||||
const commitsCount = (await this.git(['rev-list', `${latestVersionTag}..HEAD`, '--count'])).trim();
|
||||
const commitsCount = (
|
||||
await this.git(['rev-list', `${latestVersionTag}..HEAD`, '--count'])
|
||||
).trim();
|
||||
const commitHash = (await this.git(['rev-parse', '--short', 'HEAD'])).trim();
|
||||
|
||||
return `${latestVersionTag}-${commitsCount}-g${commitHash}`;
|
||||
@@ -253,7 +263,9 @@ export default class Versioning {
|
||||
*/
|
||||
static async hasAnyVersionTags() {
|
||||
const numberOfTagsAsString = await System.run('sh', undefined, {
|
||||
input: Buffer.from(`git tag --list --merged HEAD | grep -E '${this.grepCompatibleInputVersionRegex}' | wc -l`),
|
||||
input: Buffer.from(
|
||||
`git tag --list --merged HEAD | grep -E '${this.grepCompatibleInputVersionRegex}' | wc -l`,
|
||||
),
|
||||
cwd: Input.projectPath,
|
||||
silent: false,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { afterEach, beforeEach } from 'vitest';
|
||||
|
||||
// Fail tests when console.error / console.warn etc are called from
|
||||
// production code under test. Mirrors the jest-fail-on-console behaviour
|
||||
// the previous jest setup enforced. Tests can opt-out by replacing the
|
||||
// method with vi.spyOn(console, 'error') for the duration of that test.
|
||||
const original = {
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
error: console.error,
|
||||
assert: console.assert,
|
||||
};
|
||||
|
||||
const fail = (level: 'log' | 'warn' | 'error' | 'assert', args: unknown[]) => {
|
||||
throw new Error(
|
||||
`console.${level} was called with: ${args.map(String).join(' ')}\n` +
|
||||
`Tests must use vi.spyOn(console, '${level}') if console output is expected.`,
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
console.log = (...args: unknown[]) => fail('log', args);
|
||||
console.warn = (...args: unknown[]) => fail('warn', args);
|
||||
console.error = (...args: unknown[]) => fail('error', args);
|
||||
console.assert = ((...args: unknown[]) => fail('assert', args)) as typeof console.assert;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.assign(console, original);
|
||||
});
|
||||
Vendored
+19
-15
@@ -1,28 +1,32 @@
|
||||
/**
|
||||
* Type declarations for @game-ci/orchestrator.
|
||||
*
|
||||
* This optional dependency provides remote build orchestration and plugin
|
||||
* services. When installed, the plugin loader in orchestrator-plugin.ts
|
||||
* This optional dependency is one implementation of unity-builder's generic
|
||||
* plugin lifecycle. When installed, the plugin loader in plugin.ts
|
||||
* dynamically imports it.
|
||||
*/
|
||||
declare module '@game-ci/orchestrator' {
|
||||
interface Plugin {
|
||||
initialize(coreParams: Record<string, any>, workspace: string): Promise<void>;
|
||||
canHandleBuild(): boolean;
|
||||
handleBuild(baseImage: string): Promise<{ exitCode: number; fallbackToLocal?: boolean }>;
|
||||
beforeLocalBuild(workspace: string): Promise<void>;
|
||||
afterLocalBuild(workspace: string, exitCode: number): Promise<void>;
|
||||
handlePostBuild(exitCode: number): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unity-builder plugin instance.
|
||||
* The plugin reads its own configuration from environment variables and
|
||||
* GitHub Actions inputs; unity-builder does not need to proxy them.
|
||||
*/
|
||||
export function createPlugin(): Plugin;
|
||||
|
||||
// Legacy export — kept for backward compatibility with CLI and direct consumers
|
||||
export const Orchestrator: {
|
||||
run: (arg0: any, arg1: string) => Promise<{ BuildSucceeded: boolean; BuildResults: string }>;
|
||||
setup: (arg0: any) => Promise<void>;
|
||||
buildParameters: any;
|
||||
lockedWorkspace: string;
|
||||
};
|
||||
|
||||
export const BuildReliabilityService: any;
|
||||
export const TestWorkflowService: any;
|
||||
export const HotRunnerService: any;
|
||||
export const OutputService: any;
|
||||
export const OutputTypeRegistry: any;
|
||||
export const ArtifactUploadHandler: any;
|
||||
export const IncrementalSyncService: any;
|
||||
export const ChildWorkspaceService: any;
|
||||
export const LocalCacheService: any;
|
||||
export const SubmoduleProfileService: any;
|
||||
export const LfsAgentService: any;
|
||||
export const GitHooksService: any;
|
||||
}
|
||||
|
||||
+13
-8
@@ -1,14 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
|
||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
||||
"outDir": "./lib" /* Redirect output structure to the directory. */,
|
||||
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*", "types/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules", "dist", "lib"]
|
||||
}
|
||||
|
||||
Vendored
-1
@@ -13,4 +13,3 @@ declare module 'shell-quote' {
|
||||
*/
|
||||
export function parse(cmd: string): string[];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
globals: true,
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
include: ['src/**/*.test.ts'],
|
||||
coverage: {
|
||||
provider: 'istanbul',
|
||||
reporter: ['text', 'html', 'lcov'],
|
||||
include: ['src/**/*.ts'],
|
||||
exclude: ['src/**/*.test.ts', 'src/index.ts'],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user