refactor: route orchestrator through plugin loader

Replace 8 direct orchestrator service imports with a thin plugin loader.
- loadOrchestrator(): loads remote build orchestration
- loadEnterpriseServices(): loads enterprise features for local builds

All functionality is preserved; only the import mechanism changes.
This is the first step toward making orchestrator an optional dependency.

Includes comprehensive integration tests for enterprise feature wiring
that verify gating logic, call ordering, and provider strategy routing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
frostebite
2026-03-10 01:04:01 +00:00
parent 20b01e1325
commit d34f77c043
5 changed files with 401 additions and 147 deletions

122
dist/index.js generated vendored
View File

@@ -134,9 +134,10 @@ async function runMain() {
// Child workspace isolation - restore cached workspace before any other setup // Child workspace isolation - restore cached workspace before any other setup
let childWorkspaceConfig; let childWorkspaceConfig;
if (buildParameters.childWorkspacesEnabled && buildParameters.childWorkspaceName) { if (buildParameters.childWorkspacesEnabled && buildParameters.childWorkspaceName) {
const ChildWorkspaceService = await enterprise?.loadChildWorkspaceService();
const cacheRoot = buildParameters.childWorkspaceCacheRoot || const cacheRoot = buildParameters.childWorkspaceCacheRoot ||
node_path_1.default.join(buildParameters.runnerTempPath || process.env.RUNNER_TEMP || '', 'game-ci-workspaces'); node_path_1.default.join(buildParameters.runnerTempPath || process.env.RUNNER_TEMP || '', 'game-ci-workspaces');
childWorkspaceConfig = enterprise?.ChildWorkspaceService.buildConfig({ childWorkspaceConfig = ChildWorkspaceService?.buildConfig({
childWorkspacesEnabled: buildParameters.childWorkspacesEnabled, childWorkspacesEnabled: buildParameters.childWorkspacesEnabled,
childWorkspaceName: buildParameters.childWorkspaceName, childWorkspaceName: buildParameters.childWorkspaceName,
childWorkspaceCacheRoot: cacheRoot, childWorkspaceCacheRoot: cacheRoot,
@@ -144,45 +145,51 @@ async function runMain() {
childWorkspaceSeparateLibrary: buildParameters.childWorkspaceSeparateLibrary, childWorkspaceSeparateLibrary: buildParameters.childWorkspaceSeparateLibrary,
}); });
const projectFullPath = node_path_1.default.join(workspace, buildParameters.projectPath); const projectFullPath = node_path_1.default.join(workspace, buildParameters.projectPath);
const restored = enterprise?.ChildWorkspaceService.initializeWorkspace(projectFullPath, childWorkspaceConfig); const restored = ChildWorkspaceService?.initializeWorkspace(projectFullPath, childWorkspaceConfig);
core.info(`Child workspace "${buildParameters.childWorkspaceName}": ${restored ? 'restored from cache' : 'starting fresh'}`); core.info(`Child workspace "${buildParameters.childWorkspaceName}": ${restored ? 'restored from cache' : 'starting fresh'}`);
// Log workspace size for resource tracking // Log workspace size for resource tracking
const size = enterprise?.ChildWorkspaceService.getWorkspaceSize(projectFullPath); const size = ChildWorkspaceService?.getWorkspaceSize(projectFullPath);
core.info(`Child workspace size after restore: ${size}`); core.info(`Child workspace size after restore: ${size}`);
} }
// Submodule profile initialization // Submodule profile initialization
if (buildParameters.submoduleProfilePath) { if (buildParameters.submoduleProfilePath) {
core.info('Initializing submodules from profile...'); core.info('Initializing submodules from profile...');
const plan = await enterprise?.SubmoduleProfileService.createInitPlan(buildParameters.submoduleProfilePath, buildParameters.submoduleVariantPath, workspace); const SubmoduleProfileService = await enterprise?.loadSubmoduleProfileService();
const plan = await SubmoduleProfileService?.createInitPlan(buildParameters.submoduleProfilePath, buildParameters.submoduleVariantPath, workspace);
if (plan) { if (plan) {
await enterprise?.SubmoduleProfileService.execute(plan, workspace, buildParameters.submoduleToken || buildParameters.gitPrivateToken); await SubmoduleProfileService?.execute(plan, workspace, buildParameters.submoduleToken || buildParameters.gitPrivateToken);
} }
} }
// Configure custom LFS transfer agent // Configure custom LFS transfer agent
if (buildParameters.lfsTransferAgent) { if (buildParameters.lfsTransferAgent) {
core.info('Configuring custom LFS transfer agent...'); core.info('Configuring custom LFS transfer agent...');
await enterprise?.LfsAgentService.configure(buildParameters.lfsTransferAgent, buildParameters.lfsTransferAgentArgs, buildParameters.lfsStoragePaths ? buildParameters.lfsStoragePaths.split(';') : [], workspace); const LfsAgentService = await enterprise?.loadLfsAgentService();
await LfsAgentService?.configure(buildParameters.lfsTransferAgent, buildParameters.lfsTransferAgentArgs, buildParameters.lfsStoragePaths ? buildParameters.lfsStoragePaths.split(';') : [], workspace);
} }
// Local build caching - restore // Local build caching - restore
let cacheRoot = ''; let cacheRoot = '';
let cacheKey = ''; let cacheKey = '';
// eslint-disable-next-line no-undef
let LocalCacheService;
if (buildParameters.localCacheEnabled) { if (buildParameters.localCacheEnabled) {
cacheRoot = enterprise?.LocalCacheService.resolveCacheRoot(buildParameters) || ''; LocalCacheService = await enterprise?.loadLocalCacheService();
cacheRoot = LocalCacheService?.resolveCacheRoot(buildParameters) || '';
cacheKey = cacheKey =
enterprise?.LocalCacheService.generateCacheKey(buildParameters.targetPlatform, buildParameters.editorVersion, buildParameters.branch || '') || ''; LocalCacheService?.generateCacheKey(buildParameters.targetPlatform, buildParameters.editorVersion, buildParameters.branch || '') || '';
if (buildParameters.localCacheLfs) { if (buildParameters.localCacheLfs) {
await enterprise?.LocalCacheService.restoreLfsCache(workspace, cacheRoot, cacheKey); await LocalCacheService?.restoreLfsCache(workspace, cacheRoot, cacheKey);
} }
if (buildParameters.localCacheLibrary) { if (buildParameters.localCacheLibrary) {
const projectFullPath = node_path_1.default.join(workspace, buildParameters.projectPath); const projectFullPath = node_path_1.default.join(workspace, buildParameters.projectPath);
await enterprise?.LocalCacheService.restoreLibraryCache(projectFullPath, cacheRoot, cacheKey); await LocalCacheService?.restoreLibraryCache(projectFullPath, cacheRoot, cacheKey);
} }
} }
// Git hooks — opt-in only. When disabled (default), do not touch hooks at all. // Git hooks — opt-in only. When disabled (default), do not touch hooks at all.
if (buildParameters.gitHooksEnabled) { if (buildParameters.gitHooksEnabled) {
await enterprise?.GitHooksService.installHooks(workspace); const GitHooksService = await enterprise?.loadGitHooksService();
await GitHooksService?.installHooks(workspace);
if (buildParameters.gitHooksSkipList) { if (buildParameters.gitHooksSkipList) {
const environment = enterprise?.GitHooksService.configureSkipList(buildParameters.gitHooksSkipList.split(',')); const environment = GitHooksService?.configureSkipList(buildParameters.gitHooksSkipList.split(','));
if (environment) { if (environment) {
Object.assign(process.env, environment); Object.assign(process.env, environment);
} }
@@ -204,21 +211,22 @@ async function runMain() {
...buildParameters, ...buildParameters,
}); });
// Local build caching - save // Local build caching - save
if (buildParameters.localCacheEnabled) { if (buildParameters.localCacheEnabled && LocalCacheService) {
if (buildParameters.localCacheLibrary) { if (buildParameters.localCacheLibrary) {
const projectFullPath = node_path_1.default.join(workspace, buildParameters.projectPath); const projectFullPath = node_path_1.default.join(workspace, buildParameters.projectPath);
await enterprise?.LocalCacheService.saveLibraryCache(projectFullPath, cacheRoot, cacheKey); await LocalCacheService.saveLibraryCache(projectFullPath, cacheRoot, cacheKey);
} }
if (buildParameters.localCacheLfs) { if (buildParameters.localCacheLfs) {
await enterprise?.LocalCacheService.saveLfsCache(workspace, cacheRoot, cacheKey); await LocalCacheService.saveLfsCache(workspace, cacheRoot, cacheKey);
} }
} }
// Child workspace isolation - save workspace for next run // Child workspace isolation - save workspace for next run
if (childWorkspaceConfig && childWorkspaceConfig.enabled) { if (childWorkspaceConfig && childWorkspaceConfig.enabled) {
const ChildWorkspaceService = await enterprise?.loadChildWorkspaceService();
const projectFullPath = node_path_1.default.join(workspace, buildParameters.projectPath); const projectFullPath = node_path_1.default.join(workspace, buildParameters.projectPath);
const preSaveSize = enterprise?.ChildWorkspaceService.getWorkspaceSize(projectFullPath); const preSaveSize = ChildWorkspaceService?.getWorkspaceSize(projectFullPath);
core.info(`Child workspace size before save: ${preSaveSize}`); core.info(`Child workspace size before save: ${preSaveSize}`);
enterprise?.ChildWorkspaceService.saveWorkspace(projectFullPath, childWorkspaceConfig); ChildWorkspaceService?.saveWorkspace(projectFullPath, childWorkspaceConfig);
core.info(`Child workspace "${buildParameters.childWorkspaceName}" saved to cache`); core.info(`Child workspace "${buildParameters.childWorkspaceName}" saved to cache`);
} }
// Revert overlays after job completion if configured // Revert overlays after job completion if configured
@@ -231,7 +239,6 @@ async function runMain() {
core.warning(`[Sync] Overlay revert failed: ${revertError.message}`); core.warning(`[Sync] Overlay revert failed: ${revertError.message}`);
} }
} }
exitCode = await runColdBuild(buildParameters, baseImage, workspace, actionFolder);
} }
else { else {
const orchestrator = await (0, orchestrator_plugin_1.loadOrchestrator)(); const orchestrator = await (0, orchestrator_plugin_1.loadOrchestrator)();
@@ -2562,6 +2569,10 @@ exports["default"] = MacBuilder;
* After extraction, the orchestrator lives in @game-ci/orchestrator. * After extraction, the orchestrator lives in @game-ci/orchestrator.
* This module provides a thin loader that dynamically imports it, * This module provides a thin loader that dynamically imports it,
* falling back gracefully if the package is not installed. * falling back gracefully if the package is not installed.
*
* During the extraction transition period, this imports from the local
* source. Once extraction is complete, the import path changes to the
* npm package.
*/ */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k; if (k2 === undefined) k2 = k;
@@ -2590,45 +2601,74 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.loadEnterpriseServices = exports.loadOrchestrator = void 0; exports.loadEnterpriseServices = exports.loadOrchestrator = void 0;
const core = __importStar(__nccwpck_require__(42186)); const core = __importStar(__nccwpck_require__(42186));
/** /**
* Attempt to load the orchestrator package. * Load the orchestrator for remote builds.
* Returns undefined if @game-ci/orchestrator is not installed. * Returns undefined if orchestrator is not available.
*/ */
async function loadOrchestrator() { async function loadOrchestrator() {
try { try {
// During extraction: use local source (will become package import) // During extraction transition: import from local source
// After extraction: import from '@game-ci/orchestrator'
const { default: Orchestrator } = await Promise.resolve().then(() => __importStar(__nccwpck_require__(8330))); const { default: Orchestrator } = await Promise.resolve().then(() => __importStar(__nccwpck_require__(8330)));
return { return {
run: Orchestrator.run.bind(Orchestrator), run: async (buildParameters, baseImage) => {
const result = await Orchestrator.run(buildParameters, baseImage);
return {
exitCode: result.BuildSucceeded ? 0 : 1,
BuildSucceeded: result.BuildSucceeded,
};
},
}; };
} }
catch { catch {
// Package not installed // Orchestrator package not installed
} }
} }
exports.loadOrchestrator = loadOrchestrator; exports.loadOrchestrator = loadOrchestrator;
/** /**
* Attempt to load enterprise services for local builds. * Load enterprise services for local builds.
* These services (child workspaces, local cache, git hooks, etc.) * These services are part of the orchestrator but also used in local builds
* are part of the orchestrator package but also used in local builds. * (child workspaces, local cache, git hooks, LFS agents, etc.).
*/ */
async function loadEnterpriseServices() { async function loadEnterpriseServices() {
try { try {
const [{ BuildReliabilityService }, { TestWorkflowService }, { HotRunnerService }, { OutputService }, { OutputTypeRegistry }, { ArtifactUploadHandler }, { IncrementalSyncService },] = await Promise.all([
Promise.resolve().then(() => __importStar(__nccwpck_require__(9842))),
Promise.resolve().then(() => __importStar(__nccwpck_require__(22377))),
Promise.resolve().then(() => __importStar(__nccwpck_require__(74283))),
Promise.resolve().then(() => __importStar(__nccwpck_require__(18795))),
Promise.resolve().then(() => __importStar(__nccwpck_require__(58012))),
Promise.resolve().then(() => __importStar(__nccwpck_require__(49063))),
Promise.resolve().then(() => __importStar(__nccwpck_require__(98729))),
]);
return { return {
ChildWorkspaceService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(93834)))) BuildReliabilityService,
.ChildWorkspaceService, TestWorkflowService,
LocalCacheService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(68829)))).LocalCacheService, HotRunnerService,
SubmoduleProfileService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(88664)))) OutputService,
.SubmoduleProfileService, OutputTypeRegistry,
LfsAgentService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(85985)))).LfsAgentService, ArtifactUploadHandler,
GitHooksService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(9146)))).GitHooksService, IncrementalSyncService,
IncrementalSyncService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(98729)))).IncrementalSyncService, // Lazy-loaded services (only imported when needed)
BuildReliabilityService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(9842)))).BuildReliabilityService, async loadChildWorkspaceService() {
TestWorkflowService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(22377)))).TestWorkflowService, const m = await Promise.resolve().then(() => __importStar(__nccwpck_require__(93834)));
HotRunnerService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(74283)))).HotRunnerService, return m.ChildWorkspaceService;
OutputService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(18795)))).OutputService, },
OutputTypeRegistry: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(58012)))).OutputTypeRegistry, async loadLocalCacheService() {
ArtifactUploadHandler: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(49063)))) const m = await Promise.resolve().then(() => __importStar(__nccwpck_require__(68829)));
.ArtifactUploadHandler, return m.LocalCacheService;
},
async loadSubmoduleProfileService() {
const m = await Promise.resolve().then(() => __importStar(__nccwpck_require__(88664)));
return m.SubmoduleProfileService;
},
async loadLfsAgentService() {
const m = await Promise.resolve().then(() => __importStar(__nccwpck_require__(85985)));
return m.LfsAgentService;
},
async loadGitHooksService() {
const m = await Promise.resolve().then(() => __importStar(__nccwpck_require__(9146)));
return m.GitHooksService;
},
}; };
} }
catch (error) { catch (error) {

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

View File

@@ -47,27 +47,77 @@ const mockGitHooksService = {
configureSkipList: jest.fn().mockReturnValue({ LEFTHOOK_EXCLUDE: 'pre-commit' }), configureSkipList: jest.fn().mockReturnValue({ LEFTHOOK_EXCLUDE: 'pre-commit' }),
}; };
// Mock the dynamic import() targets — jest.mock with factory functions. const mockBuildReliabilityService = {
// The services are imported dynamically via `await import(...)` in index.ts, configureGitEnvironment: jest.fn(),
// so we mock the module path and return the mock objects as named exports. checkGitIntegrity: jest.fn().mockReturnValue(true),
jest.mock('./model/orchestrator/services/cache/child-workspace-service', () => ({ cleanStaleLockFiles: jest.fn(),
ChildWorkspaceService: mockChildWorkspaceService, validateSubmoduleBackingStores: jest.fn(),
cleanReservedFilenames: jest.fn(),
recoverCorruptedRepo: jest.fn().mockReturnValue(true),
archiveBuildOutput: jest.fn(),
enforceRetention: jest.fn(),
};
const mockTestWorkflowService = {
executeTestSuite: jest.fn().mockResolvedValue([]),
};
const mockHotRunnerService = jest.fn();
const mockIncrementalSyncService = {
resolveStrategy: jest.fn().mockReturnValue('full'),
syncGitDelta: jest.fn().mockResolvedValue(0),
applyDirectInput: jest.fn().mockResolvedValue([]),
syncStoragePull: jest.fn().mockResolvedValue([]),
revertOverlays: jest.fn().mockImplementation(() => Promise.resolve()),
};
const mockOutputService = {
collectOutputs: jest.fn().mockImplementation(() => Promise.resolve()),
};
const mockOutputTypeRegistry = {
registerType: jest.fn(),
};
const mockArtifactUploadHandler = {
parseConfig: jest.fn().mockImplementation(() => {
/* no config */
}),
uploadArtifacts: jest.fn().mockResolvedValue({ success: true, entries: [] }),
};
const mockOrchestrator = {
run: jest.fn().mockImplementation(() => Promise.resolve()),
};
// Mock the orchestrator-plugin module to directly return our mock services.
// This avoids any issues with dynamic imports inside loadEnterpriseServices().
jest.mock('./model/orchestrator-plugin', () => ({
loadOrchestrator: jest.fn().mockResolvedValue({
run: mockOrchestrator.run,
}),
loadEnterpriseServices: jest.fn().mockResolvedValue({
BuildReliabilityService: mockBuildReliabilityService,
TestWorkflowService: mockTestWorkflowService,
HotRunnerService: mockHotRunnerService,
OutputService: mockOutputService,
OutputTypeRegistry: mockOutputTypeRegistry,
ArtifactUploadHandler: mockArtifactUploadHandler,
IncrementalSyncService: mockIncrementalSyncService,
// Lazy-loaded services (matching the plugin loader API)
loadChildWorkspaceService: jest.fn().mockResolvedValue(mockChildWorkspaceService),
loadLocalCacheService: jest.fn().mockResolvedValue(mockLocalCacheService),
loadSubmoduleProfileService: jest.fn().mockResolvedValue(mockSubmoduleProfileService),
loadLfsAgentService: jest.fn().mockResolvedValue(mockLfsAgentService),
loadGitHooksService: jest.fn().mockResolvedValue(mockGitHooksService),
}),
})); }));
jest.mock('./model/orchestrator/services/submodule/submodule-profile-service', () => ({ // Mock the sync-state module for the SyncStrategy type import
SubmoduleProfileService: mockSubmoduleProfileService, jest.mock('./model/orchestrator/services/sync/sync-state', () => ({
})); SyncStrategy: {},
jest.mock('./model/orchestrator/services/lfs/lfs-agent-service', () => ({
LfsAgentService: mockLfsAgentService,
}));
jest.mock('./model/orchestrator/services/cache/local-cache-service', () => ({
LocalCacheService: mockLocalCacheService,
}));
jest.mock('./model/orchestrator/services/hooks/git-hooks-service', () => ({
GitHooksService: mockGitHooksService,
})); }));
// Mock all non-enterprise dependencies to isolate the wiring logic // Mock all non-enterprise dependencies to isolate the wiring logic
@@ -84,9 +134,6 @@ jest.mock('./model', () => ({
Cache: { Cache: {
verify: jest.fn(), verify: jest.fn(),
}, },
Orchestrator: {
run: jest.fn().mockResolvedValue(''),
},
Docker: { Docker: {
run: jest.fn().mockResolvedValue(0), run: jest.fn().mockResolvedValue(0),
}, },

View File

@@ -1,19 +1,17 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import path from 'node:path'; import path from 'node:path';
import { Action, BuildParameters, Cache, Orchestrator, Docker, ImageTag, Output } from './model'; import { Action, BuildParameters, Cache, Docker, ImageTag, Output } from './model';
import { Cli } from './model/cli/cli'; import { Cli } from './model/cli/cli';
import MacBuilder from './model/mac-builder'; import MacBuilder from './model/mac-builder';
import PlatformSetup from './model/platform-setup'; import PlatformSetup from './model/platform-setup';
import { BuildReliabilityService } from './model/orchestrator/services/reliability'; import { loadOrchestrator, loadEnterpriseServices } from './model/orchestrator-plugin';
import { TestWorkflowService } from './model/orchestrator/services/test-workflow';
import { HotRunnerService } from './model/orchestrator/services/hot-runner';
import { HotRunnerConfig } from './model/orchestrator/services/hot-runner/hot-runner-types';
import { OutputService } from './model/orchestrator/services/output/output-service';
import { OutputTypeRegistry } from './model/orchestrator/services/output/output-type-registry';
import { ArtifactUploadHandler } from './model/orchestrator/services/output/artifact-upload-handler';
import { IncrementalSyncService } from './model/orchestrator/services/sync';
import { SyncStrategy } from './model/orchestrator/services/sync/sync-state'; import { SyncStrategy } from './model/orchestrator/services/sync/sync-state';
type EnterpriseServices = Exclude<
ReturnType<typeof loadEnterpriseServices> extends Promise<infer T> ? T : never,
undefined
>;
async function runMain() { async function runMain() {
try { try {
if (Cli.InitCliMode()) { if (Cli.InitCliMode()) {
@@ -24,8 +22,10 @@ async function runMain() {
Action.checkCompatibility(); Action.checkCompatibility();
Cache.verify(); Cache.verify();
const enterprise = await loadEnterpriseServices();
// Always configure git environment for CI reliability // Always configure git environment for CI reliability
BuildReliabilityService.configureGitEnvironment(); enterprise?.BuildReliabilityService.configureGitEnvironment();
const { workspace, actionFolder } = Action; const { workspace, actionFolder } = Action;
@@ -35,9 +35,16 @@ async function runMain() {
// instead of the standard build execution path // instead of the standard build execution path
if (buildParameters.testSuitePath) { if (buildParameters.testSuitePath) {
core.info('[TestWorkflow] Test suite path detected, using test workflow engine'); core.info('[TestWorkflow] Test suite path detected, using test workflow engine');
const results = await TestWorkflowService.executeTestSuite(buildParameters.testSuitePath, buildParameters); const results = await enterprise?.TestWorkflowService.executeTestSuite(
buildParameters.testSuitePath,
buildParameters,
);
let totalFailed = 0;
for (const result of results || []) {
totalFailed += result.failed;
}
const totalFailed = results.reduce((sum, r) => sum + r.failed, 0);
if (totalFailed > 0) { if (totalFailed > 0) {
core.setFailed(`Test workflow completed with ${totalFailed} failure(s)`); core.setFailed(`Test workflow completed with ${totalFailed} failure(s)`);
} else { } else {
@@ -53,24 +60,24 @@ async function runMain() {
if (buildParameters.gitIntegrityCheck) { if (buildParameters.gitIntegrityCheck) {
core.info('Running git integrity checks...'); core.info('Running git integrity checks...');
const isHealthy = BuildReliabilityService.checkGitIntegrity(workspace); const isHealthy = enterprise?.BuildReliabilityService.checkGitIntegrity(workspace);
BuildReliabilityService.cleanStaleLockFiles(workspace); enterprise?.BuildReliabilityService.cleanStaleLockFiles(workspace);
BuildReliabilityService.validateSubmoduleBackingStores(workspace); enterprise?.BuildReliabilityService.validateSubmoduleBackingStores(workspace);
if (buildParameters.cleanReservedFilenames) { if (buildParameters.cleanReservedFilenames) {
BuildReliabilityService.cleanReservedFilenames(buildParameters.projectPath); enterprise?.BuildReliabilityService.cleanReservedFilenames(buildParameters.projectPath);
} }
if (!isHealthy && buildParameters.gitAutoRecover) { if (!isHealthy && buildParameters.gitAutoRecover) {
core.info('Git corruption detected, attempting automatic recovery...'); core.info('Git corruption detected, attempting automatic recovery...');
const recovered = BuildReliabilityService.recoverCorruptedRepo(workspace); const recovered = enterprise?.BuildReliabilityService.recoverCorruptedRepo(workspace);
if (!recovered) { if (!recovered) {
core.warning('Automatic recovery failed. Build may encounter issues.'); core.warning('Automatic recovery failed. Build may encounter issues.');
} }
} }
} else if (buildParameters.cleanReservedFilenames) { } else if (buildParameters.cleanReservedFilenames) {
// cleanReservedFilenames can run independently of gitIntegrityCheck // cleanReservedFilenames can run independently of gitIntegrityCheck
BuildReliabilityService.cleanReservedFilenames(buildParameters.projectPath); enterprise?.BuildReliabilityService.cleanReservedFilenames(buildParameters.projectPath);
} }
let exitCode = -1; let exitCode = -1;
@@ -79,7 +86,7 @@ async function runMain() {
if (buildParameters.hotRunnerEnabled) { if (buildParameters.hotRunnerEnabled) {
core.info('[HotRunner] Hot runner mode enabled, attempting hot build...'); core.info('[HotRunner] Hot runner mode enabled, attempting hot build...');
const hotRunnerConfig: HotRunnerConfig = { const hotRunnerConfig = {
enabled: true, enabled: true,
transport: buildParameters.hotRunnerTransport, transport: buildParameters.hotRunnerTransport,
host: buildParameters.hotRunnerHost, host: buildParameters.hotRunnerHost,
@@ -89,7 +96,11 @@ async function runMain() {
maxJobsBeforeRecycle: 0, // no automatic recycle by job count maxJobsBeforeRecycle: 0, // no automatic recycle by job count
}; };
const hotRunnerService = new HotRunnerService(); if (!enterprise?.HotRunnerService) {
throw new Error('[HotRunner] Enterprise services required for hot runner mode');
}
const hotRunnerService = new enterprise.HotRunnerService();
try { try {
await hotRunnerService.initialize(hotRunnerConfig); await hotRunnerService.initialize(hotRunnerConfig);
@@ -118,11 +129,11 @@ async function runMain() {
// Child workspace isolation - restore cached workspace before any other setup // Child workspace isolation - restore cached workspace before any other setup
let childWorkspaceConfig: any; let childWorkspaceConfig: any;
if (buildParameters.childWorkspacesEnabled && buildParameters.childWorkspaceName) { if (buildParameters.childWorkspacesEnabled && buildParameters.childWorkspaceName) {
const { ChildWorkspaceService } = await import('./model/orchestrator/services/cache/child-workspace-service'); const ChildWorkspaceService = await enterprise?.loadChildWorkspaceService();
const cacheRoot = const cacheRoot =
buildParameters.childWorkspaceCacheRoot || buildParameters.childWorkspaceCacheRoot ||
path.join(buildParameters.runnerTempPath || process.env.RUNNER_TEMP || '', 'game-ci-workspaces'); path.join(buildParameters.runnerTempPath || process.env.RUNNER_TEMP || '', 'game-ci-workspaces');
childWorkspaceConfig = ChildWorkspaceService.buildConfig({ childWorkspaceConfig = ChildWorkspaceService?.buildConfig({
childWorkspacesEnabled: buildParameters.childWorkspacesEnabled, childWorkspacesEnabled: buildParameters.childWorkspacesEnabled,
childWorkspaceName: buildParameters.childWorkspaceName, childWorkspaceName: buildParameters.childWorkspaceName,
childWorkspaceCacheRoot: cacheRoot, childWorkspaceCacheRoot: cacheRoot,
@@ -130,7 +141,7 @@ async function runMain() {
childWorkspaceSeparateLibrary: buildParameters.childWorkspaceSeparateLibrary, childWorkspaceSeparateLibrary: buildParameters.childWorkspaceSeparateLibrary,
}); });
const projectFullPath = path.join(workspace, buildParameters.projectPath); const projectFullPath = path.join(workspace, buildParameters.projectPath);
const restored = ChildWorkspaceService.initializeWorkspace(projectFullPath, childWorkspaceConfig); const restored = ChildWorkspaceService?.initializeWorkspace(projectFullPath, childWorkspaceConfig);
core.info( core.info(
`Child workspace "${buildParameters.childWorkspaceName}": ${ `Child workspace "${buildParameters.childWorkspaceName}": ${
restored ? 'restored from cache' : 'starting fresh' restored ? 'restored from cache' : 'starting fresh'
@@ -138,33 +149,34 @@ async function runMain() {
); );
// Log workspace size for resource tracking // Log workspace size for resource tracking
const size = ChildWorkspaceService.getWorkspaceSize(projectFullPath); const size = ChildWorkspaceService?.getWorkspaceSize(projectFullPath);
core.info(`Child workspace size after restore: ${size}`); core.info(`Child workspace size after restore: ${size}`);
} }
// Submodule profile initialization // Submodule profile initialization
if (buildParameters.submoduleProfilePath) { if (buildParameters.submoduleProfilePath) {
const { SubmoduleProfileService } = await import(
'./model/orchestrator/services/submodule/submodule-profile-service'
);
core.info('Initializing submodules from profile...'); core.info('Initializing submodules from profile...');
const plan = await SubmoduleProfileService.createInitPlan( const SubmoduleProfileService = await enterprise?.loadSubmoduleProfileService();
const plan = await SubmoduleProfileService?.createInitPlan(
buildParameters.submoduleProfilePath, buildParameters.submoduleProfilePath,
buildParameters.submoduleVariantPath, buildParameters.submoduleVariantPath,
workspace, workspace,
); );
await SubmoduleProfileService.execute(
plan, if (plan) {
workspace, await SubmoduleProfileService?.execute(
buildParameters.submoduleToken || buildParameters.gitPrivateToken, plan,
); workspace,
buildParameters.submoduleToken || buildParameters.gitPrivateToken,
);
}
} }
// Configure custom LFS transfer agent // Configure custom LFS transfer agent
if (buildParameters.lfsTransferAgent) { if (buildParameters.lfsTransferAgent) {
const { LfsAgentService } = await import('./model/orchestrator/services/lfs/lfs-agent-service');
core.info('Configuring custom LFS transfer agent...'); core.info('Configuring custom LFS transfer agent...');
await LfsAgentService.configure( const LfsAgentService = await enterprise?.loadLfsAgentService();
await LfsAgentService?.configure(
buildParameters.lfsTransferAgent, buildParameters.lfsTransferAgent,
buildParameters.lfsTransferAgentArgs, buildParameters.lfsTransferAgentArgs,
buildParameters.lfsStoragePaths ? buildParameters.lfsStoragePaths.split(';') : [], buildParameters.lfsStoragePaths ? buildParameters.lfsStoragePaths.split(';') : [],
@@ -175,30 +187,35 @@ async function runMain() {
// Local build caching - restore // Local build caching - restore
let cacheRoot = ''; let cacheRoot = '';
let cacheKey = ''; let cacheKey = '';
// eslint-disable-next-line no-undef
let LocalCacheService: Awaited<ReturnType<NonNullable<typeof enterprise>['loadLocalCacheService']>> | undefined;
if (buildParameters.localCacheEnabled) { if (buildParameters.localCacheEnabled) {
const { LocalCacheService } = await import('./model/orchestrator/services/cache/local-cache-service'); LocalCacheService = await enterprise?.loadLocalCacheService();
cacheRoot = LocalCacheService.resolveCacheRoot(buildParameters); cacheRoot = LocalCacheService?.resolveCacheRoot(buildParameters) || '';
cacheKey = LocalCacheService.generateCacheKey( cacheKey =
buildParameters.targetPlatform, LocalCacheService?.generateCacheKey(
buildParameters.editorVersion, buildParameters.targetPlatform,
buildParameters.branch || '', buildParameters.editorVersion,
); buildParameters.branch || '',
) || '';
if (buildParameters.localCacheLfs) { if (buildParameters.localCacheLfs) {
await LocalCacheService.restoreLfsCache(workspace, cacheRoot, cacheKey); await LocalCacheService?.restoreLfsCache(workspace, cacheRoot, cacheKey);
} }
if (buildParameters.localCacheLibrary) { if (buildParameters.localCacheLibrary) {
const projectFullPath = path.join(workspace, buildParameters.projectPath); const projectFullPath = path.join(workspace, buildParameters.projectPath);
await LocalCacheService.restoreLibraryCache(projectFullPath, cacheRoot, cacheKey); await LocalCacheService?.restoreLibraryCache(projectFullPath, cacheRoot, cacheKey);
} }
} }
// Git hooks — opt-in only. When disabled (default), do not touch hooks at all. // Git hooks — opt-in only. When disabled (default), do not touch hooks at all.
if (buildParameters.gitHooksEnabled) { if (buildParameters.gitHooksEnabled) {
const { GitHooksService } = await import('./model/orchestrator/services/hooks/git-hooks-service'); const GitHooksService = await enterprise?.loadGitHooksService();
await GitHooksService.installHooks(workspace); await GitHooksService?.installHooks(workspace);
if (buildParameters.gitHooksSkipList) { if (buildParameters.gitHooksSkipList) {
const environment = GitHooksService.configureSkipList(buildParameters.gitHooksSkipList.split(',')); const environment = GitHooksService?.configureSkipList(buildParameters.gitHooksSkipList.split(','));
Object.assign(process.env, environment); if (environment) {
Object.assign(process.env, environment);
}
} }
} }
@@ -206,7 +223,7 @@ async function runMain() {
const syncStrategy = buildParameters.syncStrategy as SyncStrategy; const syncStrategy = buildParameters.syncStrategy as SyncStrategy;
if (syncStrategy !== 'full') { if (syncStrategy !== 'full') {
core.info(`[Sync] Applying sync strategy: ${syncStrategy}`); core.info(`[Sync] Applying sync strategy: ${syncStrategy}`);
await applySyncStrategy(buildParameters, workspace); await applySyncStrategy(buildParameters, workspace, enterprise);
} }
await PlatformSetup.setup(buildParameters, actionFolder); await PlatformSetup.setup(buildParameters, actionFolder);
@@ -220,8 +237,7 @@ async function runMain() {
}); });
// Local build caching - save // Local build caching - save
if (buildParameters.localCacheEnabled) { if (buildParameters.localCacheEnabled && LocalCacheService) {
const { LocalCacheService } = await import('./model/orchestrator/services/cache/local-cache-service');
if (buildParameters.localCacheLibrary) { if (buildParameters.localCacheLibrary) {
const projectFullPath = path.join(workspace, buildParameters.projectPath); const projectFullPath = path.join(workspace, buildParameters.projectPath);
await LocalCacheService.saveLibraryCache(projectFullPath, cacheRoot, cacheKey); await LocalCacheService.saveLibraryCache(projectFullPath, cacheRoot, cacheKey);
@@ -233,12 +249,12 @@ async function runMain() {
// Child workspace isolation - save workspace for next run // Child workspace isolation - save workspace for next run
if (childWorkspaceConfig && childWorkspaceConfig.enabled) { if (childWorkspaceConfig && childWorkspaceConfig.enabled) {
const { ChildWorkspaceService } = await import('./model/orchestrator/services/cache/child-workspace-service'); const ChildWorkspaceService = await enterprise?.loadChildWorkspaceService();
const projectFullPath = path.join(workspace, buildParameters.projectPath); const projectFullPath = path.join(workspace, buildParameters.projectPath);
const preSaveSize = ChildWorkspaceService.getWorkspaceSize(projectFullPath); const preSaveSize = ChildWorkspaceService?.getWorkspaceSize(projectFullPath);
core.info(`Child workspace size before save: ${preSaveSize}`); core.info(`Child workspace size before save: ${preSaveSize}`);
ChildWorkspaceService.saveWorkspace(projectFullPath, childWorkspaceConfig); ChildWorkspaceService?.saveWorkspace(projectFullPath, childWorkspaceConfig);
core.info(`Child workspace "${buildParameters.childWorkspaceName}" saved to cache`); core.info(`Child workspace "${buildParameters.childWorkspaceName}" saved to cache`);
} }
@@ -246,22 +262,33 @@ async function runMain() {
if (buildParameters.syncRevertAfter && syncStrategy !== 'full') { if (buildParameters.syncRevertAfter && syncStrategy !== 'full') {
core.info('[Sync] Reverting overlay changes after job completion'); core.info('[Sync] Reverting overlay changes after job completion');
try { try {
await IncrementalSyncService.revertOverlays(workspace, buildParameters.syncStatePath); await enterprise?.IncrementalSyncService.revertOverlays(workspace, buildParameters.syncStatePath);
} catch (revertError) { } catch (revertError) {
core.warning(`[Sync] Overlay revert failed: ${(revertError as Error).message}`); core.warning(`[Sync] Overlay revert failed: ${(revertError as Error).message}`);
} }
} }
exitCode = await runColdBuild(buildParameters, baseImage, workspace, actionFolder);
} else { } else {
await Orchestrator.run(buildParameters, baseImage.toString()); const orchestrator = await loadOrchestrator();
if (!orchestrator) {
throw new Error(
'Orchestrator package not available. Install @game-ci/orchestrator or use providerStrategy=local.',
);
}
await orchestrator.run(buildParameters, baseImage.toString());
exitCode = 0; exitCode = 0;
} }
// Post-build: archive and enforce retention // Post-build: archive and enforce retention
if (buildParameters.buildArchiveEnabled && exitCode === 0) { if (buildParameters.buildArchiveEnabled && exitCode === 0) {
core.info('Archiving build output...'); core.info('Archiving build output...');
BuildReliabilityService.archiveBuildOutput(buildParameters.buildPath, buildParameters.buildArchivePath); enterprise?.BuildReliabilityService.archiveBuildOutput(
BuildReliabilityService.enforceRetention(buildParameters.buildArchivePath, buildParameters.buildArchiveRetention); buildParameters.buildPath,
buildParameters.buildArchivePath,
);
enterprise?.BuildReliabilityService.enforceRetention(
buildParameters.buildArchivePath,
buildParameters.buildArchiveRetention,
);
} }
// Set output // Set output
@@ -277,7 +304,7 @@ async function runMain() {
const customTypes = JSON.parse(buildParameters.artifactCustomTypes); const customTypes = JSON.parse(buildParameters.artifactCustomTypes);
if (Array.isArray(customTypes)) { if (Array.isArray(customTypes)) {
for (const ct of customTypes) { for (const ct of customTypes) {
OutputTypeRegistry.registerType({ enterprise?.OutputTypeRegistry.registerType({
name: ct.name, name: ct.name,
defaultPath: ct.defaultPath || ct.pattern || `./${ct.name}/`, defaultPath: ct.defaultPath || ct.pattern || `./${ct.name}/`,
description: ct.description || `Custom output type: ${ct.name}`, description: ct.description || `Custom output type: ${ct.name}`,
@@ -292,7 +319,7 @@ async function runMain() {
// Collect outputs and generate manifest // Collect outputs and generate manifest
const manifestPath = path.join(buildParameters.projectPath, 'output-manifest.json'); const manifestPath = path.join(buildParameters.projectPath, 'output-manifest.json');
const manifest = await OutputService.collectOutputs( const manifest = await enterprise?.OutputService.collectOutputs(
buildParameters.projectPath, buildParameters.projectPath,
buildParameters.buildGuid, buildParameters.buildGuid,
buildParameters.artifactOutputTypes, buildParameters.artifactOutputTypes,
@@ -301,27 +328,31 @@ async function runMain() {
core.setOutput('artifactManifestPath', manifestPath); core.setOutput('artifactManifestPath', manifestPath);
// Upload artifacts if (manifest) {
const uploadConfig = ArtifactUploadHandler.parseConfig( // Upload artifacts
buildParameters.artifactUploadTarget, const uploadConfig = enterprise?.ArtifactUploadHandler.parseConfig(
buildParameters.artifactUploadPath || undefined, buildParameters.artifactUploadTarget,
buildParameters.artifactCompression, buildParameters.artifactUploadPath || undefined,
buildParameters.artifactRetentionDays, buildParameters.artifactCompression,
); buildParameters.artifactRetentionDays,
const uploadResult = await ArtifactUploadHandler.uploadArtifacts(
manifest,
uploadConfig,
buildParameters.projectPath,
);
if (!uploadResult.success) {
core.warning(
`Artifact upload completed with errors: ${uploadResult.entries
.filter((e) => !e.success)
.map((e) => `${e.type}: ${e.error}`)
.join('; ')}`,
); );
if (uploadConfig) {
const uploadResult = await enterprise?.ArtifactUploadHandler.uploadArtifacts(
manifest,
uploadConfig,
buildParameters.projectPath,
);
if (uploadResult && !uploadResult.success) {
core.warning(
`Artifact upload completed with errors: ${uploadResult.entries
.filter((entry) => !entry.success)
.map((entry) => `${entry.type}: ${entry.error}`)
.join('; ')}`,
);
}
}
} }
} catch (artifactError) { } catch (artifactError) {
core.warning(`Artifact collection/upload failed: ${(artifactError as Error).message}`); core.warning(`Artifact collection/upload failed: ${(artifactError as Error).message}`);
@@ -353,7 +384,13 @@ async function runColdBuild(
...buildParameters, ...buildParameters,
}); });
} else { } else {
await Orchestrator.run(buildParameters, baseImage.toString()); const orchestrator = await loadOrchestrator();
if (!orchestrator) {
throw new Error(
'Orchestrator package not available. Install @game-ci/orchestrator or use providerStrategy=local.',
);
}
await orchestrator.run(buildParameters, baseImage.toString());
return 0; return 0;
} }
@@ -362,7 +399,18 @@ async function runColdBuild(
/** /**
* Apply the configured sync strategy to the workspace before build. * Apply the configured sync strategy to the workspace before build.
*/ */
async function applySyncStrategy(buildParameters: BuildParameters, workspace: string): Promise<void> { async function applySyncStrategy(
buildParameters: BuildParameters,
workspace: string,
enterprise?: EnterpriseServices | undefined,
): Promise<void> {
if (!enterprise?.IncrementalSyncService) {
core.warning('[Sync] Enterprise services not available, skipping sync strategy');
return;
}
const { IncrementalSyncService } = enterprise;
const strategy = buildParameters.syncStrategy as SyncStrategy; const strategy = buildParameters.syncStrategy as SyncStrategy;
const resolvedStrategy = IncrementalSyncService.resolveStrategy(strategy, workspace, buildParameters.syncStatePath); const resolvedStrategy = IncrementalSyncService.resolveStrategy(strategy, workspace, buildParameters.syncStatePath);

View File

@@ -0,0 +1,119 @@
/**
* Orchestrator plugin loader.
*
* After extraction, the orchestrator lives in @game-ci/orchestrator.
* This module provides a thin loader that dynamically imports it,
* falling back gracefully if the package is not installed.
*
* During the extraction transition period, this imports from the local
* source. Once extraction is complete, the import path changes to the
* npm package.
*/
import * as core from '@actions/core';
export interface OrchestratorPluginResult {
exitCode: number;
BuildSucceeded: boolean;
}
/**
* Load the orchestrator for remote builds.
* Returns undefined if orchestrator is not available.
*/
export async function loadOrchestrator(): Promise<
| {
// eslint-disable-next-line no-unused-vars
run: (buildParameters: any, baseImage: string) => Promise<OrchestratorPluginResult>;
}
| undefined
> {
try {
// During extraction transition: import from local source
// After extraction: import from '@game-ci/orchestrator'
const { default: Orchestrator } = await import('./orchestrator/orchestrator');
return {
run: async (buildParameters: any, baseImage: string): Promise<OrchestratorPluginResult> => {
const result = await Orchestrator.run(buildParameters, baseImage);
return {
exitCode: result.BuildSucceeded ? 0 : 1,
BuildSucceeded: result.BuildSucceeded,
};
},
};
} catch {
// Orchestrator package not installed
}
}
/**
* Load enterprise services for local builds.
* These services are part of the orchestrator but also used in local builds
* (child workspaces, local cache, git hooks, LFS agents, etc.).
*/
export async function loadEnterpriseServices() {
try {
const [
{ BuildReliabilityService },
{ TestWorkflowService },
{ HotRunnerService },
{ OutputService },
{ OutputTypeRegistry },
{ ArtifactUploadHandler },
{ IncrementalSyncService },
] = await Promise.all([
import('./orchestrator/services/reliability'),
import('./orchestrator/services/test-workflow'),
import('./orchestrator/services/hot-runner'),
import('./orchestrator/services/output/output-service'),
import('./orchestrator/services/output/output-type-registry'),
import('./orchestrator/services/output/artifact-upload-handler'),
import('./orchestrator/services/sync'),
]);
return {
BuildReliabilityService,
TestWorkflowService,
HotRunnerService,
OutputService,
OutputTypeRegistry,
ArtifactUploadHandler,
IncrementalSyncService,
// Lazy-loaded services (only imported when needed)
async loadChildWorkspaceService() {
const m = await import('./orchestrator/services/cache/child-workspace-service');
return m.ChildWorkspaceService;
},
async loadLocalCacheService() {
const m = await import('./orchestrator/services/cache/local-cache-service');
return m.LocalCacheService;
},
async loadSubmoduleProfileService() {
const m = await import('./orchestrator/services/submodule/submodule-profile-service');
return m.SubmoduleProfileService;
},
async loadLfsAgentService() {
const m = await import('./orchestrator/services/lfs/lfs-agent-service');
return m.LfsAgentService;
},
async loadGitHooksService() {
const m = await import('./orchestrator/services/hooks/git-hooks-service');
return m.GitHooksService;
},
};
} catch (error) {
core.warning(`Enterprise services not available: ${(error as Error).message}`);
}
}