Cloud runner develop - latest fixes (#524)

Cloud runner develop - latest fixes (#524)
This commit is contained in:
Frostebite
2023-03-27 12:14:23 +01:00
committed by GitHub
parent 309d668d63
commit 7abb3a409d
91 changed files with 4943 additions and 3599 deletions

View File

@@ -0,0 +1,47 @@
import * as core from '@actions/core';
class CloudRunnerLogger {
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 CloudRunnerLogger;

View File

@@ -0,0 +1,60 @@
import { exec } from 'child_process';
import { RemoteClientLogger } from '../../remote-client/remote-client-logger';
export class CloudRunnerSystem {
public static async RunAndReadLines(command: string): Promise<string[]> {
const result = await CloudRunnerSystem.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) {
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, (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}`;
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);
});
});
}
}

View File

@@ -0,0 +1,57 @@
import GitHub from '../../../github';
import CloudRunner from '../../cloud-runner';
import { CloudRunnerStatics } from '../../options/cloud-runner-statics';
import CloudRunnerLogger from './cloud-runner-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(`---${CloudRunner.buildParameters.logId}`)) {
CloudRunnerLogger.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}`;
}
if (CloudRunner.buildParameters.cloudRunnerDebug) {
output += `${message}\n`;
}
CloudRunnerLogger.log(`[${CloudRunnerStatics.logPrefix}] ${message}`);
return { shouldReadLogs, shouldCleanup, output };
}
}

View File

@@ -0,0 +1,300 @@
import { CloudRunnerSystem } from './cloud-runner-system';
import fs from 'node:fs';
import CloudRunnerLogger from './cloud-runner-logger';
import BuildParameters from '../../../build-parameters';
import CloudRunner from '../../cloud-runner';
export class SharedWorkspaceLocking {
public static get workspaceBucketRoot() {
return `s3://${CloudRunner.buildParameters.awsStackName}/`;
}
public static get workspaceRoot() {
return `${SharedWorkspaceLocking.workspaceBucketRoot}locks/`;
}
public static async GetAllWorkspaces(buildParametersContext: BuildParameters): Promise<string[]> {
if (!(await SharedWorkspaceLocking.DoesCacheKeyTopLevelExist(buildParametersContext))) {
return [];
}
return (
await SharedWorkspaceLocking.ReadLines(
`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${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.ReadLines(
`aws s3 ls ${SharedWorkspaceLocking.workspaceBucketRoot}`,
);
const lockFolderExists = rootLines.map((x) => x.replace(`/`, ``)).includes(`locks`);
if (lockFolderExists) {
const lines = await SharedWorkspaceLocking.ReadLines(`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}`);
return lines.map((x) => x.replace(`/`, ``)).includes(buildParametersContext.cacheKey);
} else {
return false;
}
} catch {
return false;
}
}
public static NewWorkspaceName() {
return `${CloudRunner.retainedWorkspacePrefix}-${CloudRunner.buildParameters.buildGuid}`;
}
public static async GetAllLocksForWorkspace(
workspace: string,
buildParametersContext: BuildParameters,
): Promise<string[]> {
if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) {
return [];
}
return (
await SharedWorkspaceLocking.ReadLines(
`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${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);
CloudRunnerLogger.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);
CloudRunnerLogger.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();
CloudRunner.lockedWorkspace = workspace;
}
const createResult = await SharedWorkspaceLocking.CreateWorkspace(workspace, buildParametersContext);
const lockResult = await SharedWorkspaceLocking.LockWorkspace(workspace, runId, buildParametersContext);
CloudRunnerLogger.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;
CloudRunnerLogger.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);
CloudRunnerLogger.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.ReadLines(
`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${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.ReadLines(
`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${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 file = `${timestamp}_${workspace}_workspace`;
fs.writeFileSync(file, '');
await CloudRunnerSystem.Run(
`aws s3 cp ./${file} ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${file}`,
false,
true,
);
fs.rmSync(file);
const workspaces = await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext);
CloudRunnerLogger.log(`All workspaces ${workspaces}`);
if (!(await SharedWorkspaceLocking.IsWorkspaceBelowMax(workspace, buildParametersContext))) {
CloudRunnerLogger.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 file = `${Date.now()}_${runId}_${ending}_lock`;
fs.writeFileSync(file, '');
await CloudRunnerSystem.Run(
`aws s3 cp ./${file} ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${file}`,
false,
true,
);
fs.rmSync(file);
const hasLock = await SharedWorkspaceLocking.HasWorkspaceLock(workspace, runId, buildParametersContext);
if (hasLock) {
CloudRunner.lockedWorkspace = workspace;
} else {
await CloudRunnerSystem.Run(
`aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${file}`,
false,
true,
);
}
return hasLock;
}
public static async ReleaseWorkspace(
workspace: string,
runId: string,
buildParametersContext: BuildParameters,
): Promise<boolean> {
const files = await SharedWorkspaceLocking.GetAllLocksForWorkspace(workspace, buildParametersContext);
const file = files.find((x) => x.includes(workspace) && x.endsWith(`_lock`) && x.includes(runId));
CloudRunnerLogger.log(`All Locks ${files} ${workspace} ${runId}`);
CloudRunnerLogger.log(`Deleting lock ${workspace}/${file}`);
CloudRunnerLogger.log(`rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${file}`);
await CloudRunnerSystem.Run(
`aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${file}`,
false,
true,
);
return !(await SharedWorkspaceLocking.HasWorkspaceLock(workspace, runId, buildParametersContext));
}
public static async CleanupWorkspace(workspace: string, buildParametersContext: BuildParameters) {
await CloudRunnerSystem.Run(
`aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey} --exclude "*" --include "*_${workspace}_*"`,
false,
true,
);
}
public static async ReadLines(command: string): Promise<string[]> {
return CloudRunnerSystem.RunAndReadLines(command);
}
}
export default SharedWorkspaceLocking;

View File

@@ -0,0 +1,174 @@
import BuildParameters from '../../../build-parameters';
import Input from '../../../input';
import CloudRunnerOptions from '../../options/cloud-runner-options';
import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable';
import CloudRunnerOptionsReader from '../../options/cloud-runner-options-reader';
import CloudRunnerQueryOverride from '../../options/cloud-runner-query-override';
import CloudRunnerSecret from '../../options/cloud-runner-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 createCloudRunnerEnvironmentVariables(
buildParameters: BuildParameters,
): CloudRunnerEnvironmentVariable[] {
const result: CloudRunnerEnvironmentVariable[] = 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.serializeCloudRunnerOptions(),
...CommandHookService.getSecrets(CommandHookService.getHooks(buildParameters.commandHooks)),
]
.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: CloudRunnerEnvironmentVariable) => item.name,
);
return result;
}
// eslint-disable-next-line no-unused-vars
static uniqBy(a: CloudRunnerEnvironmentVariable[], key: (parameters: CloudRunnerEnvironmentVariable) => 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 serializeCloudRunnerOptions() {
return TaskParameterSerializer.serializeFromType(CloudRunnerOptions);
}
public static ToEnvVarFormat(input: string): string {
return CloudRunnerOptions.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 = CloudRunnerOptionsReader.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(): CloudRunnerSecret[] {
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 CloudRunnerQueryOverride.queryOverrides !== undefined &&
CloudRunnerQueryOverride.queryOverrides[key] !== undefined
? CloudRunnerQueryOverride.queryOverrides[key]
: process.env[key];
}
private static tryAddInput(array: CloudRunnerSecret[], key: string): CloudRunnerSecret[] {
const value = TaskParameterSerializer.getValue(key);
if (value !== undefined && value !== '' && value !== 'null') {
array.push({
ParameterKey: key,
EnvironmentVariable: key,
ParameterValue: value,
});
}
return array;
}
}