diff --git a/.gitignore b/.gitignore index 1e037fb4..f9e4d2c7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ yarn-error.log *.log logs/* !**/.gitkeep +.env* +!.env*.dist diff --git a/src/commands/command/command-interface.ts b/src/commands/command/command-interface.ts index a74d16e1..7d7c6c20 100644 --- a/src/commands/command/command-interface.ts +++ b/src/commands/command/command-interface.ts @@ -6,4 +6,6 @@ export interface CommandInterface { name: string; execute: (options: Options) => Promise; parseParameters: (input: Input, parameters: Parameters) => Promise>; + configure(options: Options): this; + validate(): Promise; } diff --git a/src/commands/command/unity/build-command.ts b/src/commands/command/unity/build-command.ts index 1e655aba..624e2fc6 100644 --- a/src/commands/command/unity/build-command.ts +++ b/src/commands/command/unity/build-command.ts @@ -1,24 +1,18 @@ -import { exec, OutputMode } from 'https://deno.land/x/exec@0.0.5/mod.ts'; import { CommandInterface } from '../command-interface.ts'; -import { Options } from '../../../config/options.ts'; import { Action, Cache, Docker, ImageTag, Input, Output } from '../../../model/index.ts'; import PlatformSetup from '../../../model/platform-setup.ts'; import MacBuilder from '../../../model/mac-builder.ts'; -import Parameters from '../../../model/parameters.ts'; +import { CommandBase } from './command-base.ts'; -export class BuildCommand implements CommandInterface { - public readonly name: string; - - constructor(name: string) { - this.name = name; +export class BuildCommand extends CommandBase implements CommandInterface { + public async validate() { + await super.validate(); } - public async parseParameters(input: Input, parameters: Parameters) {} - - public async execute(options: Options): Promise { + public async execute(): Promise { try { const { workspace, actionFolder } = Action; - const { parameters, env } = options; + const { parameters, env } = this.options; Action.checkCompatibility(); Cache.verify(); @@ -35,24 +29,24 @@ export class BuildCommand implements CommandInterface { 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 await Output.setBuildVersion(parameters.buildVersion); } catch (error) { log.error(error); Deno.exit(1); } - - 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; } } diff --git a/src/commands/command/unity/build-remote-command.ts b/src/commands/command/unity/build-remote-command.ts index de203192..b702a363 100644 --- a/src/commands/command/unity/build-remote-command.ts +++ b/src/commands/command/unity/build-remote-command.ts @@ -8,24 +8,27 @@ import { Cli } from '../../../model/cli/cli.ts'; import CloudRunnerConstants from '../../../model/cloud-runner/services/cloud-runner-constants.ts'; import CloudRunnerBuildGuid from '../../../model/cloud-runner/services/cloud-runner-guid.ts'; import { GithubCliReader } from '../../../model/input-readers/github-cli.ts'; +import { CommandBase } from './command-base.ts'; // Todo - Verify this entire flow -export class BuildRemoteCommand implements CommandInterface { - public readonly name: string; - - constructor(name: string) { - this.name = name; +export class BuildRemoteCommand extends CommandBase implements CommandInterface { + public async validate() { + await super.validate(); } - public async parseParameters(input: Input, parameters: Parameters) { + public async parseParameters(input: Input, parameters: Parameters): Promise> { + const cloudRunnerCluster = Cli.isCliMode + ? this.input.getInput('cloudRunnerCluster') || 'aws' + : this.input.getInput('cloudRunnerCluster') || 'local'; + return { + cloudRunnerCluster, cloudRunnerBranch: input.cloudRunnerBranch.split('/').reverse()[0], cloudRunnerIntegrationTests: input.cloudRunnerTests, githubRepo: input.githubRepo || (await GitRepoReader.GetRemote()) || 'game-ci/unity-builder', gitPrivateToken: parameters.gitPrivateToken || (await GithubCliReader.GetGitHubAuthToken()), isCliMode: Cli.isCliMode, awsStackName: input.awsBaseStackName, - cloudRunnerCluster: input.cloudRunnerCluster, cloudRunnerBuilderPlatform: input.cloudRunnerBuilderPlatform, awsBaseStackName: input.awsBaseStackName, kubeConfig: input.kubeConfig, @@ -52,19 +55,14 @@ export class BuildRemoteCommand implements CommandInterface { } public async execute(options: Options): Promise { - try { - const { buildParameters } = options; - const baseImage = new ImageTag(buildParameters); + const { buildParameters } = options; + const baseImage = new ImageTag(buildParameters); - const result = await CloudRunner.run(buildParameters, baseImage.toString()); - const { status, output } = result; + const result = await CloudRunner.run(buildParameters, baseImage.toString()); + const { status, output } = result; - await Output.setBuildVersion(buildParameters.buildVersion); + await Output.setBuildVersion(buildParameters.buildVersion); - return status.success; - } catch (error) { - log.error(error); - Deno.exit(1); - } + return status.success; } } diff --git a/src/commands/command/unity/command-base.ts b/src/commands/command/unity/command-base.ts new file mode 100644 index 00000000..e4095812 --- /dev/null +++ b/src/commands/command/unity/command-base.ts @@ -0,0 +1,30 @@ +import { Options } from '../../../config/options.ts'; +import { Input } from '../../../model/index.ts'; +import Parameters from '../../../model/parameters.ts'; + +export class CommandBase { + public readonly name: string; + private options: Options; + + constructor(name: string) { + this.name = name; + } + + public configure(options: Options): this { + this.options = options; + + return this; + } + + public async validate(): Promise { + return this; + } + + public async parseParameters(input: Input, parameters: Parameters): Promise> { + return {}; + } + + public async execute(): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/src/core/env/environment.ts b/src/core/env/environment.ts index a34b4b8c..2b2ce111 100644 --- a/src/core/env/environment.ts +++ b/src/core/env/environment.ts @@ -4,10 +4,21 @@ export class Environment implements EnvVariables { public readonly os: string; public readonly arch: string; - constructor(env: Deno.env) { + constructor(env: Deno.env, envFile: EnvVariables) { // Make an immutable copy of the environment variables. for (const [key, value] of Object.entries(env.toObject())) { - if (value !== undefined) this[key] = value; + // Todo - check if this ever happens at all + if (value === undefined) { + // eslint-disable-next-line no-console + console.error(`Environment variable ${key} is undefined.`); + } + + this[key] = value; + } + + // Override any env variables that are set in a .env file. + for (const [key, value] of Object.entries(envFile)) { + this[key] = value; } // Override specific variables. diff --git a/src/dependencies.ts b/src/dependencies.ts index a8e84d4d..e4e4efb4 100644 --- a/src/dependencies.ts +++ b/src/dependencies.ts @@ -19,6 +19,7 @@ import * as string from 'https://deno.land/std@0.36.0/strings/mod.ts'; import { Command } from 'https://deno.land/x/cmd@v1.2.0/commander/index.ts'; import { getUnityChangeset as getUnityChangeSet } from 'https://deno.land/x/unity_changeset@2.0.0/src/index.ts'; import { Buffer } from 'https://deno.land/std@0.151.0/io/buffer.ts'; +import { config, configSync } from 'https://deno.land/std@0.151.0/dotenv/mod.ts'; // Internally managed packages import waitUntil from './modules/wait-until.ts'; @@ -53,6 +54,8 @@ export { Buffer, Command, compress, + config, + configSync, core, crypto, exec, diff --git a/src/index.ts b/src/index.ts index a1962b92..6e4b1470 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import './dependencies.ts'; +import { configSync } from './dependencies.ts'; import { configureLogger } from './core/logger/index.ts'; import { Options } from './config/options.ts'; import { CommandFactory } from './commands/command-factory.ts'; @@ -10,23 +10,26 @@ export class GameCI { private readonly env: Environment; constructor() { - this.env = new Environment(Deno.env); + this.env = new Environment(Deno.env, configSync()); this.args = Deno.args; } public async run() { try { + // Infallible configuration const { commandName, subCommands, args, verbosity } = new ArgumentsParser().parse(this.args); - await configureLogger(verbosity); + // Determine the command and its options const { engine, engineVersion } = await new EngineDetector(subCommands, args).detect(); const command = new CommandFactory().selectEngine(engine, engineVersion).createCommand(commandName, subCommands); const options = await new Options(command, this.env).registerCommand(command).generateParameters(args); + await command.configure(options).validate(); + // Execute if (log.isVerbose) log.info('Executing', command.name); - - await command.execute(options); + const success = await command.execute(); + if (!success) log.warning(`Command ${command.name} failed.`); } catch (error) { log.error(error); Deno.exit(1); diff --git a/src/logic/unity/platform-validation/validate-windows.ts b/src/logic/unity/platform-validation/validate-windows.ts index 78b46d70..f1651b55 100644 --- a/src/logic/unity/platform-validation/validate-windows.ts +++ b/src/logic/unity/platform-validation/validate-windows.ts @@ -2,13 +2,13 @@ import { fsSync as fs } from '../../../dependencies.ts'; import { Parameters } from '../../../model/index.ts'; class ValidateWindows { - public static validate(buildParameters: Parameters) { - ValidateWindows.validateWindowsPlatformRequirements(buildParameters.targetPlatform); - if (!(Deno.env.get('UNITY_EMAIL') && Deno.env.get('UNITY_PASSWORD'))) { + public static validate(parameters: Parameters) { + ValidateWindows.validateWindowsPlatformRequirements(parameters.targetPlatform); + if (!parameters.unityEmail || !parameters.unityPassword) { throw new Error(String.dedent` Unity email and password must be set for Windows based builds to authenticate the license. - Make sure to set them inside UNITY_EMAIL and UNITY_PASSWORD in Github Secrets and pass them into the environment. + Please make sure to set the unityEmail (UNITY_EMAIL) and unityPassword (UNITY_PASSWORD) parameters. `); } } @@ -38,7 +38,7 @@ class ValidateWindows { const windows10SDKPathExists = fs.existsSync('C:/Program Files (x86)/Windows Kits'); if (!windows10SDKPathExists) { throw new Error(String.dedent` - Windows 10 SDK not found in default location. Make sure this machine has a Windows 10 SDK installed. + Windows 10 SDK not found in default location. Please make sure this machine has a Windows 10 SDK installed. Download here: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/ `); @@ -54,7 +54,7 @@ class ValidateWindows { throw new Error(String.dedent` Visual Studio not found at the default location. - Make sure the runner has Visual Studio installed in the default location + Please make sure the runner has Visual Studio installed in the default location Download here: https://visualstudio.microsoft.com/downloads/ `); diff --git a/src/model/image-environment-factory.ts b/src/model/image-environment-factory.ts index 2e162e0b..8a22f610 100644 --- a/src/model/image-environment-factory.ts +++ b/src/model/image-environment-factory.ts @@ -25,14 +25,17 @@ class ImageEnvironmentFactory { return string; } public static getEnvironmentVariables(parameters: Parameters) { + // Todo - replace with simple for of loop, mapping parameters to this specific format + // All parameters should be straight forward at this point in the process. + // We can convert between camelCase and UPPER_SNAKE_CASE relatively easily. const environmentVariables: Parameter[] = [ - { name: 'UNITY_LICENSE', value: Deno.env.get('UNITY_LICENSE') || ReadLicense() }, - { name: 'UNITY_LICENSE_FILE', value: Deno.env.get('UNITY_LICENSE_FILE') }, - { name: 'UNITY_EMAIL', value: Deno.env.get('UNITY_EMAIL') }, - { name: 'UNITY_PASSWORD', value: Deno.env.get('UNITY_PASSWORD') }, + { name: 'UNITY_LICENSE', value: parameters.unityLicense || ReadLicense(parameters) }, + { name: 'UNITY_LICENSE_FILE', value: parameters.unityLicenseFile }, + { name: 'UNITY_EMAIL', value: parameters.unityEmail }, + { name: 'UNITY_PASSWORD', value: parameters.unityPassword }, { name: 'UNITY_SERIAL', value: parameters.unitySerial }, { name: 'UNITY_VERSION', value: parameters.editorVersion }, - { name: 'USYM_UPLOAD_AUTH_TOKEN', value: Deno.env.get('USYM_UPLOAD_AUTH_TOKEN') }, + { name: 'USYM_UPLOAD_AUTH_TOKEN', value: parameters.uploadAuthToken }, { name: 'PROJECT_PATH', value: parameters.projectPath }, { name: 'BUILD_TARGET', value: parameters.targetPlatform }, { name: 'BUILD_NAME', value: parameters.buildName }, diff --git a/src/model/input-readers/test-license-reader.ts b/src/model/input-readers/test-license-reader.ts index f237cef3..12c758a1 100644 --- a/src/model/input-readers/test-license-reader.ts +++ b/src/model/input-readers/test-license-reader.ts @@ -1,10 +1,11 @@ import { fsSync as fs, path, yaml, __dirname } from '../../dependencies.ts'; import Input from '../input.ts'; -export function ReadLicense() { - if (Input.cloudRunnerCluster === 'local') { +export function ReadLicense(parameters) { + if (parameters.cloudRunnerCluster === 'local') { return ''; } + const pipelineFile = path.join(__dirname, `.github`, `workflows`, `cloud-runner-k8s-pipeline.yml`); return fs.existsSync(pipelineFile) ? yaml.parse(Deno.readTextFileSync(pipelineFile, 'utf8')).env.UNITY_LICENSE : ''; diff --git a/src/model/input.ts b/src/model/input.ts index f86adbad..694d50b3 100644 --- a/src/model/input.ts +++ b/src/model/input.ts @@ -113,6 +113,30 @@ class Input { return this.getInput('unityVersion') || 'auto'; } + public get unityEmail() { + return this.getInput('unityEmail') || ''; + } + + public get unityPassword() { + return this.getInput('unityPassword') || ''; + } + + public get unityLicense() { + return this.getInput('unityLicense') || ''; + } + + public get unityLicenseFile() { + return this.getInput('unityLicenseFile') || ''; + } + + public get unitySerial() { + return this.getInput('unitySerial') || ''; + } + + public get usymUploadAuthToken() { + return this.getInput('usymUploadAuthToken') || ''; + } + public get customImage() { return this.getInput('customImage') || ''; } @@ -248,15 +272,6 @@ class Input { return this.getInput('awsBaseStackName') || 'game-ci'; } - // Todo - move to parameters - public static get cloudRunnerCluster() { - if (Cli.isCliMode) { - return this.getInput('cloudRunnerCluster') || 'aws'; - } - - return this.getInput('cloudRunnerCluster') || 'local'; - } - public get cloudRunnerCpu() { return this.getInput('cloudRunnerCpu'); } diff --git a/src/model/parameters.ts b/src/model/parameters.ts index ecca5f90..79851962 100644 --- a/src/model/parameters.ts +++ b/src/model/parameters.ts @@ -98,15 +98,24 @@ class Parameters { ); log.debug('androidSdkManagerParameters', androidSdkManagerParameters); - let unitySerial = ''; - if (!this.env.UNITY_SERIAL && this.input.githubInputEnabled) { + // Commandline takes precedence over environment variables + const unityEmail = this.input.unityEmail || this.env.get('UNITY_EMAIL'); + const unityPassword = this.input.unityPassword || this.env.get('UNITY_PASSWORD'); + const unityLicense = this.input.unityLicense || this.env.get('UNITY_LICENSE'); + const unityLicenseFile = this.input.unityLicenseFile || this.env.get('UNITY_LICENSE_FILE'); + let unitySerial = this.input.unitySerial || this.env.get('UNITY_SERIAL'); + + // For Windows, we need to use the serial from the license file + if (!unitySerial && this.input.githubInputEnabled) { // No serial was present, so it is a personal license that we need to convert - if (!this.env.UNITY_LICENSE) { - throw new Error(`Missing Unity License File and no Serial was found. If this - is a personal license, make sure to follow the activation - steps and set the UNITY_LICENSE GitHub secret or enter a Unity - serial number inside the UNITY_SERIAL GitHub secret.`); + if (!unityLicense) { + throw new Error(String.dedent` + Missing Unity License File and no Serial was found. If this is a personal license, + make sure to follow the activation steps and set the UNITY_LICENSE variable or enter + a Unity serial number inside the UNITY_SERIAL variable. + `); } + unitySerial = this.getSerialFromLicenseFile(this.env.UNITY_LICENSE); } else { unitySerial = this.env.UNITY_SERIAL!; @@ -124,7 +133,12 @@ class Parameters { const parameters = { editorVersion, customImage: this.input.customImage, + unityEmail, + unityPassword, + unityLicense, + unityLicenseFile, unitySerial, + usymUploadAuthToken: this.input.usymUploadAuthToken || this.env.get('USYM_UPLOAD_AUTH_TOKEN'), runnerTempPath: this.env.RUNNER_TEMP, targetPlatform, projectPath, diff --git a/src/model/platform-setup.ts b/src/model/platform-setup.ts index 289d21ea..cd35b7f0 100644 --- a/src/model/platform-setup.ts +++ b/src/model/platform-setup.ts @@ -3,14 +3,14 @@ import { SetupMac, SetupWindows } from '../logic/unity/platform-setup/index.ts'; import ValidateWindows from '../logic/unity/platform-validation/validate-windows.ts'; class PlatformSetup { - static async setup(buildParameters: Parameters, actionFolder: string) { + static async setup(parameters: Parameters, actionFolder: string) { switch (process.platform) { case 'win32': - ValidateWindows.validate(buildParameters); - SetupWindows.setup(buildParameters); + ValidateWindows.validate(parameters); + SetupWindows.setup(parameters); break; case 'darwin': - await SetupMac.setup(buildParameters, actionFolder); + await SetupMac.setup(parameters, actionFolder); break; // Add other baseOS's here