chore: remove all previous run/exec commands in favour of one cleaned up version System.run

pull/413/head
Webber 2022-08-21 22:31:02 +02:00
parent f4f7aba8ca
commit bd30a3cd10
12 changed files with 135 additions and 230 deletions

View File

@ -27,19 +27,6 @@ export class BuildCommand extends CommandBase implements CommandInterface {
await Docker.run(baseImage, { workspace, actionFolder, ...parameters }); await Docker.run(baseImage, { workspace, actionFolder, ...parameters });
} }
// const result = await exec('docker run -it unityci/editor:2020.3.15f2-base-1 /bin/bash -c "echo test"', {
// output: OutputMode.Capture,
// continueOnError: true,
//
// // verbose: true,
// });
//
// log.info('result', result.output);
// const { success } = result.status;
// log.info('success', success);
//
// return success;
// Set output // Set output
await Output.setBuildVersion(parameters.buildVersion); await Output.setBuildVersion(parameters.buildVersion);
} catch (error) { } catch (error) {

View File

@ -23,7 +23,7 @@ import { config, configSync } from 'https://deno.land/std@0.151.0/dotenv/mod.ts'
// Internally managed packages // Internally managed packages
import waitUntil from './modules/wait-until.ts'; import waitUntil from './modules/wait-until.ts';
import { core, exec } from './modules/actions/index.ts'; import { core } from './modules/actions/index.ts';
import { dedent } from './modules/dedent.ts'; import { dedent } from './modules/dedent.ts';
// Polyfill for https://github.com/tc39/proposal-string-dedent // Polyfill for https://github.com/tc39/proposal-string-dedent
@ -58,7 +58,6 @@ export {
configSync, configSync,
core, core,
crypto, crypto,
exec,
fs, fs,
fsSync, fsSync,
getUnityChangeSet, getUnityChangeSet,

View File

@ -1,5 +1,6 @@
import { Parameters } from '../../../model/index.ts'; import { Parameters } from '../../../model/index.ts';
import { fsSync as fs, exec, getUnityChangeSet } from '../../../dependencies.ts'; import { fsSync as fs, getUnityChangeSet } from '../../../dependencies.ts';
import System from '../../../model/system.ts';
class SetupMac { class SetupMac {
static unityHubPath = `"/Applications/Unity Hub.app/Contents/MacOS/Unity Hub"`; static unityHubPath = `"/Applications/Unity Hub.app/Contents/MacOS/Unity Hub"`;
@ -19,11 +20,10 @@ class SetupMac {
private static async installUnityHub(silent = false) { private static async installUnityHub(silent = false) {
const command = 'brew install unity-hub'; const command = 'brew install unity-hub';
if (!fs.existsSync(this.unityHubPath)) { if (!fs.existsSync(this.unityHubPath)) {
// Ignoring return code because the log seems to overflow the internal buffer which triggers try {
// a false error await System.run(command, { silent, ignoreReturnCode: true });
const { exitCode } = await exec(command, undefined, { silent, ignoreReturnCode: true }); } catch (error) {
if (exitCode) { throw new Error(`There was an error installing the Unity Editor. See logs above for details. ${error}`);
throw new Error(`There was an error installing the Unity Editor. See logs above for details.`);
} }
} }
} }
@ -36,11 +36,10 @@ class SetupMac {
--module mac-il2cpp \ --module mac-il2cpp \
--childModules`; --childModules`;
// Ignoring return code because the log seems to overflow the internal buffer which triggers try {
// a false error await System.run(command, { silent, ignoreReturnCode: true });
const { exitCode } = await exec(command, undefined, { silent, ignoreReturnCode: true }); } catch (error) {
if (exitCode) { throw new Error(`There was an error installing the Unity Editor. See logs above for details. ${error}`);
throw new Error(`There was an error installing the Unity Editor. See logs above for details.`);
} }
} }

View File

@ -1,6 +1,7 @@
import { fsSync as fs, exec } from '../../../dependencies.ts'; import { fsSync as fs } from '../../../dependencies.ts';
import { Parameters } from '../../../model/index.ts'; import { Parameters } from '../../../model/index.ts';
import ValidateWindows from '../platform-validation/validate-windows.ts'; import ValidateWindows from '../platform-validation/validate-windows.ts';
import System from '../../../model/system.ts';
class SetupWindows { class SetupWindows {
public static async setup(parameters: Parameters) { public static async setup(parameters: Parameters) {
@ -17,7 +18,7 @@ class SetupWindows {
const copyWinSdkRegistryKeyCommand = `reg export "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Microsoft SDKs\\Windows\\v10.0" ${registryKeysPath}/winsdk.reg /y`; const copyWinSdkRegistryKeyCommand = `reg export "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Microsoft SDKs\\Windows\\v10.0" ${registryKeysPath}/winsdk.reg /y`;
await fs.ensureDir(registryKeysPath); await fs.ensureDir(registryKeysPath);
await exec(copyWinSdkRegistryKeyCommand); await System.run(copyWinSdkRegistryKeyCommand);
} }
} }

View File

@ -1,67 +1,77 @@
import ImageEnvironmentFactory from './image-environment-factory.ts'; import ImageEnvironmentFactory from './image-environment-factory.ts';
import { path, exec, fs } from '../dependencies.ts'; import { path, fsSync as fs } from '../dependencies.ts';
import System from './system.ts';
class Docker { class Docker {
static async run(image, parameters, silent = false) { static async run(image, parameters, silent = false) {
let runCommand = ''; log.warning('running docker process for', process.platform, silent);
let command = '';
switch (process.platform) { switch (process.platform) {
case 'linux': case 'linux':
runCommand = this.getLinuxCommand(image, parameters); command = await this.getLinuxCommand(image, parameters);
break; break;
case 'win32': case 'win32':
runCommand = this.getWindowsCommand(image, parameters); command = await this.getWindowsCommand(image, parameters);
} }
await exec(runCommand, undefined, { silent });
const test = await System.newRun(`docker`, command.replace(/\s\s+/, ' ').split(' '), { silent, verbose: true });
log.error('test', test);
} }
static getLinuxCommand(image, parameters): string { static async getLinuxCommand(image, parameters): string {
const { workspace, actionFolder, runnerTempPath, sshAgent, gitPrivateToken } = parameters; const { workspace, actionFolder, runnerTempPath, sshAgent, gitPrivateToken } = parameters;
const githubHome = path.join(runnerTempPath, '_github_home'); const githubHome = path.join(runnerTempPath, '_github_home');
if (!fs.existsSync(githubHome)) fs.mkdirSync(githubHome); await fs.ensureDir(githubHome);
const githubWorkflow = path.join(runnerTempPath, '_github_workflow'); const githubWorkflow = path.join(runnerTempPath, '_github_workflow');
if (!fs.existsSync(githubWorkflow)) fs.mkdirSync(githubWorkflow); await fs.ensureDir(githubWorkflow);
return `docker run \ return String.dedent`
--workdir /github/workspace \ docker run \
--rm \ --workdir /github/workspace \
${ImageEnvironmentFactory.getEnvVarString(parameters)} \ --rm \
--env UNITY_SERIAL \ ${ImageEnvironmentFactory.getEnvVarString(parameters)} \
--env GITHUB_WORKSPACE=/github/workspace \ --env UNITY_SERIAL \
${gitPrivateToken ? `--env GIT_PRIVATE_TOKEN="${gitPrivateToken}"` : ''} \ --env GITHUB_WORKSPACE=/github/workspace \
${sshAgent ? '--env SSH_AUTH_SOCK=/ssh-agent' : ''} \ ${gitPrivateToken ? `--env GIT_PRIVATE_TOKEN="${gitPrivateToken}"` : ''} \
--volume "${githubHome}":"/root:z" \ ${sshAgent ? '--env SSH_AUTH_SOCK=/ssh-agent' : ''} \
--volume "${githubWorkflow}":"/github/workflow:z" \ --volume "${githubHome}":"/root:z" \
--volume "${workspace}":"/github/workspace:z" \ --volume "${githubWorkflow}":"/github/workflow:z" \
--volume "${actionFolder}/default-build-script:/UnityBuilderAction:z" \ --volume "${workspace}":"/github/workspace:z" \
--volume "${actionFolder}/platforms/ubuntu/steps:/steps:z" \ --volume "${actionFolder}/default-build-script:/UnityBuilderAction:z" \
--volume "${actionFolder}/platforms/ubuntu/entrypoint.sh:/entrypoint.sh:z" \ --volume "${actionFolder}/platforms/ubuntu/steps:/steps:z" \
${sshAgent ? `--volume ${sshAgent}:/ssh-agent` : ''} \ --volume "${actionFolder}/platforms/ubuntu/entrypoint.sh:/entrypoint.sh:z" \
${sshAgent ? '--volume /home/runner/.ssh/known_hosts:/root/.ssh/known_hosts:ro' : ''} \ ${sshAgent ? `--volume ${sshAgent}:/ssh-agent` : ''} \
${image} \ ${sshAgent ? '--volume /home/runner/.ssh/known_hosts:/root/.ssh/known_hosts:ro' : ''} \
/bin/bash -c /entrypoint.sh`; ${image} \
/bin/bash -c /entrypoint.sh
`;
} }
static getWindowsCommand(image: any, parameters: any): string { static async getWindowsCommand(image: any, parameters: any): string {
const { workspace, actionFolder, unitySerial, gitPrivateToken, cliStoragePath } = parameters; const { workspace, actionFolder, unitySerial, gitPrivateToken, cliStoragePath } = parameters;
return `docker run \ // Todo - get this to work on a non-github runner local machine
--workdir /github/workspace \ // return String.dedent`run ${image} powershell c:/steps/entrypoint.ps1`;
--rm \ return String.dedent`
${ImageEnvironmentFactory.getEnvVarString(parameters)} \ docker run \
--env UNITY_SERIAL="${unitySerial}" \ --workdir /github/workspace \
--env GITHUB_WORKSPACE=c:/github/workspace \ --rm \
${gitPrivateToken ? `--env GIT_PRIVATE_TOKEN="${gitPrivateToken}"` : ''} \ ${ImageEnvironmentFactory.getEnvVarString(parameters)} \
--volume "${workspace}":"c:/github/workspace" \ --env UNITY_SERIAL="${unitySerial}" \
--volume "${cliStoragePath}/registry-keys":"c:/registry-keys" \ --env GITHUB_WORKSPACE=c:/github/workspace \
--volume "C:/Program Files (x86)/Microsoft Visual Studio":"C:/Program Files (x86)/Microsoft Visual Studio" \ ${gitPrivateToken ? `--env GIT_PRIVATE_TOKEN="${gitPrivateToken}"` : ''} \
--volume "C:/Program Files (x86)/Windows Kits":"C:/Program Files (x86)/Windows Kits" \ --volume "${workspace}":"c:/github/workspace" \
--volume "C:/ProgramData/Microsoft/VisualStudio":"C:/ProgramData/Microsoft/VisualStudio" \ --volume "${cliStoragePath}/registry-keys":"c:/registry-keys" \
--volume "${actionFolder}/default-build-script":"c:/UnityBuilderAction" \ --volume "C:/Program Files (x86)/Microsoft Visual Studio":"C:/Program Files (x86)/Microsoft Visual Studio" \
--volume "${actionFolder}/platforms/windows":"c:/steps" \ --volume "C:/Program Files (x86)/Windows Kits":"C:/Program Files (x86)/Windows Kits" \
--volume "${actionFolder}/BlankProject":"c:/BlankProject" \ --volume "C:/ProgramData/Microsoft/VisualStudio":"C:/ProgramData/Microsoft/VisualStudio" \
${image} \ --volume "${actionFolder}/default-build-script":"c:/UnityBuilderAction" \
powershell c:/steps/entrypoint.ps1`; --volume "${actionFolder}/platforms/windows":"c:/steps" \
--volume "${actionFolder}/BlankProject":"c:/BlankProject" \
${image} \
powershell c:/steps/entrypoint.ps1
`;
} }
} }

View File

@ -19,7 +19,14 @@ class ImageEnvironmentFactory {
continue; continue;
} }
string += `--env ${p.name}="${p.value}" `; if (Deno.build.os === 'windows') {
// The ampersand (&) character is not allowed. The & operator is reserved for future use; wrap an ampersand in
// double quotation marks ("&") to pass it as part of a string.
const escapedValue = typeof p.value !== 'string' ? p.value : p.value?.replace(/&/, '\\"&\\"');
string += `--env ${p.name}='${escapedValue}' `;
} else {
string += `--env ${p.name}="${p.value}" `;
}
} }
return string; return string;

View File

@ -1,10 +1,10 @@
import { exec } from '../dependencies.ts';
import { Parameters } from './parameters.ts'; import { Parameters } from './parameters.ts';
import System from './system.ts';
class MacBuilder { class MacBuilder {
public static async run(actionFolder, workspace, buildParameters: BuildParameters, silent = false) { public static async run(actionFolder) {
await exec('bash', [`${actionFolder}/platforms/mac/entrypoint.sh`], { log.warning('running the process');
silent, await System.run(`bash ${actionFolder}/platforms/mac/entrypoint.sh`, {
ignoreReturnCode: true, ignoreReturnCode: true,
}); });
} }

View File

@ -1,11 +1,11 @@
import { core, exec } from '../../dependencies.ts'; import { core } from '../../dependencies.ts';
import System from './system.ts'; import System from './system.ts';
jest.spyOn(core, 'debug').mockImplementation(() => {}); jest.spyOn(core, 'debug').mockImplementation(() => {});
const info = jest.spyOn(core, 'info').mockImplementation(() => {}); const info = jest.spyOn(core, 'info').mockImplementation(() => {});
jest.spyOn(core, 'warning').mockImplementation(() => {}); jest.spyOn(core, 'warning').mockImplementation(() => {});
jest.spyOn(core, 'error').mockImplementation(() => {}); jest.spyOn(core, 'error').mockImplementation(() => {});
const execSpy = jest.spyOn(exec, 'exec').mockImplementation(async () => 0); const execSpy = jest.spyOn(System, 'run').mockImplementation(async () => 0);
afterEach(() => jest.clearAllMocks()); afterEach(() => jest.clearAllMocks());

View File

@ -1,6 +1,4 @@
import { exec } from '../dependencies.ts'; export interface RunOptions {
export interface ShellRunOptions {
pwd: string; pwd: string;
} }
@ -11,22 +9,45 @@ class System {
* *
* Intended to always be silent and capture the output. * Intended to always be silent and capture the output.
*/ */
static async shellRun(rawCommand: string, options: ShellRunOptions = {}) { static async run(rawCommand: string, options: RunOptions = {}) {
const { pwd } = options; const { pwd } = options;
let command = rawCommand; let command = rawCommand;
if (pwd) command = `cd ${pwd} ; ${command}`; if (pwd) command = `cd ${pwd} ; ${command}`;
const isWindows = Deno.build.os === 'windows';
const shellMethod = isWindows ? System.powershellRun : System.shellRun;
if (log.isVeryVerbose) log.debug(`The following command is run using ${shellMethod.name}`);
return shellMethod(command, options);
}
static async shellRun(command: string, options: RunOptions = {}) {
return System.newRun('sh', ['-c', command]); return System.newRun('sh', ['-c', command]);
} }
static async powershellRun(command: string, options: RunOptions = {}) {
return System.newRun('powershell', [command]);
}
/** /**
* Example: * Internal cross-platform run, that spawns a new process and captures its output.
* System.newRun(sh, ['-c', 'echo something'])
* *
* private for now, but could become public if this happens to be a great replacement for the other run method. * If any error is written to stderr, this method will throw them.
* new Error(stdoutErrors)
*
* In case of no errors, this will return an object similar to these examples
* { status: { success: true, code: 0 }, output: 'output from the command' }
* { status: { success: false, code: 1~255 }, output: 'output from the command' }
*
* Example usage:
* System.newRun(sh, ['-c', 'echo something'])
* System.newRun(powershell, ['echo something'])
*
* @deprecated use System.run instead, this method will be private
*/ */
private static async newRun(command, args: string | string[] = []) { public static async newRun(command, args: string | string[] = []) {
if (!Array.isArray(args)) args = [args]; if (!Array.isArray(args)) args = [args];
const argsString = args.join(' '); const argsString = args.join(' ');
@ -42,8 +63,8 @@ class System {
process.close(); process.close();
const output = new TextDecoder().decode(outputBuffer).replace(/\n+$/, ''); const output = new TextDecoder().decode(outputBuffer).replace(/[\n\r]+$/, '');
const error = new TextDecoder().decode(errorBuffer).replace(/\n+$/, ''); const error = new TextDecoder().decode(errorBuffer).replace(/[\n\r]+$/, '');
const result = { status, output }; const result = { status, output };
@ -61,77 +82,6 @@ class System {
return result; return result;
} }
/**
* @deprecated use more simplified `shellRun` if possible.
*/
static async run(command, arguments_: any = [], options = {}, shouldLog = true) {
let result = '';
let error = '';
let debug = '';
const listeners = {
stdout: (dataBuffer) => {
result += dataBuffer.toString();
},
stderr: (dataBuffer) => {
error += dataBuffer.toString();
},
debug: (dataString) => {
debug += dataString.toString();
},
};
const showOutput = () => {
if (debug !== '' && shouldLog) {
log.debug(debug);
}
if (result !== '' && shouldLog) {
log.info(result);
}
if (error !== '' && shouldLog) {
log.warning(error);
}
};
const throwContextualError = (message: string) => {
let commandAsString = command;
if (Array.isArray(arguments_)) {
commandAsString += ` ${arguments_.join(' ')}`;
} else if (typeof arguments_ === 'string') {
commandAsString += ` ${arguments_}`;
}
throw new Error(`Failed to run "${commandAsString}".\n ${message}`);
};
try {
if (command.trim() === '') {
throw new Error(`Failed to execute empty command`);
}
const { exitCode, success, output } = await exec(command, arguments_, { silent: true, listeners, ...options });
showOutput();
if (!success) {
throwContextualError(`Command returned non-zero exit code (${exitCode}).\nError: ${error}`);
}
// Todo - remove this after verifying it works as expected
const trimmedResult = result.replace(/\n+$/, '');
if (!output && trimmedResult) {
log.warning('returning result instead of output for backward compatibility');
return trimmedResult;
}
return output;
} catch (inCommandError) {
showOutput();
throwContextualError(`In-command error caught: ${inCommandError}`);
}
}
} }
export default System; export default System;

View File

@ -2,6 +2,7 @@ import NotImplementedException from './error/not-implemented-exception.ts';
import ValidationError from './error/validation-error.ts'; import ValidationError from './error/validation-error.ts';
import Input from './input.ts'; import Input from './input.ts';
import System from './system.ts'; import System from './system.ts';
import { Action } from './index.ts';
export default class Versioning { export default class Versioning {
static get projectPath() { static get projectPath() {
@ -225,7 +226,7 @@ export default class Versioning {
* Returns whether the repository is shallow. * Returns whether the repository is shallow.
*/ */
static async isShallow() { static async isShallow() {
const output = await this.git(['rev-parse', '--is-shallow-repository']); const output = await this.git('rev-parse --is-shallow-repository');
return output !== 'false'; return output !== 'false';
} }
@ -239,10 +240,10 @@ export default class Versioning {
*/ */
static async fetch() { static async fetch() {
try { try {
await this.git(['fetch', '--unshallow']); await this.git('fetch --unshallow');
} catch { } catch {
log.warning(`fetch --unshallow did not work, falling back to regular fetch`); log.warning(`fetch --unshallow did not work, falling back to regular fetch`);
await this.git(['fetch']); await this.git('fetch');
} }
} }
@ -255,14 +256,23 @@ export default class Versioning {
* identifies the current commit. * identifies the current commit.
*/ */
static async getVersionDescription() { static async getVersionDescription() {
return this.git(['describe', '--long', '--tags', '--always', this.sha]); let commitIsh = '';
// In CI the repo is checked out in detached head mode.
// We MUST specify the commitIsh that triggered the job.
// Todo - make this compatible with more CI systems
if (!Action.isRunningLocally) {
commitIsh = this.sha;
}
return this.git(`describe --long --tags --always ${commitIsh}`);
} }
/** /**
* Returns whether there are uncommitted changes that are not ignored. * Returns whether there are uncommitted changes that are not ignored.
*/ */
static async isDirty() { static async isDirty() {
const output = await this.git(['status', '--porcelain']); const output = await this.git('status --porcelain');
const isDirty = output !== ''; const isDirty = output !== '';
if (isDirty) { if (isDirty) {
@ -279,7 +289,7 @@ export default class Versioning {
* Get the tag if there is one pointing at HEAD * Get the tag if there is one pointing at HEAD
*/ */
static async getTag() { static async getTag() {
return await this.git(['tag', '--points-at', 'HEAD']); return await this.git('tag --points-at HEAD');
} }
/** /**
@ -309,7 +319,7 @@ export default class Versioning {
* Note: HEAD should not be used, as it may be detached, resulting in an additional count. * Note: HEAD should not be used, as it may be detached, resulting in an additional count.
*/ */
static async getTotalNumberOfCommits() { static async getTotalNumberOfCommits() {
const numberOfCommitsAsString = await this.git(['rev-list', '--count', this.sha]); const numberOfCommitsAsString = await this.git(`rev-list --count ${this.sha}`);
return Number.parseInt(numberOfCommitsAsString, 10); return Number.parseInt(numberOfCommitsAsString, 10);
} }
@ -318,6 +328,10 @@ export default class Versioning {
* Run git in the specified project path * Run git in the specified project path
*/ */
static async git(arguments_, options = {}) { static async git(arguments_, options = {}) {
return System.run('git', arguments_, { cwd: this.projectPath, ...options }); const result = await System.run(`git ${arguments_}`, { cwd: this.projectPath, ...options });
log.warning(result);
return result.output;
} }
} }

View File

@ -1,61 +0,0 @@
import { exec as originalExec } from 'https://deno.land/x/exec@0.0.5/mod.ts';
export enum OutputMode {
None = 0, // no output, just run the command
StdOut, // dump the output to stdout
Capture, // capture the output and return it
Tee, // both dump and capture the output
}
export interface ICommandResult {
status?: {
code: number;
success: boolean;
};
output: string;
}
export interface ISanitisedCommandResult {
exitCode: number;
success: boolean;
output: string;
}
interface IOptions {
silent?: boolean;
ignoreReturnCode?: boolean;
output?: OutputMode;
verbose?: boolean;
continueOnError?: boolean;
}
const exec = async (
command: string,
args: string | string[] = [],
ghActionsOptions: IOptions = {},
): Promise<ISanitisedCommandResult> => {
const options = {
output: OutputMode.Tee,
verbose: false,
continueOnError: false,
};
// log.debug('Command:', command, args);
const { silent = false, ignoreReturnCode } = ghActionsOptions;
if (silent) options.output = OutputMode.Capture;
if (ignoreReturnCode) options.continueOnError = true;
const argsString = typeof args === 'string' ? args : args.join(' ');
const result: ICommandResult = await originalExec(`${command} ${argsString}`, options);
const { status, output = '' } = result;
const { code: exitCode, success } = status;
const symbol = success ? '✅' : '❗';
log.debug('Command:', command, argsString, symbol, result);
return { exitCode, success, output: output.replace(/\n+$/, '') };
};
export { exec };

View File

@ -3,4 +3,3 @@
* This substitutes the parts we use in a Deno-compatible way. * This substitutes the parts we use in a Deno-compatible way.
*/ */
export { core } from './core.ts'; export { core } from './core.ts';
export { exec } from './exec.ts';