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
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"root": true,
"plugins": ["jest", "@typescript-eslint", "prettier", "unicorn"],
"extends": ["plugin:unicorn/recommended", "plugin:github/recommended", "plugin:prettier/recommended"],
"parser": "@typescript-eslint/parser",
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/build-tests-mac.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
buildForAllPlatformsMacOS:
name: ${{ matrix.targetPlatform }} on ${{ matrix.unityVersion }}
runs-on: macos-latest
continue-on-error: true
Copy link

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:

cat -n .github/workflows/build-tests-mac.yml

Repository: game-ci/unity-builder

Length of output: 3713


🏁 Script executed:

cat -n .github/workflows/build-tests-ubuntu.yml | head -200

Repository: game-ci/unity-builder

Length of output: 10434


🏁 Script executed:

sed -n '200,220p' .github/workflows/build-tests-ubuntu.yml

Repository: game-ci/unity-builder

Length of output: 467


Move continue-on-error to step level and guard the upload step.

At Line 15, job-level continue-on-error: true allows the workflow run to pass even when a matrix leg fails. It also does not make the later upload step run after a failed build, because steps still default to if: success() unless explicitly overridden. Move continue-on-error to the build step and give the upload step an explicit condition so artifacts are still collected on failure.

Suggested change
     buildForAllPlatformsMacOS:
       name: ${{ matrix.targetPlatform }} on ${{ matrix.unityVersion }}
       runs-on: macos-latest
-      continue-on-error: true
       strategy:
         fail-fast: false
@@
       - uses: ./
+        continue-on-error: true
         env:
           UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
           UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
           UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
           UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
@@
       - uses: actions/upload-artifact@v4
+        if: ${{ !cancelled() }}
         with:
           name: Build ${{ matrix.targetPlatform }} on MacOS (${{ matrix.unityVersion }})${{ matrix.buildProfile && '  With Build Profile' || '' }}
           path: build
           retention-days: 14
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build-tests-mac.yml at line 15, Remove the job-level
continue-on-error and instead add continue-on-error: true to the build step (the
step that runs the matrix macOS build/test commands, e.g., the "build" step);
then change the artifact upload step's condition to run regardless of build
success by setting its if to always() (or if: failure() || success()) so
artifacts are uploaded even on failure. Ensure you reference the build step name
and the upload step name in the workflow when making these changes and remove
the top-level continue-on-error key.

strategy:
fail-fast: false
matrix:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/orchestrator-async-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
# AWS_STACK_NAME: game-ci-github-pipelines
CHECKS_UPDATE: ${{ github.event.inputs.checksObject }}
run: |
git clone -b orchestrator-develop https://github.com/game-ci/unity-builder
git clone -b main https://github.com/game-ci/unity-builder
cd unity-builder
yarn
ls
Expand Down
22 changes: 20 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ inputs:
required: false
default: ''
description:
'[Orchestrator] Run a custom job instead of the standard build automation for orchestrator (in yaml format with the
keys image, secrets (name, value object array), command line string)'
'[Orchestrator] Run a custom job instead of the standard build automation for orchestrator (in yaml format with
the keys image, secrets (name, value object array), command line string)'
awsStackName:
default: 'game-ci'
required: false
Expand Down Expand Up @@ -279,6 +279,24 @@ inputs:
description:
'[Orchestrator] Specifies the repo for the unity builder. Useful if you forked the repo for testing, features, or
fixes.'
syncStrategy:
description: 'Workspace sync strategy: full, git-delta, direct-input, storage-pull'
required: false
default: 'full'
syncInputRef:
description: 'URI for direct-input or storage-pull content (storage://remote/path or file path)'
required: false
syncStorageRemote:
description: 'rclone remote name for storage-backed inputs (defaults to rcloneRemote)'
required: false
syncRevertAfter:
description: 'Revert overlaid changes after job completion'
required: false
default: 'true'
syncStatePath:
description: 'Path to sync state file for delta tracking'
required: false
default: '.game-ci/sync-state.json'

outputs:
volume:
Expand Down
568 changes: 563 additions & 5 deletions dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

74 changes: 74 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Action, BuildParameters, Cache, Orchestrator, Docker, ImageTag, Output
import { Cli } from './model/cli/cli';
import MacBuilder from './model/mac-builder';
import PlatformSetup from './model/platform-setup';
import { IncrementalSyncService } from './model/orchestrator/services/sync';
import { SyncStrategy } from './model/orchestrator/services/sync/sync-state';

async function runMain() {
try {
Expand All @@ -23,6 +25,14 @@ async function runMain() {

if (buildParameters.providerStrategy === 'local') {
core.info('Building locally');

// Apply incremental sync strategy before build
const syncStrategy = buildParameters.syncStrategy as SyncStrategy;
if (syncStrategy !== 'full') {
core.info(`[Sync] Applying sync strategy: ${syncStrategy}`);
await applySyncStrategy(buildParameters, workspace);
}

await PlatformSetup.setup(buildParameters, actionFolder);
exitCode =
process.platform === 'darwin'
Expand All @@ -32,6 +42,16 @@ async function runMain() {
actionFolder,
...buildParameters,
});

// Revert overlays after job completion if configured
if (buildParameters.syncRevertAfter && syncStrategy !== 'full') {
core.info('[Sync] Reverting overlay changes after job completion');
try {
await IncrementalSyncService.revertOverlays(workspace, buildParameters.syncStatePath);
} catch (revertError) {
core.warning(`[Sync] Overlay revert failed: ${(revertError as Error).message}`);
}
}
} else {
await Orchestrator.run(buildParameters, baseImage.toString());
exitCode = 0;
Expand All @@ -50,4 +70,58 @@ async function runMain() {
}
}

/**
* Apply the configured sync strategy to the workspace before build.
*/
async function applySyncStrategy(buildParameters: BuildParameters, workspace: string): Promise<void> {
const strategy = buildParameters.syncStrategy as SyncStrategy;
const resolvedStrategy = IncrementalSyncService.resolveStrategy(strategy, workspace, buildParameters.syncStatePath);

if (resolvedStrategy === 'full') {
core.info('[Sync] Resolved to full sync (no incremental state available)');

return;
}

switch (resolvedStrategy) {
case 'git-delta': {
const targetReference = buildParameters.gitSha || buildParameters.branch;
const changedFiles = await IncrementalSyncService.syncGitDelta(
workspace,
targetReference,
buildParameters.syncStatePath,
);
core.info(`[Sync] Git delta sync applied: ${changedFiles} file(s) changed`);
break;
}
case 'direct-input': {
if (!buildParameters.syncInputRef) {
throw new Error('[Sync] direct-input strategy requires syncInputRef to be set');
}
const overlays = await IncrementalSyncService.applyDirectInput(
workspace,
buildParameters.syncInputRef,
buildParameters.syncStorageRemote || undefined,
buildParameters.syncStatePath,
);
core.info(`[Sync] Direct input applied: ${overlays.length} overlay(s)`);
break;
}
case 'storage-pull': {
if (!buildParameters.syncInputRef) {
throw new Error('[Sync] storage-pull strategy requires syncInputRef to be set');
}
const pulledFiles = await IncrementalSyncService.syncStoragePull(workspace, buildParameters.syncInputRef, {
rcloneRemote: buildParameters.syncStorageRemote || undefined,
syncRevertAfter: buildParameters.syncRevertAfter,
statePath: buildParameters.syncStatePath,
});
core.info(`[Sync] Storage pull complete: ${pulledFiles.length} file(s)`);
break;
}
default:
core.warning(`[Sync] Unknown sync strategy: ${resolvedStrategy}`);
}
}

runMain();
10 changes: 10 additions & 0 deletions src/model/build-parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ class BuildParameters {
public cacheUnityInstallationOnMac!: boolean;
public unityHubVersionOnMac!: string;
public dockerWorkspacePath!: string;
public syncStrategy!: string;
public syncInputRef!: string;
public syncStorageRemote!: string;
public syncRevertAfter!: boolean;
public syncStatePath!: string;

public static shouldUseRetainedWorkspaceMode(buildParameters: BuildParameters) {
return buildParameters.maxRetainedWorkspaces > 0 && Orchestrator.lockedWorkspace !== ``;
Expand Down Expand Up @@ -242,6 +247,11 @@ class BuildParameters {
cacheUnityInstallationOnMac: Input.cacheUnityInstallationOnMac,
unityHubVersionOnMac: Input.unityHubVersionOnMac,
dockerWorkspacePath: Input.dockerWorkspacePath,
syncStrategy: Input.syncStrategy,
syncInputRef: Input.syncInputRef,
syncStorageRemote: Input.syncStorageRemote,
syncRevertAfter: Input.syncRevertAfter,
syncStatePath: Input.syncStatePath,
};
}

Expand Down
22 changes: 22 additions & 0 deletions src/model/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,28 @@ class Input {
return Input.getInput('dockerWorkspacePath') ?? '/github/workspace';
}

static get syncStrategy(): string {
return Input.getInput('syncStrategy') ?? 'full';
}

static get syncInputRef(): string {
return Input.getInput('syncInputRef') ?? '';
}

static get syncStorageRemote(): string {
return Input.getInput('syncStorageRemote') ?? '';
}

static get syncRevertAfter(): boolean {
const input = Input.getInput('syncRevertAfter') ?? 'true';

return input === 'true';
}
Comment on lines +256 to +260
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Parse syncRevertAfter case-insensitively.

Values like TRUE / True currently resolve to false.

Proposed fix
   static get syncRevertAfter(): boolean {
     const input = Input.getInput('syncRevertAfter') ?? 'true';
 
-    return input === 'true';
+    return input.toLowerCase() === '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.

Suggested change
static get syncRevertAfter(): boolean {
const input = Input.getInput('syncRevertAfter') ?? 'true';
return input === 'true';
}
static get syncRevertAfter(): boolean {
const input = Input.getInput('syncRevertAfter') ?? 'true';
return input.toLowerCase() === 'true';
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/input.ts` around lines 256 - 260, The syncRevertAfter getter on
class Input currently compares the raw string exactly to 'true', causing values
like 'TRUE' or 'True' to be treated as false; change the comparison in static
get syncRevertAfter() to normalize the value (e.g., call .toLowerCase() on the
result of Input.getInput('syncRevertAfter') ?? 'true') and then compare to
'true' so the check is case-insensitive while preserving the existing default
behavior.


static get syncStatePath(): string {
return Input.getInput('syncStatePath') ?? '.game-ci/sync-state.json';
}

static get dockerCpuLimit(): string {
return Input.getInput('dockerCpuLimit') ?? os.cpus().length.toString();
}
Expand Down
97 changes: 96 additions & 1 deletion src/model/orchestrator/remote-client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,24 @@ import BuildParameters from '../../build-parameters';
import { Cli } from '../../cli/cli';
import OrchestratorOptions from '../options/orchestrator-options';
import ResourceTracking from '../services/core/resource-tracking';
import { IncrementalSyncService } from '../services/sync';
import { SyncStrategy } from '../services/sync/sync-state';

export class RemoteClient {
@CliFunction(`remote-cli-pre-build`, `sets up a repository, usually before a game-ci build`)
static async setupRemoteClient() {
OrchestratorLogger.log(`bootstrap game ci orchestrator...`);
await ResourceTracking.logDiskUsageSnapshot('remote-cli-pre-build (start)');
if (!(await RemoteClient.handleRetainedWorkspace())) {

const syncStrategy = (Orchestrator.buildParameters.syncStrategy || 'full') as SyncStrategy;

if (syncStrategy !== 'full') {
OrchestratorLogger.log(`[Sync] Using incremental sync strategy: ${syncStrategy}`);
await RemoteClient.handleIncrementalSync(syncStrategy);
} else if (!(await RemoteClient.handleRetainedWorkspace())) {
await RemoteClient.bootstrapRepository();
}

await RemoteClient.replaceLargePackageReferencesWithSharedReferences();
await RemoteClient.runCustomHookFiles(`before-build`);
}
Expand Down Expand Up @@ -157,6 +166,20 @@ export class RemoteClient {

await RemoteClient.runCustomHookFiles(`after-build`);

// Revert sync overlays if configured
const syncStrategy = (Orchestrator.buildParameters.syncStrategy || 'full') as SyncStrategy;
if (Orchestrator.buildParameters.syncRevertAfter && syncStrategy !== 'full') {
try {
OrchestratorLogger.log('[Sync] Reverting overlay changes after job completion');
await IncrementalSyncService.revertOverlays(
OrchestratorFolders.repoPathAbsolute,
Orchestrator.buildParameters.syncStatePath,
);
} catch (revertError: any) {
RemoteClientLogger.logWarning(`[Sync] Overlay revert failed: ${revertError.message}`);
}
}

// WIP - need to give the pod permissions to create config map
await RemoteClientLogger.handleLogManagementPostJob();
} catch (error: any) {
Expand Down Expand Up @@ -229,6 +252,78 @@ export class RemoteClient {
RemoteClientLogger.log(JSON.stringify(error, undefined, 4));
}
}

/**
* Handle incremental sync strategies (git-delta, direct-input, storage-pull).
*
* For git-delta: requires an existing workspace with sync state; fetches and applies
* only changed files.
*
* For direct-input and storage-pull: requires an existing workspace; applies overlay
* content on top.
*
* Falls back to full bootstrapRepository() if incremental sync cannot proceed.
*/
private static async handleIncrementalSync(strategy: SyncStrategy): Promise<void> {
const buildParameters = Orchestrator.buildParameters;
const workspacePath = OrchestratorFolders.repoPathAbsolute;
const statePath = buildParameters.syncStatePath;

// Resolve strategy — may fall back to 'full' if no state exists
const resolvedStrategy = IncrementalSyncService.resolveStrategy(strategy, workspacePath, statePath);

if (resolvedStrategy === 'full') {
OrchestratorLogger.log('[Sync] Falling back to full bootstrap');
if (!(await RemoteClient.handleRetainedWorkspace())) {
await RemoteClient.bootstrapRepository();
}

return;
}

switch (resolvedStrategy) {
case 'git-delta': {
const targetReference = buildParameters.gitSha || buildParameters.branch;
OrchestratorLogger.log(`[Sync] Git delta sync to ${targetReference}`);
const changedFiles = await IncrementalSyncService.syncGitDelta(workspacePath, targetReference, statePath);
OrchestratorLogger.log(`[Sync] Git delta complete: ${changedFiles} file(s) updated`);
break;
}
case 'direct-input': {
const inputReference = buildParameters.syncInputRef;
if (!inputReference) {
throw new Error('[Sync] direct-input strategy requires syncInputRef');
}
OrchestratorLogger.log(`[Sync] Applying direct input: ${inputReference}`);
await IncrementalSyncService.applyDirectInput(
workspacePath,
inputReference,
buildParameters.syncStorageRemote || undefined,
statePath,
);
break;
}
case 'storage-pull': {
const storageUri = buildParameters.syncInputRef;
if (!storageUri) {
throw new Error('[Sync] storage-pull strategy requires syncInputRef');
}
OrchestratorLogger.log(`[Sync] Storage pull from: ${storageUri}`);
await IncrementalSyncService.syncStoragePull(workspacePath, storageUri, {
rcloneRemote: buildParameters.syncStorageRemote || undefined,
syncRevertAfter: buildParameters.syncRevertAfter,
statePath,
});
break;
}
default:
OrchestratorLogger.logWarning(`[Sync] Unknown strategy: ${resolvedStrategy}, falling back to full`);
if (!(await RemoteClient.handleRetainedWorkspace())) {
await RemoteClient.bootstrapRepository();
}
}
}

public static async bootstrapRepository() {
await OrchestratorSystem.Run(
`mkdir -p ${OrchestratorFolders.ToLinuxFolder(OrchestratorFolders.uniqueOrchestratorJobFolderAbsolute)}`,
Expand Down
Loading
Loading