Conversation
…commands Introduces a yargs-based CLI entry point (src/cli.ts) distributed as the `game-ci` command. The CLI reuses existing unity-builder modules — Input, BuildParameters, Orchestrator, Docker, MacBuilder — so the same build engine powers both the GitHub Action and the standalone CLI. Commands: build, activate, orchestrate, cache (list/restore/clear), status, version. Closes #812 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds an official CLI ("game-ci") with yargs-based commands, input mapping, packaging/release workflows, cross-platform installers, and tests; introduces command implementations for build, activate, orchestrate, cache, status, version, and update, plus packaging and release automation. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI as "CLI\n(src/cli.ts)"
participant InputMapper as "Input Mapper\n(mapCliArgumentsToInput)"
participant BuildParams as "BuildParameters"
participant PlatformSetup as "PlatformSetup"
participant LocalBuilder as "Local Builder\n(Docker/Mac)"
participant Orchestrator as "Orchestrator"
User->>CLI: game-ci build [flags]
CLI->>InputMapper: mapCliArgumentsToInput(cliArguments)
Note right of InputMapper: Cli.options set,\nmode='cli'
InputMapper-->>CLI: mapped options
CLI->>BuildParams: create BuildParameters / ImageTag
alt providerStrategy == 'local'
CLI->>PlatformSetup: setup()
PlatformSetup-->>CLI: environment ready
alt platform == macOS
CLI->>LocalBuilder: MacBuilder.build(params)
else
CLI->>LocalBuilder: Docker.build(params, image)
end
LocalBuilder-->>CLI: build results
else
CLI->>Orchestrator: Orchestrator.run(buildParameters, image)
Orchestrator-->>CLI: orchestration result
end
CLI->>User: output metadata (exit code, path, version)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Add .github/workflows/publish-cli.yml for publishing the CLI to npm on release or via manual workflow_dispatch with dry-run support. Add comprehensive test coverage for the CLI: - input-mapper.test.ts: 16 tests covering argument mapping, boolean conversion, yargs internal property filtering, and Cli.options population - commands.test.ts: 26 tests verifying command exports, builder flags, default values, and camelCase aliases for all six commands - cli-integration.test.ts: 8 integration tests spawning the CLI process to verify help output, version info, and error handling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (2)
src/cli/commands/orchestrate.ts (1)
14-149: Consider extracting shared CLI option registration.This option block largely duplicates
src/cli/commands/build.ts. A shared helper would reduce drift and keep defaults/descriptions consistent across commands.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cli/commands/orchestrate.ts` around lines 14 - 149, The CLI option block duplicated in the builder function in orchestrate.ts should be extracted into a shared helper (e.g., registerCommonBuildOptions or addOrchestrateOptions) that encapsulates the repeated .option(...) calls and defaults; move that helper into a new module (e.g., cli/options.ts) and replace the large option chains in builder of orchestrate.ts and the equivalent builder in build.ts with a call to this helper (pass the yargs instance and any command-specific overrides like providerStrategy defaults), ensuring unique symbols referenced are the builder function in orchestrate.ts, the corresponding builder in build.ts, and the new registerCommonBuildOptions helper so both commands import and call it.src/cli/commands/cache.ts (1)
57-61: Avoid double failure reporting in CLI error flow.Calling
core.setFailed(...)and rethrowing can produce duplicate error output/noisy stacks in yargs CLIs. Prefer normalizing the message and returning after setting failure state.Proposed refactor
- } catch (error: any) { - core.setFailed(`Cache operation failed: ${error.message}`); - - throw error; + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + core.setFailed(`Cache operation failed: ${message}`); + return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cli/commands/cache.ts` around lines 57 - 61, The catch block in src/cli/commands/cache.ts currently calls core.setFailed(...) and then rethrows the error, causing duplicate/noisy output; update the error handling in that catch (the code using core.setFailed and throw error) to instead normalize the error message, call core.setFailed(normalizedMessage) and then return (or set a non-zero exit/return code) without rethrowing; remove the throw error to avoid double reporting while preserving the failure state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@package.json`:
- Around line 62-70: The package.json currently pins "yargs" at "^18.0.0", which
is incompatible with the repository's Node engine (>=18.x); change the "yargs"
dependency entry in package.json to "17.7.2" (or "^17.7.2") to match Node >=12
compatibility, then regenerate lockfiles by running your package manager (npm
install or yarn install) so package-lock.json / yarn.lock reflects the
downgrade; ensure any related type package "@types/yargs" remains compatible
after the change and adjust it only if type errors appear.
In `@src/cli.ts`:
- Line 4: Remove the stale eslint-disable directive "eslint-disable-next-line
import/no-unresolved" from the top of the file; locate the exact comment and
delete that line so eslint-comments/no-unused-disable no longer flags it, and
only re-add a disable if an actual unresolved-import warning later reappears.
- Around line 29-36: In the main function's catch block (around cli.parse()),
detect YError cases and ensure they exit with code 2: when error.name ===
'YError' call core.error with the error message (or the error itself) and then
invoke process.exit(2); keep the existing behavior for non-YError errors (log
with core.error) but ensure they still exit (e.g., process.exit(1) or propagate)
as appropriate so parser/configuration errors from cli.parse() are mapped to
exit code 2.
In `@src/cli/commands/activate.ts`:
- Around line 42-45: Replace the direct environment/CLI reads for unitySerial,
unityLicense, and licensingServer with the unified Input accessors so activation
goes through the same input resolution pipeline as other commands: stop using
process.env.UNITY_SERIAL, process.env.UNITY_LICENSE and
cliArguments.unityLicensingServer and instead read those values via the
project's Input API (e.g., Input.get / Input.getOptional or the project's
equivalent) for the keys used by the rest of the CLI (UNITY_SERIAL,
UNITY_LICENSE, UNITY_LICENSING_SERVER); update the variables unitySerial,
unityLicense, and licensingServer in activate.ts to pull from Input and preserve
the same fallback/empty-string behavior.
In `@src/cli/commands/cache.ts`:
- Around line 33-35: The file always sets cacheDirectory to
(cliArguments.cacheDir as string) || path.join(projectPath, 'Library'), making
the restoreCache guard dead and causing restore/clear to silently use
<project>/Library; change this so --cache-dir is action-specific: do not default
cacheDirectory to 'Library' at the top-level — only resolve a default when
handling the specific actions that need it (e.g., inside the restore and clear
command handlers or inside restoreCache), or keep cacheDir undefined unless
provided and then in restoreCache (function restoreCache) compute
path.join(projectPath, 'Library') only when cacheDir is missing and the action
is restore/clear; update references to projectPath and cacheDirectory
accordingly so other actions ignore the default.
- Around line 122-137: The cache restore command currently only lists archives
and exits successfully; change the behavior to fail fast until restore is
implemented: after computing sorted (using cacheFiles, cacheDirectory, sorted),
replace the final core.info guidance messages with a non-success exit (e.g.,
call core.setFailed('cache restore not implemented - no restore performed') or
throw an Error or set process.exitCode = 1) so CI/automation surfaces the
missing restore step instead of succeeding silently.
---
Nitpick comments:
In `@src/cli/commands/cache.ts`:
- Around line 57-61: The catch block in src/cli/commands/cache.ts currently
calls core.setFailed(...) and then rethrows the error, causing duplicate/noisy
output; update the error handling in that catch (the code using core.setFailed
and throw error) to instead normalize the error message, call
core.setFailed(normalizedMessage) and then return (or set a non-zero exit/return
code) without rethrowing; remove the throw error to avoid double reporting while
preserving the failure state.
In `@src/cli/commands/orchestrate.ts`:
- Around line 14-149: The CLI option block duplicated in the builder function in
orchestrate.ts should be extracted into a shared helper (e.g.,
registerCommonBuildOptions or addOrchestrateOptions) that encapsulates the
repeated .option(...) calls and defaults; move that helper into a new module
(e.g., cli/options.ts) and replace the large option chains in builder of
orchestrate.ts and the equivalent builder in build.ts with a call to this helper
(pass the yargs instance and any command-specific overrides like
providerStrategy defaults), ensuring unique symbols referenced are the builder
function in orchestrate.ts, the corresponding builder in build.ts, and the new
registerCommonBuildOptions helper so both commands import and call it.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: ae38484e-46f1-444b-b655-b92b497e7fd8
⛔ Files ignored due to path filters (2)
dist/index.js.mapis excluded by!**/dist/**,!**/*.mapyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (9)
package.jsonsrc/cli.tssrc/cli/commands/activate.tssrc/cli/commands/build.tssrc/cli/commands/cache.tssrc/cli/commands/orchestrate.tssrc/cli/commands/status.tssrc/cli/commands/version.tssrc/cli/input-mapper.ts
| "yargs": "^18.0.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/base-64": "^1.0.0", | ||
| "@types/jest": "^27.4.1", | ||
| "@types/node": "^17.0.23", | ||
| "@types/semver": "^7.3.9", | ||
| "@types/uuid": "^9.0.0", | ||
| "@types/yargs": "^17.0.35", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Project constraints ==="
if [ -f package.json ]; then
echo "Node engine from package.json:"
jq -r '.engines.node // "not specified"' package.json
echo ""
echo "yargs version from dependencies:"
jq -r '.dependencies.yargs // "not found"' package.json
fi
echo ""
if [ -f tsconfig.json ]; then
echo "TypeScript module setting:"
jq -r '.compilerOptions.module // "not specified"' tsconfig.json
fi
echo ""
echo "=== yargs@18.0.0 metadata ==="
npm view yargs@18.0.0 engines type main --json 2>/dev/null || echo "Failed to fetch"
echo ""
echo "=== yargs@17.7.2 metadata ==="
npm view yargs@17.7.2 engines type main --json 2>/dev/null || echo "Failed to fetch"Repository: game-ci/unity-builder
Length of output: 275
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Project constraints ==="
echo "Node engine from package.json:"
grep -A 1 '"engines"' package.json || echo "not specified"
echo ""
echo "yargs version from dependencies:"
grep '"yargs"' package.json
echo ""
echo "TypeScript module setting from tsconfig.json:"
cat tsconfig.json | head -30
echo ""
echo "=== yargs@18.0.0 full package info ==="
npm view yargs@18.0.0 2>/dev/null | head -50 || echo "Failed to fetch"
echo ""
echo "=== yargs@17.7.2 full package info ==="
npm view yargs@17.7.2 2>/dev/null | head -50 || echo "Failed to fetch"Repository: game-ci/unity-builder
Length of output: 3159
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== yargs@18.0.0 detailed metadata ==="
npm view yargs@18.0.0 --json | jq '.type, .engines, .main, .exports | keys' 2>/dev/null || npm view yargs@18.0.0 engines.node type main 2>/dev/null || echo "Fetching direct metadata..."
echo ""
echo "=== yargs@17.7.2 detailed metadata ==="
npm view yargs@17.7.2 --json | jq '.type, .engines, .main, .exports | keys' 2>/dev/null || npm view yargs@17.7.2 engines.node type main 2>/dev/null || echo "Fetching direct metadata..."
echo ""
echo "=== Checking actual installed yargs versions in node_modules (if available) ==="
if [ -d "node_modules/yargs" ]; then
echo "yargs package.json type and engines:"
jq '.type, .engines' node_modules/yargs/package.json
else
echo "yargs not installed in node_modules"
fiRepository: game-ci/unity-builder
Length of output: 433
yargs@18 is incompatible with the current Node engine declaration.
yargs@18 requires Node ^20.19.0 || ^22.12.0 || >=23, but the project declares >=18.x, creating a version mismatch that can cause installation or runtime failures.
Downgrade to yargs@17.7.2, which supports Node >=12:
- "yargs": "^18.0.0"
+ "yargs": "^17.7.2"📝 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.
| "yargs": "^18.0.0" | |
| }, | |
| "devDependencies": { | |
| "@types/base-64": "^1.0.0", | |
| "@types/jest": "^27.4.1", | |
| "@types/node": "^17.0.23", | |
| "@types/semver": "^7.3.9", | |
| "@types/uuid": "^9.0.0", | |
| "@types/yargs": "^17.0.35", | |
| "yargs": "^17.7.2" | |
| }, | |
| "devDependencies": { | |
| "@types/base-64": "^1.0.0", | |
| "@types/jest": "^27.4.1", | |
| "@types/node": "^17.0.23", | |
| "@types/semver": "^7.3.9", | |
| "@types/uuid": "^9.0.0", | |
| "@types/yargs": "^17.0.35", |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@package.json` around lines 62 - 70, The package.json currently pins "yargs"
at "^18.0.0", which is incompatible with the repository's Node engine (>=18.x);
change the "yargs" dependency entry in package.json to "17.7.2" (or "^17.7.2")
to match Node >=12 compatibility, then regenerate lockfiles by running your
package manager (npm install or yarn install) so package-lock.json / yarn.lock
reflects the downgrade; ensure any related type package "@types/yargs" remains
compatible after the change and adjust it only if type errors appear.
| #!/usr/bin/env node | ||
|
|
||
| import yargs from 'yargs'; | ||
| // eslint-disable-next-line import/no-unresolved |
There was a problem hiding this comment.
Remove the stale eslint-disable directive.
Line 4 triggers eslint-comments/no-unused-disable and should be deleted unless a real unresolved-import warning is reintroduced.
🧰 Tools
🪛 ESLint
[error] 4-4: 'import/no-unresolved' rule is disabled but never reported.
(eslint-comments/no-unused-disable)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/cli.ts` at line 4, Remove the stale eslint-disable directive
"eslint-disable-next-line import/no-unresolved" from the top of the file; locate
the exact comment and delete that line so eslint-comments/no-unused-disable no
longer flags it, and only re-add a disable if an actual unresolved-import
warning later reappears.
| async function main() { | ||
| try { | ||
| await cli.parse(); | ||
| } catch (error: any) { | ||
| if (error.name !== 'YError') { | ||
| core.error(`Error: ${error.message}`); | ||
| } | ||
| } |
There was a problem hiding this comment.
Map parser/configuration errors to exit code 2.
The current YError path does not enforce the config-error exit code contract.
🔧 Proposed fix
async function main() {
try {
await cli.parse();
} catch (error: any) {
- if (error.name !== 'YError') {
- core.error(`Error: ${error.message}`);
- }
+ if (error?.name === 'YError') {
+ process.exitCode = 2;
+ return;
+ }
+ process.exitCode = process.exitCode || 1;
+ core.error(`Error: ${error?.message ?? String(error)}`);
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/cli.ts` around lines 29 - 36, In the main function's catch block (around
cli.parse()), detect YError cases and ensure they exit with code 2: when
error.name === 'YError' call core.error with the error message (or the error
itself) and then invoke process.exit(2); keep the existing behavior for
non-YError errors (log with core.error) but ensure they still exit (e.g.,
process.exit(1) or propagate) as appropriate so parser/configuration errors from
cli.parse() are mapped to exit code 2.
| const unitySerial = process.env.UNITY_SERIAL; | ||
| const unityLicense = process.env.UNITY_LICENSE; | ||
| const licensingServer = cliArguments.unityLicensingServer || process.env.UNITY_LICENSING_SERVER || ''; | ||
|
|
There was a problem hiding this comment.
Use unified Input accessors instead of direct env reads.
At Line 42-Line 45, activation bypasses the mapped CLI input pipeline and can behave differently from build/orchestrate input resolution.
🔧 Proposed fix
import type { CommandModule } from 'yargs';
import * as core from '@actions/core';
import { mapCliArgumentsToInput, CliArguments } from '../input-mapper';
+import Input from '../../model/input';
@@
- const unitySerial = process.env.UNITY_SERIAL;
- const unityLicense = process.env.UNITY_LICENSE;
- const licensingServer = cliArguments.unityLicensingServer || process.env.UNITY_LICENSING_SERVER || '';
+ const unitySerial = Input.unitySerial;
+ const unityLicense = Input.unityLicense;
+ const licensingServer = Input.unityLicensingServer || '';📝 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.
| const unitySerial = process.env.UNITY_SERIAL; | |
| const unityLicense = process.env.UNITY_LICENSE; | |
| const licensingServer = cliArguments.unityLicensingServer || process.env.UNITY_LICENSING_SERVER || ''; | |
| import type { CommandModule } from 'yargs'; | |
| import * as core from '@actions/core'; | |
| import { mapCliArgumentsToInput, CliArguments } from '../input-mapper'; | |
| import Input from '../../model/input'; | |
| const unitySerial = Input.unitySerial; | |
| const unityLicense = Input.unityLicense; | |
| const licensingServer = Input.unityLicensingServer || ''; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/cli/commands/activate.ts` around lines 42 - 45, Replace the direct
environment/CLI reads for unitySerial, unityLicense, and licensingServer with
the unified Input accessors so activation goes through the same input resolution
pipeline as other commands: stop using process.env.UNITY_SERIAL,
process.env.UNITY_LICENSE and cliArguments.unityLicensingServer and instead read
those values via the project's Input API (e.g., Input.get / Input.getOptional or
the project's equivalent) for the keys used by the rest of the CLI
(UNITY_SERIAL, UNITY_LICENSE, UNITY_LICENSING_SERVER); update the variables
unitySerial, unityLicense, and licensingServer in activate.ts to pull from Input
and preserve the same fallback/empty-string behavior.
src/cli/commands/cache.ts
Outdated
| const projectPath = (cliArguments.projectPath as string) || '.'; | ||
| const cacheDirectory = (cliArguments.cacheDir as string) || path.join(projectPath, 'Library'); | ||
|
|
There was a problem hiding this comment.
Make --cache-dir action-specific instead of globally defaulting to Library.
cacheDirectory is always populated (Line 34), so the guard in restoreCache (Line 111) is effectively dead. This causes restore/clear to silently target <project>/Library, which doesn’t match the command intent or examples.
Proposed fix
- const projectPath = (cliArguments.projectPath as string) || '.';
- const cacheDirectory = (cliArguments.cacheDir as string) || path.join(projectPath, 'Library');
+ const projectPath = (cliArguments.projectPath as string) || '.';
+ const cacheDirectory = (cliArguments.cacheDir as string) || '';
try {
switch (action) {
case 'list': {
- await listCache(cacheDirectory, projectPath);
+ await listCache(cacheDirectory || path.join(projectPath, 'Library'), projectPath);
break;
}
case 'restore': {
+ if (!cacheDirectory) {
+ throw new Error('--cache-dir is required for restore');
+ }
await restoreCache(cacheDirectory);
break;
}
case 'clear': {
+ if (!cacheDirectory) {
+ throw new Error('--cache-dir is required for clear');
+ }
await clearCache(cacheDirectory);
break;
}Also applies to: 110-113
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/cli/commands/cache.ts` around lines 33 - 35, The file always sets
cacheDirectory to (cliArguments.cacheDir as string) || path.join(projectPath,
'Library'), making the restoreCache guard dead and causing restore/clear to
silently use <project>/Library; change this so --cache-dir is action-specific:
do not default cacheDirectory to 'Library' at the top-level — only resolve a
default when handling the specific actions that need it (e.g., inside the
restore and clear command handlers or inside restoreCache), or keep cacheDir
undefined unless provided and then in restoreCache (function restoreCache)
compute path.join(projectPath, 'Library') only when cacheDir is missing and the
action is restore/clear; update references to projectPath and cacheDirectory
accordingly so other actions ignore the default.
src/cli/commands/cache.ts
Outdated
| const cacheFiles = fs.readdirSync(cacheDirectory).filter((f) => f.endsWith('.tar') || f.endsWith('.tar.lz4')); | ||
| if (cacheFiles.length === 0) { | ||
| core.info('No cache archives found to restore.'); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| // Sort by modification time, newest first | ||
| const sorted = cacheFiles | ||
| .map((f) => ({ name: f, mtime: fs.statSync(path.join(cacheDirectory, f)).mtime })) | ||
| .sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); | ||
|
|
||
| core.info(`Found ${sorted.length} cache archive(s). Latest: ${sorted[0].name}`); | ||
| core.info('Use the orchestrator cache system for full restore functionality:'); | ||
| core.info(' game-ci orchestrate --cache-key <key> ...'); | ||
| } |
There was a problem hiding this comment.
cache restore currently succeeds without restoring anything.
The command only discovers archives and prints guidance, then exits successfully. This is misleading for automation and can mask a missing restore step.
Minimal safe behavior until restore is implemented
- core.info(`Found ${sorted.length} cache archive(s). Latest: ${sorted[0].name}`);
- core.info('Use the orchestrator cache system for full restore functionality:');
- core.info(' game-ci orchestrate --cache-key <key> ...');
+ core.info(`Found ${sorted.length} cache archive(s). Latest: ${sorted[0].name}`);
+ throw new Error(
+ 'cache restore is not implemented yet in this command. Use `game-ci orchestrate --cache-key <key> ...`.'
+ );📝 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.
| const cacheFiles = fs.readdirSync(cacheDirectory).filter((f) => f.endsWith('.tar') || f.endsWith('.tar.lz4')); | |
| if (cacheFiles.length === 0) { | |
| core.info('No cache archives found to restore.'); | |
| return; | |
| } | |
| // Sort by modification time, newest first | |
| const sorted = cacheFiles | |
| .map((f) => ({ name: f, mtime: fs.statSync(path.join(cacheDirectory, f)).mtime })) | |
| .sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); | |
| core.info(`Found ${sorted.length} cache archive(s). Latest: ${sorted[0].name}`); | |
| core.info('Use the orchestrator cache system for full restore functionality:'); | |
| core.info(' game-ci orchestrate --cache-key <key> ...'); | |
| } | |
| const cacheFiles = fs.readdirSync(cacheDirectory).filter((f) => f.endsWith('.tar') || f.endsWith('.tar.lz4')); | |
| if (cacheFiles.length === 0) { | |
| core.info('No cache archives found to restore.'); | |
| return; | |
| } | |
| // Sort by modification time, newest first | |
| const sorted = cacheFiles | |
| .map((f) => ({ name: f, mtime: fs.statSync(path.join(cacheDirectory, f)).mtime })) | |
| .sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); | |
| core.info(`Found ${sorted.length} cache archive(s). Latest: ${sorted[0].name}`); | |
| throw new Error( | |
| 'cache restore is not implemented yet in this command. Use `game-ci orchestrate --cache-key <key> ...`.' | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/cli/commands/cache.ts` around lines 122 - 137, The cache restore command
currently only lists archives and exits successfully; change the behavior to
fail fast until restore is implemented: after computing sorted (using
cacheFiles, cacheDirectory, sorted), replace the final core.info guidance
messages with a non-success exit (e.g., call core.setFailed('cache restore not
implemented - no restore performed') or throw an Error or set process.exitCode =
1) so CI/automation surfaces the missing restore step instead of succeeding
silently.
…mand Replace the npm-only publish-cli.yml with a comprehensive release-cli.yml that builds standalone binaries via pkg for all platforms (Linux/macOS/Windows, x64/arm64), uploads them as GitHub Release assets with SHA256 checksums, and retains npm publish as an optional job. Add curl-pipe-sh installer (install.sh) and PowerShell installer (install.ps1) for one-liner installation from GitHub Releases. Both scripts auto-detect platform/architecture, verify checksums, and guide PATH configuration. Add `game-ci update` command for self-updating standalone binaries: checks GitHub releases for newer versions, downloads the correct platform binary, verifies it, and atomically replaces the running executable. Distribution strategy: GitHub Releases (primary), npm (optional), with winget/Homebrew/Chocolatey/Scoop as future providers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
.github/workflows/publish-cli.yml (1)
48-55: Simplify boolean input comparison using the moderninputscontext.Lines 48 and 54 use string comparison (
== 'true'/== 'false') with theinputscontext. Theinputscontext (the recommended approach forworkflow_dispatchinputs) handles boolean conversion automatically, so the string comparisons are unnecessary. Use the inputs directly:inputs['dry-run']and!inputs['dry-run'].Suggested refactor
- name: Publish (dry run) - if: github.event_name == 'workflow_dispatch' && inputs.dry-run == 'true' + if: github.event_name == 'workflow_dispatch' && inputs['dry-run'] run: npm publish --dry-run env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Publish - if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.dry-run == 'false') + if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs['dry-run']) run: npm publish --provenance --access public🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/publish-cli.yml around lines 48 - 55, Replace the string boolean comparisons against the workflow_dispatch input with direct boolean checks using the inputs context: update the dry-run step condition from "github.event_name == 'workflow_dispatch' && inputs.dry-run == 'true'" to use "github.event_name == 'workflow_dispatch' && inputs['dry-run']" and update the Publish step condition from "... && inputs.dry-run == 'false'" to use "... && !inputs['dry-run']"; ensure you modify the conditions that guard the npm publish --dry-run and npm publish --provenance steps (the steps currently using inputs.dry-run string comparisons).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/publish-cli.yml:
- Around line 4-5: The workflow file path is wrong and the release trigger and
publish step need hardening: rename `.github/workflows/publish-cli.yml` to
`.github/workflows/release-cli.yml`, update the workflow so the `release`
trigger distinguishes prereleases (e.g., use `types: [published]` plus a
condition checking `github.event.release.prerelease`), and modify the npm
publish step (the step running `npm publish`) to publish prereleases with `--tag
next` while only publishing non-prereleases without an explicit tag (or with
`--tag latest`) so prereleases are not promoted to latest.
In `@src/cli/__tests__/input-mapper.test.ts`:
- Around line 5-8: The teardown in afterEach currently restores mocks and
Cli.options but doesn't reset the global flag GitHub.githubInputEnabled modified
by mapCliArgumentsToInput; update the afterEach block to also reset
GitHub.githubInputEnabled (e.g., set GitHub.githubInputEnabled = undefined or
its default) after jest.restoreAllMocks() and Cli.options = undefined so tests
don't leak state between runs.
---
Nitpick comments:
In @.github/workflows/publish-cli.yml:
- Around line 48-55: Replace the string boolean comparisons against the
workflow_dispatch input with direct boolean checks using the inputs context:
update the dry-run step condition from "github.event_name == 'workflow_dispatch'
&& inputs.dry-run == 'true'" to use "github.event_name == 'workflow_dispatch' &&
inputs['dry-run']" and update the Publish step condition from "... &&
inputs.dry-run == 'false'" to use "... && !inputs['dry-run']"; ensure you modify
the conditions that guard the npm publish --dry-run and npm publish --provenance
steps (the steps currently using inputs.dry-run string comparisons).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: f9eb4f67-7105-46c1-976d-d30991a1c862
📒 Files selected for processing (4)
.github/workflows/publish-cli.ymlsrc/cli/__tests__/cli-integration.test.tssrc/cli/__tests__/commands.test.tssrc/cli/__tests__/input-mapper.test.ts
.github/workflows/publish-cli.yml
Outdated
| release: | ||
| types: [published] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n .github/workflows/publish-cli.ymlRepository: game-ci/unity-builder
Length of output: 130
🌐 Web query:
npm publish default dist-tag prerelease versions recommended approach
💡 Result:
- Default behavior:
npm publish(with no--tag) publishes your version under thelatestdist-tag. That meansnpm install your-pkg(with no version/tag) will install whatever is taggedlatest. [1], [2] - Problem: If you publish a SemVer prerelease like
1.2.0-beta.1without--tag, you’ll still movelatestto that prerelease, and users doing a normal install may get it. [1], [3] - Recommended approach (common practice):
- Publish prereleases to a non-latest tag (often
next,beta,alpha,rc):npm publish --tag next[3]
- Tell testers to install explicitly:
npm install your-pkg@next(or@beta, etc.) [2], [3]
- When ready, publish the stable release without
--tagso it becomeslatest:npm publish[1], [3]
- Publish prereleases to a non-latest tag (often
- If you need to adjust tags after publishing: use dist-tag commands (e.g.,
npm dist-tag add your-pkg@1.2.0 latest,npm dist-tag rm your-pkg next). [2]
Sources: npm dist-tags docs and guidance on publishing prereleases with explicit tags. [1], [2], [3], [4]
🏁 Script executed:
find .github/workflows -type f -name "*.yml" -o -name "*.yaml" | head -20Repository: game-ci/unity-builder
Length of output: 443
🏁 Script executed:
ls -la .github/workflows/ 2>/dev/null || echo "Directory not found"Repository: game-ci/unity-builder
Length of output: 878
🏁 Script executed:
cat -n .github/workflows/release-cli.ymlRepository: game-ci/unity-builder
Length of output: 6284
Correct file path and handle prerelease publication separately to prevent accidental promotion to latest.
The workflow file is .github/workflows/release-cli.yml (not publish-cli.yml). The trigger release: types: [published] fires for both stable releases and prereleases, and the npm publish command at line 168 does not specify a dist-tag, defaulting to latest. This causes prerelease versions to be tagged as latest, which users will receive on standard installations.
Separate prerelease publishing to use --tag next and add a condition to prevent prereleases from being tagged as latest:
Proposed hardening
+ - name: Publish prerelease
+ if: github.event_name == 'release' && github.event.release.prerelease
+ run: npm publish --provenance --access public --tag next
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
- name: Publish to npm
- if: >-
- (github.event_name == 'release') || (github.event_name == 'workflow_dispatch' && inputs.publish-npm)
+ if: (github.event_name == 'release' && !github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && inputs.publish-npm)
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/publish-cli.yml around lines 4 - 5, The workflow file path
is wrong and the release trigger and publish step need hardening: rename
`.github/workflows/publish-cli.yml` to `.github/workflows/release-cli.yml`,
update the workflow so the `release` trigger distinguishes prereleases (e.g.,
use `types: [published]` plus a condition checking
`github.event.release.prerelease`), and modify the npm publish step (the step
running `npm publish`) to publish prereleases with `--tag next` while only
publishing non-prereleases without an explicit tag (or with `--tag latest`) so
prereleases are not promoted to latest.
| afterEach(() => { | ||
| jest.restoreAllMocks(); | ||
| Cli.options = undefined; | ||
| }); |
There was a problem hiding this comment.
Reset GitHub.githubInputEnabled in teardown to avoid cross-test state leakage.
mapCliArgumentsToInput disables GitHub input globally, but teardown currently restores only Cli.options. This can make other tests order-dependent.
✅ Suggested fix
afterEach(() => {
jest.restoreAllMocks();
Cli.options = undefined;
+ GitHub.githubInputEnabled = true;
});📝 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.
| afterEach(() => { | |
| jest.restoreAllMocks(); | |
| Cli.options = undefined; | |
| }); | |
| afterEach(() => { | |
| jest.restoreAllMocks(); | |
| Cli.options = undefined; | |
| GitHub.githubInputEnabled = true; | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/cli/__tests__/input-mapper.test.ts` around lines 5 - 8, The teardown in
afterEach currently restores mocks and Cli.options but doesn't reset the global
flag GitHub.githubInputEnabled modified by mapCliArgumentsToInput; update the
afterEach block to also reset GitHub.githubInputEnabled (e.g., set
GitHub.githubInputEnabled = undefined or its default) after
jest.restoreAllMocks() and Cli.options = undefined so tests don't leak state
between runs.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
src/cli/commands/update.ts (2)
31-76: Inconsistent redirect limits betweenfetchJson(5) anddownloadFile(10).Both functions follow redirects, but
fetchJsonallows 5 whiledownloadFileallows 10. For consistency and since GitHub's asset downloads may involve multiple redirects, consider aligning both to 10.♻️ Proposed fix
function fetchJson(url: string): Promise<any> { return new Promise((resolve, reject) => { const get = (targetUrl: string, redirectCount: number) => { - if (redirectCount > 5) { + if (redirectCount > 10) { reject(new Error('Too many redirects'));Also applies to: 81-116
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cli/commands/update.ts` around lines 31 - 76, fetchJson currently stops after 5 redirects while downloadFile allows 10, causing inconsistent behavior and potential failures for GitHub asset downloads; update the redirect limit in fetchJson (inside the inner get function) from 5 to 10 to match downloadFile and ensure both functions follow the same redirect policy, keeping the same error message logic but changing the numeric threshold and any related condition checks that reference that limit; verify the change in the fetchJson function definition and its get(targetUrl: string, redirectCount: number) implementation.
210-235: Semver comparison ignores pre-release suffixes.
parseVersionPartsstrips non-numeric suffixes, sov2.0.0-beta.1andv2.0.0compare as equal. If pre-release versions are used, this could cause unexpected behavior (e.g., not updating from beta to stable).Consider using the existing
semverpackage (already a dependency in package.json line 71) for robust comparison:♻️ Proposed fix using semver package
+import semver from 'semver'; -function parseVersionParts(version: string): number[] { - return version - .replace(/^v/, '') - .split('.') - .map((part) => Number(part)); -} - -function compareSemver(a: string, b: string): number { - const partsA = parseVersionParts(a); - const partsB = parseVersionParts(b); - - for (let index = 0; index < 3; index++) { - const x = partsA[index] || 0; - const y = partsB[index] || 0; - if (x < y) return -1; - if (x > y) return 1; - } - - return 0; -} +function compareSemver(a: string, b: string): number { + const cleanA = a.replace(/^v/, ''); + const cleanB = b.replace(/^v/, ''); + return semver.compare(cleanA, cleanB); +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cli/commands/update.ts` around lines 210 - 235, The current parseVersionParts/compareSemver pair strips pre-release suffixes and treats "v2.0.0-beta.1" as equal to "v2.0.0"; replace this custom logic with the semver package to correctly handle pre-release and other semver edge cases: import/require the existing semver dependency and update compareSemver to delegate to semver.compare (or semver.rcompare as appropriate) returning -1/0/1, and remove or stop using parseVersionParts so pre-release tags (e.g., "-beta.1") are preserved in comparisons.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@install.ps1`:
- Around line 85-87: The empty catch block after the checksum download silently
swallows errors; update that catch to log a clear warning or informational
message (including the caught error's message) so users know checksum
verification was skipped — in the catch following the checksum fetch/download
logic, call Write-Warning or Write-Host with a descriptive message and include
$_ or $_.Exception.Message to surface the failure details.
- Around line 112-119: The interactive Read-Host prompt can hang when the script
is piped; update the block around Read-Host/$AddToPath to first detect
non-interactive execution (e.g. check if $Host.Name -ne 'ConsoleHost' or wrap a
test of [Console]::KeyAvailable in a try/catch) and, when non-interactive or an
exception occurs, default $AddToPath to 'n' (do not modify PATH); otherwise
proceed with the current logic that calls
[Environment]::SetEnvironmentVariable('PATH', "$InstallDir;$UserPath", 'User'),
updates $env:PATH and calls Write-Info — ensure the detection and fallback are
applied before calling Read-Host so piped/redirected runs never hang.
In `@install.sh`:
- Around line 141-144: The PROFILE assignments in the shell case block (the
lines setting PROFILE in the zsh/bash/fish/* branches) use quoted tildes which
won't expand; update those assignments to use $HOME (e.g.,
PROFILE="$HOME/.zshrc", PROFILE="$HOME/.bashrc",
PROFILE="$HOME/.config/fish/config.fish", PROFILE="$HOME/.profile") so the path
expands correctly when used later in the script.
- Around line 192-196: The install flow currently calls install before
verify_checksum so a potentially tampered binary can be placed and made
executable; modify the flow so checksum verification occurs before making the
binary final: either (A) adjust the sequence to call verify_checksum immediately
after get_latest_version and before install makes the file executable, or (B)
change install to download to a temporary file, run verify_checksum against that
temp file, and only move/rename it into place and set permissions if
verification succeeds; if verification fails, ensure the temp file is removed
and the script exits non‑zero (also add cleanup in the install failure path).
Reference the functions detect_platform, get_latest_version, install and
verify_checksum when updating the flow.
---
Nitpick comments:
In `@src/cli/commands/update.ts`:
- Around line 31-76: fetchJson currently stops after 5 redirects while
downloadFile allows 10, causing inconsistent behavior and potential failures for
GitHub asset downloads; update the redirect limit in fetchJson (inside the inner
get function) from 5 to 10 to match downloadFile and ensure both functions
follow the same redirect policy, keeping the same error message logic but
changing the numeric threshold and any related condition checks that reference
that limit; verify the change in the fetchJson function definition and its
get(targetUrl: string, redirectCount: number) implementation.
- Around line 210-235: The current parseVersionParts/compareSemver pair strips
pre-release suffixes and treats "v2.0.0-beta.1" as equal to "v2.0.0"; replace
this custom logic with the semver package to correctly handle pre-release and
other semver edge cases: import/require the existing semver dependency and
update compareSemver to delegate to semver.compare (or semver.rcompare as
appropriate) returning -1/0/1, and remove or stop using parseVersionParts so
pre-release tags (e.g., "-beta.1") are preserved in comparisons.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: d88889dc-4419-4e14-b1c9-82dea49dde25
⛔ Files ignored due to path filters (3)
dist/index.jsis excluded by!**/dist/**dist/index.js.mapis excluded by!**/dist/**,!**/*.mapyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (8)
.github/workflows/release-cli.ymlinstall.ps1install.shpackage.jsonsrc/cli.tssrc/cli/__tests__/cli-integration.test.tssrc/cli/__tests__/commands.test.tssrc/cli/commands/update.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/cli/tests/cli-integration.test.ts
- src/cli/tests/commands.test.ts
| } catch { | ||
| # Checksums not available for this release; continue without verification | ||
| } |
There was a problem hiding this comment.
Empty catch block silently swallows checksum fetch errors.
When the checksum file download fails, the script continues without any indication. Add a message for transparency.
🔧 Proposed fix
} catch {
# Checksums not available for this release; continue without verification
+ Write-Warn "Checksum file not available; skipping verification."
}📝 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.
| } catch { | |
| # Checksums not available for this release; continue without verification | |
| } | |
| } catch { | |
| # Checksums not available for this release; continue without verification | |
| Write-Warning "Checksum file not available; skipping verification." | |
| } |
🧰 Tools
🪛 PSScriptAnalyzer (1.24.0)
[warning] 85-87: Empty catch block is used. Please use Write-Error or throw statements in catch blocks.
(PSAvoidUsingEmptyCatchBlock)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@install.ps1` around lines 85 - 87, The empty catch block after the checksum
download silently swallows errors; update that catch to log a clear warning or
informational message (including the caught error's message) so users know
checksum verification was skipped — in the catch following the checksum
fetch/download logic, call Write-Warning or Write-Host with a descriptive
message and include $_ or $_.Exception.Message to surface the failure details.
| # Offer to add automatically | ||
| Write-Host "" | ||
| $AddToPath = Read-Host "Add to PATH now? (Y/n)" | ||
| if ($AddToPath -ne 'n' -and $AddToPath -ne 'N') { | ||
| [Environment]::SetEnvironmentVariable('PATH', "$InstallDir;$UserPath", 'User') | ||
| $env:PATH = "$InstallDir;$env:PATH" | ||
| Write-Info "Added to PATH. You can now run: game-ci --help" | ||
| } |
There was a problem hiding this comment.
Interactive Read-Host may fail or hang when script is piped.
The documented usage (irm ... | iex) pipes the script, which can cause Read-Host to behave unexpectedly (hang or return empty). Consider detecting non-interactive mode or defaulting to not modifying PATH when piped.
🔧 Proposed fix
# Offer to add automatically
Write-Host ""
- $AddToPath = Read-Host "Add to PATH now? (Y/n)"
- if ($AddToPath -ne 'n' -and $AddToPath -ne 'N') {
+ # Check if running interactively
+ if ([Environment]::UserInteractive -and [Console]::KeyAvailable -ne $null) {
+ $AddToPath = Read-Host "Add to PATH now? (Y/n)"
+ if ($AddToPath -ne 'n' -and $AddToPath -ne 'N') {
+ [Environment]::SetEnvironmentVariable('PATH', "$InstallDir;$UserPath", 'User')
+ $env:PATH = "$InstallDir;$env:PATH"
+ Write-Info "Added to PATH. You can now run: game-ci --help"
+ }
+ } else {
+ Write-Info "Run the command above to add game-ci to your PATH."
+ }Alternatively, wrap in a try/catch to handle the piped scenario gracefully.
📝 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.
| # Offer to add automatically | |
| Write-Host "" | |
| $AddToPath = Read-Host "Add to PATH now? (Y/n)" | |
| if ($AddToPath -ne 'n' -and $AddToPath -ne 'N') { | |
| [Environment]::SetEnvironmentVariable('PATH', "$InstallDir;$UserPath", 'User') | |
| $env:PATH = "$InstallDir;$env:PATH" | |
| Write-Info "Added to PATH. You can now run: game-ci --help" | |
| } | |
| # Offer to add automatically | |
| Write-Host "" | |
| # Check if running interactively | |
| if ([Environment]::UserInteractive -and [Console]::KeyAvailable -ne $null) { | |
| $AddToPath = Read-Host "Add to PATH now? (Y/n)" | |
| if ($AddToPath -ne 'n' -and $AddToPath -ne 'N') { | |
| [Environment]::SetEnvironmentVariable('PATH', "$InstallDir;$UserPath", 'User') | |
| $env:PATH = "$InstallDir;$env:PATH" | |
| Write-Info "Added to PATH. You can now run: game-ci --help" | |
| } | |
| } else { | |
| Write-Info "Run the command above to add game-ci to your PATH." | |
| } |
🧰 Tools
🪛 PSScriptAnalyzer (1.24.0)
[warning] 113-113: File 'install.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.
(PSAvoidUsingWriteHost)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@install.ps1` around lines 112 - 119, The interactive Read-Host prompt can
hang when the script is piped; update the block around Read-Host/$AddToPath to
first detect non-interactive execution (e.g. check if $Host.Name -ne
'ConsoleHost' or wrap a test of [Console]::KeyAvailable in a try/catch) and,
when non-interactive or an exception occurs, default $AddToPath to 'n' (do not
modify PATH); otherwise proceed with the current logic that calls
[Environment]::SetEnvironmentVariable('PATH', "$InstallDir;$UserPath", 'User'),
updates $env:PATH and calls Write-Info — ensure the detection and fallback are
applied before calling Read-Host so piped/redirected runs never hang.
| zsh) PROFILE="~/.zshrc" ;; | ||
| bash) PROFILE="~/.bashrc" ;; | ||
| fish) PROFILE="~/.config/fish/config.fish" ;; | ||
| *) PROFILE="~/.profile" ;; |
There was a problem hiding this comment.
Tilde does not expand inside quotes; use $HOME instead.
The PROFILE assignments use quoted tildes (e.g., "~/.zshrc"), which won't expand to the user's home directory when printed or used.
🔧 Proposed fix
case "$SHELL_NAME" in
- zsh) PROFILE="~/.zshrc" ;;
- bash) PROFILE="~/.bashrc" ;;
- fish) PROFILE="~/.config/fish/config.fish" ;;
- *) PROFILE="~/.profile" ;;
+ zsh) PROFILE="$HOME/.zshrc" ;;
+ bash) PROFILE="$HOME/.bashrc" ;;
+ fish) PROFILE="$HOME/.config/fish/config.fish" ;;
+ *) PROFILE="$HOME/.profile" ;;
esac📝 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.
| zsh) PROFILE="~/.zshrc" ;; | |
| bash) PROFILE="~/.bashrc" ;; | |
| fish) PROFILE="~/.config/fish/config.fish" ;; | |
| *) PROFILE="~/.profile" ;; | |
| zsh) PROFILE="$HOME/.zshrc" ;; | |
| bash) PROFILE="$HOME/.bashrc" ;; | |
| fish) PROFILE="$HOME/.config/fish/config.fish" ;; | |
| *) PROFILE="$HOME/.profile" ;; |
🧰 Tools
🪛 Shellcheck (0.11.0)
[warning] 141-141: Tilde does not expand in quotes. Use $HOME.
(SC2088)
[warning] 142-142: Tilde does not expand in quotes. Use $HOME.
(SC2088)
[warning] 143-143: Tilde does not expand in quotes. Use $HOME.
(SC2088)
[warning] 144-144: Tilde does not expand in quotes. Use $HOME.
(SC2088)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@install.sh` around lines 141 - 144, The PROFILE assignments in the shell case
block (the lines setting PROFILE in the zsh/bash/fish/* branches) use quoted
tildes which won't expand; update those assignments to use $HOME (e.g.,
PROFILE="$HOME/.zshrc", PROFILE="$HOME/.bashrc",
PROFILE="$HOME/.config/fish/config.fish", PROFILE="$HOME/.profile") so the path
expands correctly when used later in the script.
| # Main | ||
| detect_platform | ||
| get_latest_version | ||
| install | ||
| verify_checksum |
There was a problem hiding this comment.
Checksum verification occurs after binary is already installed.
The verify_checksum function is called after install, meaning a potentially corrupted or tampered binary is already written to disk and made executable before verification. If checksum verification fails, the binary remains installed.
Reorder to verify checksum before making the binary executable, or clean up on failure:
🔧 Proposed fix
# Main
detect_platform
get_latest_version
install
-verify_checksum
+verify_checksum || {
+ rm -f "${INSTALL_DIR}/${BINARY_NAME}"
+ error "Checksum verification failed. Binary removed."
+}Alternatively, restructure to download to a temp file first, verify, then move to final location (similar to how install.ps1 handles this with Remove-Item on failure at line 81).
📝 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.
| # Main | |
| detect_platform | |
| get_latest_version | |
| install | |
| verify_checksum | |
| # Main | |
| detect_platform | |
| get_latest_version | |
| install | |
| verify_checksum || { | |
| rm -f "${INSTALL_DIR}/${BINARY_NAME}" | |
| error "Checksum verification failed. Binary removed." | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@install.sh` around lines 192 - 196, The install flow currently calls install
before verify_checksum so a potentially tampered binary can be placed and made
executable; modify the flow so checksum verification occurs before making the
binary final: either (A) adjust the sequence to call verify_checksum immediately
after get_latest_version and before install makes the file executable, or (B)
change install to download to a temporary file, run verify_checksum against that
temp file, and only move/rename it into place and set permissions if
verification succeeds; if verification fails, ensure the temp file is removed
and the script exits non‑zero (also add cleanup in the install failure path).
Reference the functions detect_platform, get_latest_version, install and
verify_checksum when updating the flow.
…safety - Add process.exit(1) in cli.ts catch block so failures produce non-zero exit codes - Add 6 missing build inputs: containerRegistryRepository, containerRegistryImageVersion, dockerIsolationMode, sshPublicKeysDirectoryPath, cacheUnityInstallationOnMac, unityHubVersionOnMac - Add 6 missing orchestrate inputs: kubeStorageClass, readInputFromOverrideList, readInputOverrideCommand, postBuildSteps, preBuildSteps, customJob - Fix activate command description to accurately reflect verification behavior - Add null check before accessing result.BuildResults in orchestrate handler Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (3)
src/cli.ts (2)
34-38:⚠️ Potential issue | 🟠 MajorMap
YErrorto exit code2in the top-level catch.Line 35 currently skips
YErrorentirely, so parser/configuration failures can miss the documented config-error exit code.Suggested fix
} catch (error: any) { - if (error.name !== 'YError') { - core.error(`Error: ${error.message}`); - process.exit(1); - } + if (error?.name === 'YError') { + core.error(`Error: ${error?.message ?? String(error)}`); + process.exit(2); + } + core.error(`Error: ${error?.message ?? String(error)}`); + process.exit(1); }#!/bin/bash # Verify current top-level error mapping and related tests. rg -n -C3 "YError|process\\.exit\\(|cli\\.parse\\(" src/cli.ts rg -n -C3 "exit code|YError|parse" src/cli/__tests__🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cli.ts` around lines 34 - 38, The top-level catch in main CLI (catch block around cli.parse in src/cli.ts) currently ignores errors with error.name === 'YError', causing config/parse failures to not trigger the documented config-error exit code; update that catch to detect YError and map it to process.exit(2) (and emit a clear error via core.error or similar before exiting), while preserving the existing behavior for non-YError cases (still logging error.message and exiting with code 1).
4-4:⚠️ Potential issue | 🟡 MinorRemove the stale eslint-disable directive.
Line 4 still suppresses
import/no-unresolvedwithout an active violation and triggerseslint-comments/no-unused-disable.Suggested fix
import yargs from 'yargs'; -// eslint-disable-next-line import/no-unresolved import { hideBin } from 'yargs/helpers';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cli.ts` at line 4, Remove the stale ESLint disable comment "// eslint-disable-next-line import/no-unresolved" at the top of src/cli.ts (the unused inline eslint-disable directive) so eslint-comments/no-unused-disable no longer flags it; simply delete that line (or replace it with a contextual, valid comment only if an actual import/no-unresolved violation exists), leaving the rest of the file and imports unchanged.src/cli/commands/activate.ts (1)
42-45:⚠️ Potential issue | 🟠 MajorUse unified
Inputaccessors for activation inputs.Lines 42-45 bypass the mapped CLI input pipeline, so activation resolution can diverge from build/orchestrate behavior.
Suggested fix
import type { CommandModule } from 'yargs'; import * as core from '@actions/core'; import { mapCliArgumentsToInput, CliArguments } from '../input-mapper'; +import Input from '../../model/input'; @@ - const unitySerial = process.env.UNITY_SERIAL; - const unityLicense = process.env.UNITY_LICENSE; - const licensingServer = cliArguments.unityLicensingServer || process.env.UNITY_LICENSING_SERVER || ''; + const unitySerial = Input.unitySerial; + const unityLicense = Input.unityLicense; + const licensingServer = Input.unityLicensingServer || '';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cli/commands/activate.ts` around lines 42 - 45, The activation code is reading environment/cli args directly (unitySerial, unityLicense, licensingServer) instead of the project's unified Input accessors; replace those direct reads with the mapped Input API (e.g., use Input.get or the project's Input.resolve accessor for the keys used elsewhere such as "unitySerial", "unityLicense", and "unityLicensingServer"), preserve the fallback/default behavior (empty string) and ensure Input is imported where activate.ts declares const unitySerial/const unityLicense/const licensingServer so activation uses the same input pipeline as build/orchestrate.
🧹 Nitpick comments (1)
src/cli/commands/build.ts (1)
167-172: Convertrun-as-host-userandskip-activationflags to boolean type for improved CLI ergonomics.These options are currently defined as strings, which requires users to pass
--skip-activation=trueinstead of just--skip-activation. Converting to boolean types aligns with their semantic meaning and the existinginput-mapperalready handles conversion to strings (line 96) for downstream compatibility.Apply to:
src/cli/commands/build.ts:run-as-host-user(lines 167–172) andskip-activation(lines 197–202)src/cli/commands/orchestrate.ts:skip-activation(lines 135–140)src/cli/input-mapper.ts: UpdateCliArgumentsinterface to declarerunAsHostUserandskipActivationas boolean (lines 44, 62)Suggested refactor for build.ts
.option('run-as-host-user', { alias: 'runAsHostUser', - type: 'string', + type: 'boolean', description: 'Whether to run as a user that matches the host system', - default: 'false', + default: false, }) @@ .option('skip-activation', { alias: 'skipActivation', - type: 'string', + type: 'boolean', description: 'Skip the activation/deactivation of Unity', - default: 'false', + default: false, })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cli/commands/build.ts` around lines 167 - 172, Change the CLI flags from string to boolean: update the .option entries for 'run-as-host-user' and 'skip-activation' in the build command (the .option calls with alias runAsHostUser and skipActivation) and the 'skip-activation' option in the orchestrate command to use type: 'boolean' and default: false (leave aliases unchanged). Also update the CliArguments interface in input-mapper (fields runAsHostUser and skipActivation) to be boolean types so downstream code and the existing string conversion remain compatible. Ensure no other logic expects string values for these flags (adjust any parsing that assumes strings if present).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/cli.ts`:
- Around line 34-38: The top-level catch in main CLI (catch block around
cli.parse in src/cli.ts) currently ignores errors with error.name === 'YError',
causing config/parse failures to not trigger the documented config-error exit
code; update that catch to detect YError and map it to process.exit(2) (and emit
a clear error via core.error or similar before exiting), while preserving the
existing behavior for non-YError cases (still logging error.message and exiting
with code 1).
- Line 4: Remove the stale ESLint disable comment "// eslint-disable-next-line
import/no-unresolved" at the top of src/cli.ts (the unused inline eslint-disable
directive) so eslint-comments/no-unused-disable no longer flags it; simply
delete that line (or replace it with a contextual, valid comment only if an
actual import/no-unresolved violation exists), leaving the rest of the file and
imports unchanged.
In `@src/cli/commands/activate.ts`:
- Around line 42-45: The activation code is reading environment/cli args
directly (unitySerial, unityLicense, licensingServer) instead of the project's
unified Input accessors; replace those direct reads with the mapped Input API
(e.g., use Input.get or the project's Input.resolve accessor for the keys used
elsewhere such as "unitySerial", "unityLicense", and "unityLicensingServer"),
preserve the fallback/default behavior (empty string) and ensure Input is
imported where activate.ts declares const unitySerial/const unityLicense/const
licensingServer so activation uses the same input pipeline as build/orchestrate.
---
Nitpick comments:
In `@src/cli/commands/build.ts`:
- Around line 167-172: Change the CLI flags from string to boolean: update the
.option entries for 'run-as-host-user' and 'skip-activation' in the build
command (the .option calls with alias runAsHostUser and skipActivation) and the
'skip-activation' option in the orchestrate command to use type: 'boolean' and
default: false (leave aliases unchanged). Also update the CliArguments interface
in input-mapper (fields runAsHostUser and skipActivation) to be boolean types so
downstream code and the existing string conversion remain compatible. Ensure no
other logic expects string values for these flags (adjust any parsing that
assumes strings if present).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 8ea8550a-1f3b-4d29-bc10-148f4b7f603e
📒 Files selected for processing (5)
src/cli.tssrc/cli/commands/activate.tssrc/cli/commands/build.tssrc/cli/commands/orchestrate.tssrc/cli/input-mapper.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/cli/commands/orchestrate.ts
| name: Build ${{ matrix.target }} | ||
| runs-on: ${{ matrix.os }} | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| include: | ||
| - target: linux-x64 | ||
| os: ubuntu-latest | ||
| pkg-target: node20-linux-x64 | ||
| binary-name: game-ci-linux-x64 | ||
| - target: linux-arm64 | ||
| os: ubuntu-latest | ||
| pkg-target: node20-linux-arm64 | ||
| binary-name: game-ci-linux-arm64 | ||
| - target: macos-x64 | ||
| os: macos-latest | ||
| pkg-target: node20-macos-x64 | ||
| binary-name: game-ci-macos-x64 | ||
| - target: macos-arm64 | ||
| os: macos-latest | ||
| pkg-target: node20-macos-arm64 | ||
| binary-name: game-ci-macos-arm64 | ||
| - target: windows-x64 | ||
| os: windows-latest | ||
| pkg-target: node20-win-x64 | ||
| binary-name: game-ci-windows-x64.exe | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| ref: ${{ github.event.release.tag_name || inputs.tag || github.ref }} | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
|
|
||
| - name: Install dependencies | ||
| run: yarn install --frozen-lockfile | ||
|
|
||
| - name: Build TypeScript | ||
| run: yarn build | ||
|
|
||
| - name: Verify CLI before packaging | ||
| run: node lib/cli.js version | ||
|
|
||
| - name: Build standalone binary | ||
| run: npx pkg lib/cli.js --target ${{ matrix.pkg-target }} --output ${{ matrix.binary-name }} --compress GZip | ||
|
|
||
| - name: Verify standalone binary (non-cross-compiled) | ||
| if: | | ||
| (matrix.target == 'linux-x64' && runner.os == 'Linux') || | ||
| (matrix.target == 'macos-arm64' && runner.os == 'macOS' && runner.arch == 'ARM64') || | ||
| (matrix.target == 'macos-x64' && runner.os == 'macOS' && runner.arch == 'X64') || | ||
| (matrix.target == 'windows-x64' && runner.os == 'Windows') | ||
| run: ./${{ matrix.binary-name }} version | ||
| shell: bash | ||
|
|
||
| - uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: binary-${{ matrix.target }} | ||
| path: ${{ matrix.binary-name }} | ||
| retention-days: 5 | ||
|
|
||
| create-checksums-and-upload: |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 days ago
Generally, the fix is to explicitly set least-privilege permissions for any job (or workflow) that uses the default GITHUB_TOKEN permissions. For this workflow, the build-binaries job only needs to read repository contents and interact with artifacts; it does not write to the repository, issues, or releases. Therefore, we should give it contents: read.
The best minimal fix without changing functionality is to add a permissions block under the build-binaries job, similar to the other jobs. Concretely, in .github/workflows/release-cli.yml, under jobs:, within the build-binaries: job definition and alongside runs-on and strategy, insert:
permissions:
contents: readNo additional imports, methods, or definitions are needed; this is purely a YAML configuration change in the workflow file.
| @@ -23,6 +23,8 @@ | ||
| build-binaries: | ||
| name: Build ${{ matrix.target }} | ||
| runs-on: ${{ matrix.os }} | ||
| permissions: | ||
| contents: read | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: |
yargs@18.0.0 requires Node.js >=20.19.0 but the integrity-check workflow was using Node 18, causing `yarn install` to fail with an engine incompatibility error. Updated the workflow to Node 20 (matching volta config and all other workflows) and aligned the engines field in package.json accordingly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
package.json (1)
48-49:⚠️ Potential issue | 🟠 MajorTighten the Node floor to match
yargs@18requirements.Line 48 (
>=20.x) still allows unsupported Node 20 minors (20.0–20.18) for Line 77 (yargs@^18). This can break installs for valid-on-paper environments (including current Volta pin at Line 109).Proposed fix
"engines": { - "node": ">=20.x" + "node": ">=20.19.0" }, @@ "volta": { - "node": "20.5.1", + "node": "20.19.0", "yarn": "1.22.19" }#!/bin/bash set -euo pipefail echo "Project engine:" jq -r '.engines.node' package.json echo "Volta node pin:" jq -r '.volta.node' package.json echo "yargs dependency range:" jq -r '.dependencies.yargs' package.json echo "yargs@18.0.0 engine metadata:" npm view yargs@18.0.0 engines --jsonExpected verification result:
yargs@18.0.0requires Node^20.19.0 || ^22.12.0 || >=23, so project constraints should not permit lower 20.x versions.Also applies to: 77-77
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` around lines 48 - 49, The project Node engine floor ("engines.node") is too loose (">=20.x") for yargs@18 which requires Node >=20.19.0; update package.json to tighten engines.node to at least ">=20.19.0" (or mirror yargs' engines string "^20.19.0 || ^22.12.0 || >=23") and also update the Volta pin ("volta.node") to a matching minimum (e.g., "20.19.0") so installs cannot select unsupported 20.x minors; ensure you verify changes against dependencies.yargs (which is ^18) to keep constraints consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@package.json`:
- Around line 48-49: The project Node engine floor ("engines.node") is too loose
(">=20.x") for yargs@18 which requires Node >=20.19.0; update package.json to
tighten engines.node to at least ">=20.19.0" (or mirror yargs' engines string
"^20.19.0 || ^22.12.0 || >=23") and also update the Volta pin ("volta.node") to
a matching minimum (e.g., "20.19.0") so installs cannot select unsupported 20.x
minors; ensure you verify changes against dependencies.yargs (which is ^18) to
keep constraints consistent.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e7851364-63cd-4f42-88f5-99ca8a729d47
📒 Files selected for processing (2)
.github/workflows/integrity-check.ymlpackage.json
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #813 +/- ##
==========================================
- Coverage 31.25% 29.64% -1.61%
==========================================
Files 84 100 +16
Lines 4563 5188 +625
Branches 1103 1241 +138
==========================================
+ Hits 1426 1538 +112
- Misses 3137 3650 +513
🚀 New features to boost your workflow:
|
…, extract shared options - Add `test` / `t` command mirroring unity-test-runner (EditMode, PlayMode, All, code coverage, test filters) - Add `o` short alias for `orchestrate` command - Extract shared option builders (project, docker, android, orchestrator) to eliminate duplication across build/test/orchestrate - Make `build` local-only — remove --provider-strategy (use `orchestrate` for remote builds) - Absorb `cache` command into `status` (--cache-dir flag) — remove standalone cache command that was half-implemented - Fix `list-worfklow` typo in internal CLI → `list-workflow` - Add test-related fields to CliArguments input mapper - Update all unit and integration tests (64 passing) Command structure is now: game-ci build Local build (Docker/macOS) game-ci test / t Run Unity tests game-ci orchestrate / o Remote build (AWS/K8s/etc) game-ci activate License validation game-ci status Project info + cache status game-ci version Version info game-ci update Self-update Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use the Checks API to flip failed macOS build conclusions to neutral (gray dash) so unstable builds don't show red X marks on PRs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Adds official CLI support to unity-builder, distributed as the
game-cicommand. Resolves #812.Commands
game-ci build-- Build a Unity project locally or via Docker, mirroring all action inputs as CLI flagsgame-ci activate-- Verify Unity license configuration (serial, license file, or floating license server)game-ci orchestrate-- Run builds via orchestrator providers (AWS ECS, Kubernetes, local-docker)game-ci cache list|restore|clear-- Inspect and manage build cachesgame-ci status-- Show workspace info, Unity version detection, environment, and Docker availabilitygame-ci version-- Show version infogame-ci update-- Self-update to the latest release (standalone binary only)Distribution Strategy
Primary: GitHub Releases with Standalone Binaries
Every release builds standalone binaries (no Node.js required) for:
game-ci-linux-x64game-ci-linux-arm64game-ci-macos-x64game-ci-macos-arm64game-ci-windows-x64.exeAll binaries are uploaded as release assets with SHA256 checksums (
checksums.txt).Install Scripts
Linux / macOS:
curl -fsSL https://raw.githubusercontent.com/game-ci/unity-builder/main/install.sh | shWindows (PowerShell):
Environment variables:
GAME_CI_VERSION-- Install a specific version (e.g.,v2.0.0)GAME_CI_INSTALL-- Custom install directory (default:~/.game-ci/bin)Self-Update
npm (Optional, enabled on release)
Future Providers
Design
The CLI reuses the existing unity-builder modules --
Input,BuildParameters,Orchestrator,Docker,MacBuilder-- rather than duplicating logic. A thininput-mapperlayer bridges yargs CLI flags into the existingCli.optionsmechanism thatInput.getInput()andOrchestratorOptions.getInput()already query.Key files
src/cli.tssrc/cli/input-mapper.tssrc/cli/commands/build.tssrc/cli/commands/activate.tssrc/cli/commands/orchestrate.tssrc/cli/commands/cache.tssrc/cli/commands/status.tssrc/cli/commands/version.tssrc/cli/commands/update.ts.github/workflows/release-cli.ymlinstall.shinstall.ps1src/cli/__tests__/input-mapper.test.tssrc/cli/__tests__/commands.test.tssrc/cli/__tests__/cli-integration.test.tsRelease Workflow (
release-cli.yml)Triggers on GitHub Release published or manual workflow_dispatch.
pkgTracking
Test plan
npx tsc --noEmitpasses (zero TypeScript errors)game-ci --helpdisplays all commands (including update)game-ci build --helpshows all build flags with descriptionsgame-ci versionshows correct version infogame-ci statusshows workspace detection and environmentgame-ci cache listreports cache statusgame-ci activatecorrectly reports missing license with exit code 2game-ci update --helpshows force and version flagsSummary by CodeRabbit
New Features
Tests
Chores