feedback improvements

feature/provider-loader-dynamic-imports
Frostebite 2025-11-27 15:33:40 +00:00
parent e6686e4d61
commit cb6b30300e
5 changed files with 133 additions and 36 deletions

38
dist/index.js vendored
View File

@ -3281,13 +3281,14 @@ class TaskService {
if (taskElement === undefined) {
continue;
}
taskElement.overrides = {};
taskElement.attachments = [];
if (taskElement.createdAt === undefined) {
cloud_runner_logger_1.default.log(`Skipping ${taskElement.taskDefinitionArn} no createdAt date`);
const extendedTask = taskElement;
extendedTask.overrides = {};
extendedTask.attachments = [];
if (extendedTask.createdAt === undefined) {
cloud_runner_logger_1.default.log(`Skipping ${extendedTask.taskDefinitionArn} no createdAt date`);
continue;
}
result.push({ taskElement, element });
result.push({ taskElement: extendedTask, element });
}
}
}
@ -4421,12 +4422,33 @@ class LocalCloudRunner {
cloud_runner_logger_1.default.log(commands);
// On Windows, many built-in hooks use POSIX shell syntax. Execute via bash if available.
if (process.platform === 'win32') {
const inline = commands
.replace(/"/g, '\\"')
// Properly escape the command string for embedding in a double-quoted bash string.
// Order matters: backslashes must be escaped first to avoid double-escaping.
const escapeForBashDoubleQuotes = (stringValue) => {
return stringValue
.replace(/\\/g, '\\\\') // Escape backslashes first
.replace(/\$/g, '\\$') // Escape dollar signs to prevent variable expansion
.replace(/`/g, '\\`') // Escape backticks to prevent command substitution
.replace(/"/g, '\\"'); // Escape double quotes
};
// Split commands by newlines and escape each line
const lines = commands
.replace(/\r/g, '')
.split('\n')
.filter((x) => x.trim().length > 0)
.join(' ; ');
.map((line) => escapeForBashDoubleQuotes(line));
// Join with semicolons, but don't add semicolon after control flow keywords
// Control flow keywords that shouldn't be followed by semicolons: then, else, do, fi, done, esac
const controlFlowKeywords = /\b(then|else|do|fi|done|esac)\s*$/;
const inline = lines
.map((line, index) => {
// Don't add semicolon if this line ends with a control flow keyword
if (controlFlowKeywords.test(line.trim()) || index === lines.length - 1) {
return line;
}
return `${line} ;`;
})
.join(' ');
const bashWrapped = `bash -lc "${inline}"`;
return await cloud_runner_system_1.CloudRunnerSystem.Run(bashWrapped);
}

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,16 @@
# Provider Loader Dynamic Imports
The provider loader now supports dynamic loading of providers from multiple sources including local file paths, GitHub repositories, and NPM packages.
The provider loader now supports dynamic loading of providers from multiple sources including local file paths, GitHub
repositories, and NPM packages.
## What is a Provider?
A provider is a pluggable backend that Cloud Runner uses to run builds and workflows. Examples include AWS, Kubernetes,
or local execution. Each provider implements the `ProviderInterface`, which defines the common lifecycle methods (setup,
run, cleanup, garbage collection, etc.).
This abstraction makes Cloud Runner flexible: you can switch execution environments or add your own provider (via npm
package, GitHub repo, or local path) without changing the rest of your pipeline.
## Features
@ -37,21 +47,18 @@ const absoluteProvider = await ProviderLoader.loadProvider('/path/to/provider',
```typescript
// Load from GitHub URL
const githubProvider = await ProviderLoader.loadProvider(
'https://github.com/user/my-provider',
buildParameters
);
const githubProvider = await ProviderLoader.loadProvider('https://github.com/user/my-provider', buildParameters);
// Load from specific branch
const branchProvider = await ProviderLoader.loadProvider(
'https://github.com/user/my-provider/tree/develop',
buildParameters
'https://github.com/user/my-provider/tree/develop',
buildParameters,
);
// Load from specific path in repository
const pathProvider = await ProviderLoader.loadProvider(
'https://github.com/user/my-provider/tree/main/src/providers',
buildParameters
'https://github.com/user/my-provider/tree/main/src/providers',
buildParameters,
);
// Shorthand notation
@ -76,8 +83,20 @@ All providers must implement the `ProviderInterface`:
```typescript
interface ProviderInterface {
cleanupWorkflow(): Promise<void>;
setupWorkflow(buildGuid: string, buildParameters: BuildParameters, branchName: string, defaultSecretsArray: any[]): Promise<void>;
runTaskInWorkflow(buildGuid: string, task: string, workingDirectory: string, buildVolumeFolder: string, environmentVariables: any[], secrets: any[]): Promise<string>;
setupWorkflow(
buildGuid: string,
buildParameters: BuildParameters,
branchName: string,
defaultSecretsArray: any[],
): Promise<void>;
runTaskInWorkflow(
buildGuid: string,
task: string,
workingDirectory: string,
buildVolumeFolder: string,
environmentVariables: any[],
secrets: any[],
): Promise<string>;
garbageCollect(): Promise<void>;
listResources(): Promise<ProviderResource[]>;
listWorkflow(): Promise<ProviderWorkflow[]>;
@ -99,11 +118,23 @@ export default class MyProvider implements ProviderInterface {
// Cleanup logic
}
async setupWorkflow(buildGuid: string, buildParameters: BuildParameters, branchName: string, defaultSecretsArray: any[]): Promise<void> {
async setupWorkflow(
buildGuid: string,
buildParameters: BuildParameters,
branchName: string,
defaultSecretsArray: any[],
): Promise<void> {
// Setup logic
}
async runTaskInWorkflow(buildGuid: string, task: string, workingDirectory: string, buildVolumeFolder: string, environmentVariables: any[], secrets: any[]): Promise<string> {
async runTaskInWorkflow(
buildGuid: string,
task: string,
workingDirectory: string,
buildVolumeFolder: string,
environmentVariables: any[],
secrets: any[],
): Promise<string> {
// Task execution logic
return 'Task completed';
}
@ -159,6 +190,7 @@ console.log(providers); // ['aws', 'k8s', 'test', 'local-docker', 'local-system'
## Supported URL Formats
### GitHub URLs
- `https://github.com/user/repo`
- `https://github.com/user/repo.git`
- `https://github.com/user/repo/tree/branch`
@ -166,23 +198,27 @@ console.log(providers); // ['aws', 'k8s', 'test', 'local-docker', 'local-system'
- `git@github.com:user/repo.git`
### Shorthand GitHub References
- `user/repo`
- `user/repo@branch`
- `user/repo@branch/path/to/provider`
### Local Paths
- `./relative/path`
- `../relative/path`
- `/absolute/path`
- `C:\\path\\to\\provider` (Windows)
### NPM Packages
- `package-name`
- `@scope/package-name`
## Caching
GitHub repositories are automatically cached in the `.provider-cache` directory. The cache key is generated based on the repository owner, name, and branch. This ensures that:
GitHub repositories are automatically cached in the `.provider-cache` directory. The cache key is generated based on the
repository owner, name, and branch. This ensures that:
1. Repositories are only cloned once
2. Updates are checked and applied automatically
@ -207,7 +243,7 @@ The provider loader can be configured through environment variables:
## Best Practices
1. **Use specific branches**: Always specify the branch when loading from GitHub
1. **Use specific branches or versions**: Always specify the branch or specific tag when loading from GitHub
2. **Implement proper error handling**: Wrap provider loading in try-catch blocks
3. **Clean up regularly**: Use the cleanup utility to manage cache size
4. **Test locally first**: Test providers locally before deploying

View File

@ -3,8 +3,11 @@ import {
DescribeStacksCommand,
ListStacksCommand,
} from '@aws-sdk/client-cloudformation';
import type { ListStacksCommandOutput } from '@aws-sdk/client-cloudformation';
import { DescribeLogGroupsCommand } from '@aws-sdk/client-cloudwatch-logs';
import type { DescribeLogGroupsCommandInput, DescribeLogGroupsCommandOutput } from '@aws-sdk/client-cloudwatch-logs';
import { DescribeTasksCommand, ListClustersCommand, ListTasksCommand } from '@aws-sdk/client-ecs';
import type { DescribeTasksCommandOutput } from '@aws-sdk/client-ecs';
import { ListObjectsCommand } from '@aws-sdk/client-s3';
import Input from '../../../../input';
import CloudRunnerLogger from '../../../services/core/cloud-runner-logger';
@ -14,6 +17,10 @@ import CloudRunner from '../../../cloud-runner';
import { AwsClientFactory } from '../aws-client-factory';
import SharedWorkspaceLocking from '../../../services/core/shared-workspace-locking';
type StackSummary = NonNullable<ListStacksCommandOutput['StackSummaries']>[number];
type LogGroup = NonNullable<DescribeLogGroupsCommandOutput['logGroups']>[number];
type Task = NonNullable<DescribeTasksCommandOutput['tasks']>[number];
export class TaskService {
static async watch() {
// eslint-disable-next-line no-unused-vars
@ -26,7 +33,7 @@ export class TaskService {
return output;
}
public static async getCloudFormationJobStacks() {
const result: any[] = [];
const result: StackSummary[] = [];
CloudRunnerLogger.log(``);
CloudRunnerLogger.log(`List Cloud Formation Stacks`);
process.env.AWS_REGION = Input.region;
@ -78,7 +85,12 @@ export class TaskService {
return result;
}
public static async getTasks() {
const result: { taskElement: any; element: string }[] = [];
// Extended Task type to include custom properties added in this method
type ExtendedTask = Task & {
overrides?: Record<string, unknown>;
attachments?: unknown[];
};
const result: { taskElement: ExtendedTask; element: string }[] = [];
CloudRunnerLogger.log(``);
CloudRunnerLogger.log(`List Tasks`);
process.env.AWS_REGION = Input.region;
@ -102,13 +114,14 @@ export class TaskService {
if (taskElement === undefined) {
continue;
}
taskElement.overrides = {};
taskElement.attachments = [];
if (taskElement.createdAt === undefined) {
CloudRunnerLogger.log(`Skipping ${taskElement.taskDefinitionArn} no createdAt date`);
const extendedTask = taskElement as ExtendedTask;
extendedTask.overrides = {};
extendedTask.attachments = [];
if (extendedTask.createdAt === undefined) {
CloudRunnerLogger.log(`Skipping ${extendedTask.taskDefinitionArn} no createdAt date`);
continue;
}
result.push({ taskElement, element });
result.push({ taskElement: extendedTask, element });
}
}
}
@ -149,10 +162,10 @@ export class TaskService {
}
}
public static async getLogGroups() {
const result: any[] = [];
const result: LogGroup[] = [];
process.env.AWS_REGION = Input.region;
const ecs = AwsClientFactory.getCloudWatchLogs();
let logStreamInput: any = {
let logStreamInput: DescribeLogGroupsCommandInput = {
/* logGroupNamePrefix: 'game-ci' */
};
let logGroupsDescribe = await ecs.send(new DescribeLogGroupsCommand(logStreamInput));
@ -185,6 +198,7 @@ export class TaskService {
process.env.AWS_REGION = Input.region;
if (CloudRunner.buildParameters.storageProvider === 'rclone') {
const objects = await (SharedWorkspaceLocking as any).listObjects('');
return objects.map((x: string) => ({ Key: x }));
}
const s3 = AwsClientFactory.getS3();

View File

@ -68,13 +68,38 @@ class LocalCloudRunner implements ProviderInterface {
// On Windows, many built-in hooks use POSIX shell syntax. Execute via bash if available.
if (process.platform === 'win32') {
const inline = commands
.replace(/"/g, '\\"')
// Properly escape the command string for embedding in a double-quoted bash string.
// Order matters: backslashes must be escaped first to avoid double-escaping.
const escapeForBashDoubleQuotes = (stringValue: string): string => {
return stringValue
.replace(/\\/g, '\\\\') // Escape backslashes first
.replace(/\$/g, '\\$') // Escape dollar signs to prevent variable expansion
.replace(/`/g, '\\`') // Escape backticks to prevent command substitution
.replace(/"/g, '\\"'); // Escape double quotes
};
// Split commands by newlines and escape each line
const lines = commands
.replace(/\r/g, '')
.split('\n')
.filter((x) => x.trim().length > 0)
.join(' ; ');
.map((line) => escapeForBashDoubleQuotes(line));
// Join with semicolons, but don't add semicolon after control flow keywords
// Control flow keywords that shouldn't be followed by semicolons: then, else, do, fi, done, esac
const controlFlowKeywords = /\b(then|else|do|fi|done|esac)\s*$/;
const inline = lines
.map((line, index) => {
// Don't add semicolon if this line ends with a control flow keyword
if (controlFlowKeywords.test(line.trim()) || index === lines.length - 1) {
return line;
}
return `${line} ;`;
})
.join(' ');
const bashWrapped = `bash -lc "${inline}"`;
return await CloudRunnerSystem.Run(bashWrapped);
}