mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-05-31 13:56:13 -07:00
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:
122
dist/index.js
generated
vendored
122
dist/index.js
generated
vendored
@@ -134,9 +134,10 @@ async function runMain() {
|
||||
// Child workspace isolation - restore cached workspace before any other setup
|
||||
let childWorkspaceConfig;
|
||||
if (buildParameters.childWorkspacesEnabled && buildParameters.childWorkspaceName) {
|
||||
const ChildWorkspaceService = await enterprise?.loadChildWorkspaceService();
|
||||
const cacheRoot = buildParameters.childWorkspaceCacheRoot ||
|
||||
node_path_1.default.join(buildParameters.runnerTempPath || process.env.RUNNER_TEMP || '', 'game-ci-workspaces');
|
||||
childWorkspaceConfig = enterprise?.ChildWorkspaceService.buildConfig({
|
||||
childWorkspaceConfig = ChildWorkspaceService?.buildConfig({
|
||||
childWorkspacesEnabled: buildParameters.childWorkspacesEnabled,
|
||||
childWorkspaceName: buildParameters.childWorkspaceName,
|
||||
childWorkspaceCacheRoot: cacheRoot,
|
||||
@@ -144,45 +145,51 @@ async function runMain() {
|
||||
childWorkspaceSeparateLibrary: buildParameters.childWorkspaceSeparateLibrary,
|
||||
});
|
||||
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'}`);
|
||||
// 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}`);
|
||||
}
|
||||
// Submodule profile initialization
|
||||
if (buildParameters.submoduleProfilePath) {
|
||||
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) {
|
||||
await enterprise?.SubmoduleProfileService.execute(plan, workspace, buildParameters.submoduleToken || buildParameters.gitPrivateToken);
|
||||
await SubmoduleProfileService?.execute(plan, workspace, buildParameters.submoduleToken || buildParameters.gitPrivateToken);
|
||||
}
|
||||
}
|
||||
// Configure custom LFS transfer agent
|
||||
if (buildParameters.lfsTransferAgent) {
|
||||
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
|
||||
let cacheRoot = '';
|
||||
let cacheKey = '';
|
||||
// eslint-disable-next-line no-undef
|
||||
let LocalCacheService;
|
||||
if (buildParameters.localCacheEnabled) {
|
||||
cacheRoot = enterprise?.LocalCacheService.resolveCacheRoot(buildParameters) || '';
|
||||
LocalCacheService = await enterprise?.loadLocalCacheService();
|
||||
cacheRoot = LocalCacheService?.resolveCacheRoot(buildParameters) || '';
|
||||
cacheKey =
|
||||
enterprise?.LocalCacheService.generateCacheKey(buildParameters.targetPlatform, buildParameters.editorVersion, buildParameters.branch || '') || '';
|
||||
LocalCacheService?.generateCacheKey(buildParameters.targetPlatform, buildParameters.editorVersion, buildParameters.branch || '') || '';
|
||||
if (buildParameters.localCacheLfs) {
|
||||
await enterprise?.LocalCacheService.restoreLfsCache(workspace, cacheRoot, cacheKey);
|
||||
await LocalCacheService?.restoreLfsCache(workspace, cacheRoot, cacheKey);
|
||||
}
|
||||
if (buildParameters.localCacheLibrary) {
|
||||
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.
|
||||
if (buildParameters.gitHooksEnabled) {
|
||||
await enterprise?.GitHooksService.installHooks(workspace);
|
||||
const GitHooksService = await enterprise?.loadGitHooksService();
|
||||
await GitHooksService?.installHooks(workspace);
|
||||
if (buildParameters.gitHooksSkipList) {
|
||||
const environment = enterprise?.GitHooksService.configureSkipList(buildParameters.gitHooksSkipList.split(','));
|
||||
const environment = GitHooksService?.configureSkipList(buildParameters.gitHooksSkipList.split(','));
|
||||
if (environment) {
|
||||
Object.assign(process.env, environment);
|
||||
}
|
||||
@@ -204,21 +211,22 @@ async function runMain() {
|
||||
...buildParameters,
|
||||
});
|
||||
// Local build caching - save
|
||||
if (buildParameters.localCacheEnabled) {
|
||||
if (buildParameters.localCacheEnabled && LocalCacheService) {
|
||||
if (buildParameters.localCacheLibrary) {
|
||||
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) {
|
||||
await enterprise?.LocalCacheService.saveLfsCache(workspace, cacheRoot, cacheKey);
|
||||
await LocalCacheService.saveLfsCache(workspace, cacheRoot, cacheKey);
|
||||
}
|
||||
}
|
||||
// Child workspace isolation - save workspace for next run
|
||||
if (childWorkspaceConfig && childWorkspaceConfig.enabled) {
|
||||
const ChildWorkspaceService = await enterprise?.loadChildWorkspaceService();
|
||||
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}`);
|
||||
enterprise?.ChildWorkspaceService.saveWorkspace(projectFullPath, childWorkspaceConfig);
|
||||
ChildWorkspaceService?.saveWorkspace(projectFullPath, childWorkspaceConfig);
|
||||
core.info(`Child workspace "${buildParameters.childWorkspaceName}" saved to cache`);
|
||||
}
|
||||
// Revert overlays after job completion if configured
|
||||
@@ -231,7 +239,6 @@ async function runMain() {
|
||||
core.warning(`[Sync] Overlay revert failed: ${revertError.message}`);
|
||||
}
|
||||
}
|
||||
exitCode = await runColdBuild(buildParameters, baseImage, workspace, actionFolder);
|
||||
}
|
||||
else {
|
||||
const orchestrator = await (0, orchestrator_plugin_1.loadOrchestrator)();
|
||||
@@ -2562,6 +2569,10 @@ exports["default"] = MacBuilder;
|
||||
* 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.
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
@@ -2590,45 +2601,74 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.loadEnterpriseServices = exports.loadOrchestrator = void 0;
|
||||
const core = __importStar(__nccwpck_require__(42186));
|
||||
/**
|
||||
* Attempt to load the orchestrator package.
|
||||
* Returns undefined if @game-ci/orchestrator is not installed.
|
||||
* Load the orchestrator for remote builds.
|
||||
* Returns undefined if orchestrator is not available.
|
||||
*/
|
||||
async function loadOrchestrator() {
|
||||
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)));
|
||||
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 {
|
||||
// Package not installed
|
||||
// Orchestrator package not installed
|
||||
}
|
||||
}
|
||||
exports.loadOrchestrator = loadOrchestrator;
|
||||
/**
|
||||
* Attempt to load enterprise services for local builds.
|
||||
* These services (child workspaces, local cache, git hooks, etc.)
|
||||
* are part of the orchestrator package but also used in local builds.
|
||||
* 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.).
|
||||
*/
|
||||
async function loadEnterpriseServices() {
|
||||
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 {
|
||||
ChildWorkspaceService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(93834))))
|
||||
.ChildWorkspaceService,
|
||||
LocalCacheService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(68829)))).LocalCacheService,
|
||||
SubmoduleProfileService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(88664))))
|
||||
.SubmoduleProfileService,
|
||||
LfsAgentService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(85985)))).LfsAgentService,
|
||||
GitHooksService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(9146)))).GitHooksService,
|
||||
IncrementalSyncService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(98729)))).IncrementalSyncService,
|
||||
BuildReliabilityService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(9842)))).BuildReliabilityService,
|
||||
TestWorkflowService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(22377)))).TestWorkflowService,
|
||||
HotRunnerService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(74283)))).HotRunnerService,
|
||||
OutputService: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(18795)))).OutputService,
|
||||
OutputTypeRegistry: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(58012)))).OutputTypeRegistry,
|
||||
ArtifactUploadHandler: (await Promise.resolve().then(() => __importStar(__nccwpck_require__(49063))))
|
||||
.ArtifactUploadHandler,
|
||||
BuildReliabilityService,
|
||||
TestWorkflowService,
|
||||
HotRunnerService,
|
||||
OutputService,
|
||||
OutputTypeRegistry,
|
||||
ArtifactUploadHandler,
|
||||
IncrementalSyncService,
|
||||
// Lazy-loaded services (only imported when needed)
|
||||
async loadChildWorkspaceService() {
|
||||
const m = await Promise.resolve().then(() => __importStar(__nccwpck_require__(93834)));
|
||||
return m.ChildWorkspaceService;
|
||||
},
|
||||
async loadLocalCacheService() {
|
||||
const m = await Promise.resolve().then(() => __importStar(__nccwpck_require__(68829)));
|
||||
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) {
|
||||
|
||||
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -47,27 +47,77 @@ const mockGitHooksService = {
|
||||
configureSkipList: jest.fn().mockReturnValue({ LEFTHOOK_EXCLUDE: 'pre-commit' }),
|
||||
};
|
||||
|
||||
// Mock the dynamic import() targets — jest.mock with factory functions.
|
||||
// The services are imported dynamically via `await import(...)` in index.ts,
|
||||
// so we mock the module path and return the mock objects as named exports.
|
||||
jest.mock('./model/orchestrator/services/cache/child-workspace-service', () => ({
|
||||
ChildWorkspaceService: mockChildWorkspaceService,
|
||||
const mockBuildReliabilityService = {
|
||||
configureGitEnvironment: jest.fn(),
|
||||
checkGitIntegrity: jest.fn().mockReturnValue(true),
|
||||
cleanStaleLockFiles: jest.fn(),
|
||||
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', () => ({
|
||||
SubmoduleProfileService: mockSubmoduleProfileService,
|
||||
}));
|
||||
|
||||
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 the sync-state module for the SyncStrategy type import
|
||||
jest.mock('./model/orchestrator/services/sync/sync-state', () => ({
|
||||
SyncStrategy: {},
|
||||
}));
|
||||
|
||||
// Mock all non-enterprise dependencies to isolate the wiring logic
|
||||
@@ -84,9 +134,6 @@ jest.mock('./model', () => ({
|
||||
Cache: {
|
||||
verify: jest.fn(),
|
||||
},
|
||||
Orchestrator: {
|
||||
run: jest.fn().mockResolvedValue(''),
|
||||
},
|
||||
Docker: {
|
||||
run: jest.fn().mockResolvedValue(0),
|
||||
},
|
||||
|
||||
214
src/index.ts
214
src/index.ts
@@ -1,19 +1,17 @@
|
||||
import * as core from '@actions/core';
|
||||
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 MacBuilder from './model/mac-builder';
|
||||
import PlatformSetup from './model/platform-setup';
|
||||
import { BuildReliabilityService } from './model/orchestrator/services/reliability';
|
||||
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 { loadOrchestrator, loadEnterpriseServices } from './model/orchestrator-plugin';
|
||||
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() {
|
||||
try {
|
||||
if (Cli.InitCliMode()) {
|
||||
@@ -24,8 +22,10 @@ async function runMain() {
|
||||
Action.checkCompatibility();
|
||||
Cache.verify();
|
||||
|
||||
const enterprise = await loadEnterpriseServices();
|
||||
|
||||
// Always configure git environment for CI reliability
|
||||
BuildReliabilityService.configureGitEnvironment();
|
||||
enterprise?.BuildReliabilityService.configureGitEnvironment();
|
||||
|
||||
const { workspace, actionFolder } = Action;
|
||||
|
||||
@@ -35,9 +35,16 @@ async function runMain() {
|
||||
// instead of the standard build execution path
|
||||
if (buildParameters.testSuitePath) {
|
||||
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) {
|
||||
core.setFailed(`Test workflow completed with ${totalFailed} failure(s)`);
|
||||
} else {
|
||||
@@ -53,24 +60,24 @@ async function runMain() {
|
||||
if (buildParameters.gitIntegrityCheck) {
|
||||
core.info('Running git integrity checks...');
|
||||
|
||||
const isHealthy = BuildReliabilityService.checkGitIntegrity(workspace);
|
||||
BuildReliabilityService.cleanStaleLockFiles(workspace);
|
||||
BuildReliabilityService.validateSubmoduleBackingStores(workspace);
|
||||
const isHealthy = enterprise?.BuildReliabilityService.checkGitIntegrity(workspace);
|
||||
enterprise?.BuildReliabilityService.cleanStaleLockFiles(workspace);
|
||||
enterprise?.BuildReliabilityService.validateSubmoduleBackingStores(workspace);
|
||||
|
||||
if (buildParameters.cleanReservedFilenames) {
|
||||
BuildReliabilityService.cleanReservedFilenames(buildParameters.projectPath);
|
||||
enterprise?.BuildReliabilityService.cleanReservedFilenames(buildParameters.projectPath);
|
||||
}
|
||||
|
||||
if (!isHealthy && buildParameters.gitAutoRecover) {
|
||||
core.info('Git corruption detected, attempting automatic recovery...');
|
||||
const recovered = BuildReliabilityService.recoverCorruptedRepo(workspace);
|
||||
const recovered = enterprise?.BuildReliabilityService.recoverCorruptedRepo(workspace);
|
||||
if (!recovered) {
|
||||
core.warning('Automatic recovery failed. Build may encounter issues.');
|
||||
}
|
||||
}
|
||||
} else if (buildParameters.cleanReservedFilenames) {
|
||||
// cleanReservedFilenames can run independently of gitIntegrityCheck
|
||||
BuildReliabilityService.cleanReservedFilenames(buildParameters.projectPath);
|
||||
enterprise?.BuildReliabilityService.cleanReservedFilenames(buildParameters.projectPath);
|
||||
}
|
||||
|
||||
let exitCode = -1;
|
||||
@@ -79,7 +86,7 @@ async function runMain() {
|
||||
if (buildParameters.hotRunnerEnabled) {
|
||||
core.info('[HotRunner] Hot runner mode enabled, attempting hot build...');
|
||||
|
||||
const hotRunnerConfig: HotRunnerConfig = {
|
||||
const hotRunnerConfig = {
|
||||
enabled: true,
|
||||
transport: buildParameters.hotRunnerTransport,
|
||||
host: buildParameters.hotRunnerHost,
|
||||
@@ -89,7 +96,11 @@ async function runMain() {
|
||||
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 {
|
||||
await hotRunnerService.initialize(hotRunnerConfig);
|
||||
@@ -118,11 +129,11 @@ async function runMain() {
|
||||
// Child workspace isolation - restore cached workspace before any other setup
|
||||
let childWorkspaceConfig: any;
|
||||
if (buildParameters.childWorkspacesEnabled && buildParameters.childWorkspaceName) {
|
||||
const { ChildWorkspaceService } = await import('./model/orchestrator/services/cache/child-workspace-service');
|
||||
const ChildWorkspaceService = await enterprise?.loadChildWorkspaceService();
|
||||
const cacheRoot =
|
||||
buildParameters.childWorkspaceCacheRoot ||
|
||||
path.join(buildParameters.runnerTempPath || process.env.RUNNER_TEMP || '', 'game-ci-workspaces');
|
||||
childWorkspaceConfig = ChildWorkspaceService.buildConfig({
|
||||
childWorkspaceConfig = ChildWorkspaceService?.buildConfig({
|
||||
childWorkspacesEnabled: buildParameters.childWorkspacesEnabled,
|
||||
childWorkspaceName: buildParameters.childWorkspaceName,
|
||||
childWorkspaceCacheRoot: cacheRoot,
|
||||
@@ -130,7 +141,7 @@ async function runMain() {
|
||||
childWorkspaceSeparateLibrary: buildParameters.childWorkspaceSeparateLibrary,
|
||||
});
|
||||
const projectFullPath = path.join(workspace, buildParameters.projectPath);
|
||||
const restored = ChildWorkspaceService.initializeWorkspace(projectFullPath, childWorkspaceConfig);
|
||||
const restored = ChildWorkspaceService?.initializeWorkspace(projectFullPath, childWorkspaceConfig);
|
||||
core.info(
|
||||
`Child workspace "${buildParameters.childWorkspaceName}": ${
|
||||
restored ? 'restored from cache' : 'starting fresh'
|
||||
@@ -138,33 +149,34 @@ async function runMain() {
|
||||
);
|
||||
|
||||
// Log workspace size for resource tracking
|
||||
const size = ChildWorkspaceService.getWorkspaceSize(projectFullPath);
|
||||
const size = ChildWorkspaceService?.getWorkspaceSize(projectFullPath);
|
||||
core.info(`Child workspace size after restore: ${size}`);
|
||||
}
|
||||
|
||||
// Submodule profile initialization
|
||||
if (buildParameters.submoduleProfilePath) {
|
||||
const { SubmoduleProfileService } = await import(
|
||||
'./model/orchestrator/services/submodule/submodule-profile-service'
|
||||
);
|
||||
core.info('Initializing submodules from profile...');
|
||||
const plan = await SubmoduleProfileService.createInitPlan(
|
||||
const SubmoduleProfileService = await enterprise?.loadSubmoduleProfileService();
|
||||
const plan = await SubmoduleProfileService?.createInitPlan(
|
||||
buildParameters.submoduleProfilePath,
|
||||
buildParameters.submoduleVariantPath,
|
||||
workspace,
|
||||
);
|
||||
await SubmoduleProfileService.execute(
|
||||
plan,
|
||||
workspace,
|
||||
buildParameters.submoduleToken || buildParameters.gitPrivateToken,
|
||||
);
|
||||
|
||||
if (plan) {
|
||||
await SubmoduleProfileService?.execute(
|
||||
plan,
|
||||
workspace,
|
||||
buildParameters.submoduleToken || buildParameters.gitPrivateToken,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Configure custom LFS transfer agent
|
||||
if (buildParameters.lfsTransferAgent) {
|
||||
const { LfsAgentService } = await import('./model/orchestrator/services/lfs/lfs-agent-service');
|
||||
core.info('Configuring custom LFS transfer agent...');
|
||||
await LfsAgentService.configure(
|
||||
const LfsAgentService = await enterprise?.loadLfsAgentService();
|
||||
await LfsAgentService?.configure(
|
||||
buildParameters.lfsTransferAgent,
|
||||
buildParameters.lfsTransferAgentArgs,
|
||||
buildParameters.lfsStoragePaths ? buildParameters.lfsStoragePaths.split(';') : [],
|
||||
@@ -175,30 +187,35 @@ async function runMain() {
|
||||
// Local build caching - restore
|
||||
let cacheRoot = '';
|
||||
let cacheKey = '';
|
||||
// eslint-disable-next-line no-undef
|
||||
let LocalCacheService: Awaited<ReturnType<NonNullable<typeof enterprise>['loadLocalCacheService']>> | undefined;
|
||||
if (buildParameters.localCacheEnabled) {
|
||||
const { LocalCacheService } = await import('./model/orchestrator/services/cache/local-cache-service');
|
||||
cacheRoot = LocalCacheService.resolveCacheRoot(buildParameters);
|
||||
cacheKey = LocalCacheService.generateCacheKey(
|
||||
buildParameters.targetPlatform,
|
||||
buildParameters.editorVersion,
|
||||
buildParameters.branch || '',
|
||||
);
|
||||
LocalCacheService = await enterprise?.loadLocalCacheService();
|
||||
cacheRoot = LocalCacheService?.resolveCacheRoot(buildParameters) || '';
|
||||
cacheKey =
|
||||
LocalCacheService?.generateCacheKey(
|
||||
buildParameters.targetPlatform,
|
||||
buildParameters.editorVersion,
|
||||
buildParameters.branch || '',
|
||||
) || '';
|
||||
if (buildParameters.localCacheLfs) {
|
||||
await LocalCacheService.restoreLfsCache(workspace, cacheRoot, cacheKey);
|
||||
await LocalCacheService?.restoreLfsCache(workspace, cacheRoot, cacheKey);
|
||||
}
|
||||
if (buildParameters.localCacheLibrary) {
|
||||
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.
|
||||
if (buildParameters.gitHooksEnabled) {
|
||||
const { GitHooksService } = await import('./model/orchestrator/services/hooks/git-hooks-service');
|
||||
await GitHooksService.installHooks(workspace);
|
||||
const GitHooksService = await enterprise?.loadGitHooksService();
|
||||
await GitHooksService?.installHooks(workspace);
|
||||
if (buildParameters.gitHooksSkipList) {
|
||||
const environment = GitHooksService.configureSkipList(buildParameters.gitHooksSkipList.split(','));
|
||||
Object.assign(process.env, environment);
|
||||
const environment = GitHooksService?.configureSkipList(buildParameters.gitHooksSkipList.split(','));
|
||||
if (environment) {
|
||||
Object.assign(process.env, environment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +223,7 @@ async function runMain() {
|
||||
const syncStrategy = buildParameters.syncStrategy as SyncStrategy;
|
||||
if (syncStrategy !== 'full') {
|
||||
core.info(`[Sync] Applying sync strategy: ${syncStrategy}`);
|
||||
await applySyncStrategy(buildParameters, workspace);
|
||||
await applySyncStrategy(buildParameters, workspace, enterprise);
|
||||
}
|
||||
|
||||
await PlatformSetup.setup(buildParameters, actionFolder);
|
||||
@@ -220,8 +237,7 @@ async function runMain() {
|
||||
});
|
||||
|
||||
// Local build caching - save
|
||||
if (buildParameters.localCacheEnabled) {
|
||||
const { LocalCacheService } = await import('./model/orchestrator/services/cache/local-cache-service');
|
||||
if (buildParameters.localCacheEnabled && LocalCacheService) {
|
||||
if (buildParameters.localCacheLibrary) {
|
||||
const projectFullPath = path.join(workspace, buildParameters.projectPath);
|
||||
await LocalCacheService.saveLibraryCache(projectFullPath, cacheRoot, cacheKey);
|
||||
@@ -233,12 +249,12 @@ async function runMain() {
|
||||
|
||||
// Child workspace isolation - save workspace for next run
|
||||
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 preSaveSize = ChildWorkspaceService.getWorkspaceSize(projectFullPath);
|
||||
const preSaveSize = ChildWorkspaceService?.getWorkspaceSize(projectFullPath);
|
||||
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`);
|
||||
}
|
||||
|
||||
@@ -246,22 +262,33 @@ async function runMain() {
|
||||
if (buildParameters.syncRevertAfter && syncStrategy !== 'full') {
|
||||
core.info('[Sync] Reverting overlay changes after job completion');
|
||||
try {
|
||||
await IncrementalSyncService.revertOverlays(workspace, buildParameters.syncStatePath);
|
||||
await enterprise?.IncrementalSyncService.revertOverlays(workspace, buildParameters.syncStatePath);
|
||||
} catch (revertError) {
|
||||
core.warning(`[Sync] Overlay revert failed: ${(revertError as Error).message}`);
|
||||
}
|
||||
}
|
||||
exitCode = await runColdBuild(buildParameters, baseImage, workspace, actionFolder);
|
||||
} 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;
|
||||
}
|
||||
|
||||
// Post-build: archive and enforce retention
|
||||
if (buildParameters.buildArchiveEnabled && exitCode === 0) {
|
||||
core.info('Archiving build output...');
|
||||
BuildReliabilityService.archiveBuildOutput(buildParameters.buildPath, buildParameters.buildArchivePath);
|
||||
BuildReliabilityService.enforceRetention(buildParameters.buildArchivePath, buildParameters.buildArchiveRetention);
|
||||
enterprise?.BuildReliabilityService.archiveBuildOutput(
|
||||
buildParameters.buildPath,
|
||||
buildParameters.buildArchivePath,
|
||||
);
|
||||
enterprise?.BuildReliabilityService.enforceRetention(
|
||||
buildParameters.buildArchivePath,
|
||||
buildParameters.buildArchiveRetention,
|
||||
);
|
||||
}
|
||||
|
||||
// Set output
|
||||
@@ -277,7 +304,7 @@ async function runMain() {
|
||||
const customTypes = JSON.parse(buildParameters.artifactCustomTypes);
|
||||
if (Array.isArray(customTypes)) {
|
||||
for (const ct of customTypes) {
|
||||
OutputTypeRegistry.registerType({
|
||||
enterprise?.OutputTypeRegistry.registerType({
|
||||
name: ct.name,
|
||||
defaultPath: ct.defaultPath || ct.pattern || `./${ct.name}/`,
|
||||
description: ct.description || `Custom output type: ${ct.name}`,
|
||||
@@ -292,7 +319,7 @@ async function runMain() {
|
||||
|
||||
// Collect outputs and generate manifest
|
||||
const manifestPath = path.join(buildParameters.projectPath, 'output-manifest.json');
|
||||
const manifest = await OutputService.collectOutputs(
|
||||
const manifest = await enterprise?.OutputService.collectOutputs(
|
||||
buildParameters.projectPath,
|
||||
buildParameters.buildGuid,
|
||||
buildParameters.artifactOutputTypes,
|
||||
@@ -301,27 +328,31 @@ async function runMain() {
|
||||
|
||||
core.setOutput('artifactManifestPath', manifestPath);
|
||||
|
||||
// Upload artifacts
|
||||
const uploadConfig = ArtifactUploadHandler.parseConfig(
|
||||
buildParameters.artifactUploadTarget,
|
||||
buildParameters.artifactUploadPath || undefined,
|
||||
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 (manifest) {
|
||||
// Upload artifacts
|
||||
const uploadConfig = enterprise?.ArtifactUploadHandler.parseConfig(
|
||||
buildParameters.artifactUploadTarget,
|
||||
buildParameters.artifactUploadPath || undefined,
|
||||
buildParameters.artifactCompression,
|
||||
buildParameters.artifactRetentionDays,
|
||||
);
|
||||
|
||||
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) {
|
||||
core.warning(`Artifact collection/upload failed: ${(artifactError as Error).message}`);
|
||||
@@ -353,7 +384,13 @@ async function runColdBuild(
|
||||
...buildParameters,
|
||||
});
|
||||
} 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;
|
||||
}
|
||||
@@ -362,7 +399,18 @@ async function runColdBuild(
|
||||
/**
|
||||
* 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 resolvedStrategy = IncrementalSyncService.resolveStrategy(strategy, workspace, buildParameters.syncStatePath);
|
||||
|
||||
|
||||
119
src/model/orchestrator-plugin.ts
Normal file
119
src/model/orchestrator-plugin.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user