feat(cache): add child workspace isolation for multi-product CI builds (#777)

Implement two-level workspace isolation pattern for enterprise-scale CI:
- Atomic O(1) workspace restore via filesystem move (no tar/download/extract)
- Separate Library caching for independent restore
- .git preservation for delta operations
- Stale workspace cleanup with configurable retention policies
- 5 new action inputs: childWorkspacesEnabled, childWorkspaceName,
  childWorkspaceCacheRoot, childWorkspacePreserveGit,
  childWorkspaceSeparateLibrary
- 28 unit tests covering all service methods

This enables enterprise CI where workspaces are 50GB+ and traditional
caching via actions/cache is impractical. On NTFS, workspace restore
is O(1) via atomic rename when source and destination are on the same volume.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
frostebite
2026-03-05 12:02:03 +00:00
parent 26903e96dd
commit 007852a800
8 changed files with 1423 additions and 13 deletions
Generated Vendored
+485 -2
View File
@@ -53,6 +53,26 @@ async function runMain() {
let exitCode = -1;
if (buildParameters.providerStrategy === 'local') {
core.info('Building locally');
// Child workspace isolation - restore cached workspace before any other setup
let childWorkspaceConfig;
if (buildParameters.childWorkspacesEnabled && buildParameters.childWorkspaceName) {
const { ChildWorkspaceService } = await Promise.resolve().then(() => __importStar(__nccwpck_require__(93834)));
const cacheRoot = buildParameters.childWorkspaceCacheRoot ||
node_path_1.default.join(buildParameters.runnerTempPath || process.env.RUNNER_TEMP || '', 'game-ci-workspaces');
childWorkspaceConfig = ChildWorkspaceService.buildConfig({
childWorkspacesEnabled: buildParameters.childWorkspacesEnabled,
childWorkspaceName: buildParameters.childWorkspaceName,
childWorkspaceCacheRoot: cacheRoot,
childWorkspacePreserveGit: buildParameters.childWorkspacePreserveGit,
childWorkspaceSeparateLibrary: buildParameters.childWorkspaceSeparateLibrary,
});
const projectFullPath = node_path_1.default.join(workspace, buildParameters.projectPath);
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 = ChildWorkspaceService.getWorkspaceSize(projectFullPath);
core.info(`Child workspace size after restore: ${size}`);
}
// Submodule profile initialization
if (buildParameters.submoduleProfilePath) {
const { SubmoduleProfileService } = await Promise.resolve().then(() => __importStar(__nccwpck_require__(88664)));
@@ -86,8 +106,8 @@ async function runMain() {
const { GitHooksService } = await Promise.resolve().then(() => __importStar(__nccwpck_require__(9146)));
await GitHooksService.installHooks(workspace);
if (buildParameters.gitHooksSkipList) {
const env = GitHooksService.configureSkipList(buildParameters.gitHooksSkipList.split(','));
Object.assign(process.env, env);
const environment = GitHooksService.configureSkipList(buildParameters.gitHooksSkipList.split(','));
Object.assign(process.env, environment);
}
}
else {
@@ -114,6 +134,15 @@ async function runMain() {
await LocalCacheService.saveLfsCache(workspace, cacheRoot, cacheKey);
}
}
// Child workspace isolation - save workspace for next run
if (childWorkspaceConfig && childWorkspaceConfig.enabled) {
const { ChildWorkspaceService } = await Promise.resolve().then(() => __importStar(__nccwpck_require__(93834)));
const projectFullPath = node_path_1.default.join(workspace, buildParameters.projectPath);
const preSaveSize = ChildWorkspaceService.getWorkspaceSize(projectFullPath);
core.info(`Child workspace size before save: ${preSaveSize}`);
ChildWorkspaceService.saveWorkspace(projectFullPath, childWorkspaceConfig);
core.info(`Child workspace "${buildParameters.childWorkspaceName}" saved to cache`);
}
}
else {
await model_1.Orchestrator.run(buildParameters, baseImage.toString());
@@ -435,11 +464,17 @@ class BuildParameters {
localCacheRoot: input_1.default.localCacheRoot,
localCacheLibrary: input_1.default.localCacheLibrary,
localCacheLfs: input_1.default.localCacheLfs,
childWorkspacesEnabled: input_1.default.childWorkspacesEnabled,
childWorkspaceName: input_1.default.childWorkspaceName,
childWorkspaceCacheRoot: input_1.default.childWorkspaceCacheRoot,
childWorkspacePreserveGit: input_1.default.childWorkspacePreserveGit,
childWorkspaceSeparateLibrary: input_1.default.childWorkspaceSeparateLibrary,
lfsTransferAgent: input_1.default.lfsTransferAgent,
lfsTransferAgentArgs: input_1.default.lfsTransferAgentArgs,
lfsStoragePaths: input_1.default.lfsStoragePaths,
gitHooksEnabled: input_1.default.gitHooksEnabled,
gitHooksSkipList: input_1.default.gitHooksSkipList,
gitHooksRunBeforeBuild: input_1.default.gitHooksRunBeforeBuild,
providerExecutable: input_1.default.providerExecutable,
};
}
@@ -1944,6 +1979,21 @@ class Input {
static get localCacheLfs() {
return (Input.getInput('localCacheLfs') ?? 'false') === 'true';
}
static get childWorkspacesEnabled() {
return (Input.getInput('childWorkspacesEnabled') ?? 'false') === 'true';
}
static get childWorkspaceName() {
return Input.getInput('childWorkspaceName') ?? '';
}
static get childWorkspaceCacheRoot() {
return Input.getInput('childWorkspaceCacheRoot') ?? '';
}
static get childWorkspacePreserveGit() {
return (Input.getInput('childWorkspacePreserveGit') ?? 'true') === 'true';
}
static get childWorkspaceSeparateLibrary() {
return (Input.getInput('childWorkspaceSeparateLibrary') ?? 'true') === 'true';
}
static get lfsTransferAgent() {
return Input.getInput('lfsTransferAgent') ?? '';
}
@@ -1959,6 +2009,9 @@ class Input {
static get gitHooksSkipList() {
return Input.getInput('gitHooksSkipList') ?? '';
}
static get gitHooksRunBeforeBuild() {
return Input.getInput('gitHooksRunBeforeBuild') ?? '';
}
static get providerExecutable() {
return Input.getInput('providerExecutable') ?? '';
}
@@ -8782,6 +8835,317 @@ exports.RemoteClientLogger = RemoteClientLogger;
RemoteClientLogger.value = '';
/***/ }),
/***/ 93834:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.ChildWorkspaceService = void 0;
const node_fs_1 = __importDefault(__nccwpck_require__(87561));
const node_path_1 = __importDefault(__nccwpck_require__(49411));
const orchestrator_logger_1 = __importDefault(__nccwpck_require__(32549));
/**
* Child workspace isolation service for enterprise-scale CI builds.
*
* Instead of building in the git checkout directory, this service:
* 1. Keeps the root workspace lean (no LFS files in the checkout dir)
* 2. Creates isolated child workspaces per product/build-target
* 3. Each child workspace gets its own submodule profile, LFS hydration, and Library folder
* 4. After build, the child workspace (.git preserved) is moved to a parent-level backup directory
* 5. On next CI run, the child workspace is restored via atomic filesystem move (O(1) on NTFS)
* 6. Library folders are cached separately for independent restore
*
* This is orders of magnitude faster than actions/cache for 50GB+ workspaces.
*/
class ChildWorkspaceService {
/**
* Initialize child workspace by restoring from cache if available.
* Uses atomic filesystem move (rename) for O(1) restore on same volume.
*
* @param projectPath - Target path where the workspace should live during build
* @param config - Child workspace configuration
* @returns true if restored from cache, false if starting fresh
*/
static initializeWorkspace(projectPath, config) {
const cachedWorkspacePath = node_path_1.default.join(config.parentCacheRoot, config.workspaceName);
try {
if (!node_fs_1.default.existsSync(cachedWorkspacePath)) {
orchestrator_logger_1.default.log(`[ChildWorkspace] No cached workspace found at ${cachedWorkspacePath}, starting fresh`);
return false;
}
// Verify the cached workspace has content
const entries = node_fs_1.default.readdirSync(cachedWorkspacePath);
if (entries.length === 0) {
orchestrator_logger_1.default.log(`[ChildWorkspace] Cached workspace at ${cachedWorkspacePath} is empty, starting fresh`);
node_fs_1.default.rmSync(cachedWorkspacePath, { recursive: true, force: true });
return false;
}
// If the target path already exists, remove it to make way for the move
if (node_fs_1.default.existsSync(projectPath)) {
orchestrator_logger_1.default.log(`[ChildWorkspace] Removing existing target path: ${projectPath}`);
node_fs_1.default.rmSync(projectPath, { recursive: true, force: true });
}
// Ensure parent directory of project path exists
const parentDirectory = node_path_1.default.dirname(projectPath);
if (!node_fs_1.default.existsSync(parentDirectory)) {
node_fs_1.default.mkdirSync(parentDirectory, { recursive: true });
}
// Atomic move (rename) - O(1) on NTFS when on same volume
orchestrator_logger_1.default.log(`[ChildWorkspace] Restoring workspace: ${cachedWorkspacePath} -> ${projectPath}`);
node_fs_1.default.renameSync(cachedWorkspacePath, projectPath);
orchestrator_logger_1.default.log(`[ChildWorkspace] Workspace restored via atomic move`);
// Restore Library cache separately if configured
if (config.separateLibraryCache) {
ChildWorkspaceService.restoreLibraryCache(projectPath, config);
}
return true;
}
catch (error) {
orchestrator_logger_1.default.logWarning(`[ChildWorkspace] Workspace restore failed: ${error.message}. Starting fresh.`);
return false;
}
}
/**
* Save child workspace after build for reuse on next CI run.
* Moves the entire workspace to the cache directory via atomic filesystem move.
*
* @param projectPath - Path to the workspace to save
* @param config - Child workspace configuration
*/
static saveWorkspace(projectPath, config) {
const cachedWorkspacePath = node_path_1.default.join(config.parentCacheRoot, config.workspaceName);
try {
if (!node_fs_1.default.existsSync(projectPath)) {
orchestrator_logger_1.default.log(`[ChildWorkspace] Project path ${projectPath} does not exist, skipping save`);
return;
}
// Remove .git directory if not preserving it (saves space but loses delta capability)
if (!config.preserveGitDirectory) {
const gitDirectory = node_path_1.default.join(projectPath, '.git');
if (node_fs_1.default.existsSync(gitDirectory)) {
orchestrator_logger_1.default.log(`[ChildWorkspace] Removing .git directory (preserveGit=false)`);
node_fs_1.default.rmSync(gitDirectory, { recursive: true, force: true });
}
}
// If separateLibraryCache, move Library/ to its own backup path before saving workspace
if (config.separateLibraryCache) {
ChildWorkspaceService.saveLibraryCache(projectPath, config);
}
// Ensure parent cache root exists
if (!node_fs_1.default.existsSync(config.parentCacheRoot)) {
node_fs_1.default.mkdirSync(config.parentCacheRoot, { recursive: true });
}
// Remove any existing cached workspace to make room
if (node_fs_1.default.existsSync(cachedWorkspacePath)) {
orchestrator_logger_1.default.log(`[ChildWorkspace] Removing previous cached workspace: ${cachedWorkspacePath}`);
node_fs_1.default.rmSync(cachedWorkspacePath, { recursive: true, force: true });
}
// Atomic move (rename) - O(1) on NTFS when on same volume
orchestrator_logger_1.default.log(`[ChildWorkspace] Saving workspace: ${projectPath} -> ${cachedWorkspacePath}`);
node_fs_1.default.renameSync(projectPath, cachedWorkspacePath);
orchestrator_logger_1.default.log(`[ChildWorkspace] Workspace saved via atomic move`);
}
catch (error) {
orchestrator_logger_1.default.logWarning(`[ChildWorkspace] Workspace save failed: ${error.message}`);
}
}
/**
* Restore Library folder from separate cache location.
*
* @param projectPath - Path to the workspace where Library should be restored
* @param config - Child workspace configuration
* @returns true if Library was restored from cache
*/
static restoreLibraryCache(projectPath, config) {
const libraryBackup = ChildWorkspaceService.resolveLibraryBackupPath(config);
const libraryDestination = node_path_1.default.join(projectPath, 'Library');
try {
if (!node_fs_1.default.existsSync(libraryBackup)) {
orchestrator_logger_1.default.log(`[ChildWorkspace] No Library cache found at ${libraryBackup}`);
return false;
}
const entries = node_fs_1.default.readdirSync(libraryBackup);
if (entries.length === 0) {
orchestrator_logger_1.default.log(`[ChildWorkspace] Library cache at ${libraryBackup} is empty`);
node_fs_1.default.rmSync(libraryBackup, { recursive: true, force: true });
return false;
}
// Remove existing Library directory if present
if (node_fs_1.default.existsSync(libraryDestination)) {
node_fs_1.default.rmSync(libraryDestination, { recursive: true, force: true });
}
// Atomic move
orchestrator_logger_1.default.log(`[ChildWorkspace] Restoring Library cache: ${libraryBackup} -> ${libraryDestination}`);
node_fs_1.default.renameSync(libraryBackup, libraryDestination);
orchestrator_logger_1.default.log(`[ChildWorkspace] Library cache restored`);
return true;
}
catch (error) {
orchestrator_logger_1.default.logWarning(`[ChildWorkspace] Library cache restore failed: ${error.message}`);
return false;
}
}
/**
* Save Library folder to a separate cache location for independent restore.
* Moves Library/ out of the workspace before workspace save.
*
* @param projectPath - Path to the workspace containing Library/
* @param config - Child workspace configuration
*/
static saveLibraryCache(projectPath, config) {
const libraryPath = node_path_1.default.join(projectPath, 'Library');
const libraryBackup = ChildWorkspaceService.resolveLibraryBackupPath(config);
try {
if (!node_fs_1.default.existsSync(libraryPath)) {
orchestrator_logger_1.default.log(`[ChildWorkspace] No Library folder to cache`);
return;
}
const entries = node_fs_1.default.readdirSync(libraryPath);
if (entries.length === 0) {
orchestrator_logger_1.default.log(`[ChildWorkspace] Library folder is empty, skipping cache`);
return;
}
// Ensure parent of backup path exists
const backupParent = node_path_1.default.dirname(libraryBackup);
if (!node_fs_1.default.existsSync(backupParent)) {
node_fs_1.default.mkdirSync(backupParent, { recursive: true });
}
// Remove existing Library backup
if (node_fs_1.default.existsSync(libraryBackup)) {
node_fs_1.default.rmSync(libraryBackup, { recursive: true, force: true });
}
// Atomic move
orchestrator_logger_1.default.log(`[ChildWorkspace] Caching Library: ${libraryPath} -> ${libraryBackup}`);
node_fs_1.default.renameSync(libraryPath, libraryBackup);
orchestrator_logger_1.default.log(`[ChildWorkspace] Library cached separately`);
}
catch (error) {
orchestrator_logger_1.default.logWarning(`[ChildWorkspace] Library cache save failed: ${error.message}`);
}
}
/**
* Calculate the total size of a directory in human-readable format.
*
* @param directoryPath - Path to the directory to measure
* @returns Human-readable size string (e.g., "1.23 GB", "456.78 MB")
*/
static getWorkspaceSize(directoryPath) {
try {
if (!node_fs_1.default.existsSync(directoryPath)) {
return '0 B';
}
const totalBytes = ChildWorkspaceService.calculateDirectorySize(directoryPath);
return ChildWorkspaceService.formatBytes(totalBytes);
}
catch (error) {
orchestrator_logger_1.default.logWarning(`[ChildWorkspace] Failed to calculate workspace size: ${error.message}`);
return 'unknown';
}
}
/**
* Clean stale child workspaces that haven't been used within the retention period.
*
* @param parentCacheRoot - Root directory containing cached workspaces
* @param retentionDays - Maximum age in days before a workspace is considered stale
*/
static cleanStaleWorkspaces(parentCacheRoot, retentionDays) {
try {
if (!node_fs_1.default.existsSync(parentCacheRoot)) {
orchestrator_logger_1.default.log(`[ChildWorkspace] Cache root ${parentCacheRoot} does not exist, nothing to clean`);
return;
}
const now = Date.now();
const maxAgeMs = retentionDays * 24 * 60 * 60 * 1000;
const entries = node_fs_1.default.readdirSync(parentCacheRoot);
let removedCount = 0;
let freedBytes = 0;
for (const entry of entries) {
const entryPath = node_path_1.default.join(parentCacheRoot, entry);
try {
const stat = node_fs_1.default.statSync(entryPath);
if (stat.isDirectory() && now - stat.mtimeMs > maxAgeMs) {
const size = ChildWorkspaceService.calculateDirectorySize(entryPath);
node_fs_1.default.rmSync(entryPath, { recursive: true, force: true });
removedCount++;
freedBytes += size;
orchestrator_logger_1.default.log(`[ChildWorkspace] Cleaned stale workspace: ${entry} (age: ${Math.floor((now - stat.mtimeMs) / (24 * 60 * 60 * 1000))} days)`);
}
}
catch (error) {
orchestrator_logger_1.default.logWarning(`[ChildWorkspace] Failed to clean ${entryPath}: ${error.message}`);
}
}
orchestrator_logger_1.default.log(`[ChildWorkspace] Cleanup complete: ${removedCount} stale workspaces removed, ${ChildWorkspaceService.formatBytes(freedBytes)} freed`);
}
catch (error) {
orchestrator_logger_1.default.logWarning(`[ChildWorkspace] Stale workspace cleanup failed: ${error.message}`);
}
}
/**
* Build a ChildWorkspaceConfig from build parameters and action inputs.
*/
static buildConfig(parameters) {
return {
enabled: parameters.childWorkspacesEnabled,
workspaceName: parameters.childWorkspaceName,
parentCacheRoot: parameters.childWorkspaceCacheRoot,
preserveGitDirectory: parameters.childWorkspacePreserveGit,
separateLibraryCache: parameters.childWorkspaceSeparateLibrary,
};
}
/**
* Resolve the Library backup path from config, using a default if not overridden.
*/
static resolveLibraryBackupPath(config) {
if (config.libraryBackupPath) {
return config.libraryBackupPath;
}
return node_path_1.default.join(config.parentCacheRoot, `${config.workspaceName}-Library`);
}
/**
* Recursively calculate total size of a directory in bytes.
*/
static calculateDirectorySize(directoryPath) {
let totalSize = 0;
try {
const entries = node_fs_1.default.readdirSync(directoryPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = node_path_1.default.join(directoryPath, entry.name);
if (entry.isDirectory()) {
totalSize += ChildWorkspaceService.calculateDirectorySize(fullPath);
}
else if (entry.isFile()) {
totalSize += node_fs_1.default.statSync(fullPath).size;
}
}
}
catch {
// Permission errors or race conditions — return what we have
}
return totalSize;
}
/**
* Format bytes into human-readable string.
*/
static formatBytes(bytes) {
if (bytes === 0)
return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const k = 1024;
const index = Math.floor(Math.log(bytes) / Math.log(k));
const value = bytes / Math.pow(k, index);
return `${value.toFixed(2)} ${units[index]}`;
}
}
exports.ChildWorkspaceService = ChildWorkspaceService;
/***/ }),
/***/ 68829:
@@ -10386,8 +10750,83 @@ class GitHooksService {
}
return 'none';
}
/**
* Detect if Unity Git Hooks (com.frostebite.unitygithooks) is installed as a UPM package.
* Checks Packages/manifest.json for the package dependency.
*/
static detectUnityGitHooks(repoPath) {
const manifestPath = node_path_1.default.join(repoPath, 'Packages', 'manifest.json');
if (!node_fs_1.default.existsSync(manifestPath)) {
return false;
}
try {
const content = node_fs_1.default.readFileSync(manifestPath, 'utf8');
return content.includes(GitHooksService.UNITY_GIT_HOOKS_PACKAGE);
}
catch {
return false;
}
}
/**
* Find the Unity Git Hooks package directory in the Library/PackageCache.
* Returns the path to the package directory, or empty string if not found.
*/
static findUnityGitHooksPackagePath(repoPath) {
const packageCacheDir = node_path_1.default.join(repoPath, 'Library', 'PackageCache');
if (!node_fs_1.default.existsSync(packageCacheDir)) {
return '';
}
try {
const entries = node_fs_1.default.readdirSync(packageCacheDir);
const match = entries.find((entry) => entry.startsWith(GitHooksService.UNITY_GIT_HOOKS_PACKAGE));
if (match) {
return node_path_1.default.join(packageCacheDir, match);
}
}
catch {
// PackageCache not available
}
return '';
}
/**
* Initialize Unity Git Hooks by running its init script.
* This installs the required npm modules that the hooks depend on.
* Should be called before installHooks() when Unity Git Hooks is detected.
*/
static async initUnityGitHooks(repoPath) {
const packagePath = GitHooksService.findUnityGitHooksPackagePath(repoPath);
if (!packagePath) {
orchestrator_logger_1.default.log(`[GitHooks] Unity Git Hooks package not found in Library/PackageCache, skipping init`);
return;
}
const initScript = node_path_1.default.join(packagePath, '~js', 'init-unity-lefthook.js');
if (!node_fs_1.default.existsSync(initScript)) {
orchestrator_logger_1.default.logWarning(`[GitHooks] Unity Git Hooks init script not found at ${initScript}`);
return;
}
orchestrator_logger_1.default.log(`[GitHooks] Initializing Unity Git Hooks from ${packagePath}`);
try {
await orchestrator_system_1.OrchestratorSystem.Run(`cd "${repoPath}" && node "${initScript}"`, true);
orchestrator_logger_1.default.log(`[GitHooks] Unity Git Hooks initialized successfully`);
}
catch (error) {
orchestrator_logger_1.default.logWarning(`[GitHooks] Unity Git Hooks init failed: ${error.message}`);
}
}
/**
* Configure CI-friendly environment variables for Unity Git Hooks.
* Disables background project mode (CI already has an isolated workspace)
* and sets other env vars appropriate for headless CI environments.
*/
static configureUnityGitHooksCIEnv() {
return {
UNITY_GITHOOKS_BACKGROUND_PROJECT_ENABLED: 'false',
CI: 'true',
};
}
/**
* Install git hooks using the detected framework.
* If Unity Git Hooks is detected, initializes it first.
* Errors are caught and logged as warnings - hook installation should not fail the build.
*/
static async installHooks(repoPath) {
@@ -10397,6 +10836,16 @@ class GitHooksService {
return;
}
orchestrator_logger_1.default.log(`[GitHooks] Detected hook framework: ${framework}`);
// If Unity Git Hooks is present, initialize it before installing hooks
if (framework === 'lefthook' && GitHooksService.detectUnityGitHooks(repoPath)) {
orchestrator_logger_1.default.log(`[GitHooks] Unity Git Hooks (UPM) detected, running init`);
// Set CI-friendly env vars
const ciEnv = GitHooksService.configureUnityGitHooksCIEnv();
for (const [key, value] of Object.entries(ciEnv)) {
process.env[key] = value;
}
await GitHooksService.initUnityGitHooks(repoPath);
}
try {
if (framework === 'lefthook') {
await orchestrator_system_1.OrchestratorSystem.Run(`cd "${repoPath}" && npx lefthook install`, true);
@@ -10411,6 +10860,39 @@ class GitHooksService {
orchestrator_logger_1.default.logWarning(`[GitHooks] Hook installation failed: ${error.message}`);
}
}
/**
* Explicitly run specific lefthook hook groups before the build.
* This allows CI to trigger pre-commit, pre-push, or other checks
* that would normally only run on git events.
*
* @param repoPath - Path to the repository
* @param hookGroups - Lefthook group names to run (e.g., ['pre-commit', 'pre-push'])
* @returns Map of group name to success/failure
*/
static async runHookGroups(repoPath, hookGroups) {
const results = {};
if (hookGroups.length === 0) {
return results;
}
const framework = GitHooksService.detectHookFramework(repoPath);
if (framework !== 'lefthook') {
orchestrator_logger_1.default.logWarning(`[GitHooks] runHookGroups requires lefthook, but detected: ${framework}`);
return results;
}
orchestrator_logger_1.default.log(`[GitHooks] Running ${hookGroups.length} hook group(s): ${hookGroups.join(', ')}`);
for (const group of hookGroups) {
try {
await orchestrator_system_1.OrchestratorSystem.Run(`cd "${repoPath}" && npx lefthook run ${group}`, true);
orchestrator_logger_1.default.log(`[GitHooks] Hook group '${group}' passed`);
results[group] = true;
}
catch (error) {
orchestrator_logger_1.default.logWarning(`[GitHooks] Hook group '${group}' failed: ${error.message}`);
results[group] = false;
}
}
return results;
}
/**
* Return environment variables that will skip the listed hooks.
* For lefthook: sets LEFTHOOK_EXCLUDE to a comma-separated list.
@@ -10449,6 +10931,7 @@ class GitHooksService {
}
}
exports.GitHooksService = GitHooksService;
GitHooksService.UNITY_GIT_HOOKS_PACKAGE = 'com.frostebite.unitygithooks';
/***/ }),
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long