mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-06-13 01:13:54 -07:00
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:
@@ -0,0 +1,20 @@
|
||||
export interface CliProviderRequest {
|
||||
command: CliProviderSubcommand;
|
||||
params: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface CliProviderResponse {
|
||||
success: boolean;
|
||||
result?: any;
|
||||
error?: string;
|
||||
output?: string;
|
||||
}
|
||||
|
||||
export type CliProviderSubcommand =
|
||||
| 'setup-workflow'
|
||||
| 'cleanup-workflow'
|
||||
| 'run-task'
|
||||
| 'garbage-collect'
|
||||
| 'list-resources'
|
||||
| 'list-workflow'
|
||||
| 'watch-workflow';
|
||||
@@ -0,0 +1,226 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { ProviderLoader } from '../provider-loader';
|
||||
|
||||
// Mock child_process
|
||||
jest.mock('child_process', () => ({
|
||||
spawn: jest.fn(),
|
||||
exec: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock @actions/core to prevent GitHub Actions API calls
|
||||
jest.mock('@actions/core', () => ({
|
||||
info: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
error: jest.fn(),
|
||||
setOutput: jest.fn(),
|
||||
getInput: jest.fn(() => ''),
|
||||
}));
|
||||
|
||||
// Mock provider-git-manager (required by provider-loader)
|
||||
jest.mock('../provider-git-manager');
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import CliProvider from './cli-provider';
|
||||
|
||||
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
|
||||
|
||||
/**
|
||||
* Creates a mock child process with stdin, stdout, stderr as EventEmitters.
|
||||
*/
|
||||
function createMockChildProcess() {
|
||||
const child = new EventEmitter() as any;
|
||||
child.stdin = { write: jest.fn(), end: jest.fn() };
|
||||
child.stdout = new EventEmitter();
|
||||
child.stderr = new EventEmitter();
|
||||
child.kill = jest.fn();
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
describe('CliProvider', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('validates that executable path is non-empty', () => {
|
||||
expect(() => new CliProvider('', {} as any)).toThrow('executablePath must be a non-empty string');
|
||||
});
|
||||
|
||||
it('validates that executable path is not just whitespace', () => {
|
||||
expect(() => new CliProvider(' ', {} as any)).toThrow('executablePath must be a non-empty string');
|
||||
});
|
||||
|
||||
it('accepts a valid executable path', () => {
|
||||
const provider = new CliProvider('/usr/bin/my-provider', {} as any);
|
||||
expect(provider).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('request serialization', () => {
|
||||
it('sends JSON request to stdin with correct command and params', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.listResources();
|
||||
|
||||
// Simulate successful response
|
||||
child.stdout.emit('data', Buffer.from(JSON.stringify({ success: true, result: [] }) + '\n'));
|
||||
child.emit('close', 0);
|
||||
|
||||
await promise;
|
||||
|
||||
expect(child.stdin.write).toHaveBeenCalledTimes(1);
|
||||
const writtenData = child.stdin.write.mock.calls[0][0];
|
||||
const parsed = JSON.parse(writtenData);
|
||||
expect(parsed.command).toBe('list-resources');
|
||||
expect(parsed.params).toEqual({});
|
||||
expect(child.stdin.end).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('serializes setupWorkflow params correctly', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.setupWorkflow('guid-123', { editorVersion: '2022.3' } as any, 'main', []);
|
||||
|
||||
child.stdout.emit('data', Buffer.from(JSON.stringify({ success: true, result: {} }) + '\n'));
|
||||
child.emit('close', 0);
|
||||
|
||||
await promise;
|
||||
|
||||
const writtenData = child.stdin.write.mock.calls[0][0];
|
||||
const parsed = JSON.parse(writtenData);
|
||||
expect(parsed.command).toBe('setup-workflow');
|
||||
expect(parsed.params.buildGuid).toBe('guid-123');
|
||||
expect(parsed.params.branchName).toBe('main');
|
||||
});
|
||||
});
|
||||
|
||||
describe('response parsing', () => {
|
||||
it('resolves on successful JSON response', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.listResources();
|
||||
|
||||
const resources = [{ Name: 'resource-1' }, { Name: 'resource-2' }];
|
||||
child.stdout.emit('data', Buffer.from(JSON.stringify({ success: true, result: resources }) + '\n'));
|
||||
child.emit('close', 0);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toEqual(resources);
|
||||
});
|
||||
|
||||
it('rejects on error JSON response', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.garbageCollect('', false, 30, false, false);
|
||||
|
||||
child.stdout.emit('data', Buffer.from(JSON.stringify({ success: false, error: 'something went wrong' }) + '\n'));
|
||||
child.emit('close', 1);
|
||||
|
||||
await expect(promise).rejects.toThrow('something went wrong');
|
||||
});
|
||||
|
||||
it('rejects when process exits with non-zero code and no JSON response', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.listWorkflow();
|
||||
|
||||
child.stderr.emit('data', Buffer.from('segfault\n'));
|
||||
child.emit('close', 139);
|
||||
|
||||
await expect(promise).rejects.toThrow('exited with code 139');
|
||||
});
|
||||
|
||||
it('resolves when process exits with code 0 and no JSON response', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.listResources();
|
||||
|
||||
child.stdout.emit('data', Buffer.from('some plain text output\n'));
|
||||
child.emit('close', 0);
|
||||
|
||||
const result = await promise;
|
||||
// listResources falls back to empty array when result is missing
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects on spawn error', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/nonexistent/path', {} as any);
|
||||
const promise = provider.listResources();
|
||||
|
||||
child.emit('error', new Error('ENOENT'));
|
||||
|
||||
await expect(promise).rejects.toThrow('failed to spawn executable');
|
||||
});
|
||||
});
|
||||
|
||||
describe('runTaskInWorkflow', () => {
|
||||
it('forwards non-JSON stdout lines as build output and returns final response', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.runTaskInWorkflow('guid', 'image', 'cmd', '/mnt', '/work', [], []);
|
||||
|
||||
// Simulate build output followed by JSON response
|
||||
child.stdout.emit('data', Buffer.from('Building project...\nCompiling scripts...\n'));
|
||||
child.stdout.emit('data', Buffer.from(JSON.stringify({ success: true, output: 'Build succeeded' }) + '\n'));
|
||||
child.emit('close', 0);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBe('Build succeeded');
|
||||
});
|
||||
|
||||
it('rejects on run-task failure', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.runTaskInWorkflow('guid', 'image', 'cmd', '/mnt', '/work', [], []);
|
||||
|
||||
child.stdout.emit(
|
||||
'data',
|
||||
Buffer.from(JSON.stringify({ success: false, error: 'Build failed: compilation errors' }) + '\n'),
|
||||
);
|
||||
child.emit('close', 1);
|
||||
|
||||
await expect(promise).rejects.toThrow('Build failed: compilation errors');
|
||||
});
|
||||
|
||||
it('returns collected output lines when no JSON response and exit code 0', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.runTaskInWorkflow('guid', 'image', 'cmd', '/mnt', '/work', [], []);
|
||||
|
||||
child.stdout.emit('data', Buffer.from('line 1\nline 2\n'));
|
||||
child.emit('close', 0);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBe('line 1\nline 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('available providers list', () => {
|
||||
it('includes cli in the available providers', () => {
|
||||
const providers = ProviderLoader.getAvailableProviders();
|
||||
expect(providers).toContain('cli');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,344 @@
|
||||
import { spawn } from 'child_process';
|
||||
import { ProviderInterface } from '../provider-interface';
|
||||
import BuildParameters from '../../../build-parameters';
|
||||
import OrchestratorEnvironmentVariable from '../../options/orchestrator-environment-variable';
|
||||
import OrchestratorSecret from '../../options/orchestrator-secret';
|
||||
import { ProviderResource } from '../provider-resource';
|
||||
import { ProviderWorkflow } from '../provider-workflow';
|
||||
import OrchestratorLogger from '../../services/core/orchestrator-logger';
|
||||
import { CliProviderRequest, CliProviderResponse, CliProviderSubcommand } from './cli-provider-protocol';
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 300_000; // 300 seconds
|
||||
|
||||
class CliProvider implements ProviderInterface {
|
||||
private readonly executablePath: string;
|
||||
private readonly buildParameters: BuildParameters;
|
||||
|
||||
constructor(executablePath: string, buildParameters: BuildParameters) {
|
||||
if (!executablePath || executablePath.trim() === '') {
|
||||
throw new Error('CliProvider: executablePath must be a non-empty string');
|
||||
}
|
||||
this.executablePath = executablePath;
|
||||
this.buildParameters = buildParameters;
|
||||
}
|
||||
|
||||
async setupWorkflow(
|
||||
buildGuid: string,
|
||||
buildParameters: BuildParameters,
|
||||
branchName: string,
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
): Promise<any> {
|
||||
const response = await this.execute('setup-workflow', {
|
||||
buildGuid,
|
||||
buildParameters,
|
||||
branchName,
|
||||
defaultSecretsArray,
|
||||
});
|
||||
|
||||
return response.result;
|
||||
}
|
||||
|
||||
async cleanupWorkflow(
|
||||
buildParameters: BuildParameters,
|
||||
branchName: string,
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
): Promise<any> {
|
||||
const response = await this.execute('cleanup-workflow', {
|
||||
buildParameters,
|
||||
branchName,
|
||||
defaultSecretsArray,
|
||||
});
|
||||
|
||||
return response.result;
|
||||
}
|
||||
|
||||
async runTaskInWorkflow(
|
||||
buildGuid: string,
|
||||
image: string,
|
||||
commands: string,
|
||||
mountdir: string,
|
||||
workingdir: string,
|
||||
environment: OrchestratorEnvironmentVariable[],
|
||||
secrets: OrchestratorSecret[],
|
||||
): Promise<string> {
|
||||
const request: CliProviderRequest = {
|
||||
command: 'run-task',
|
||||
params: {
|
||||
buildGuid,
|
||||
image,
|
||||
commands,
|
||||
mountdir,
|
||||
workingdir,
|
||||
environment,
|
||||
secrets,
|
||||
},
|
||||
};
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const child = spawn(this.executablePath, ['run-task'], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
|
||||
let lastJsonResponse: CliProviderResponse | undefined;
|
||||
const outputLines: string[] = [];
|
||||
let stderrOutput = '';
|
||||
|
||||
child.stdin.write(JSON.stringify(request));
|
||||
child.stdin.end();
|
||||
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
const lines = data.toString().split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
// Try to parse as JSON response
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (typeof parsed === 'object' && parsed !== null && 'success' in parsed) {
|
||||
lastJsonResponse = parsed as CliProviderResponse;
|
||||
continue;
|
||||
}
|
||||
} catch {
|
||||
// Not JSON — treat as build output
|
||||
}
|
||||
|
||||
// Forward non-JSON lines as real-time build output
|
||||
OrchestratorLogger.log(trimmed);
|
||||
outputLines.push(trimmed);
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
stderrOutput += text;
|
||||
// Forward stderr to logger
|
||||
for (const line of text.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed) {
|
||||
OrchestratorLogger.log(`[cli-provider stderr] ${trimmed}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error: Error) => {
|
||||
reject(new Error(`CliProvider: failed to spawn executable '${this.executablePath}': ${error.message}`));
|
||||
});
|
||||
|
||||
child.on('close', (code: number | null) => {
|
||||
if (lastJsonResponse) {
|
||||
if (lastJsonResponse.success) {
|
||||
resolve(lastJsonResponse.output || outputLines.join('\n'));
|
||||
} else {
|
||||
reject(
|
||||
new Error(`CliProvider run-task failed: ${lastJsonResponse.error || 'Unknown error from CLI provider'}`),
|
||||
);
|
||||
}
|
||||
} else if (code === 0) {
|
||||
resolve(outputLines.join('\n'));
|
||||
} else {
|
||||
reject(
|
||||
new Error(`CliProvider run-task exited with code ${code}${stderrOutput ? ': ' + stderrOutput.trim() : ''}`),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async garbageCollect(
|
||||
filter: string,
|
||||
previewOnly: boolean,
|
||||
olderThan: Number,
|
||||
fullCache: boolean,
|
||||
baseDependencies: boolean,
|
||||
): Promise<string> {
|
||||
const response = await this.execute('garbage-collect', {
|
||||
filter,
|
||||
previewOnly,
|
||||
olderThan,
|
||||
fullCache,
|
||||
baseDependencies,
|
||||
});
|
||||
|
||||
return response.output || '';
|
||||
}
|
||||
|
||||
async listResources(): Promise<ProviderResource[]> {
|
||||
const response = await this.execute('list-resources', {});
|
||||
|
||||
return (response.result as ProviderResource[]) || [];
|
||||
}
|
||||
|
||||
async listWorkflow(): Promise<ProviderWorkflow[]> {
|
||||
const response = await this.execute('list-workflow', {});
|
||||
|
||||
return (response.result as ProviderWorkflow[]) || [];
|
||||
}
|
||||
|
||||
async watchWorkflow(): Promise<string> {
|
||||
const request: CliProviderRequest = {
|
||||
command: 'watch-workflow',
|
||||
params: {},
|
||||
};
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const child = spawn(this.executablePath, ['watch-workflow'], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
|
||||
let lastJsonResponse: CliProviderResponse | undefined;
|
||||
const outputLines: string[] = [];
|
||||
|
||||
child.stdin.write(JSON.stringify(request));
|
||||
child.stdin.end();
|
||||
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
const lines = data.toString().split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (typeof parsed === 'object' && parsed !== null && 'success' in parsed) {
|
||||
lastJsonResponse = parsed as CliProviderResponse;
|
||||
continue;
|
||||
}
|
||||
} catch {
|
||||
// Not JSON
|
||||
}
|
||||
|
||||
OrchestratorLogger.log(trimmed);
|
||||
outputLines.push(trimmed);
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
for (const line of data.toString().split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed) {
|
||||
OrchestratorLogger.log(`[cli-provider stderr] ${trimmed}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error: Error) => {
|
||||
reject(new Error(`CliProvider: failed to spawn executable '${this.executablePath}': ${error.message}`));
|
||||
});
|
||||
|
||||
child.on('close', (code: number | null) => {
|
||||
if (lastJsonResponse) {
|
||||
if (lastJsonResponse.success) {
|
||||
resolve(lastJsonResponse.output || outputLines.join('\n'));
|
||||
} else {
|
||||
reject(new Error(`CliProvider watch-workflow failed: ${lastJsonResponse.error || 'Unknown error'}`));
|
||||
}
|
||||
} else if (code === 0) {
|
||||
resolve(outputLines.join('\n'));
|
||||
} else {
|
||||
reject(new Error(`CliProvider watch-workflow exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a CLI provider subcommand with a default timeout.
|
||||
* Used for all methods except runTaskInWorkflow and watchWorkflow (which have no timeout).
|
||||
*/
|
||||
private execute(
|
||||
command: CliProviderSubcommand,
|
||||
params: Record<string, any>,
|
||||
timeoutMs: number = DEFAULT_TIMEOUT_MS,
|
||||
): Promise<CliProviderResponse> {
|
||||
const request: CliProviderRequest = { command, params };
|
||||
|
||||
return new Promise<CliProviderResponse>((resolve, reject) => {
|
||||
const child = spawn(this.executablePath, [command], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
|
||||
let stdoutData = '';
|
||||
let stderrData = '';
|
||||
let timedOut = false;
|
||||
|
||||
// Set up timeout
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true;
|
||||
child.kill('SIGTERM');
|
||||
reject(new Error(`CliProvider: command '${command}' timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
|
||||
child.stdin.write(JSON.stringify(request));
|
||||
child.stdin.end();
|
||||
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
stdoutData += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
stderrData += text;
|
||||
// Forward stderr to logger
|
||||
for (const line of text.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed) {
|
||||
OrchestratorLogger.log(`[cli-provider stderr] ${trimmed}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error: Error) => {
|
||||
clearTimeout(timer);
|
||||
if (!timedOut) {
|
||||
reject(new Error(`CliProvider: failed to spawn executable '${this.executablePath}': ${error.message}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on('close', (code: number | null) => {
|
||||
clearTimeout(timer);
|
||||
if (timedOut) return;
|
||||
|
||||
// Find the last JSON line in stdout
|
||||
const lines = stdoutData.split('\n').filter((l) => l.trim());
|
||||
let response: CliProviderResponse | undefined;
|
||||
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const parsed = JSON.parse(lines[i].trim());
|
||||
if (typeof parsed === 'object' && parsed !== null && 'success' in parsed) {
|
||||
response = parsed as CliProviderResponse;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Not valid JSON, skip
|
||||
}
|
||||
}
|
||||
|
||||
if (response) {
|
||||
if (response.success) {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(new Error(`CliProvider ${command} failed: ${response.error || 'Unknown error from CLI provider'}`));
|
||||
}
|
||||
} else if (code === 0) {
|
||||
// No JSON response but exit code 0 — treat as success with raw output
|
||||
resolve({ success: true, output: stdoutData.trim() });
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`CliProvider ${command} exited with code ${code}` +
|
||||
(stderrData ? `: ${stderrData.trim()}` : '') +
|
||||
(!stderrData && stdoutData ? `: ${stdoutData.trim()}` : ''),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default CliProvider;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './cli-provider';
|
||||
@@ -58,6 +58,7 @@ export default async function loadProvider(
|
||||
const providerModuleMap: Record<string, string> = {
|
||||
aws: './aws',
|
||||
k8s: './k8s',
|
||||
cli: './cli',
|
||||
test: './test',
|
||||
'local-docker': './docker',
|
||||
'local-system': './local',
|
||||
@@ -136,7 +137,7 @@ export class ProviderLoader {
|
||||
* @returns string[] - Array of available provider names
|
||||
*/
|
||||
static getAvailableProviders(): string[] {
|
||||
return ['aws', 'k8s', 'test', 'local-docker', 'local-system', 'local'];
|
||||
return ['aws', 'k8s', 'cli', 'test', 'local-docker', 'local-system', 'local'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user