feat(orchestrator): enterprise feature support — CLI provider, submodule profiles, caching, LFS, hooks

Add generic enterprise-grade features to the orchestrator, enabling Unity projects with
complex CI/CD pipelines to adopt game-ci/unity-builder with built-in support for:

- CLI provider protocol: JSON-over-stdin/stdout bridge enabling providers in any language
  (Go, Python, Rust, shell) via the `providerExecutable` input
- Submodule profiles: YAML-based selective submodule initialization with glob patterns
  and variant overlays (`submoduleProfilePath`, `submoduleVariantPath`)
- Local build caching: Filesystem-based Library and LFS caching for local builds without
  external cache actions (`localCacheEnabled`, `localCacheRoot`)
- Custom LFS transfer agents: Register external transfer agents like elastic-git-storage
  (`lfsTransferAgent`, `lfsTransferAgentArgs`, `lfsStoragePaths`)
- Git hooks support: Detect and install lefthook/husky with configurable skip lists
  (`gitHooksEnabled`, `gitHooksSkipList`)

Also removes all `orchestrator-develop` branch references, replacing with `main`.

13 new action inputs, 13 new files, 14 new CLI provider tests, 17 submodule tests,
plus cache/LFS/hooks unit tests. All 452 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
frostebite
2026-03-05 06:54:46 +00:00
parent 9d475434d3
commit 5268630ef0
27 changed files with 3302 additions and 15 deletions
@@ -0,0 +1,96 @@
import fs from 'node:fs';
import path from 'node:path';
import { LfsAgentService } from './lfs-agent-service';
// Mock dependencies
jest.mock('node:fs');
jest.mock('../core/orchestrator-system', () => ({
OrchestratorSystem: {
Run: jest.fn().mockResolvedValue(''),
},
}));
jest.mock('../core/orchestrator-logger', () => ({
__esModule: true,
default: {
log: jest.fn(),
logWarning: jest.fn(),
error: jest.fn(),
},
}));
const mockFs = fs as jest.Mocked<typeof fs>;
describe('LfsAgentService', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('configure', () => {
it('should call correct git config commands when agent exists', async () => {
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
const { OrchestratorSystem } = require('../core/orchestrator-system');
await LfsAgentService.configure(
'/usr/local/bin/elastic-git-storage',
'--verbose',
['/storage/path1', '/storage/path2'],
'/repo',
);
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
`git -C "/repo" config lfs.customtransfer.elastic-git-storage.path "/usr/local/bin/elastic-git-storage"`,
);
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
`git -C "/repo" config lfs.customtransfer.elastic-git-storage.args "--verbose"`,
);
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
`git -C "/repo" config lfs.standalonetransferagent elastic-git-storage`,
);
});
it('should set LFS_STORAGE_PATHS environment variable when storagePaths provided', async () => {
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
await LfsAgentService.configure('/usr/local/bin/elastic-git-storage', '', ['/path/a', '/path/b'], '/repo');
expect(process.env.LFS_STORAGE_PATHS).toBe('/path/a;/path/b');
});
it('should log warning and return early when agent executable does not exist', async () => {
(mockFs.existsSync as jest.Mock).mockReturnValue(false);
const { OrchestratorSystem } = require('../core/orchestrator-system');
await LfsAgentService.configure('/nonexistent/agent', '', [], '/repo');
expect(OrchestratorSystem.Run).not.toHaveBeenCalled();
});
it('should derive agent name from executable filename', async () => {
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
const { OrchestratorSystem } = require('../core/orchestrator-system');
await LfsAgentService.configure('/tools/my-custom-agent.exe', '', [], '/repo');
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
`git -C "/repo" config lfs.customtransfer.my-custom-agent.path "/tools/my-custom-agent.exe"`,
);
});
});
describe('validate', () => {
it('should return true when agent executable exists', async () => {
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
const result = await LfsAgentService.validate('/usr/local/bin/elastic-git-storage');
expect(result).toBe(true);
});
it('should return false when agent executable does not exist', async () => {
(mockFs.existsSync as jest.Mock).mockReturnValue(false);
const result = await LfsAgentService.validate('/nonexistent/agent');
expect(result).toBe(false);
});
});
});
@@ -0,0 +1,59 @@
import fs from 'node:fs';
import path from 'node:path';
import { OrchestratorSystem } from '../core/orchestrator-system';
import OrchestratorLogger from '../core/orchestrator-logger';
export class LfsAgentService {
/**
* Configure a custom LFS transfer agent in a git repository.
* Sets up the git config entries and environment variables needed for the agent.
*/
static async configure(
agentPath: string,
agentArgs: string,
storagePaths: string[],
repoPath: string,
): Promise<void> {
// Validate the agent executable exists
if (!fs.existsSync(agentPath)) {
OrchestratorLogger.logWarning(
`[LfsAgent] Agent executable not found at ${agentPath}, continuing without custom LFS agent`,
);
return;
}
// Derive agent name from executable filename (without extension)
const agentName = path.basename(agentPath, path.extname(agentPath));
OrchestratorLogger.log(`[LfsAgent] Configuring custom LFS transfer agent: ${agentName}`);
OrchestratorLogger.log(`[LfsAgent] Path: ${agentPath}`);
OrchestratorLogger.log(`[LfsAgent] Args: ${agentArgs}`);
// Set git config entries for the custom transfer agent
await OrchestratorSystem.Run(`git -C "${repoPath}" config lfs.customtransfer.${agentName}.path "${agentPath}"`);
await OrchestratorSystem.Run(`git -C "${repoPath}" config lfs.customtransfer.${agentName}.args "${agentArgs}"`);
await OrchestratorSystem.Run(`git -C "${repoPath}" config lfs.standalonetransferagent ${agentName}`);
// Set storage paths environment variable if provided
if (storagePaths.length > 0) {
const storagePathsValue = storagePaths.join(';');
process.env.LFS_STORAGE_PATHS = storagePathsValue;
OrchestratorLogger.log(`[LfsAgent] Storage paths: ${storagePathsValue}`);
}
OrchestratorLogger.log(`[LfsAgent] Custom LFS transfer agent configured successfully`);
}
/**
* Validate that the LFS transfer agent executable exists.
*/
static async validate(agentPath: string): Promise<boolean> {
const exists = fs.existsSync(agentPath);
if (!exists) {
OrchestratorLogger.logWarning(`[LfsAgent] Agent executable not found: ${agentPath}`);
}
return exists;
}
}