Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"version": "3.0.0",
"description": "Build Unity projects for different platforms.",
"main": "dist/index.js",
"exports": {
".": "./dist/index.js",
"./cli-plugin": "./src/cli-plugin/index.ts"
},
Comment on lines +6 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "package exports:"
jq '.exports' package.json

echo "tsconfig output dirs:"
jq '.compilerOptions | {rootDir, outDir}' tsconfig.json

echo "cli-plugin source entry:"
fd -p '^index\.ts$' src/cli-plugin

echo "compiled cli-plugin JS tracked in repo (if present):"
fd -p '^index\.js$' lib/cli-plugin dist/cli-plugin || true

Repository: game-ci/unity-builder

Length of output: 254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== tsconfig.json content ==="
cat tsconfig.json | head -30

echo "=== Checking for compiled cli-plugin output ==="
ls -la lib/cli-plugin 2>/dev/null || echo "lib/cli-plugin does not exist"
ls -la dist/cli-plugin 2>/dev/null || echo "dist/cli-plugin does not exist"

echo "=== Build scripts in package.json ==="
jq '.scripts | keys[] as $k | "\($k): \(.[$k])"' package.json 2>/dev/null | head -20 || grep -A 20 '"scripts"' package.json

echo "=== cli-plugin source files ==="
find src/cli-plugin -type f -name "*.ts" -o -name "*.js" 2>/dev/null | head -10

Repository: game-ci/unity-builder

Length of output: 2985


🏁 Script executed:

#!/bin/bash

echo "=== Directory structure of dist/ ==="
ls -la dist/ 2>/dev/null | head -20 || echo "dist does not exist or is empty"

echo "=== Directory structure of lib/ ==="
ls -la lib/ 2>/dev/null | head -20 || echo "lib does not exist or is empty"

echo "=== Checking .gitignore for exclusions ==="
cat .gitignore | grep -E "(^lib|^dist|/lib|/dist)" || echo "No explicit lib/dist gitignore rules found"

echo "=== Content of dist/index.js (first 50 lines) ==="
head -50 dist/index.js 2>/dev/null || echo "dist/index.js not found"

Repository: game-ci/unity-builder

Length of output: 3115


🏁 Script executed:

#!/bin/bash

echo "=== package.json files and exports fields ==="
jq '{files, exports, main, bin}' package.json 2>/dev/null || cat package.json | grep -A 10 '"files"\|"exports"\|"main"\|"bin"'

echo "=== Checking for ncc or webpack config ==="
ls -la | grep -E "(ncc|webpack|build)" || echo "No ncc/webpack config files found"

echo "=== How cli-plugin is imported in tests/usage ==="
grep -r "cli-plugin" src/ --include="*.ts" | grep -E "(import|require)" | head -5 || echo "No local imports of cli-plugin found"

echo "=== Check if cli-plugin is meant to be external or internal ==="
grep -r "from.*cli-plugin\|require.*cli-plugin" . --include="*.ts" --include="*.js" 2>/dev/null | grep -v node_modules | head -10

Repository: game-ci/unity-builder

Length of output: 459


🏁 Script executed:

#!/bin/bash

echo "=== Check if cli-plugin is referenced in README or docs ==="
find . -name "README*" -o -name "*.md" 2>/dev/null | xargs grep -l "cli-plugin" 2>/dev/null || echo "No docs mention cli-plugin"

echo "=== Check GitHub workflows/actions for how this is published ==="
cat .github/workflows/*.yml 2>/dev/null | grep -A 5 -B 5 "publish\|npm\|registry" || echo "No publish workflows found"

echo "=== Check if ts-node or similar is listed as dependency ==="
jq '{devDependencies, dependencies}' package.json | grep -E "ts-node|tsx|register" || echo "Checking dependencies..."

grep -E "ts-node|tsx" package.json || echo "No ts-node detected"

echo "=== What does the cli-plugin export contain? ==="
head -30 src/cli-plugin/index.ts

Repository: game-ci/unity-builder

Length of output: 2325


🏁 Script executed:

#!/bin/bash

echo "=== Check what npm would publish ==="
npm ls --depth=0 2>/dev/null || echo "Checking published files..."

echo "=== Simulate npm publish: list files that would be included ==="
git ls-files 2>/dev/null | head -30

echo "=== Is src/ tracked in git? ==="
git ls-files | grep "^src/" | wc -l

echo "=== Is lib/ tracked? ==="
git ls-files | grep "^lib/" | wc -l

echo "=== Full exports and file info ==="
cat package.json | jq '{name, version, main, exports, files, publishConfig}' 

echo "=== What actually happens when you require the subpath? ==="
echo "Checking if there are any test files that import cli-plugin..."
find . -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "cli-plugin" 2>/dev/null | head -3

Repository: game-ci/unity-builder

Length of output: 3397


Subpath export points to raw TypeScript source that requires TypeScript runtime support.

"./cli-plugin": "./src/cli-plugin/index.ts" will be published as-is since src/ is tracked in git and no "files" field limits what npm includes. This breaks import 'unity-builder/cli-plugin' for standard Node.js consumers without TypeScript loaders (ts-node, tsx, etc.).

The proposed fix of "./cli-plugin": "./lib/cli-plugin/index.js" is incorrect—lib/ is gitignored and will not be included in the published package.

Recommended solutions:

  1. Add "files" field to explicitly control published outputs and ensure dist/ is included:

    {
      "files": ["dist", "src"]
    }

    Then update the export to point to a pre-compiled dist/cli-plugin/index.js (requires build changes to output subpaths).

  2. Or update the build to generate dist/cli-plugin/index.js during ncc build instead of bundling everything into dist/index.js.

  3. Or document that this package requires TypeScript tooling and update consumers to use ts-node or similar.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 6 - 9, The package exports point "./cli-plugin" at
the raw TypeScript source ("./src/cli-plugin/index.ts"), which will be published
and break Node consumers; change this by adding a "files" field to package.json
to explicitly include the compiled output (e.g., "dist/") and exclude raw "src",
update the "./cli-plugin" export to point to the compiled entry (e.g.,
"dist/cli-plugin/index.js"), and adjust the build pipeline (the ncc build or
your build script) to emit a dist/cli-plugin/index.js artifact (or alternatively
document that TypeScript runtime tooling is required); reference the "exports"
key and the "./cli-plugin" subpath and ensure the build task that produces
"dist/" is updated to generate the cli-plugin subpath.

"repository": "git@github.com:game-ci/unity-builder.git",
"author": "Webber <webber@takken.io>",
"license": "MIT",
Expand Down
131 changes: 131 additions & 0 deletions src/cli-plugin/build-parameters-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import BuildParameters from '../model/build-parameters';
import { customAlphabet } from 'nanoid';
import OrchestratorConstants from '../model/orchestrator/options/orchestrator-constants';

/**
* Maps CLI yargs options (flat key-value pairs) into a BuildParameters object.
*
* When the orchestrator is consumed as a CLI plugin, the CLI collects options via
* yargs and passes them as a plain object. This adapter bridges that format to the
* BuildParameters class that all providers expect.
*/
export function createBuildParametersFromCliOptions(options: Record<string, any>): BuildParameters {
const bp = new BuildParameters();

// Unity / build settings
bp.editorVersion = options.engineVersion || options.editorVersion || options.unityVersion || '';
bp.customImage = options.customImage || '';
bp.unitySerial = options.unitySerial || process.env.UNITY_SERIAL || '';
bp.unityLicensingServer = options.unityLicensingServer || '';
bp.skipActivation = options.skipActivation || 'false';
bp.runnerTempPath = options.runnerTempPath || process.env.RUNNER_TEMP || '';
bp.targetPlatform = options.targetPlatform || 'StandaloneLinux64';
bp.projectPath = options.projectPath || '.';
bp.buildProfile = options.buildProfile || '';
bp.buildName = options.buildName || options.targetPlatform || 'StandaloneLinux64';
bp.buildPath = options.buildPath || `build/${bp.targetPlatform}`;
bp.buildFile = options.buildFile || bp.buildName;
bp.buildMethod = options.buildMethod || '';
bp.buildVersion = options.buildVersion || '0.0.1';
bp.manualExit = options.manualExit === true || options.manualExit === 'true';
bp.enableGpu = options.enableGpu === true || options.enableGpu === 'true';

// Android
bp.androidVersionCode = options.androidVersionCode || '';
bp.androidKeystoreName = options.androidKeystoreName || '';
bp.androidKeystoreBase64 = options.androidKeystoreBase64 || '';
bp.androidKeystorePass = options.androidKeystorePass || '';
bp.androidKeyaliasName = options.androidKeyaliasName || '';
bp.androidKeyaliasPass = options.androidKeyaliasPass || '';
bp.androidTargetSdkVersion = options.androidTargetSdkVersion || '';
bp.androidSdkManagerParameters = options.androidSdkManagerParameters || '';
bp.androidExportType = options.androidExportType || 'androidPackage';
bp.androidSymbolType = options.androidSymbolType || 'none';

// Docker / container settings
bp.dockerCpuLimit = options.dockerCpuLimit || '';
bp.dockerMemoryLimit = options.dockerMemoryLimit || '';
bp.dockerIsolationMode = options.dockerIsolationMode || 'default';
bp.containerRegistryRepository = options.containerRegistryRepository || 'unityci/editor';
bp.containerRegistryImageVersion = options.containerRegistryImageVersion || '3';
bp.dockerWorkspacePath = options.dockerWorkspacePath || '/github/workspace';

// Provider / orchestrator settings
bp.providerStrategy = options.providerStrategy || 'local-docker';
bp.customParameters = options.customParameters || '';
bp.sshAgent = options.sshAgent || '';
bp.sshPublicKeysDirectoryPath = options.sshPublicKeysDirectoryPath || '';
bp.gitPrivateToken = options.gitPrivateToken || '';
bp.runAsHostUser = options.runAsHostUser || 'false';
bp.chownFilesTo = options.chownFilesTo || '';

// AWS
bp.awsStackName = options.awsStackName || 'game-ci';
bp.awsEndpoint = options.awsEndpoint;
bp.awsCloudFormationEndpoint = options.awsCloudFormationEndpoint || options.awsEndpoint;
bp.awsEcsEndpoint = options.awsEcsEndpoint || options.awsEndpoint;
bp.awsKinesisEndpoint = options.awsKinesisEndpoint || options.awsEndpoint;
bp.awsCloudWatchLogsEndpoint = options.awsCloudWatchLogsEndpoint || options.awsEndpoint;
bp.awsS3Endpoint = options.awsS3Endpoint || options.awsEndpoint;

// Storage
bp.storageProvider = options.storageProvider || 's3';
bp.rcloneRemote = options.rcloneRemote || '';

// Kubernetes
bp.kubeConfig = options.kubeConfig || '';
bp.kubeVolume = options.kubeVolume || '';
bp.kubeVolumeSize = options.kubeVolumeSize || '25Gi';
bp.kubeStorageClass = options.kubeStorageClass || '';

// Container resources
bp.containerMemory = options.containerMemory || '3072';
bp.containerCpu = options.containerCpu || '1024';
bp.containerNamespace = options.containerNamespace || 'default';

// Hooks
bp.commandHooks = options.commandHooks || '';
bp.postBuildContainerHooks = options.postBuildContainerHooks || '';
bp.preBuildContainerHooks = options.preBuildContainerHooks || '';
bp.customJob = options.customJob || '';

// Git / CI
bp.runNumber = options.runNumber || process.env.GITHUB_RUN_NUMBER || '0';
bp.branch = options.branch || process.env.GITHUB_REF?.replace('refs/', '').replace('heads/', '') || '';
bp.githubRepo = options.githubRepo || process.env.GITHUB_REPOSITORY || '';
bp.orchestratorRepoName = options.orchestratorRepoName || 'game-ci/unity-builder';
bp.cloneDepth = Number.parseInt(options.cloneDepth || '50', 10);
bp.gitSha = options.gitSha || process.env.GITHUB_SHA || '';
bp.orchestratorBranch = options.orchestratorBranch || 'main';
bp.orchestratorDebug = options.orchestratorDebug === true || options.orchestratorDebug === 'true';

// Build platform
bp.buildPlatform = options.buildPlatform || (bp.providerStrategy !== 'local' ? 'linux' : process.platform);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

local-system gets the wrong default buildPlatform.

Current default treats only providerStrategy === 'local' as host-local. local-system is also host-local and should not default to 'linux' on macOS/Windows.

🔧 Proposed fix
-  bp.buildPlatform = options.buildPlatform || (bp.providerStrategy !== 'local' ? 'linux' : process.platform);
+  const isHostLocalProvider =
+    bp.providerStrategy === 'local' || bp.providerStrategy === 'local-system';
+  bp.buildPlatform = options.buildPlatform || (isHostLocalProvider ? process.platform : 'linux');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bp.buildPlatform = options.buildPlatform || (bp.providerStrategy !== 'local' ? 'linux' : process.platform);
const isHostLocalProvider =
bp.providerStrategy === 'local' || bp.providerStrategy === 'local-system';
bp.buildPlatform = options.buildPlatform || (isHostLocalProvider ? process.platform : 'linux');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli-plugin/build-parameters-adapter.ts` at line 103, The default
buildPlatform assignment in build-parameters-adapter.ts incorrectly treats only
providerStrategy === 'local' as host-local; update the logic around
bp.buildPlatform (the assignment that uses options.buildPlatform ||
(bp.providerStrategy !== 'local' ? 'linux' : process.platform)) so that
providerStrategy values 'local' and 'local-system' are both considered
host-local (i.e., if bp.providerStrategy is neither 'local' nor 'local-system'
default to 'linux', otherwise use process.platform); change the conditional to
check for both identifiers (e.g., via explicit comparisons or an inclusion
check) to ensure local-system uses the host platform default.

bp.isCliMode = true;

// Caching
bp.cacheKey = options.cacheKey || bp.branch;
bp.pullInputList = options.pullInputList ? options.pullInputList.split(',') : [];
bp.inputPullCommand = options.inputPullCommand || '';

// Advanced
bp.maxRetainedWorkspaces = Number.parseInt(options.maxRetainedWorkspaces || '0', 10);
bp.useLargePackages = options.useLargePackages === true || options.useLargePackages === 'true';
bp.useCompressionStrategy = options.useCompressionStrategy === true || options.useCompressionStrategy === 'true';
bp.garbageMaxAge = Number(options.garbageMaxAge) || 24;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

garbageMaxAge cannot be set to 0.

Number(options.garbageMaxAge) || 24 forces 0 back to 24. This breaks a valid boundary value.

🔧 Proposed fix
-  bp.garbageMaxAge = Number(options.garbageMaxAge) || 24;
+  const parsedGarbageMaxAge = Number(options.garbageMaxAge);
+  bp.garbageMaxAge = Number.isNaN(parsedGarbageMaxAge) ? 24 : parsedGarbageMaxAge;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bp.garbageMaxAge = Number(options.garbageMaxAge) || 24;
const parsedGarbageMaxAge = Number(options.garbageMaxAge);
bp.garbageMaxAge = Number.isNaN(parsedGarbageMaxAge) ? 24 : parsedGarbageMaxAge;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli-plugin/build-parameters-adapter.ts` at line 115, The assignment
bp.garbageMaxAge = Number(options.garbageMaxAge) || 24 erroneously treats 0 as
falsy and resets it to 24; instead parse the value and only fallback when the
parsed value is NaN/invalid—e.g., compute const parsed =
Number(options.garbageMaxAge) and set bp.garbageMaxAge = Number.isFinite(parsed)
? parsed : 24 (or use a nullish/NaN check) so that 0 is preserved; update the
code around bp.garbageMaxAge and options.garbageMaxAge accordingly.

bp.githubChecks = options.githubChecks === true || options.githubChecks === 'true';
bp.asyncWorkflow = options.asyncOrchestrator === true || options.asyncOrchestrator === 'true';
bp.githubCheckId = options.githubCheckId || '';
bp.finalHooks = options.finalHooks ? options.finalHooks.split(',') : [];
bp.skipLfs = options.skipLfs === true || options.skipLfs === 'true';
bp.skipCache = options.skipCache === true || options.skipCache === 'true';
bp.cacheUnityInstallationOnMac =
options.cacheUnityInstallationOnMac === true || options.cacheUnityInstallationOnMac === 'true';
bp.unityHubVersionOnMac = options.unityHubVersionOnMac || '';
Comment on lines +53 to +124
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Several declared CLI flags are silently discarded during mapping.

region, containerHookFiles, commandHookFiles, resourceTracking, useSharedBuilder, and useCleanupCron are registered options but never copied into BuildParameters, so user-provided values are lost.

🔧 Proposed mapping additions
   // Provider / orchestrator settings
+  bp.region = options.region || 'eu-west-2';
   bp.providerStrategy = options.providerStrategy || 'local-docker';
   bp.customParameters = options.customParameters || '';
@@
   // Hooks
+  bp.containerHookFiles = options.containerHookFiles || '';
+  bp.commandHookFiles = options.commandHookFiles || '';
   bp.commandHooks = options.commandHooks || '';
@@
   // Advanced
+  bp.resourceTracking = options.resourceTracking === true || options.resourceTracking === 'true';
+  bp.useSharedBuilder = options.useSharedBuilder === true || options.useSharedBuilder === 'true';
+  bp.useCleanupCron = options.useCleanupCron === true || options.useCleanupCron === 'true';
   bp.maxRetainedWorkspaces = Number.parseInt(options.maxRetainedWorkspaces || '0', 10);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Provider / orchestrator settings
bp.providerStrategy = options.providerStrategy || 'local-docker';
bp.customParameters = options.customParameters || '';
bp.sshAgent = options.sshAgent || '';
bp.sshPublicKeysDirectoryPath = options.sshPublicKeysDirectoryPath || '';
bp.gitPrivateToken = options.gitPrivateToken || '';
bp.runAsHostUser = options.runAsHostUser || 'false';
bp.chownFilesTo = options.chownFilesTo || '';
// AWS
bp.awsStackName = options.awsStackName || 'game-ci';
bp.awsEndpoint = options.awsEndpoint;
bp.awsCloudFormationEndpoint = options.awsCloudFormationEndpoint || options.awsEndpoint;
bp.awsEcsEndpoint = options.awsEcsEndpoint || options.awsEndpoint;
bp.awsKinesisEndpoint = options.awsKinesisEndpoint || options.awsEndpoint;
bp.awsCloudWatchLogsEndpoint = options.awsCloudWatchLogsEndpoint || options.awsEndpoint;
bp.awsS3Endpoint = options.awsS3Endpoint || options.awsEndpoint;
// Storage
bp.storageProvider = options.storageProvider || 's3';
bp.rcloneRemote = options.rcloneRemote || '';
// Kubernetes
bp.kubeConfig = options.kubeConfig || '';
bp.kubeVolume = options.kubeVolume || '';
bp.kubeVolumeSize = options.kubeVolumeSize || '25Gi';
bp.kubeStorageClass = options.kubeStorageClass || '';
// Container resources
bp.containerMemory = options.containerMemory || '3072';
bp.containerCpu = options.containerCpu || '1024';
bp.containerNamespace = options.containerNamespace || 'default';
// Hooks
bp.commandHooks = options.commandHooks || '';
bp.postBuildContainerHooks = options.postBuildContainerHooks || '';
bp.preBuildContainerHooks = options.preBuildContainerHooks || '';
bp.customJob = options.customJob || '';
// Git / CI
bp.runNumber = options.runNumber || process.env.GITHUB_RUN_NUMBER || '0';
bp.branch = options.branch || process.env.GITHUB_REF?.replace('refs/', '').replace('heads/', '') || '';
bp.githubRepo = options.githubRepo || process.env.GITHUB_REPOSITORY || '';
bp.orchestratorRepoName = options.orchestratorRepoName || 'game-ci/unity-builder';
bp.cloneDepth = Number.parseInt(options.cloneDepth || '50', 10);
bp.gitSha = options.gitSha || process.env.GITHUB_SHA || '';
bp.orchestratorBranch = options.orchestratorBranch || 'main';
bp.orchestratorDebug = options.orchestratorDebug === true || options.orchestratorDebug === 'true';
// Build platform
bp.buildPlatform = options.buildPlatform || (bp.providerStrategy !== 'local' ? 'linux' : process.platform);
bp.isCliMode = true;
// Caching
bp.cacheKey = options.cacheKey || bp.branch;
bp.pullInputList = options.pullInputList ? options.pullInputList.split(',') : [];
bp.inputPullCommand = options.inputPullCommand || '';
// Advanced
bp.maxRetainedWorkspaces = Number.parseInt(options.maxRetainedWorkspaces || '0', 10);
bp.useLargePackages = options.useLargePackages === true || options.useLargePackages === 'true';
bp.useCompressionStrategy = options.useCompressionStrategy === true || options.useCompressionStrategy === 'true';
bp.garbageMaxAge = Number(options.garbageMaxAge) || 24;
bp.githubChecks = options.githubChecks === true || options.githubChecks === 'true';
bp.asyncWorkflow = options.asyncOrchestrator === true || options.asyncOrchestrator === 'true';
bp.githubCheckId = options.githubCheckId || '';
bp.finalHooks = options.finalHooks ? options.finalHooks.split(',') : [];
bp.skipLfs = options.skipLfs === true || options.skipLfs === 'true';
bp.skipCache = options.skipCache === true || options.skipCache === 'true';
bp.cacheUnityInstallationOnMac =
options.cacheUnityInstallationOnMac === true || options.cacheUnityInstallationOnMac === 'true';
bp.unityHubVersionOnMac = options.unityHubVersionOnMac || '';
// Provider / orchestrator settings
bp.region = options.region || 'eu-west-2';
bp.providerStrategy = options.providerStrategy || 'local-docker';
bp.customParameters = options.customParameters || '';
bp.sshAgent = options.sshAgent || '';
bp.sshPublicKeysDirectoryPath = options.sshPublicKeysDirectoryPath || '';
bp.gitPrivateToken = options.gitPrivateToken || '';
bp.runAsHostUser = options.runAsHostUser || 'false';
bp.chownFilesTo = options.chownFilesTo || '';
// AWS
bp.awsStackName = options.awsStackName || 'game-ci';
bp.awsEndpoint = options.awsEndpoint;
bp.awsCloudFormationEndpoint = options.awsCloudFormationEndpoint || options.awsEndpoint;
bp.awsEcsEndpoint = options.awsEcsEndpoint || options.awsEndpoint;
bp.awsKinesisEndpoint = options.awsKinesisEndpoint || options.awsEndpoint;
bp.awsCloudWatchLogsEndpoint = options.awsCloudWatchLogsEndpoint || options.awsEndpoint;
bp.awsS3Endpoint = options.awsS3Endpoint || options.awsEndpoint;
// Storage
bp.storageProvider = options.storageProvider || 's3';
bp.rcloneRemote = options.rcloneRemote || '';
// Kubernetes
bp.kubeConfig = options.kubeConfig || '';
bp.kubeVolume = options.kubeVolume || '';
bp.kubeVolumeSize = options.kubeVolumeSize || '25Gi';
bp.kubeStorageClass = options.kubeStorageClass || '';
// Container resources
bp.containerMemory = options.containerMemory || '3072';
bp.containerCpu = options.containerCpu || '1024';
bp.containerNamespace = options.containerNamespace || 'default';
// Hooks
bp.containerHookFiles = options.containerHookFiles || '';
bp.commandHookFiles = options.commandHookFiles || '';
bp.commandHooks = options.commandHooks || '';
bp.postBuildContainerHooks = options.postBuildContainerHooks || '';
bp.preBuildContainerHooks = options.preBuildContainerHooks || '';
bp.customJob = options.customJob || '';
// Git / CI
bp.runNumber = options.runNumber || process.env.GITHUB_RUN_NUMBER || '0';
bp.branch = options.branch || process.env.GITHUB_REF?.replace('refs/', '').replace('heads/', '') || '';
bp.githubRepo = options.githubRepo || process.env.GITHUB_REPOSITORY || '';
bp.orchestratorRepoName = options.orchestratorRepoName || 'game-ci/unity-builder';
bp.cloneDepth = Number.parseInt(options.cloneDepth || '50', 10);
bp.gitSha = options.gitSha || process.env.GITHUB_SHA || '';
bp.orchestratorBranch = options.orchestratorBranch || 'main';
bp.orchestratorDebug = options.orchestratorDebug === true || options.orchestratorDebug === 'true';
// Build platform
bp.buildPlatform = options.buildPlatform || (bp.providerStrategy !== 'local' ? 'linux' : process.platform);
bp.isCliMode = true;
// Caching
bp.cacheKey = options.cacheKey || bp.branch;
bp.pullInputList = options.pullInputList ? options.pullInputList.split(',') : [];
bp.inputPullCommand = options.inputPullCommand || '';
// Advanced
bp.resourceTracking = options.resourceTracking === true || options.resourceTracking === 'true';
bp.useSharedBuilder = options.useSharedBuilder === true || options.useSharedBuilder === 'true';
bp.useCleanupCron = options.useCleanupCron === true || options.useCleanupCron === 'true';
bp.maxRetainedWorkspaces = Number.parseInt(options.maxRetainedWorkspaces || '0', 10);
bp.useLargePackages = options.useLargePackages === true || options.useLargePackages === 'true';
bp.useCompressionStrategy = options.useCompressionStrategy === true || options.useCompressionStrategy === 'true';
bp.garbageMaxAge = Number(options.garbageMaxAge) || 24;
bp.githubChecks = options.githubChecks === true || options.githubChecks === 'true';
bp.asyncWorkflow = options.asyncOrchestrator === true || options.asyncOrchestrator === 'true';
bp.githubCheckId = options.githubCheckId || '';
bp.finalHooks = options.finalHooks ? options.finalHooks.split(',') : [];
bp.skipLfs = options.skipLfs === true || options.skipLfs === 'true';
bp.skipCache = options.skipCache === true || options.skipCache === 'true';
bp.cacheUnityInstallationOnMac =
options.cacheUnityInstallationOnMac === true || options.cacheUnityInstallationOnMac === 'true';
bp.unityHubVersionOnMac = options.unityHubVersionOnMac || '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli-plugin/build-parameters-adapter.ts` around lines 53 - 124, The
BuildParameters mapping in build-parameters-adapter.ts is dropping several
registered CLI flags (region, containerHookFiles, commandHookFiles,
resourceTracking, useSharedBuilder, useCleanupCron); update the bp assignments
(the BuildParameters object referenced as bp) to copy these options from the
incoming options object: set bp.region = options.region || '' (or appropriate
default), map bp.containerHookFiles and bp.commandHookFiles to arrays (e.g.
options.containerHookFiles ? options.containerHookFiles.split(',') : []), set
bp.resourceTracking = options.resourceTracking === true ||
options.resourceTracking === 'true', and set bp.useSharedBuilder and
bp.useCleanupCron similarly to boolean conversions (=== true || === 'true'); add
these lines alongside the other mappings in the same adapter so user-provided
values are preserved.


// IDs
bp.logId = customAlphabet(OrchestratorConstants.alphabet, 9)();
bp.buildGuid = options.buildGuid || `${bp.runNumber}-${bp.targetPlatform}`;

return bp;
}
94 changes: 94 additions & 0 deletions src/cli-plugin/cli-plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import orchestratorPlugin from './index';
import { createBuildParametersFromCliOptions } from './build-parameters-adapter';

describe('CLI Plugin Adapter', () => {
describe('orchestratorPlugin', () => {
it('has required GameCIPlugin fields', () => {
expect(orchestratorPlugin.name).toBe('orchestrator');
expect(orchestratorPlugin.version).toBe('3.0.0');
});

it('registers options plugin with wildcard engine', () => {
expect(orchestratorPlugin.options).toHaveLength(1);
expect(orchestratorPlugin.options[0].engine).toBe('*');
expect(typeof orchestratorPlugin.options[0].configure).toBe('function');
});

it('exposes all built-in provider strategies', () => {
const providers = orchestratorPlugin.providers;
expect(providers).toHaveProperty('aws');
expect(providers).toHaveProperty('k8s');
expect(providers).toHaveProperty('local-docker');
expect(providers).toHaveProperty('local-system');
expect(providers).toHaveProperty('local');
expect(providers).toHaveProperty('test');
});

it('provider constructors are functions', () => {
for (const [, Ctor] of Object.entries(orchestratorPlugin.providers)) {
expect(typeof Ctor).toBe('function');
}
});
});

describe('configureOrchestratorOptions', () => {
it('registers options on a yargs-like object', () => {
const registered: Record<string, any> = {};
const mockYargs = {
option(name: string, config: any) {
registered[name] = config;

return mockYargs;
},
};

orchestratorPlugin.options[0].configure(mockYargs);

// Spot-check key options

expect(registered).toHaveProperty('containerCpu');
expect(registered).toHaveProperty('containerMemory');
expect(registered).toHaveProperty('awsStackName');
expect(registered).toHaveProperty('kubeConfig');
expect(registered).toHaveProperty('storageProvider');
expect(registered).toHaveProperty('commandHooks');
expect(registered).toHaveProperty('orchestratorDebug');
expect(registered).toHaveProperty('region');
});
});

describe('createBuildParametersFromCliOptions', () => {
it('maps yargs options to BuildParameters', () => {
const bp = createBuildParametersFromCliOptions({
providerStrategy: 'aws',
containerMemory: '4096',
containerCpu: '2048',
awsStackName: 'my-stack',
targetPlatform: 'StandaloneLinux64',
buildName: 'MyGame',
kubeConfig: 'base64config',
});

expect(bp.providerStrategy).toBe('aws');
expect(bp.containerMemory).toBe('4096');
expect(bp.containerCpu).toBe('2048');
expect(bp.awsStackName).toBe('my-stack');
expect(bp.targetPlatform).toBe('StandaloneLinux64');
expect(bp.buildName).toBe('MyGame');
expect(bp.kubeConfig).toBe('base64config');
expect(bp.isCliMode).toBe(true);
});

it('applies defaults for missing options', () => {
const bp = createBuildParametersFromCliOptions({});

expect(bp.providerStrategy).toBe('local-docker');
expect(bp.containerMemory).toBe('3072');
expect(bp.containerCpu).toBe('1024');
expect(bp.awsStackName).toBe('game-ci');
expect(bp.containerNamespace).toBe('default');
expect(bp.kubeVolumeSize).toBe('25Gi');
expect(bp.storageProvider).toBe('s3');
});
});
});
65 changes: 65 additions & 0 deletions src/cli-plugin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* @game-ci/orchestrator-plugin
*
* CLI plugin adapter for the unity-builder orchestrator.
* Exports a GameCIPlugin that the CLI consumes via PluginRegistry.
*
* Usage in CLI:
* import orchestratorPlugin from '@game-ci/orchestrator-plugin';
* await PluginRegistry.register(orchestratorPlugin);
*
* Or via plugin loader:
* await PluginLoader.load('@game-ci/orchestrator-plugin');
*/

import AwsBuildPlatform from '../model/orchestrator/providers/aws';
import Kubernetes from '../model/orchestrator/providers/k8s';
import LocalDockerOrchestrator from '../model/orchestrator/providers/docker';
import LocalOrchestrator from '../model/orchestrator/providers/local';
import TestOrchestrator from '../model/orchestrator/providers/test';
import { configureOrchestratorOptions } from './orchestrator-options-plugin';
import { createProviderAdapter } from './provider-adapter';

/**
* GameCIPlugin-compatible export.
*
* This object matches the GameCIPlugin interface defined in @game-ci/cli:
* - name, version: plugin metadata
* - options: registers orchestrator-specific CLI options (aws, k8s, hooks, etc.)
* - providers: maps strategy names to provider constructors
*/
const orchestratorPlugin = {
name: 'orchestrator',
version: '3.0.0',

/**
* Options plugins — register orchestrator-specific yargs options.
* engine: '*' means these options apply regardless of which engine is detected.
*/
options: [
{
engine: '*',
configure: configureOrchestratorOptions,
},
],

/**
* Provider constructors keyed by strategy name.
* Each is wrapped via createProviderAdapter so the CLI can instantiate them
* with yargs options (flat key-value) instead of BuildParameters directly.
*/
providers: {
aws: createProviderAdapter(AwsBuildPlatform),
k8s: createProviderAdapter(Kubernetes),
'local-docker': createProviderAdapter(LocalDockerOrchestrator),
'local-system': createProviderAdapter(LocalOrchestrator),
local: createProviderAdapter(LocalOrchestrator),
test: createProviderAdapter(TestOrchestrator),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --type=ts -C2 'runTaskInWorkflow\s*\(' \
  src/model/orchestrator/providers/provider-interface.ts \
  src/model/orchestrator/providers/test/index.ts \
  src/cli-plugin/provider-adapter.ts

Repository: game-ci/unity-builder

Length of output: 1646


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get full runTaskInWorkflow signature from test provider
echo "=== Test Provider Implementation ===" 
sed -n '45,60p' src/model/orchestrator/providers/test/index.ts

echo -e "\n=== Provider Interface Definition ===" 
sed -n '26,42p' src/model/orchestrator/providers/provider-interface.ts

echo -e "\n=== Adapter Implementation ===" 
sed -n '33,43p' src/cli-plugin/provider-adapter.ts

echo -e "\n=== Line 57 context in index.ts ===" 
sed -n '50,65p' src/cli-plugin/index.ts

Repository: game-ci/unity-builder

Length of output: 2340


test provider exposes a critical interface violation: TestOrchestrator.runTaskInWorkflow parameters are in the wrong order.

The interface defines runTaskInWorkflow(buildGuid, image, commands, ...), but TestOrchestrator implements runTaskInWorkflow(commands, buildGuid, image, ...). The adapter forwards arguments in interface-compliant order, causing TestOrchestrator to receive them misaligned: buildGuid receives the buildGuid value interpreted as commands, image receives buildGuid as image, and commands receives image as commands. This violates the provider contract and produces incorrect runtime behavior when test is selected in production.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli-plugin/index.ts` at line 57, The Test provider is violating the
provider interface because TestOrchestrator implements
runTaskInWorkflow(commands, buildGuid, image, ...) while the expected signature
is runTaskInWorkflow(buildGuid, image, commands, ...); update
TestOrchestrator.runTaskInWorkflow to take parameters in the correct order
(buildGuid, image, commands, ...) and adjust the internal use of those
parameters accordingly so that createProviderAdapter(TestOrchestrator) forwards
arguments correctly to the implementation.

},
};

export default orchestratorPlugin;
export { orchestratorPlugin };
export { createBuildParametersFromCliOptions } from './build-parameters-adapter';
export { configureOrchestratorOptions } from './orchestrator-options-plugin';
export { createProviderAdapter } from './provider-adapter';
Loading
Loading