feat: add dynamic provider loader

codex/create-provider-loader-and-update-setup-method
Frostebite 2025-09-03 20:34:08 +01:00
parent c6c8236152
commit ebb637d57e
4 changed files with 78 additions and 2 deletions

View File

@ -13,6 +13,7 @@ import CloudRunnerEnvironmentVariable from './options/cloud-runner-environment-v
import TestCloudRunner from './providers/test';
import LocalCloudRunner from './providers/local';
import LocalDockerCloudRunner from './providers/docker';
import loadProvider from './providers/provider-loader';
import GitHub from '../github';
import SharedWorkspaceLocking from './services/core/shared-workspace-locking';
import { FollowLogStreamService } from './services/core/follow-log-stream-service';
@ -38,7 +39,7 @@ class CloudRunner {
if (CloudRunner.buildParameters.githubCheckId === ``) {
CloudRunner.buildParameters.githubCheckId = await GitHub.createGitHubCheck(CloudRunner.buildParameters.buildGuid);
}
CloudRunner.setupSelectedBuildPlatform();
await CloudRunner.setupSelectedBuildPlatform();
CloudRunner.defaultSecrets = TaskParameterSerializer.readDefaultSecrets();
CloudRunner.cloudRunnerEnvironmentVariables =
TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameters);
@ -62,7 +63,7 @@ class CloudRunner {
FollowLogStreamService.Reset();
}
private static setupSelectedBuildPlatform() {
private static async setupSelectedBuildPlatform() {
CloudRunnerLogger.log(`Cloud Runner platform selected ${CloudRunner.buildParameters.providerStrategy}`);
switch (CloudRunner.buildParameters.providerStrategy) {
case 'k8s':
@ -80,6 +81,13 @@ class CloudRunner {
case 'local-system':
CloudRunner.Provider = new LocalCloudRunner();
break;
default:
if (CloudRunner.buildParameters.providerStrategy !== 'local') {
CloudRunner.Provider = await loadProvider(
CloudRunner.buildParameters.providerStrategy,
CloudRunner.buildParameters,
);
}
}
}

View File

@ -0,0 +1 @@
export default class InvalidProvider {}

View File

@ -0,0 +1,19 @@
import loadProvider from './provider-loader';
import { ProviderInterface } from './provider-interface';
describe('provider-loader', () => {
it('loads a provider dynamically', async () => {
const provider: ProviderInterface = await loadProvider('./test', {} as any);
expect(typeof provider.runTaskInWorkflow).toBe('function');
});
it('throws when provider package is missing', async () => {
await expect(loadProvider('non-existent-package', {} as any)).rejects.toThrow('non-existent-package');
});
it('throws when provider does not implement ProviderInterface', async () => {
await expect(loadProvider('./fixtures/invalid-provider', {} as any)).rejects.toThrow(
'does not implement ProviderInterface',
);
});
});

View File

@ -0,0 +1,48 @@
import { ProviderInterface } from './provider-interface';
import BuildParameters from '../../build-parameters';
/**
* Dynamically load a provider package by name.
* @param providerName Name of the provider package to load
* @param buildParameters Build parameters passed to the provider constructor
* @throws Error when the provider cannot be loaded or does not implement ProviderInterface
*/
export default async function loadProvider(
providerName: string,
buildParameters: BuildParameters,
): Promise<ProviderInterface> {
let importedModule: any;
try {
importedModule = await import(providerName);
} catch (error) {
throw new Error(`Failed to load provider package '${providerName}': ${(error as Error).message}`);
}
const Provider = importedModule.default || importedModule;
let instance: any;
try {
instance = new Provider(buildParameters);
} catch (error) {
throw new Error(`Failed to instantiate provider '${providerName}': ${(error as Error).message}`);
}
const requiredMethods = [
'cleanupWorkflow',
'setupWorkflow',
'runTaskInWorkflow',
'garbageCollect',
'listResources',
'listWorkflow',
'watchWorkflow',
];
for (const method of requiredMethods) {
if (typeof instance[method] !== 'function') {
throw new Error(
`Provider package '${providerName}' does not implement ProviderInterface. Missing method '${method}'.`,
);
}
}
return instance as ProviderInterface;
}