Rename Cloud Runner to Orchestrator (#775)

* Rename "Cloud Runner" to "Orchestrator" across entire codebase

Breaking change: All CloudRunner classes, options, environment variables,
and action.yml inputs have been renamed to Orchestrator equivalents.

- Renamed src/model/cloud-runner/ directory to src/model/orchestrator/
- Renamed all cloud-runner-* files to orchestrator-*
- Renamed all CloudRunner* classes to Orchestrator* (15+ classes)
- Renamed all cloudRunner* properties to orchestrator* equivalents
- Renamed CLOUD_RUNNER_* env vars to ORCHESTRATOR_*
- Updated action.yml [CloudRunner] markers to [Orchestrator]
- Updated workflow files and package.json test scripts
- Updated all runtime strings (cache paths, log messages, branch refs)
- Rebuilt dist/index.js

No backward compatibility layer is provided.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove tracked log/temp files and add to .gitignore

Remove $LOG_FILE and temp/job-log.txt debug artifacts that should
not be in the repository.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Frostebite
2026-03-04 21:53:47 +00:00
committed by GitHub
parent f3849ee1c9
commit 9d475434d3
117 changed files with 4756 additions and 4745 deletions
@@ -0,0 +1,57 @@
import GitHub from '../../../github';
import Orchestrator from '../../orchestrator';
import { OrchestratorStatics } from '../../options/orchestrator-statics';
import OrchestratorLogger from './orchestrator-logger';
import * as core from '@actions/core';
export class FollowLogStreamService {
static Reset() {
FollowLogStreamService.DidReceiveEndOfTransmission = false;
}
static errors = ``;
public static DidReceiveEndOfTransmission = false;
public static handleIteration(message: string, shouldReadLogs: boolean, shouldCleanup: boolean, output: string) {
if (message.includes(`---${Orchestrator.buildParameters.logId}`)) {
OrchestratorLogger.log('End of log transmission received');
FollowLogStreamService.DidReceiveEndOfTransmission = true;
shouldReadLogs = false;
} else if (message.includes('Rebuilding Library because the asset database could not be found!')) {
GitHub.updateGitHubCheck(`Library was not found, importing new Library`, ``);
core.warning('LIBRARY NOT FOUND!');
core.setOutput('library-found', 'false');
} else if (message.includes('Build succeeded')) {
GitHub.updateGitHubCheck(`Build succeeded`, `Build succeeded`);
core.setOutput('build-result', 'success');
} else if (message.includes('Build fail')) {
GitHub.updateGitHubCheck(
`Build failed\n${FollowLogStreamService.errors}`,
`Build failed`,
`failure`,
`completed`,
);
core.setOutput('build-result', 'failed');
core.setFailed('unity build failed');
core.error('BUILD FAILED!');
} else if (message.toLowerCase().includes('error ')) {
core.error(message);
FollowLogStreamService.errors += `\n${message}`;
} else if (message.toLowerCase().includes('error: ')) {
core.error(message);
FollowLogStreamService.errors += `\n${message}`;
} else if (message.toLowerCase().includes('command failed: ')) {
FollowLogStreamService.errors += `\n${message}`;
} else if (message.toLowerCase().includes('invalid ')) {
FollowLogStreamService.errors += `\n${message}`;
} else if (message.toLowerCase().includes('incompatible ')) {
FollowLogStreamService.errors += `\n${message}`;
} else if (message.toLowerCase().includes('cannot be found')) {
FollowLogStreamService.errors += `\n${message}`;
}
// Always append log lines to output so tests can assert on BuildResults
output += `${message}\n`;
OrchestratorLogger.log(`[${OrchestratorStatics.logPrefix}] ${message}`);
return { shouldReadLogs, shouldCleanup, output };
}
}
@@ -0,0 +1,47 @@
import * as core from '@actions/core';
class OrchestratorLogger {
private static timestamp: number;
private static globalTimestamp: number;
public static setup() {
this.timestamp = this.createTimestamp();
this.globalTimestamp = this.timestamp;
}
public static log(message: string) {
core.info(message);
}
public static logWarning(message: string) {
core.warning(message);
}
public static logLine(message: string) {
core.info(`${message}\n`);
}
public static error(message: string) {
core.error(message);
}
public static logWithTime(message: string) {
const newTimestamp = this.createTimestamp();
core.info(
`${message} (Since previous: ${this.calculateTimeDiff(
newTimestamp,
this.timestamp,
)}, Total time: ${this.calculateTimeDiff(newTimestamp, this.globalTimestamp)})`,
);
this.timestamp = newTimestamp;
}
private static calculateTimeDiff(x: number, y: number) {
return Math.floor((x - y) / 1000);
}
private static createTimestamp() {
return Date.now();
}
}
export default OrchestratorLogger;
@@ -0,0 +1,24 @@
import BuildParameters from '../../../build-parameters';
class OrchestratorResult {
public BuildParameters: BuildParameters;
public BuildResults: string;
public BuildSucceeded: boolean;
public BuildFinished: boolean;
public LibraryCacheUsed: boolean;
public constructor(
buildParameters: BuildParameters,
buildResults: string,
buildSucceeded: boolean,
buildFinished: boolean,
libraryCacheUsed: boolean,
) {
this.BuildParameters = buildParameters;
this.BuildResults = buildResults;
this.BuildSucceeded = buildSucceeded;
this.BuildFinished = buildFinished;
this.LibraryCacheUsed = libraryCacheUsed;
}
}
export default OrchestratorResult;
@@ -0,0 +1,69 @@
import { exec } from 'child_process';
import { RemoteClientLogger } from '../../remote-client/remote-client-logger';
export class OrchestratorSystem {
public static async RunAndReadLines(command: string): Promise<string[]> {
const result = await OrchestratorSystem.Run(command, false, true);
return result
.split(`\n`)
.map((x) => x.replace(`\r`, ``))
.filter((x) => x !== ``)
.map((x) => {
const lineValues = x.split(` `);
return lineValues[lineValues.length - 1];
});
}
public static async Run(
command: string,
suppressError = false,
suppressLogs = false,
// eslint-disable-next-line no-unused-vars
outputCallback?: (output: string) => void,
) {
for (const element of command.split(`\n`)) {
if (!suppressLogs) {
RemoteClientLogger.log(element);
}
}
return await new Promise<string>((promise, throwError) => {
let output = '';
const child = exec(command, { maxBuffer: 1024 * 10000 }, (error, stdout, stderr) => {
if (!suppressError && error) {
RemoteClientLogger.log(error.toString());
throwError(error);
}
if (stderr) {
const diagnosticOutput = `${stderr.toString()}`;
if (!suppressLogs) {
RemoteClientLogger.logCliDiagnostic(diagnosticOutput);
}
output += diagnosticOutput;
}
const outputChunk = `${stdout}`;
if (outputCallback) {
outputCallback(outputChunk);
}
output += outputChunk;
});
child.on('close', (code) => {
if (!suppressLogs) {
RemoteClientLogger.log(`[${code}]`);
}
if (code !== 0 && !suppressError) {
throwError(output);
}
const outputLines = output.split(`\n`);
for (const element of outputLines) {
if (!suppressLogs) {
RemoteClientLogger.log(element);
}
}
promise(output);
});
});
}
}
@@ -0,0 +1,84 @@
import OrchestratorLogger from './orchestrator-logger';
import OrchestratorOptions from '../../options/orchestrator-options';
import Orchestrator from '../../orchestrator';
import { OrchestratorSystem } from './orchestrator-system';
class ResourceTracking {
static isEnabled(): boolean {
return (
OrchestratorOptions.resourceTracking ||
OrchestratorOptions.orchestratorDebug ||
process.env['orchestratorTests'] === 'true'
);
}
static logAllocationSummary(context: string) {
if (!ResourceTracking.isEnabled()) {
return;
}
const buildParameters = Orchestrator.buildParameters;
const allocations = {
providerStrategy: buildParameters.providerStrategy,
containerCpu: buildParameters.containerCpu,
containerMemory: buildParameters.containerMemory,
dockerCpuLimit: buildParameters.dockerCpuLimit,
dockerMemoryLimit: buildParameters.dockerMemoryLimit,
kubeVolumeSize: buildParameters.kubeVolumeSize,
kubeStorageClass: buildParameters.kubeStorageClass,
kubeVolume: buildParameters.kubeVolume,
containerNamespace: buildParameters.containerNamespace,
storageProvider: buildParameters.storageProvider,
rcloneRemote: buildParameters.rcloneRemote,
dockerWorkspacePath: buildParameters.dockerWorkspacePath,
cacheKey: buildParameters.cacheKey,
maxRetainedWorkspaces: buildParameters.maxRetainedWorkspaces,
useCompressionStrategy: buildParameters.useCompressionStrategy,
useLargePackages: buildParameters.useLargePackages,
ephemeralStorageRequest: process.env['orchestratorTests'] === 'true' ? 'not set' : '2Gi',
};
OrchestratorLogger.log(`[ResourceTracking] Allocation summary (${context}):`);
OrchestratorLogger.log(JSON.stringify(allocations, undefined, 2));
}
static async logDiskUsageSnapshot(context: string) {
if (!ResourceTracking.isEnabled()) {
return;
}
OrchestratorLogger.log(`[ResourceTracking] Disk usage snapshot (${context})`);
await ResourceTracking.runAndLog('df -h', 'df -h');
await ResourceTracking.runAndLog('du -sh .', 'du -sh .');
await ResourceTracking.runAndLog('du -sh ./orchestrator-cache', 'du -sh ./orchestrator-cache');
await ResourceTracking.runAndLog('du -sh ./temp', 'du -sh ./temp');
await ResourceTracking.runAndLog('du -sh ./logs', 'du -sh ./logs');
}
static async logK3dNodeDiskUsage(context: string) {
if (!ResourceTracking.isEnabled()) {
return;
}
const nodes = ['k3d-unity-builder-agent-0', 'k3d-unity-builder-server-0'];
OrchestratorLogger.log(`[ResourceTracking] K3d node disk usage (${context})`);
for (const node of nodes) {
await ResourceTracking.runAndLog(
`k3d node ${node}`,
`docker exec ${node} sh -c "df -h /var/lib/rancher/k3s 2>/dev/null || df -h / 2>/dev/null || true" || true`,
);
}
}
private static async runAndLog(label: string, command: string) {
try {
const output = await OrchestratorSystem.Run(command, true, true);
const trimmed = output.trim();
OrchestratorLogger.log(`[ResourceTracking] ${label}:\n${trimmed || 'no output'}`);
} catch (error: any) {
OrchestratorLogger.log(`[ResourceTracking] ${label} failed: ${error?.message || error}`);
}
}
}
export default ResourceTracking;
@@ -0,0 +1,402 @@
import OrchestratorLogger from './orchestrator-logger';
import BuildParameters from '../../../build-parameters';
import Orchestrator from '../../orchestrator';
import Input from '../../../input';
import {
CreateBucketCommand,
DeleteObjectCommand,
HeadBucketCommand,
ListObjectsV2Command,
PutObjectCommand,
S3,
} from '@aws-sdk/client-s3';
import { AwsClientFactory } from '../../providers/aws/aws-client-factory';
import { promisify } from 'node:util';
import { exec as execCallback } from 'node:child_process';
const exec = promisify(execCallback);
export class SharedWorkspaceLocking {
private static _s3: S3;
private static get s3(): S3 {
if (!SharedWorkspaceLocking._s3) {
// Use factory so LocalStack endpoint/path-style settings are honored
SharedWorkspaceLocking._s3 = AwsClientFactory.getS3();
}
return SharedWorkspaceLocking._s3;
}
private static get useRclone() {
return Orchestrator.buildParameters.storageProvider === 'rclone';
}
private static async rclone(command: string): Promise<string> {
const { stdout } = await exec(`rclone ${command}`);
return stdout.toString();
}
private static get bucket() {
return SharedWorkspaceLocking.useRclone
? Orchestrator.buildParameters.rcloneRemote
: Orchestrator.buildParameters.awsStackName;
}
public static get workspaceBucketRoot() {
return SharedWorkspaceLocking.useRclone
? `${SharedWorkspaceLocking.bucket}/`
: `s3://${SharedWorkspaceLocking.bucket}/`;
}
public static get workspaceRoot() {
return `${SharedWorkspaceLocking.workspaceBucketRoot}locks/`;
}
private static get workspacePrefix() {
return `locks/`;
}
private static async ensureBucketExists(): Promise<void> {
const bucket = SharedWorkspaceLocking.bucket;
if (SharedWorkspaceLocking.useRclone) {
try {
await SharedWorkspaceLocking.rclone(`lsf ${bucket}`);
} catch {
await SharedWorkspaceLocking.rclone(`mkdir ${bucket}`);
}
return;
}
try {
await SharedWorkspaceLocking.s3.send(new HeadBucketCommand({ Bucket: bucket }));
} catch {
const region = Input.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1';
const createParameters: any = { Bucket: bucket };
if (region && region !== 'us-east-1') {
createParameters.CreateBucketConfiguration = { LocationConstraint: region };
}
await SharedWorkspaceLocking.s3.send(new CreateBucketCommand(createParameters));
}
}
private static async listObjects(prefix: string, bucket = SharedWorkspaceLocking.bucket): Promise<string[]> {
await SharedWorkspaceLocking.ensureBucketExists();
if (prefix !== '' && !prefix.endsWith('/')) {
prefix += '/';
}
if (SharedWorkspaceLocking.useRclone) {
const path = `${bucket}/${prefix}`;
try {
const output = await SharedWorkspaceLocking.rclone(`lsjson ${path}`);
const json = JSON.parse(output) as { Name: string; IsDir: boolean }[];
return json.map((entry) => (entry.IsDir ? `${entry.Name}/` : entry.Name));
} catch {
return [];
}
}
const result = await SharedWorkspaceLocking.s3.send(
new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix, Delimiter: '/' }),
);
const entries: string[] = [];
for (const p of result.CommonPrefixes || []) {
if (p.Prefix) entries.push(p.Prefix.slice(prefix.length));
}
for (const c of result.Contents || []) {
if (c.Key && c.Key !== prefix) entries.push(c.Key.slice(prefix.length));
}
return entries;
}
public static async GetAllWorkspaces(buildParametersContext: BuildParameters): Promise<string[]> {
if (!(await SharedWorkspaceLocking.DoesCacheKeyTopLevelExist(buildParametersContext))) {
return [];
}
return (
await SharedWorkspaceLocking.listObjects(
`${SharedWorkspaceLocking.workspacePrefix}${buildParametersContext.cacheKey}/`,
)
)
.map((x) => x.replace(`/`, ``))
.filter((x) => x.endsWith(`_workspace`))
.map((x) => x.split(`_`)[1]);
}
public static async DoesCacheKeyTopLevelExist(buildParametersContext: BuildParameters) {
try {
const rootLines = await SharedWorkspaceLocking.listObjects('');
const lockFolderExists = rootLines.map((x) => x.replace(`/`, ``)).includes(`locks`);
if (lockFolderExists) {
const lines = await SharedWorkspaceLocking.listObjects(SharedWorkspaceLocking.workspacePrefix);
return lines.map((x) => x.replace(`/`, ``)).includes(buildParametersContext.cacheKey);
} else {
return false;
}
} catch {
return false;
}
}
public static NewWorkspaceName() {
return `${Orchestrator.retainedWorkspacePrefix}-${Orchestrator.buildParameters.buildGuid}`;
}
public static async GetAllLocksForWorkspace(
workspace: string,
buildParametersContext: BuildParameters,
): Promise<string[]> {
if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) {
return [];
}
return (
await SharedWorkspaceLocking.listObjects(
`${SharedWorkspaceLocking.workspacePrefix}${buildParametersContext.cacheKey}/`,
)
)
.map((x) => x.replace(`/`, ``))
.filter((x) => x.includes(workspace) && x.endsWith(`_lock`));
}
public static async GetLockedWorkspace(workspace: string, runId: string, buildParametersContext: BuildParameters) {
if (buildParametersContext.maxRetainedWorkspaces === 0) {
return false;
}
if (await SharedWorkspaceLocking.DoesCacheKeyTopLevelExist(buildParametersContext)) {
const workspaces = await SharedWorkspaceLocking.GetFreeWorkspaces(buildParametersContext);
OrchestratorLogger.log(`run agent ${runId} is trying to access a workspace, free: ${JSON.stringify(workspaces)}`);
for (const element of workspaces) {
const lockResult = await SharedWorkspaceLocking.LockWorkspace(element, runId, buildParametersContext);
OrchestratorLogger.log(
`run agent: ${runId} try lock workspace: ${element} locking attempt result: ${lockResult}`,
);
if (lockResult) {
return true;
}
}
}
if (await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext)) {
workspace = SharedWorkspaceLocking.NewWorkspaceName();
Orchestrator.lockedWorkspace = workspace;
}
const createResult = await SharedWorkspaceLocking.CreateWorkspace(workspace, buildParametersContext);
const lockResult = await SharedWorkspaceLocking.LockWorkspace(workspace, runId, buildParametersContext);
OrchestratorLogger.log(
`run agent ${runId} didn't find a free workspace so created: ${workspace} createWorkspaceSuccess: ${createResult} Lock:${lockResult}`,
);
return createResult && lockResult;
}
public static async DoesWorkspaceExist(workspace: string, buildParametersContext: BuildParameters) {
return (
(await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext)).filter((x) => x.includes(workspace))
.length > 0
);
}
public static async HasWorkspaceLock(
workspace: string,
runId: string,
buildParametersContext: BuildParameters,
): Promise<boolean> {
const locks = (await SharedWorkspaceLocking.GetAllLocksForWorkspace(workspace, buildParametersContext))
.map((x) => {
return {
name: x,
timestamp: Number(x.split(`_`)[0]),
};
})
.sort((x) => x.timestamp);
const lockMatches = locks.filter((x) => x.name.includes(runId));
const includesRunLock = lockMatches.length > 0 && locks.indexOf(lockMatches[0]) === 0;
OrchestratorLogger.log(
`Checking has workspace lock, runId: ${runId}, workspace: ${workspace}, success: ${includesRunLock} \n- Num of locks created by Run Agent: ${
lockMatches.length
} Num of Locks: ${locks.length}, Time ordered index for Run Agent: ${locks.indexOf(lockMatches[0])} \n \n`,
);
return includesRunLock;
}
public static async GetFreeWorkspaces(buildParametersContext: BuildParameters): Promise<string[]> {
const result: string[] = [];
const workspaces = await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext);
for (const element of workspaces) {
const isLocked = await SharedWorkspaceLocking.IsWorkspaceLocked(element, buildParametersContext);
const isBelowMax = await SharedWorkspaceLocking.IsWorkspaceBelowMax(element, buildParametersContext);
OrchestratorLogger.log(`workspace ${element} locked:${isLocked} below max:${isBelowMax}`);
if (!isLocked && isBelowMax) {
result.push(element);
}
}
return result;
}
public static async IsWorkspaceBelowMax(
workspace: string,
buildParametersContext: BuildParameters,
): Promise<boolean> {
const workspaces = await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext);
if (workspace === ``) {
return (
workspaces.length < buildParametersContext.maxRetainedWorkspaces ||
buildParametersContext.maxRetainedWorkspaces === 0
);
}
const ordered: any[] = [];
for (const ws of workspaces) {
ordered.push({
name: ws,
timestamp: await SharedWorkspaceLocking.GetWorkspaceTimestamp(ws, buildParametersContext),
});
}
ordered.sort((x) => x.timestamp);
const matches = ordered.filter((x) => x.name.includes(workspace));
const isWorkspaceBelowMax =
matches.length > 0 &&
(ordered.indexOf(matches[0]) < buildParametersContext.maxRetainedWorkspaces ||
buildParametersContext.maxRetainedWorkspaces === 0);
return isWorkspaceBelowMax;
}
public static async GetWorkspaceTimestamp(
workspace: string,
buildParametersContext: BuildParameters,
): Promise<Number> {
if (workspace.split(`_`).length > 0) {
return Number(workspace.split(`_`)[1]);
}
if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) {
throw new Error("Workspace doesn't exist, can't call get all locks");
}
return (
await SharedWorkspaceLocking.listObjects(
`${SharedWorkspaceLocking.workspacePrefix}${buildParametersContext.cacheKey}/`,
)
)
.map((x) => x.replace(`/`, ``))
.filter((x) => x.includes(workspace) && x.endsWith(`_workspace`))
.map((x) => Number(x))[0];
}
public static async IsWorkspaceLocked(workspace: string, buildParametersContext: BuildParameters): Promise<boolean> {
if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) {
throw new Error(`workspace doesn't exist ${workspace}`);
}
const files = await SharedWorkspaceLocking.listObjects(
`${SharedWorkspaceLocking.workspacePrefix}${buildParametersContext.cacheKey}/`,
);
const lockFilesExist =
files.filter((x) => {
return x.includes(workspace) && x.endsWith(`_lock`);
}).length > 0;
return lockFilesExist;
}
public static async CreateWorkspace(workspace: string, buildParametersContext: BuildParameters): Promise<boolean> {
if (await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext)) {
throw new Error(`${workspace} already exists`);
}
const timestamp = Date.now();
const key = `${SharedWorkspaceLocking.workspacePrefix}${buildParametersContext.cacheKey}/${timestamp}_${workspace}_workspace`;
await SharedWorkspaceLocking.ensureBucketExists();
await (SharedWorkspaceLocking.useRclone
? SharedWorkspaceLocking.rclone(`touch ${SharedWorkspaceLocking.bucket}/${key}`)
: SharedWorkspaceLocking.s3.send(
new PutObjectCommand({ Bucket: SharedWorkspaceLocking.bucket, Key: key, Body: new Uint8Array(0) }),
));
const workspaces = await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext);
OrchestratorLogger.log(`All workspaces ${workspaces}`);
if (!(await SharedWorkspaceLocking.IsWorkspaceBelowMax(workspace, buildParametersContext))) {
OrchestratorLogger.log(`Workspace is above max ${workspaces} ${buildParametersContext.maxRetainedWorkspaces}`);
await SharedWorkspaceLocking.CleanupWorkspace(workspace, buildParametersContext);
return false;
}
return true;
}
public static async LockWorkspace(
workspace: string,
runId: string,
buildParametersContext: BuildParameters,
): Promise<boolean> {
const existingWorkspace = workspace.endsWith(`_workspace`);
const ending = existingWorkspace ? workspace : `${workspace}_workspace`;
const key = `${SharedWorkspaceLocking.workspacePrefix}${
buildParametersContext.cacheKey
}/${Date.now()}_${runId}_${ending}_lock`;
await SharedWorkspaceLocking.ensureBucketExists();
await (SharedWorkspaceLocking.useRclone
? SharedWorkspaceLocking.rclone(`touch ${SharedWorkspaceLocking.bucket}/${key}`)
: SharedWorkspaceLocking.s3.send(
new PutObjectCommand({ Bucket: SharedWorkspaceLocking.bucket, Key: key, Body: new Uint8Array(0) }),
));
const hasLock = await SharedWorkspaceLocking.HasWorkspaceLock(workspace, runId, buildParametersContext);
if (hasLock) {
Orchestrator.lockedWorkspace = workspace;
} else {
await (SharedWorkspaceLocking.useRclone
? SharedWorkspaceLocking.rclone(`delete ${SharedWorkspaceLocking.bucket}/${key}`)
: SharedWorkspaceLocking.s3.send(new DeleteObjectCommand({ Bucket: SharedWorkspaceLocking.bucket, Key: key })));
}
return hasLock;
}
public static async ReleaseWorkspace(
workspace: string,
runId: string,
buildParametersContext: BuildParameters,
): Promise<boolean> {
await SharedWorkspaceLocking.ensureBucketExists();
const files = await SharedWorkspaceLocking.GetAllLocksForWorkspace(workspace, buildParametersContext);
const file = files.find((x) => x.includes(workspace) && x.endsWith(`_lock`) && x.includes(runId));
OrchestratorLogger.log(`All Locks ${files} ${workspace} ${runId}`);
OrchestratorLogger.log(`Deleting lock ${workspace}/${file}`);
OrchestratorLogger.log(`rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${file}`);
if (file) {
await (SharedWorkspaceLocking.useRclone
? SharedWorkspaceLocking.rclone(
`delete ${SharedWorkspaceLocking.bucket}/${SharedWorkspaceLocking.workspacePrefix}${buildParametersContext.cacheKey}/${file}`,
)
: SharedWorkspaceLocking.s3.send(
new DeleteObjectCommand({
Bucket: SharedWorkspaceLocking.bucket,
Key: `${SharedWorkspaceLocking.workspacePrefix}${buildParametersContext.cacheKey}/${file}`,
}),
));
}
return !(await SharedWorkspaceLocking.HasWorkspaceLock(workspace, runId, buildParametersContext));
}
public static async CleanupWorkspace(workspace: string, buildParametersContext: BuildParameters) {
const prefix = `${SharedWorkspaceLocking.workspacePrefix}${buildParametersContext.cacheKey}/`;
const files = await SharedWorkspaceLocking.listObjects(prefix);
for (const file of files.filter((x) => x.includes(`_${workspace}_`))) {
await (SharedWorkspaceLocking.useRclone
? SharedWorkspaceLocking.rclone(`delete ${SharedWorkspaceLocking.bucket}/${prefix}${file}`)
: SharedWorkspaceLocking.s3.send(
new DeleteObjectCommand({ Bucket: SharedWorkspaceLocking.bucket, Key: `${prefix}${file}` }),
));
}
}
public static async ReadLines(command: string): Promise<string[]> {
const path = command.replace('aws s3 ls', '').replace('rclone lsf', '').trim();
const withoutScheme = path.replace('s3://', '');
const [bucket, ...rest] = withoutScheme.split('/');
const prefix = rest.join('/');
return SharedWorkspaceLocking.listObjects(prefix, bucket);
}
}
export default SharedWorkspaceLocking;
@@ -0,0 +1,200 @@
import BuildParameters from '../../../build-parameters';
import Input from '../../../input';
import OrchestratorOptions from '../../options/orchestrator-options';
import OrchestratorEnvironmentVariable from '../../options/orchestrator-environment-variable';
import OrchestratorOptionsReader from '../../options/orchestrator-options-reader';
import OrchestratorQueryOverride from '../../options/orchestrator-query-override';
import OrchestratorSecret from '../../options/orchestrator-secret';
import { CommandHookService } from '../hooks/command-hook-service';
export class TaskParameterSerializer {
static readonly blockedParameterNames: Set<string> = new Set([
'0',
'length',
'prototype',
'',
'unityVersion',
'CACHE_UNITY_INSTALLATION_ON_MAC',
'RUNNER_TEMP_PATH',
'NAME',
'CUSTOM_JOB',
]);
public static createOrchestratorEnvironmentVariables(
buildParameters: BuildParameters,
): OrchestratorEnvironmentVariable[] {
const result: OrchestratorEnvironmentVariable[] = this.uniqBy(
[
...[
{ name: 'BUILD_TARGET', value: buildParameters.targetPlatform },
{ name: 'UNITY_VERSION', value: buildParameters.editorVersion },
{ name: 'GITHUB_TOKEN', value: process.env.GITHUB_TOKEN },
],
...TaskParameterSerializer.serializeFromObject(buildParameters),
...TaskParameterSerializer.serializeInput(),
...TaskParameterSerializer.serializeOrchestratorOptions(),
...CommandHookService.getSecrets(CommandHookService.getHooks(buildParameters.commandHooks)),
// Include AWS environment variables for LocalStack compatibility
...TaskParameterSerializer.serializeAwsEnvironmentVariables(),
]
.filter(
(x) =>
!TaskParameterSerializer.blockedParameterNames.has(x.name) &&
x.value !== '' &&
x.value !== undefined &&
x.value !== `undefined`,
)
.map((x) => {
x.name = `${TaskParameterSerializer.ToEnvVarFormat(x.name)}`;
x.value = `${x.value}`;
return x;
}),
(item: OrchestratorEnvironmentVariable) => item.name,
);
return result;
}
// eslint-disable-next-line no-unused-vars
static uniqBy(a: OrchestratorEnvironmentVariable[], key: (parameters: OrchestratorEnvironmentVariable) => string) {
const seen: { [key: string]: boolean } = {};
return a.filter(function (item) {
const k = key(item);
return seen.hasOwnProperty(k) ? false : (seen[k] = true);
});
}
public static readBuildParameterFromEnvironment(): BuildParameters {
const buildParameters = new BuildParameters();
const keys = [
...new Set(
Object.getOwnPropertyNames(process.env)
.filter((x) => !this.blockedParameterNames.has(x) && x.startsWith(''))
.map((x) => TaskParameterSerializer.UndoEnvVarFormat(x)),
),
];
for (const element of keys) {
if (element !== `customJob`) {
buildParameters[element] = process.env[`${TaskParameterSerializer.ToEnvVarFormat(element)}`];
}
}
return buildParameters;
}
private static serializeInput() {
return TaskParameterSerializer.serializeFromType(Input);
}
private static serializeOrchestratorOptions() {
return TaskParameterSerializer.serializeFromType(OrchestratorOptions);
}
private static serializeAwsEnvironmentVariables() {
const awsEnvironmentVariables = [
'AWS_ACCESS_KEY_ID',
'AWS_SECRET_ACCESS_KEY',
'AWS_DEFAULT_REGION',
'AWS_REGION',
'AWS_S3_ENDPOINT',
'AWS_ENDPOINT',
'AWS_CLOUD_FORMATION_ENDPOINT',
'AWS_ECS_ENDPOINT',
'AWS_KINESIS_ENDPOINT',
'AWS_CLOUD_WATCH_LOGS_ENDPOINT',
];
return awsEnvironmentVariables
.filter((key) => process.env[key] !== undefined)
.map((key) => ({
name: key,
value: process.env[key] || '',
}));
}
public static ToEnvVarFormat(input: string): string {
return OrchestratorOptions.ToEnvVarFormat(input);
}
public static UndoEnvVarFormat(element: string): string {
return this.camelize(element.toLowerCase().replace(/_+/g, ' '));
}
private static camelize(string: string) {
return TaskParameterSerializer.uncapitalizeFirstLetter(
string
.replace(/(^\w)|([A-Z])|(\b\w)/g, function (word: string, index: number) {
return index === 0 ? word.toLowerCase() : word.toUpperCase();
})
.replace(/\s+/g, ''),
);
}
private static uncapitalizeFirstLetter(string: string) {
return string.charAt(0).toLowerCase() + string.slice(1);
}
private static serializeFromObject(buildParameters: any) {
const array: any[] = [];
const keys = Object.getOwnPropertyNames(buildParameters).filter((x) => !this.blockedParameterNames.has(x));
for (const element of keys) {
array.push({
name: TaskParameterSerializer.ToEnvVarFormat(element),
value: buildParameters[element],
});
}
return array;
}
private static serializeFromType(type: any) {
const array: any[] = [];
const input = OrchestratorOptionsReader.GetProperties();
for (const element of input) {
if (typeof type[element] !== 'function' && array.filter((x) => x.name === element).length === 0) {
array.push({
name: element,
value: `${type[element]}`,
});
}
}
return array;
}
public static readDefaultSecrets(): OrchestratorSecret[] {
let array = new Array();
array = TaskParameterSerializer.tryAddInput(array, 'UNITY_SERIAL');
array = TaskParameterSerializer.tryAddInput(array, 'UNITY_EMAIL');
array = TaskParameterSerializer.tryAddInput(array, 'UNITY_PASSWORD');
// array = TaskParameterSerializer.tryAddInput(array, 'UNITY_LICENSE');
array = TaskParameterSerializer.tryAddInput(array, 'GIT_PRIVATE_TOKEN');
return array;
}
private static getValue(key: string) {
return OrchestratorQueryOverride.queryOverrides !== undefined &&
OrchestratorQueryOverride.queryOverrides[key] !== undefined
? OrchestratorQueryOverride.queryOverrides[key]
: process.env[key];
}
private static tryAddInput(array: OrchestratorSecret[], key: string): OrchestratorSecret[] {
const value = TaskParameterSerializer.getValue(key);
if (value !== undefined && value !== '' && value !== 'null') {
array.push({
ParameterKey: key,
EnvironmentVariable: key,
ParameterValue: value,
});
}
return array;
}
}
@@ -0,0 +1,118 @@
import { BuildParameters, Input } from '../../..';
import YAML from 'yaml';
import { RemoteClientLogger } from '../../remote-client/remote-client-logger';
import path from 'node:path';
import OrchestratorOptions from '../../options/orchestrator-options';
import * as fs from 'node:fs';
import OrchestratorLogger from '../core/orchestrator-logger';
import { CommandHook } from './command-hook';
// import OrchestratorLogger from './orchestrator-logger';
export class CommandHookService {
public static ApplyHooksToCommands(commands: string, buildParameters: BuildParameters): string {
const hooks = CommandHookService.getHooks(buildParameters.commandHooks);
OrchestratorLogger.log(`Applying hooks ${hooks.length}`);
return `echo "---"
echo "start orchestrator init"
${OrchestratorOptions.orchestratorDebug ? `printenv` : `#`}
echo "start of orchestrator job"
${hooks.filter((x) => x.hook.includes(`before`)).map((x) => x.commands) || ' '}
${commands}
${hooks.filter((x) => x.hook.includes(`after`)).map((x) => x.commands) || ' '}
echo "end of orchestrator job"
echo "---${buildParameters.logId}"`;
}
public static getHooks(customCommandHooks: string): CommandHook[] {
const experimentHooks = customCommandHooks;
let output = new Array<CommandHook>();
if (experimentHooks && experimentHooks !== '') {
try {
output = YAML.parse(experimentHooks);
} catch (error) {
throw error;
}
}
return [
...output.filter((x) => x.hook !== undefined && x.hook.length > 0),
...CommandHookService.GetCustomHooksFromFiles(`before`),
...CommandHookService.GetCustomHooksFromFiles(`after`),
];
}
static GetCustomHooksFromFiles(hookLifecycle: string): CommandHook[] {
const results: CommandHook[] = [];
// RemoteClientLogger.log(`GetCustomHookFiles: ${hookLifecycle}`);
try {
const gameCiCustomHooksPath = path.join(process.cwd(), `game-ci`, `command-hooks`);
const files = fs.readdirSync(gameCiCustomHooksPath);
for (const file of files) {
if (!OrchestratorOptions.commandHookFiles.includes(file.replace(`.yaml`, ``))) {
continue;
}
const fileContents = fs.readFileSync(path.join(gameCiCustomHooksPath, file), `utf8`);
const fileContentsObject = CommandHookService.ParseHooks(fileContents)[0];
if (fileContentsObject.hook.includes(hookLifecycle)) {
results.push(fileContentsObject);
}
}
} catch (error) {
RemoteClientLogger.log(`Failed Getting: ${hookLifecycle} \n ${JSON.stringify(error, undefined, 4)}`);
}
// RemoteClientLogger.log(`Active Steps From Hooks: \n ${JSON.stringify(results, undefined, 4)}`);
return results;
}
private static ConvertYamlSecrets(object: CommandHook) {
if (object.secrets === undefined) {
object.secrets = [];
return;
}
object.secrets = object.secrets.map((x: any) => {
return {
ParameterKey: x.name,
EnvironmentVariable: Input.ToEnvVarFormat(x.name),
ParameterValue: x.value,
};
});
}
public static ParseHooks(hooks: string): CommandHook[] {
if (hooks === '') {
return [];
}
// if (Orchestrator.buildParameters?.orchestratorIntegrationTests) {
// OrchestratorLogger.log(`Parsing build hooks: ${steps}`);
// }
const isArray = hooks.replace(/\s/g, ``)[0] === `-`;
const object: CommandHook[] = isArray ? YAML.parse(hooks) : [YAML.parse(hooks)];
for (const hook of object) {
CommandHookService.ConvertYamlSecrets(hook);
if (hook.secrets === undefined) {
hook.secrets = [];
}
}
if (object === undefined) {
throw new Error(`Failed to parse ${hooks}`);
}
return object;
}
public static getSecrets(hooks: any) {
const secrets = hooks.map((x: any) => x.secrets).filter((x: any) => x !== undefined && x.length > 0);
// eslint-disable-next-line unicorn/no-array-reduce
return secrets.length > 0 ? secrets.reduce((x: any, y: any) => [...x, ...y]) : [];
}
}
@@ -0,0 +1,9 @@
import OrchestratorSecret from '../../options/orchestrator-secret';
export class CommandHook {
public commands: string[] = new Array<string>();
public secrets: OrchestratorSecret[] = new Array<OrchestratorSecret>();
public name!: string;
public hook!: string[];
public step!: string[];
}
@@ -0,0 +1,420 @@
import YAML from 'yaml';
import Orchestrator from '../../orchestrator';
import { CustomWorkflow } from '../../workflows/custom-workflow';
import { RemoteClientLogger } from '../../remote-client/remote-client-logger';
import path from 'node:path';
import fs from 'node:fs';
import Input from '../../../input';
import OrchestratorOptions from '../../options/orchestrator-options';
import { ContainerHook as ContainerHook } from './container-hook';
import { OrchestratorStepParameters } from '../../options/orchestrator-step-parameters';
export class ContainerHookService {
static GetContainerHooksFromFiles(hookLifecycle: string): ContainerHook[] {
const results: ContainerHook[] = [];
try {
const gameCiCustomStepsPath = path.join(process.cwd(), `game-ci`, `container-hooks`);
const files = fs.readdirSync(gameCiCustomStepsPath);
for (const file of files) {
if (!OrchestratorOptions.containerHookFiles.includes(file.replace(`.yaml`, ``))) {
// RemoteClientLogger.log(`Skipping CustomStepFile: ${file}`);
continue;
}
const fileContents = fs.readFileSync(path.join(gameCiCustomStepsPath, file), `utf8`);
const fileContentsObject = ContainerHookService.ParseContainerHooks(fileContents)[0];
if (fileContentsObject.hook === hookLifecycle) {
results.push(fileContentsObject);
}
}
} catch (error) {
RemoteClientLogger.log(`Failed Getting: ${hookLifecycle} \n ${JSON.stringify(error, undefined, 4)}`);
}
// RemoteClientLogger.log(`Active Steps From Files: \n ${JSON.stringify(results, undefined, 4)}`);
const builtInContainerHooks: ContainerHook[] = ContainerHookService.ParseContainerHooks(
`- name: aws-s3-upload-build
image: amazon/aws-cli
hook: after
commands: |
if command -v aws > /dev/null 2>&1; then
if [ -n "$AWS_ACCESS_KEY_ID" ]; then
aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID" --profile default || true
fi
if [ -n "$AWS_SECRET_ACCESS_KEY" ]; then
aws configure set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY" --profile default || true
fi
if [ -n "$AWS_DEFAULT_REGION" ]; then
aws configure set region "$AWS_DEFAULT_REGION" --profile default || true
fi
ENDPOINT_ARGS=""
if [ -n "$AWS_S3_ENDPOINT" ]; then ENDPOINT_ARGS="--endpoint-url $AWS_S3_ENDPOINT"; fi
aws $ENDPOINT_ARGS s3 cp /data/cache/$CACHE_KEY/build/build-${Orchestrator.buildParameters.buildGuid}.tar${
Orchestrator.buildParameters.useCompressionStrategy ? '.lz4' : ''
} s3://${Orchestrator.buildParameters.awsStackName}/orchestrator-cache/$CACHE_KEY/build/build-$BUILD_GUID.tar${
Orchestrator.buildParameters.useCompressionStrategy ? '.lz4' : ''
} || true
rm /data/cache/$CACHE_KEY/build/build-${Orchestrator.buildParameters.buildGuid}.tar${
Orchestrator.buildParameters.useCompressionStrategy ? '.lz4' : ''
} || true
else
echo "AWS CLI not available, skipping aws-s3-upload-build"
fi
secrets:
- name: awsAccessKeyId
value: ${process.env.AWS_ACCESS_KEY_ID || ``}
- name: awsSecretAccessKey
value: ${process.env.AWS_SECRET_ACCESS_KEY || ``}
- name: awsDefaultRegion
value: ${process.env.AWS_REGION || ``}
- name: AWS_S3_ENDPOINT
value: ${OrchestratorOptions.awsS3Endpoint || process.env.AWS_S3_ENDPOINT || ``}
- name: aws-s3-pull-build
image: amazon/aws-cli
commands: |
mkdir -p /data/cache/$CACHE_KEY/build/
if command -v aws > /dev/null 2>&1; then
if [ -n "$AWS_ACCESS_KEY_ID" ]; then
aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID" --profile default || true
fi
if [ -n "$AWS_SECRET_ACCESS_KEY" ]; then
aws configure set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY" --profile default || true
fi
if [ -n "$AWS_DEFAULT_REGION" ]; then
aws configure set region "$AWS_DEFAULT_REGION" --profile default || true
fi
ENDPOINT_ARGS=""
if [ -n "$AWS_S3_ENDPOINT" ]; then ENDPOINT_ARGS="--endpoint-url $AWS_S3_ENDPOINT"; fi
aws $ENDPOINT_ARGS s3 ls ${Orchestrator.buildParameters.awsStackName}/orchestrator-cache/ || true
aws $ENDPOINT_ARGS s3 ls ${Orchestrator.buildParameters.awsStackName}/orchestrator-cache/$CACHE_KEY/build || true
aws s3 cp s3://${
Orchestrator.buildParameters.awsStackName
}/orchestrator-cache/$CACHE_KEY/build/build-$BUILD_GUID_TARGET.tar${
Orchestrator.buildParameters.useCompressionStrategy ? '.lz4' : ''
} /data/cache/$CACHE_KEY/build/build-$BUILD_GUID_TARGET.tar${
Orchestrator.buildParameters.useCompressionStrategy ? '.lz4' : ''
} || true
else
echo "AWS CLI not available, skipping aws-s3-pull-build"
fi
secrets:
- name: AWS_ACCESS_KEY_ID
- name: AWS_SECRET_ACCESS_KEY
- name: AWS_DEFAULT_REGION
- name: BUILD_GUID_TARGET
- name: AWS_S3_ENDPOINT
- name: steam-deploy-client
image: steamcmd/steamcmd
commands: |
apt-get update
apt-get install -y curl tar coreutils git tree > /dev/null
curl -s https://gist.githubusercontent.com/frostebite/1d56f5505b36b403b64193b7a6e54cdc/raw/fa6639ed4ef750c4268ea319d63aa80f52712ffb/deploy-client-steam.sh | bash
secrets:
- name: STEAM_USERNAME
- name: STEAM_PASSWORD
- name: STEAM_APPID
- name: STEAM_SSFN_FILE_NAME
- name: STEAM_SSFN_FILE_CONTENTS
- name: STEAM_CONFIG_VDF_1
- name: STEAM_CONFIG_VDF_2
- name: STEAM_CONFIG_VDF_3
- name: STEAM_CONFIG_VDF_4
- name: BUILD_GUID_TARGET
- name: RELEASE_BRANCH
- name: steam-deploy-project
image: steamcmd/steamcmd
commands: |
apt-get update
apt-get install -y curl tar coreutils git tree > /dev/null
curl -s https://gist.githubusercontent.com/frostebite/969da6a41002a0e901174124b643709f/raw/02403e53fb292026cba81ddcf4ff35fc1eba111d/steam-deploy-project.sh | bash
secrets:
- name: STEAM_USERNAME
- name: STEAM_PASSWORD
- name: STEAM_APPID
- name: STEAM_SSFN_FILE_NAME
- name: STEAM_SSFN_FILE_CONTENTS
- name: STEAM_CONFIG_VDF_1
- name: STEAM_CONFIG_VDF_2
- name: STEAM_CONFIG_VDF_3
- name: STEAM_CONFIG_VDF_4
- name: BUILD_GUID_2
- name: RELEASE_BRANCH
- name: aws-s3-upload-cache
image: amazon/aws-cli
hook: after
commands: |
if command -v aws > /dev/null 2>&1; then
if [ -n "$AWS_ACCESS_KEY_ID" ]; then
aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID" --profile default || true
fi
if [ -n "$AWS_SECRET_ACCESS_KEY" ]; then
aws configure set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY" --profile default || true
fi
if [ -n "$AWS_DEFAULT_REGION" ]; then
aws configure set region "$AWS_DEFAULT_REGION" --profile default || true
fi
ENDPOINT_ARGS=""
if [ -n "$AWS_S3_ENDPOINT" ]; then ENDPOINT_ARGS="--endpoint-url $AWS_S3_ENDPOINT"; fi
aws $ENDPOINT_ARGS s3 cp --recursive /data/cache/$CACHE_KEY/lfs s3://${
Orchestrator.buildParameters.awsStackName
}/orchestrator-cache/$CACHE_KEY/lfs || true
rm -r /data/cache/$CACHE_KEY/lfs || true
aws $ENDPOINT_ARGS s3 cp --recursive /data/cache/$CACHE_KEY/Library s3://${
Orchestrator.buildParameters.awsStackName
}/orchestrator-cache/$CACHE_KEY/Library || true
rm -r /data/cache/$CACHE_KEY/Library || true
else
echo "AWS CLI not available, skipping aws-s3-upload-cache"
fi
secrets:
- name: AWS_ACCESS_KEY_ID
value: ${process.env.AWS_ACCESS_KEY_ID || ``}
- name: AWS_SECRET_ACCESS_KEY
value: ${process.env.AWS_SECRET_ACCESS_KEY || ``}
- name: AWS_DEFAULT_REGION
value: ${process.env.AWS_REGION || ``}
- name: AWS_S3_ENDPOINT
value: ${OrchestratorOptions.awsS3Endpoint || process.env.AWS_S3_ENDPOINT || ``}
- name: aws-s3-pull-cache
image: amazon/aws-cli
hook: before
commands: |
mkdir -p /data/cache/$CACHE_KEY/Library/
mkdir -p /data/cache/$CACHE_KEY/lfs/
if command -v aws > /dev/null 2>&1; then
if [ -n "$AWS_ACCESS_KEY_ID" ]; then
aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID" --profile default || true
fi
if [ -n "$AWS_SECRET_ACCESS_KEY" ]; then
aws configure set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY" --profile default || true
fi
if [ -n "$AWS_DEFAULT_REGION" ]; then
aws configure set region "$AWS_DEFAULT_REGION" --profile default || true
fi
ENDPOINT_ARGS=""
if [ -n "$AWS_S3_ENDPOINT" ]; then ENDPOINT_ARGS="--endpoint-url $AWS_S3_ENDPOINT"; fi
aws $ENDPOINT_ARGS s3 ls ${Orchestrator.buildParameters.awsStackName}/orchestrator-cache/ 2>/dev/null || true
aws $ENDPOINT_ARGS s3 ls ${
Orchestrator.buildParameters.awsStackName
}/orchestrator-cache/$CACHE_KEY/ 2>/dev/null || true
BUCKET1="${Orchestrator.buildParameters.awsStackName}/orchestrator-cache/$CACHE_KEY/Library/"
OBJECT1=""
LS_OUTPUT1="$(aws $ENDPOINT_ARGS s3 ls $BUCKET1 2>/dev/null || echo '')"
if [ -n "$LS_OUTPUT1" ] && [ "$LS_OUTPUT1" != "" ]; then
OBJECT1="$(echo "$LS_OUTPUT1" | sort | tail -n 1 | awk '{print $4}' || '')"
if [ -n "$OBJECT1" ] && [ "$OBJECT1" != "" ]; then
aws $ENDPOINT_ARGS s3 cp s3://$BUCKET1$OBJECT1 /data/cache/$CACHE_KEY/Library/ 2>/dev/null || true
fi
fi
BUCKET2="${Orchestrator.buildParameters.awsStackName}/orchestrator-cache/$CACHE_KEY/lfs/"
OBJECT2=""
LS_OUTPUT2="$(aws $ENDPOINT_ARGS s3 ls $BUCKET2 2>/dev/null || echo '')"
if [ -n "$LS_OUTPUT2" ] && [ "$LS_OUTPUT2" != "" ]; then
OBJECT2="$(echo "$LS_OUTPUT2" | sort | tail -n 1 | awk '{print $4}' || '')"
if [ -n "$OBJECT2" ] && [ "$OBJECT2" != "" ]; then
aws $ENDPOINT_ARGS s3 cp s3://$BUCKET2$OBJECT2 /data/cache/$CACHE_KEY/lfs/ 2>/dev/null || true
fi
fi
else
echo "AWS CLI not available, skipping aws-s3-pull-cache"
fi
- name: rclone-upload-build
image: rclone/rclone
hook: after
commands: |
if command -v rclone > /dev/null 2>&1; then
rclone copy /data/cache/$CACHE_KEY/build/build-${Orchestrator.buildParameters.buildGuid}.tar${
Orchestrator.buildParameters.useCompressionStrategy ? '.lz4' : ''
} ${Orchestrator.buildParameters.rcloneRemote}/orchestrator-cache/$CACHE_KEY/build/ || true
rm /data/cache/$CACHE_KEY/build/build-${Orchestrator.buildParameters.buildGuid}.tar${
Orchestrator.buildParameters.useCompressionStrategy ? '.lz4' : ''
} || true
else
echo "rclone not available, skipping rclone-upload-build"
fi
secrets:
- name: RCLONE_REMOTE
value: ${Orchestrator.buildParameters.rcloneRemote || ``}
- name: rclone-pull-build
image: rclone/rclone
commands: |
mkdir -p /data/cache/$CACHE_KEY/build/
if command -v rclone > /dev/null 2>&1; then
rclone copy ${
Orchestrator.buildParameters.rcloneRemote
}/orchestrator-cache/$CACHE_KEY/build/build-$BUILD_GUID_TARGET.tar${
Orchestrator.buildParameters.useCompressionStrategy ? '.lz4' : ''
} /data/cache/$CACHE_KEY/build/build-$BUILD_GUID_TARGET.tar${
Orchestrator.buildParameters.useCompressionStrategy ? '.lz4' : ''
} || true
else
echo "rclone not available, skipping rclone-pull-build"
fi
secrets:
- name: BUILD_GUID_TARGET
- name: RCLONE_REMOTE
value: ${Orchestrator.buildParameters.rcloneRemote || ``}
- name: rclone-upload-cache
image: rclone/rclone
hook: after
commands: |
if command -v rclone > /dev/null 2>&1; then
rclone copy /data/cache/$CACHE_KEY/lfs ${
Orchestrator.buildParameters.rcloneRemote
}/orchestrator-cache/$CACHE_KEY/lfs || true
rm -r /data/cache/$CACHE_KEY/lfs || true
rclone copy /data/cache/$CACHE_KEY/Library ${
Orchestrator.buildParameters.rcloneRemote
}/orchestrator-cache/$CACHE_KEY/Library || true
rm -r /data/cache/$CACHE_KEY/Library || true
else
echo "rclone not available, skipping rclone-upload-cache"
fi
secrets:
- name: RCLONE_REMOTE
value: ${Orchestrator.buildParameters.rcloneRemote || ``}
- name: rclone-pull-cache
image: rclone/rclone
hook: before
commands: |
mkdir -p /data/cache/$CACHE_KEY/Library/
mkdir -p /data/cache/$CACHE_KEY/lfs/
if command -v rclone > /dev/null 2>&1; then
rclone copy ${
Orchestrator.buildParameters.rcloneRemote
}/orchestrator-cache/$CACHE_KEY/Library /data/cache/$CACHE_KEY/Library/ || true
rclone copy ${
Orchestrator.buildParameters.rcloneRemote
}/orchestrator-cache/$CACHE_KEY/lfs /data/cache/$CACHE_KEY/lfs/ || true
else
echo "rclone not available, skipping rclone-pull-cache"
fi
secrets:
- name: RCLONE_REMOTE
value: ${Orchestrator.buildParameters.rcloneRemote || ``}
- name: debug-cache
image: ubuntu
hook: after
commands: |
apt-get update > /dev/null || true
${OrchestratorOptions.orchestratorDebug ? `apt-get install -y tree > /dev/null || true` : `#`}
${OrchestratorOptions.orchestratorDebug ? `tree -L 3 /data/cache || true` : `#`}
secrets:
- name: awsAccessKeyId
value: ${process.env.AWS_ACCESS_KEY_ID || ``}
- name: awsSecretAccessKey
value: ${process.env.AWS_SECRET_ACCESS_KEY || ``}
- name: awsDefaultRegion
value: ${process.env.AWS_REGION || ``}
- name: AWS_S3_ENDPOINT
value: ${OrchestratorOptions.awsS3Endpoint || process.env.AWS_S3_ENDPOINT || ``}`,
).filter((x) => OrchestratorOptions.containerHookFiles.includes(x.name) && x.hook === hookLifecycle);
// In local provider mode (non-container) or when AWS credentials are not present, skip AWS S3 hooks
const provider = Orchestrator.buildParameters?.providerStrategy;
const isContainerized = provider === 'aws' || provider === 'k8s' || provider === 'local-docker';
const hasAwsCreds =
(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) ||
(process.env.awsAccessKeyId && process.env.awsSecretAccessKey);
// Always include AWS hooks on the AWS provider (task role provides creds),
// otherwise require explicit creds for other containerized providers.
const shouldIncludeAwsHooks =
isContainerized && !Orchestrator.buildParameters?.skipCache && (provider === 'aws' || Boolean(hasAwsCreds));
const filteredBuiltIns = shouldIncludeAwsHooks
? builtInContainerHooks
: builtInContainerHooks.filter((x) => x.image !== 'amazon/aws-cli');
if (filteredBuiltIns.length > 0) {
results.push(...filteredBuiltIns);
}
return results;
}
private static ConvertYamlSecrets(object: ContainerHook) {
if (object.secrets === undefined) {
object.secrets = [];
return;
}
object.secrets = object.secrets.map((x: { [key: string]: any }) => {
return {
ParameterKey: x.name,
EnvironmentVariable: Input.ToEnvVarFormat(x.name),
ParameterValue: x.value,
};
});
}
public static ParseContainerHooks(steps: string): ContainerHook[] {
if (steps === '') {
return [];
}
const isArray = steps.replace(/\s/g, ``)[0] === `-`;
const object: ContainerHook[] = isArray ? YAML.parse(steps) : [YAML.parse(steps)];
for (const step of object) {
ContainerHookService.ConvertYamlSecrets(step);
if (step.secrets === undefined) {
step.secrets = [];
} else {
for (const secret of step.secrets) {
if (secret.ParameterValue === undefined && process.env[secret.EnvironmentVariable] !== undefined) {
if (Orchestrator.buildParameters?.orchestratorDebug) {
// OrchestratorLogger.log(`Injecting custom step ${step.name} from env var ${secret.ParameterKey}`);
}
secret.ParameterValue = process.env[secret.ParameterKey] || ``;
}
}
}
if (step.image === undefined) {
step.image = `ubuntu`;
}
// Ensure allowFailure defaults to false if not explicitly set
if (step.allowFailure === undefined) {
step.allowFailure = false;
}
}
if (object === undefined) {
throw new Error(`Failed to parse ${steps}`);
}
return object;
}
static async RunPostBuildSteps(orchestratorStepState: OrchestratorStepParameters) {
let output = ``;
const steps: ContainerHook[] = [
...ContainerHookService.ParseContainerHooks(Orchestrator.buildParameters.postBuildContainerHooks),
...ContainerHookService.GetContainerHooksFromFiles(`after`),
];
if (steps.length > 0) {
output += await CustomWorkflow.runContainerJob(
steps,
orchestratorStepState.environment,
orchestratorStepState.secrets,
);
}
return output;
}
static async RunPreBuildSteps(orchestratorStepState: OrchestratorStepParameters) {
let output = ``;
const steps: ContainerHook[] = [
...ContainerHookService.ParseContainerHooks(Orchestrator.buildParameters.preBuildContainerHooks),
...ContainerHookService.GetContainerHooksFromFiles(`before`),
];
if (steps.length > 0) {
output += await CustomWorkflow.runContainerJob(
steps,
orchestratorStepState.environment,
orchestratorStepState.secrets,
);
}
return output;
}
}
@@ -0,0 +1,10 @@
import OrchestratorSecret from '../../options/orchestrator-secret';
export class ContainerHook {
public commands!: string;
public secrets: OrchestratorSecret[] = new Array<OrchestratorSecret>();
public name!: string;
public image: string = `ubuntu`;
public hook!: string;
public allowFailure: boolean = false; // If true, hook failures won't stop the build
}
@@ -0,0 +1,43 @@
import path from 'node:path';
import { OrchestratorFolders } from '../../options/orchestrator-folders';
import { OrchestratorSystem } from '../core/orchestrator-system';
import fs from 'node:fs';
import { Cli } from '../../../cli/cli';
import { CliFunction } from '../../../cli/cli-functions-repository';
export class LfsHashing {
public static async createLFSHashFiles() {
await OrchestratorSystem.Run(`git lfs ls-files -l | cut -d ' ' -f1 | sort > .lfs-assets-guid`);
await OrchestratorSystem.Run(`md5sum .lfs-assets-guid > .lfs-assets-guid-sum`);
const lfsHashes = {
lfsGuid: fs
.readFileSync(`${path.join(OrchestratorFolders.repoPathAbsolute, `.lfs-assets-guid`)}`, 'utf8')
.replace(/\n/g, ``),
lfsGuidSum: fs
.readFileSync(`${path.join(OrchestratorFolders.repoPathAbsolute, `.lfs-assets-guid-sum`)}`, 'utf8')
.replace(' .lfs-assets-guid', '')
.replace(/\n/g, ``),
};
return lfsHashes;
}
public static async hashAllFiles(folder: string) {
const startPath = process.cwd();
process.chdir(folder);
const result = await (await OrchestratorSystem.Run(`find -type f -exec md5sum "{}" + | sort | md5sum`))
.replace(/\n/g, '')
.split(` `)[0];
process.chdir(startPath);
return result;
}
@CliFunction(`hash`, `hash all folder contents`)
static async hash() {
if (!Cli.options) {
return;
}
const folder = Cli.options['cachePushFrom'];
LfsHashing.hashAllFiles(folder);
}
}