fix(testing): use async exec for parallel test group execution

Replace execSync with promisified exec so Promise.all actually runs
test groups in parallel. Add native timeout support via exec options.
Add 50MB maxBuffer for large Unity output. Fix ESLint violations
(variable naming, padding lines, array push consolidation).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
frostebite
2026-03-05 12:51:52 +00:00
parent 118671778f
commit 5e54bcd4dd
3 changed files with 111 additions and 105 deletions
Generated Vendored
+49 -51
View File
@@ -10416,11 +10416,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.TestWorkflowService = void 0; exports.TestWorkflowService = void 0;
const node_child_process_1 = __nccwpck_require__(17718); const node_child_process_1 = __nccwpck_require__(17718);
const node_util_1 = __nccwpck_require__(47261);
const node_path_1 = __importDefault(__nccwpck_require__(49411)); const node_path_1 = __importDefault(__nccwpck_require__(49411));
const core = __importStar(__nccwpck_require__(42186)); const core = __importStar(__nccwpck_require__(42186));
const test_suite_parser_1 = __nccwpck_require__(92105); const test_suite_parser_1 = __nccwpck_require__(92105);
const taxonomy_filter_service_1 = __nccwpck_require__(9983); const taxonomy_filter_service_1 = __nccwpck_require__(9983);
const test_result_reporter_1 = __nccwpck_require__(25147); const test_result_reporter_1 = __nccwpck_require__(25147);
const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
/** /**
* Main entry point for the test workflow engine. * Main entry point for the test workflow engine.
* Orchestrates parsing of YAML suite definitions, resolving run order, * Orchestrates parsing of YAML suite definitions, resolving run order,
@@ -10433,7 +10435,7 @@ class TestWorkflowService {
* group sequentially (runs within a group execute concurrently), and * group sequentially (runs within a group execute concurrently), and
* collects all results. * collects all results.
*/ */
static async executeTestSuite(suitePath, params) { static async executeTestSuite(suitePath, parameters) {
core.info(`[TestWorkflow] Loading test suite from: ${suitePath}`); core.info(`[TestWorkflow] Loading test suite from: ${suitePath}`);
const suite = test_suite_parser_1.TestSuiteParser.parseSuiteFile(suitePath); const suite = test_suite_parser_1.TestSuiteParser.parseSuiteFile(suitePath);
core.info(`[TestWorkflow] Suite '${suite.name}' loaded with ${suite.runs.length} run(s)`); core.info(`[TestWorkflow] Suite '${suite.name}' loaded with ${suite.runs.length} run(s)`);
@@ -10449,7 +10451,7 @@ class TestWorkflowService {
const runNames = group.map((r) => r.name).join(', '); const runNames = group.map((r) => r.name).join(', ');
core.info(`[TestWorkflow] Executing group ${groupIndex}/${groups.length}: [${runNames}]`); core.info(`[TestWorkflow] Executing group ${groupIndex}/${groups.length}: [${runNames}]`);
// Execute runs within a group concurrently // Execute runs within a group concurrently
const groupResults = await Promise.all(group.map((run) => TestWorkflowService.executeTestRun(run, params))); const groupResults = await Promise.all(group.map((run) => TestWorkflowService.executeTestRun(run, parameters)));
allResults.push(...groupResults); allResults.push(...groupResults);
// Check for failures -- if any run in this group failed, log a warning // Check for failures -- if any run in this group failed, log a warning
// but continue to the next group (fail-forward for maximum feedback) // but continue to the next group (fail-forward for maximum feedback)
@@ -10463,8 +10465,8 @@ class TestWorkflowService {
const summary = test_result_reporter_1.TestResultReporter.generateSummary(allResults); const summary = test_result_reporter_1.TestResultReporter.generateSummary(allResults);
core.info(summary); core.info(summary);
// Write results if output path is configured // Write results if output path is configured
const resultPath = params.testResultPath; const resultPath = parameters.testResultPath;
const resultFormat = params.testResultFormat; const resultFormat = parameters.testResultFormat;
if (resultPath) { if (resultPath) {
test_result_reporter_1.TestResultReporter.writeResults(allResults, resultPath, resultFormat); test_result_reporter_1.TestResultReporter.writeResults(allResults, resultPath, resultFormat);
core.info(`[TestWorkflow] Results written to: ${resultPath}`); core.info(`[TestWorkflow] Results written to: ${resultPath}`);
@@ -10474,27 +10476,30 @@ class TestWorkflowService {
/** /**
* Execute a single test run definition. * Execute a single test run definition.
* Builds the Unity CLI arguments based on the run configuration (edit mode, * Builds the Unity CLI arguments based on the run configuration (edit mode,
* play mode, built client) and taxonomy filters, executes the command, and * play mode, built client) and taxonomy filters, executes the command
* parses the result output. * asynchronously, and parses the result output.
*
* Uses promisified exec instead of execSync so that Promise.all can
* actually run multiple test groups in parallel without blocking the
* Node.js event loop.
*/ */
static async executeTestRun(run, params) { static async executeTestRun(run, parameters) {
core.info(`[TestWorkflow] Starting run: '${run.name}'`); core.info(`[TestWorkflow] Starting run: '${run.name}'`);
const args = TestWorkflowService.buildUnityArgs(run, params); const unityArguments = TestWorkflowService.buildUnityArgs(run, parameters);
const timeoutMs = (run.timeout ?? 600) * 1000; const timeoutMs = (run.timeout ?? 600) * 1000;
core.info(`[TestWorkflow] Unity args: ${args}`); core.info(`[TestWorkflow] Unity args: ${unityArguments}`);
const startTime = Date.now(); const startTime = Date.now();
try { try {
const resultDir = node_path_1.default.join(params.testResultPath ?? './test-results', run.name); const resultDirectory = node_path_1.default.join(parameters.testResultPath ?? './test-results', run.name);
const resultFile = node_path_1.default.join(resultDir, 'results.xml'); const resultFile = node_path_1.default.join(resultDirectory, 'results.xml');
// Build the full Unity command // Build the full Unity command
const unityPath = TestWorkflowService.resolveUnityPath(params); const unityPath = TestWorkflowService.resolveUnityPath(parameters);
const command = `"${unityPath}" ${args} -testResults "${resultFile}"`; const command = `"${unityPath}" ${unityArguments} -testResults "${resultFile}"`;
core.info(`[TestWorkflow] Executing: ${command}`); core.info(`[TestWorkflow] Executing: ${command}`);
(0, node_child_process_1.execSync)(command, { await execAsync(command, {
timeout: timeoutMs, timeout: timeoutMs,
stdio: 'pipe', maxBuffer: 50 * 1024 * 1024,
encoding: 'utf8', cwd: parameters.projectPath || process.cwd(),
cwd: params.projectPath || process.cwd(),
}); });
const duration = (Date.now() - startTime) / 1000; const duration = (Date.now() - startTime) / 1000;
// Parse the result file // Parse the result file
@@ -10519,16 +10524,18 @@ class TestWorkflowService {
} }
catch (error) { catch (error) {
const duration = (Date.now() - startTime) / 1000; const duration = (Date.now() - startTime) / 1000;
const isTimeout = error.killed || error.signal === 'SIGTERM'; // The promisified exec sets error.killed when the process is terminated
// due to timeout, and error.signal will be 'SIGTERM'
const isTimeout = error.killed === true || error.signal === 'SIGTERM';
if (isTimeout) { if (isTimeout) {
core.error(`[TestWorkflow] Run '${run.name}' timed out after ${run.timeout}s`); core.error(`[TestWorkflow] Run '${run.name}' timed out after ${run.timeout ?? 600}s`);
} }
else { else {
core.error(`[TestWorkflow] Run '${run.name}' failed: ${error.message}`); core.error(`[TestWorkflow] Run '${run.name}' failed: ${error.message}`);
} }
// Try to parse partial results even on failure // Try to parse partial results even on failure
const resultDir = node_path_1.default.join(params.testResultPath ?? './test-results', run.name); const resultDirectory = node_path_1.default.join(parameters.testResultPath ?? './test-results', run.name);
const resultFile = node_path_1.default.join(resultDir, 'results.xml'); const resultFile = node_path_1.default.join(resultDirectory, 'results.xml');
try { try {
const result = test_result_reporter_1.TestResultReporter.parseJUnitResults(resultFile); const result = test_result_reporter_1.TestResultReporter.parseJUnitResults(resultFile);
result.runName = run.name; result.runName = run.name;
@@ -10547,7 +10554,7 @@ class TestWorkflowService {
testName: isTimeout ? 'Timeout' : 'ExecutionError', testName: isTimeout ? 'Timeout' : 'ExecutionError',
className: run.name, className: run.name,
message: isTimeout message: isTimeout
? `Test run timed out after ${run.timeout}s` ? `Test run timed out after ${run.timeout ?? 600}s`
: error.message ?? 'Unknown execution error', : error.message ?? 'Unknown execution error',
stackTrace: error.stderr ?? undefined, stackTrace: error.stderr ?? undefined,
}, },
@@ -10559,65 +10566,56 @@ class TestWorkflowService {
/** /**
* Build Unity CLI arguments for a test run based on its configuration. * Build Unity CLI arguments for a test run based on its configuration.
*/ */
static buildUnityArgs(run, params) { static buildUnityArgs(run, parameters) {
const args = []; const unityArguments = ['-batchmode', '-nographics'];
// Batch mode and no-graphics for CI
args.push('-batchmode');
args.push('-nographics');
// Project path // Project path
if (params.projectPath) { if (parameters.projectPath) {
args.push(`-projectPath "${params.projectPath}"`); unityArguments.push(`-projectPath "${parameters.projectPath}"`);
} }
// Test mode // Test mode
if (run.builtClient && run.builtClientPath) { if (run.builtClient && run.builtClientPath) {
// Built client testing: run tests against a built player // Built client testing: run tests against a built player
args.push('-runTests'); unityArguments.push('-runTests', `-testPlatform StandalonePlayer`, `-assemblyNames Assembly-CSharp-Tests`, `-builtPlayerPath "${run.builtClientPath}"`);
args.push(`-testPlatform StandalonePlayer`);
args.push(`-assemblyNames Assembly-CSharp-Tests`);
args.push(`-builtPlayerPath "${run.builtClientPath}"`);
} }
else if (run.editMode && run.playMode) { else if (run.editMode && run.playMode) {
// Both modes: run EditMode first, then PlayMode will require a separate invocation // Both modes: run EditMode first, then PlayMode will require a separate invocation
// For combined mode, use EditMode (the service handles sequencing) // For combined mode, use EditMode (the service handles sequencing)
args.push('-runTests'); unityArguments.push('-runTests', '-testPlatform EditMode');
args.push('-testPlatform EditMode');
} }
else if (run.playMode) { else if (run.playMode) {
args.push('-runTests'); unityArguments.push('-runTests', '-testPlatform PlayMode');
args.push('-testPlatform PlayMode');
} }
else if (run.editMode) { else if (run.editMode) {
args.push('-runTests'); unityArguments.push('-runTests', '-testPlatform EditMode');
args.push('-testPlatform EditMode');
} }
// Apply taxonomy filters // Apply taxonomy filters
if (run.filters && Object.keys(run.filters).length > 0) { if (run.filters && Object.keys(run.filters).length > 0) {
const filterArgs = taxonomy_filter_service_1.TaxonomyFilterService.buildFilterArgs(run.filters); const filterArguments = taxonomy_filter_service_1.TaxonomyFilterService.buildFilterArgs(run.filters);
if (filterArgs) { if (filterArguments) {
args.push(filterArgs); unityArguments.push(filterArguments);
} }
} }
// Target platform // Target platform
if (params.targetPlatform) { if (parameters.targetPlatform) {
args.push(`-buildTarget ${params.targetPlatform}`); unityArguments.push(`-buildTarget ${parameters.targetPlatform}`);
} }
return args.join(' '); return unityArguments.join(' ');
} }
/** /**
* Resolve the path to the Unity editor executable. * Resolve the path to the Unity editor executable.
*/ */
static resolveUnityPath(params) { static resolveUnityPath(parameters) {
// In CI, Unity path is typically set via environment or the docker container // In CI, Unity path is typically set via environment or the docker container
const envUnityPath = process.env.UNITY_PATH ?? process.env.UNITY_EDITOR; const environmentUnityPath = process.env.UNITY_PATH ?? process.env.UNITY_EDITOR;
if (envUnityPath) { if (environmentUnityPath) {
return envUnityPath; return environmentUnityPath;
} }
// Default paths by platform // Default paths by platform
if (process.platform === 'win32') { if (process.platform === 'win32') {
return `C:/Program Files/Unity/Hub/Editor/${params.editorVersion}/Editor/Unity.exe`; return `C:/Program Files/Unity/Hub/Editor/${parameters.editorVersion}/Editor/Unity.exe`;
} }
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
return `/Applications/Unity/Hub/Editor/${params.editorVersion}/Unity.app/Contents/MacOS/Unity`; return `/Applications/Unity/Hub/Editor/${parameters.editorVersion}/Unity.app/Contents/MacOS/Unity`;
} }
// Linux default (Docker container path) // Linux default (Docker container path)
return '/opt/unity/Editor/Unity'; return '/opt/unity/Editor/Unity';
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -1,4 +1,5 @@
import { execSync } from 'node:child_process'; import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import path from 'node:path'; import path from 'node:path';
import * as core from '@actions/core'; import * as core from '@actions/core';
import BuildParameters from '../../../build-parameters'; import BuildParameters from '../../../build-parameters';
@@ -7,6 +8,8 @@ import { TaxonomyFilterService } from './taxonomy-filter-service';
import { TestResultReporter } from './test-result-reporter'; import { TestResultReporter } from './test-result-reporter';
import { TestRunDefinition, TestResult } from './test-workflow-types'; import { TestRunDefinition, TestResult } from './test-workflow-types';
const execAsync = promisify(exec);
/** /**
* Main entry point for the test workflow engine. * Main entry point for the test workflow engine.
* Orchestrates parsing of YAML suite definitions, resolving run order, * Orchestrates parsing of YAML suite definitions, resolving run order,
@@ -19,7 +22,7 @@ export class TestWorkflowService {
* group sequentially (runs within a group execute concurrently), and * group sequentially (runs within a group execute concurrently), and
* collects all results. * collects all results.
*/ */
static async executeTestSuite(suitePath: string, params: BuildParameters): Promise<TestResult[]> { static async executeTestSuite(suitePath: string, parameters: BuildParameters): Promise<TestResult[]> {
core.info(`[TestWorkflow] Loading test suite from: ${suitePath}`); core.info(`[TestWorkflow] Loading test suite from: ${suitePath}`);
const suite = TestSuiteParser.parseSuiteFile(suitePath); const suite = TestSuiteParser.parseSuiteFile(suitePath);
@@ -41,7 +44,7 @@ export class TestWorkflowService {
core.info(`[TestWorkflow] Executing group ${groupIndex}/${groups.length}: [${runNames}]`); core.info(`[TestWorkflow] Executing group ${groupIndex}/${groups.length}: [${runNames}]`);
// Execute runs within a group concurrently // Execute runs within a group concurrently
const groupResults = await Promise.all(group.map((run) => TestWorkflowService.executeTestRun(run, params))); const groupResults = await Promise.all(group.map((run) => TestWorkflowService.executeTestRun(run, parameters)));
allResults.push(...groupResults); allResults.push(...groupResults);
@@ -59,8 +62,8 @@ export class TestWorkflowService {
core.info(summary); core.info(summary);
// Write results if output path is configured // Write results if output path is configured
const resultPath = params.testResultPath; const resultPath = parameters.testResultPath;
const resultFormat = params.testResultFormat; const resultFormat = parameters.testResultFormat;
if (resultPath) { if (resultPath) {
TestResultReporter.writeResults(allResults, resultPath, resultFormat as 'junit' | 'json' | 'both'); TestResultReporter.writeResults(allResults, resultPath, resultFormat as 'junit' | 'json' | 'both');
core.info(`[TestWorkflow] Results written to: ${resultPath}`); core.info(`[TestWorkflow] Results written to: ${resultPath}`);
@@ -72,34 +75,37 @@ export class TestWorkflowService {
/** /**
* Execute a single test run definition. * Execute a single test run definition.
* Builds the Unity CLI arguments based on the run configuration (edit mode, * Builds the Unity CLI arguments based on the run configuration (edit mode,
* play mode, built client) and taxonomy filters, executes the command, and * play mode, built client) and taxonomy filters, executes the command
* parses the result output. * asynchronously, and parses the result output.
*
* Uses promisified exec instead of execSync so that Promise.all can
* actually run multiple test groups in parallel without blocking the
* Node.js event loop.
*/ */
static async executeTestRun(run: TestRunDefinition, params: BuildParameters): Promise<TestResult> { static async executeTestRun(run: TestRunDefinition, parameters: BuildParameters): Promise<TestResult> {
core.info(`[TestWorkflow] Starting run: '${run.name}'`); core.info(`[TestWorkflow] Starting run: '${run.name}'`);
const args = TestWorkflowService.buildUnityArgs(run, params); const unityArguments = TestWorkflowService.buildUnityArgs(run, parameters);
const timeoutMs = (run.timeout ?? 600) * 1000; const timeoutMs = (run.timeout ?? 600) * 1000;
core.info(`[TestWorkflow] Unity args: ${args}`); core.info(`[TestWorkflow] Unity args: ${unityArguments}`);
const startTime = Date.now(); const startTime = Date.now();
try { try {
const resultDir = path.join(params.testResultPath ?? './test-results', run.name); const resultDirectory = path.join(parameters.testResultPath ?? './test-results', run.name);
const resultFile = path.join(resultDir, 'results.xml'); const resultFile = path.join(resultDirectory, 'results.xml');
// Build the full Unity command // Build the full Unity command
const unityPath = TestWorkflowService.resolveUnityPath(params); const unityPath = TestWorkflowService.resolveUnityPath(parameters);
const command = `"${unityPath}" ${args} -testResults "${resultFile}"`; const command = `"${unityPath}" ${unityArguments} -testResults "${resultFile}"`;
core.info(`[TestWorkflow] Executing: ${command}`); core.info(`[TestWorkflow] Executing: ${command}`);
execSync(command, { await execAsync(command, {
timeout: timeoutMs, timeout: timeoutMs,
stdio: 'pipe', maxBuffer: 50 * 1024 * 1024, // 50 MB to handle large Unity output
encoding: 'utf8', cwd: parameters.projectPath || process.cwd(),
cwd: params.projectPath || process.cwd(),
}); });
const duration = (Date.now() - startTime) / 1000; const duration = (Date.now() - startTime) / 1000;
@@ -109,10 +115,12 @@ export class TestWorkflowService {
const result = TestResultReporter.parseJUnitResults(resultFile); const result = TestResultReporter.parseJUnitResults(resultFile);
result.runName = run.name; result.runName = run.name;
result.duration = duration; result.duration = duration;
return result; return result;
} catch { } catch {
// Result file may not exist if Unity exited early // Result file may not exist if Unity exited early
core.warning(`[TestWorkflow] Could not parse results for run '${run.name}' -- result file may be missing`); core.warning(`[TestWorkflow] Could not parse results for run '${run.name}' -- result file may be missing`);
return { return {
runName: run.name, runName: run.name,
passed: 0, passed: 0,
@@ -124,22 +132,26 @@ export class TestWorkflowService {
} }
} catch (error: any) { } catch (error: any) {
const duration = (Date.now() - startTime) / 1000; const duration = (Date.now() - startTime) / 1000;
const isTimeout = error.killed || error.signal === 'SIGTERM';
// The promisified exec sets error.killed when the process is terminated
// due to timeout, and error.signal will be 'SIGTERM'
const isTimeout = error.killed === true || error.signal === 'SIGTERM';
if (isTimeout) { if (isTimeout) {
core.error(`[TestWorkflow] Run '${run.name}' timed out after ${run.timeout}s`); core.error(`[TestWorkflow] Run '${run.name}' timed out after ${run.timeout ?? 600}s`);
} else { } else {
core.error(`[TestWorkflow] Run '${run.name}' failed: ${error.message}`); core.error(`[TestWorkflow] Run '${run.name}' failed: ${error.message}`);
} }
// Try to parse partial results even on failure // Try to parse partial results even on failure
const resultDir = path.join(params.testResultPath ?? './test-results', run.name); const resultDirectory = path.join(parameters.testResultPath ?? './test-results', run.name);
const resultFile = path.join(resultDir, 'results.xml'); const resultFile = path.join(resultDirectory, 'results.xml');
try { try {
const result = TestResultReporter.parseJUnitResults(resultFile); const result = TestResultReporter.parseJUnitResults(resultFile);
result.runName = run.name; result.runName = run.name;
result.duration = duration; result.duration = duration;
return result; return result;
} catch { } catch {
return { return {
@@ -153,7 +165,7 @@ export class TestWorkflowService {
testName: isTimeout ? 'Timeout' : 'ExecutionError', testName: isTimeout ? 'Timeout' : 'ExecutionError',
className: run.name, className: run.name,
message: isTimeout message: isTimeout
? `Test run timed out after ${run.timeout}s` ? `Test run timed out after ${run.timeout ?? 600}s`
: error.message ?? 'Unknown execution error', : error.message ?? 'Unknown execution error',
stackTrace: error.stderr ?? undefined, stackTrace: error.stderr ?? undefined,
}, },
@@ -166,70 +178,66 @@ export class TestWorkflowService {
/** /**
* Build Unity CLI arguments for a test run based on its configuration. * Build Unity CLI arguments for a test run based on its configuration.
*/ */
static buildUnityArgs(run: TestRunDefinition, params: BuildParameters): string { static buildUnityArgs(run: TestRunDefinition, parameters: BuildParameters): string {
const args: string[] = []; const unityArguments: string[] = ['-batchmode', '-nographics'];
// Batch mode and no-graphics for CI
args.push('-batchmode');
args.push('-nographics');
// Project path // Project path
if (params.projectPath) { if (parameters.projectPath) {
args.push(`-projectPath "${params.projectPath}"`); unityArguments.push(`-projectPath "${parameters.projectPath}"`);
} }
// Test mode // Test mode
if (run.builtClient && run.builtClientPath) { if (run.builtClient && run.builtClientPath) {
// Built client testing: run tests against a built player // Built client testing: run tests against a built player
args.push('-runTests'); unityArguments.push(
args.push(`-testPlatform StandalonePlayer`); '-runTests',
args.push(`-assemblyNames Assembly-CSharp-Tests`); `-testPlatform StandalonePlayer`,
args.push(`-builtPlayerPath "${run.builtClientPath}"`); `-assemblyNames Assembly-CSharp-Tests`,
`-builtPlayerPath "${run.builtClientPath}"`,
);
} else if (run.editMode && run.playMode) { } else if (run.editMode && run.playMode) {
// Both modes: run EditMode first, then PlayMode will require a separate invocation // Both modes: run EditMode first, then PlayMode will require a separate invocation
// For combined mode, use EditMode (the service handles sequencing) // For combined mode, use EditMode (the service handles sequencing)
args.push('-runTests'); unityArguments.push('-runTests', '-testPlatform EditMode');
args.push('-testPlatform EditMode');
} else if (run.playMode) { } else if (run.playMode) {
args.push('-runTests'); unityArguments.push('-runTests', '-testPlatform PlayMode');
args.push('-testPlatform PlayMode');
} else if (run.editMode) { } else if (run.editMode) {
args.push('-runTests'); unityArguments.push('-runTests', '-testPlatform EditMode');
args.push('-testPlatform EditMode');
} }
// Apply taxonomy filters // Apply taxonomy filters
if (run.filters && Object.keys(run.filters).length > 0) { if (run.filters && Object.keys(run.filters).length > 0) {
const filterArgs = TaxonomyFilterService.buildFilterArgs(run.filters); const filterArguments = TaxonomyFilterService.buildFilterArgs(run.filters);
if (filterArgs) { if (filterArguments) {
args.push(filterArgs); unityArguments.push(filterArguments);
} }
} }
// Target platform // Target platform
if (params.targetPlatform) { if (parameters.targetPlatform) {
args.push(`-buildTarget ${params.targetPlatform}`); unityArguments.push(`-buildTarget ${parameters.targetPlatform}`);
} }
return args.join(' '); return unityArguments.join(' ');
} }
/** /**
* Resolve the path to the Unity editor executable. * Resolve the path to the Unity editor executable.
*/ */
private static resolveUnityPath(params: BuildParameters): string { private static resolveUnityPath(parameters: BuildParameters): string {
// In CI, Unity path is typically set via environment or the docker container // In CI, Unity path is typically set via environment or the docker container
const envUnityPath = process.env.UNITY_PATH ?? process.env.UNITY_EDITOR; const environmentUnityPath = process.env.UNITY_PATH ?? process.env.UNITY_EDITOR;
if (envUnityPath) { if (environmentUnityPath) {
return envUnityPath; return environmentUnityPath;
} }
// Default paths by platform // Default paths by platform
if (process.platform === 'win32') { if (process.platform === 'win32') {
return `C:/Program Files/Unity/Hub/Editor/${params.editorVersion}/Editor/Unity.exe`; return `C:/Program Files/Unity/Hub/Editor/${parameters.editorVersion}/Editor/Unity.exe`;
} }
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
return `/Applications/Unity/Hub/Editor/${params.editorVersion}/Unity.app/Contents/MacOS/Unity`; return `/Applications/Unity/Hub/Editor/${parameters.editorVersion}/Unity.app/Contents/MacOS/Unity`;
} }
// Linux default (Docker container path) // Linux default (Docker container path)