diff --git a/.github/workflows/build-tests.yml b/.github/workflows/build-tests.yml index 239f2b0f..69a28450 100644 --- a/.github/workflows/build-tests.yml +++ b/.github/workflows/build-tests.yml @@ -11,14 +11,14 @@ env: jobs: buildForAllPlatformsUbuntu: - name: Build for ${{ matrix.targetPlatform }} on version ${{ matrix.unityVersion }} + name: Build for ${{ matrix.targetPlatform }} on version ${{ matrix.engineVersion }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: projectPath: - test-project - unityVersion: + engineVersion: - 2019.2.11f1 - 2019.3.15f1 targetPlatform: @@ -59,7 +59,7 @@ jobs: - uses: ./ with: projectPath: ${{ matrix.projectPath }} - unityVersion: ${{ matrix.unityVersion }} + engineVersion: ${{ matrix.engineVersion }} targetPlatform: ${{ matrix.targetPlatform }} customParameters: -profile SomeProfile -someBoolean -someValue exampleValue @@ -68,6 +68,6 @@ jobs: ########################### - uses: actions/upload-artifact@v2 with: - name: Build Ubuntu (${{ matrix.unityVersion }}) + name: Build Ubuntu (${{ matrix.engineVersion }}) path: build retention-days: 14 diff --git a/.github/workflows/cloud-runner-pipeline.yml b/.github/workflows/cloud-runner-pipeline.yml index 1b7c2293..a0a44bd9 100644 --- a/.github/workflows/cloud-runner-pipeline.yml +++ b/.github/workflows/cloud-runner-pipeline.yml @@ -35,7 +35,7 @@ jobs: matrix: projectPath: - test-project - unityVersion: + engineVersion: # - 2019.2.11f1 - 2019.3.15f1 targetPlatform: @@ -82,7 +82,7 @@ jobs: cloudRunnerCluster: aws versioning: None projectPath: ${{ matrix.projectPath }} - unityVersion: ${{ matrix.unityVersion }} + engineVersion: ${{ matrix.engineVersion }} targetPlatform: ${{ matrix.targetPlatform }} githubToken: ${{ secrets.GITHUB_TOKEN }} postBuildSteps: | @@ -118,12 +118,12 @@ jobs: path: build-${{ steps.aws-fargate-unity-build.outputs.BUILD_GUID }}.tar retention-days: 14 k8sBuilds: - name: K8s (GKE Autopilot) build for ${{ matrix.targetPlatform }} on version ${{ matrix.unityVersion }} + name: K8s (GKE Autopilot) build for ${{ matrix.targetPlatform }} on version ${{ matrix.engineVersion }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: - unityVersion: + engineVersion: # - 2019.2.11f1 - 2019.3.15f1 targetPlatform: @@ -166,7 +166,7 @@ jobs: TARGET_PLATFORM: ${{ matrix.targetPlatform }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} KUBE_CONFIG: ${{ steps.read-base64.outputs.base64 }} - unityVersion: ${{ matrix.unityVersion }} + engineVersion: ${{ matrix.engineVersion }} cloudRunnerTests: true versioning: None @@ -184,7 +184,7 @@ jobs: kubeConfig: ${{ steps.read-base64.outputs.base64 }} githubToken: ${{ secrets.GITHUB_TOKEN }} projectPath: test-project - unityVersion: ${{ matrix.unityVersion }} + engineVersion: ${{ matrix.engineVersion }} versioning: None postBuildSteps: | - name: upload diff --git a/.github/workflows/mac-build-tests.yml b/.github/workflows/mac-build-tests.yml index 72ffe4a4..5de7178b 100644 --- a/.github/workflows/mac-build-tests.yml +++ b/.github/workflows/mac-build-tests.yml @@ -10,14 +10,14 @@ env: jobs: buildForAllPlatformsWindows: - name: Build for ${{ matrix.targetPlatform }} on version ${{ matrix.unityVersion }} + name: Build for ${{ matrix.targetPlatform }} on version ${{ matrix.engineVersion }} runs-on: macos-latest strategy: fail-fast: false matrix: projectPath: - test-project - unityVersion: + engineVersion: - 2020.3.24f1 targetPlatform: - StandaloneOSX # Build a MacOS executable @@ -58,7 +58,7 @@ jobs: UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} with: projectPath: ${{ matrix.projectPath }} - unityVersion: ${{ matrix.unityVersion }} + engineVersion: ${{ matrix.engineVersion }} targetPlatform: ${{ matrix.targetPlatform }} customParameters: -profile SomeProfile -someBoolean -someValue exampleValue # We use dirty build because we are replacing the default project settings file above @@ -69,6 +69,6 @@ jobs: ########################### - uses: actions/upload-artifact@v2 with: - name: Build MacOS (${{ matrix.unityVersion }}) + name: Build MacOS (${{ matrix.engineVersion }}) path: build retention-days: 14 diff --git a/.github/workflows/windows-build-tests.yml b/.github/workflows/windows-build-tests.yml index 90998f3e..1caa8ab2 100644 --- a/.github/workflows/windows-build-tests.yml +++ b/.github/workflows/windows-build-tests.yml @@ -10,14 +10,14 @@ env: jobs: buildForAllPlatformsWindows: - name: Build for ${{ matrix.targetPlatform }} on version ${{ matrix.unityVersion }} + name: Build for ${{ matrix.targetPlatform }} on version ${{ matrix.engineVersion }} runs-on: windows-2019 strategy: fail-fast: false matrix: projectPath: - test-project - unityVersion: + engineVersion: - 2020.3.24f1 targetPlatform: - StandaloneWindows64 # Build a Windows 64-bit standalone. @@ -61,7 +61,7 @@ jobs: UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} with: projectPath: ${{ matrix.projectPath }} - unityVersion: ${{ matrix.unityVersion }} + engineVersion: ${{ matrix.engineVersion }} targetPlatform: ${{ matrix.targetPlatform }} customParameters: -profile SomeProfile -someBoolean -someValue exampleValue allowDirtyBuild: true @@ -72,6 +72,6 @@ jobs: ########################### - uses: actions/upload-artifact@v2 with: - name: Build Windows (${{ matrix.unityVersion }}) + name: Build Windows (${{ matrix.engineVersion }}) path: build retention-days: 14 diff --git a/action.yml b/action.yml index 07d42d35..cf63576b 100644 --- a/action.yml +++ b/action.yml @@ -6,7 +6,7 @@ inputs: required: true default: '' description: 'Platform that the build should target.' - unityVersion: + engineVersion: required: false default: 'auto' description: 'Version of unity to use for building the project. Use "auto" to get from your ProjectSettings/ProjectVersion.txt' @@ -184,7 +184,8 @@ runs: - run: | deno run --allow-run ./src/index.ts build \ --targetPlatform="${{ inputs.targetPlatform }}" \ - --unityVersion="${{ inputs.unityVersion }}" \ + --engineVersion="${{ inputs.engineVersion }}" \ + --engineVersion="${{ inputs.engineVersion }}" \ --customImage="${{ inputs.customImage }}" \ --projectPath="${{ inputs.projectPath }}" \ --buildName="${{ inputs.buildName }}" \ diff --git a/src/cli.ts b/src/cli.ts index 6b7f785c..30a34e95 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,12 +1,14 @@ -import yargs from 'https://deno.land/x/yargs@v17.5.1-deno/deno.ts'; +import { yargs, YargsInstance, YargsArguments } from './dependencies.ts'; import { default as getHomeDir } from 'https://deno.land/x/dir@1.5.1/home_dir/mod.ts'; import { engineDetection } from './middleware/engine-detection/index.ts'; import { CommandInterface } from './command/command-interface.ts'; import { configureLogger } from './middleware/logger-verbosity/index.ts'; import { CommandFactory } from './command/command-factory.ts'; +import { Engine } from './model/engine/engine.ts'; +import { branchDetection } from './middleware/branch-detection/index.ts'; export class Cli { - private readonly yargs: yargs.Argv; + private readonly yargs: YargsInstance; private readonly cliStorageAbsolutePath: string; private readonly cliStorageCanonicalPath: string; private readonly configFileName: string; @@ -28,6 +30,11 @@ export class Cli { await this.parse(); + if (log.isVeryVerbose) { + log.debug(`Parsed command: ${this.command.name} (${this.command.constructor.name})`); + log.debug(`Parsed arguments: ${JSON.stringify(this.options, null, 2)}`); + } + return { command: this.command, options: this.options, @@ -60,35 +67,43 @@ export class Cli { this.yargs .options('quiet', { alias: 'q', - default: false, description: 'Suppress all output', type: 'boolean', + demandOption: false, + default: false, }) .options('verbose', { alias: 'v', - default: false, description: 'Enable verbose logging', type: 'boolean', + demandOption: false, + default: false, }) .options('veryVerbose', { alias: 'vv', - default: false, description: 'Enable very verbose logging', type: 'boolean', + demandOption: false, + default: false, }) .options('maxVerbose', { alias: 'vvv', - default: false, description: 'Enable debug logging', + demandOption: false, + type: 'boolean', + default: false, }) + .default([{ logLevel: 'placeholder' }, { logLevelName: 'placeholder' }]) .middleware([configureLogger], true); } private globalOptions() { this.yargs + .help('help') + .showHelpOnFail(false, 'Specify --help for available options') .epilogue('for more information, find our manual at https://game.ci/docs/cli') .middleware([]) - .showHelpOnFail(true) + .exitProcess(true) // prevents `_handle` from being lost .strict(true); } @@ -98,17 +113,35 @@ export class Cli { .positional('projectPath', { describe: 'Path to the project', type: 'string', + demandOption: false, + default: '.', }) + .coerce('projectPath', async (arg) => { + return arg.replace(/^~/, getHomeDir()).replace(/\/$/, ''); + }) + .middleware([engineDetection, branchDetection]) + + // Todo - remove these lines with release 3.0.0 + .option('unityVersion', { + describe: 'Override the engine version to be used', + type: 'string', + }) + .deprecateOption('unityVersion', 'This parameter will be removed. Use engineVersion instead') .middleware([ - engineDetection, // Command is engine specific async (args) => { - await this.registerCommand(args, yargs); + if (!args.unityVersion || args.unityVersion === 'auto' || args.engine !== Engine.unity) return; + + args.engineVersion = args.unityVersion; + args.unityVersion = undefined; }, - ]); + ]) + + // End todo + .middleware([async (args) => this.registerCommand(args, yargs)]); }); } - private async registerCommand(args: yargs.Arguments, yargs) { + private async registerCommand(args: YargsArguments, yargs: YargsInstance) { const { engine, engineVersion, _: command } = args; this.command = new CommandFactory().selectEngine(engine, engineVersion).createCommand(command); @@ -117,6 +150,8 @@ export class Cli { } private async parse() { - this.options = await this.yargs.parseAsync(); + const { _, $0, ...options } = await this.yargs.parseAsync(); + + this.options = options; } } diff --git a/src/command-options/android-options.ts b/src/command-options/android-options.ts new file mode 100644 index 00000000..633e7d49 --- /dev/null +++ b/src/command-options/android-options.ts @@ -0,0 +1,83 @@ +import { YargsInstance, YargsArguments } from '../dependencies.ts'; + +export class AndroidOptions { + public static configureCommonOptions(yargs: YargsInstance): void { + yargs + .option('androidAppBundle', { + description: 'Build an Android App Bundle', + type: 'boolean', + demandOption: false, + default: false, + }) + .options({ + androidKeystoreName: { + description: 'Name of the keystore', + type: 'string', + demandOption: false, + default: '', + }, + androidKeystoreBase64: { + description: 'Base64 encoded contents of the keystore', + type: 'string', + demandOption: false, + default: '', + }, + androidKeystorePass: { + description: 'Password for the keystore', + type: 'string', + demandOption: false, + default: '', + deprecated: 'Use androidKeystorePassword instead', + }, + androidKeystorePassword: { + description: 'Password for the keystore', + type: 'string', + demandOption: false, + default: '', + }, + androidKeyAlias: { + description: 'Alias for the keystore', + type: 'string', + demandOption: false, + default: '', + }, + androidKeyAliasName: { + description: 'Name of the keystore', + type: 'string', + demandOption: false, + default: '', + deprecated: 'Use androidKeyAlias instead', + }, + androidKeyAliasPassword: { + description: 'Password for the androidKeyAlias', + type: 'string', + demandOption: false, + default: '', + requires: ['androidKeyAlias'], + }, + androidKeyAliasPass: { + description: 'Password for the androidKeyAlias', + type: 'string', + demandOption: false, + default: '', + deprecated: 'Use androidKeyAliasPassword instead', + }, + }) + .option('androidTargetSdkVersion', { + description: 'Custom Android SDK target version', + type: 'number', + demandOption: false, + default: '', + }) + .default('androidSdkManagerParameters', '') // Placeholder, consumed in middleware + .middleware([AndroidOptions.determineSdkManagerParameters]); + } + + private static determineSdkManagerParameters(argv: YargsArguments) { + const { androidTargetSdkVersion } = argv; + + if (!androidTargetSdkVersion) return; + + argv.androidSdkManagerParameters = `platforms;android-${androidTargetSdkVersion}`; + } +} diff --git a/src/command-options/build-options.ts b/src/command-options/build-options.ts new file mode 100644 index 00000000..628f41c9 --- /dev/null +++ b/src/command-options/build-options.ts @@ -0,0 +1,28 @@ +import { YargsInstance } from '../dependencies.ts'; +import Unity from '../model/unity/unity.ts'; + +export class BuildOptions { + public static configure(yargs: YargsInstance): void { + yargs + .demandOption('targetPlatform', 'Target platform is mandatory for builds') + .option('buildName', { + description: 'Name of the build', + type: 'string', + default: '', + }) + .option('buildsPath', { + description: 'Path for outputting the builds to', + type: 'string', + demandOption: false, + default: 'build', + }) + .default('buildPath', '') + .default('buildFile', '') + .middleware(async (argv) => { + const { buildName, buildsPath, targetPlatform, androidAppBundle } = argv; + argv.buildName = buildName || targetPlatform; + argv.buildPath = `${buildsPath}/${targetPlatform}`; + argv.buildFile = Unity.determineBuildFileName(buildName, targetPlatform, androidAppBundle); + }); + } +} diff --git a/src/command-options/unity-options.ts b/src/command-options/unity-options.ts new file mode 100644 index 00000000..1b847623 --- /dev/null +++ b/src/command-options/unity-options.ts @@ -0,0 +1,61 @@ +import type { YargsInstance } from '../dependencies.ts'; +import UnityTargetPlatform from '../model/unity/unity-target-platform.ts'; +import { UnityTargetPlatforms } from '../model/unity/unity-target-platforms.ts'; + +export class UnityOptions { + public static configure = async (yargs: YargsInstance) => { + yargs + .option('targetPlatform', { + alias: 't', + description: 'The platform to build your project for', + choices: UnityTargetPlatforms.all, + demandOption: false, + default: UnityTargetPlatform.default, + }) + .options({ + unityEmail: { + alias: 'u', + description: 'Email address for your Unity account', + type: 'string', + demandOption: false, + default: '', + }, + unityPassword: { + alias: 'p', + description: 'Password for your Unity account', + type: 'string', + demandOption: false, + default: '', + }, + unitySerial: { + alias: 's', + description: 'Serial number identifying a pro-license seat', + type: 'string', + demandOption: false, + default: '', + }, + unityLicense: { + alias: 'l', + description: 'Contents of, or path to your Unity License File (.ulf)', + type: 'string', + demandOption: false, + default: '', + }, + }) + .coerce('unityLicense', async (arg) => { + return arg.endsWith('.ulf') ? Deno.readTextFile(arg, { encoding: 'utf8' }) : arg; + }) + .option('customImage', { + description: String.dedent` + Custom docker image to use inside the command. + For more information see https://game.ci/docs/docker/versions`, + type: 'string', + }) + .option('usymUploadAuthToken', { + description: '', + type: 'string', + demandOption: false, + default: '', + }); + }; +} diff --git a/src/command-options/versioning-options.ts b/src/command-options/versioning-options.ts new file mode 100644 index 00000000..58de00a7 --- /dev/null +++ b/src/command-options/versioning-options.ts @@ -0,0 +1,37 @@ +import { YargsInstance } from '../dependencies.ts'; +import { VersioningStrategies } from '../model/versioning/versioning-strategies.ts'; +import { VersioningStrategy } from '../model/versioning/versioning-strategy.ts'; +import { buildVersioning } from '../middleware/build-versioning/index.ts'; + +export class VersioningOptions { + public static async configure(yargs: YargsInstance): void { + yargs + .option('versioningStrategy', { + description: 'Versioning strategy', + choices: VersioningStrategies.all, + demandOption: true, + default: VersioningStrategy.Semantic, + }) + .option('version', { + description: String.dedent` + Custom version to use for the build. + Only used when versioningStrategy is set to Custom`, + type: 'string', + default: '', + }) + .option('androidVersionCode', { + description: String.dedent` + Custom version code for android specifically.`, + type: 'string', + default: '', + }) + .option('allowDirtyBuild', { + description: 'Allow a dirty build', + type: 'boolean', + demandOption: false, + default: false, + }) + .default('buildVersion', 'placeholder') + .middleware([buildVersioning]); + } +} diff --git a/src/command/build/unity-build-command.ts b/src/command/build/unity-build-command.ts index f4e3b077..cb83f2b3 100644 --- a/src/command/build/unity-build-command.ts +++ b/src/command/build/unity-build-command.ts @@ -3,6 +3,10 @@ import { Action, Cache, Docker, ImageTag, Input, Output } from '../../model/inde import PlatformSetup from '../../model/platform-setup.ts'; import MacBuilder from '../../model/mac-builder.ts'; import { CommandBase } from '../command-base.ts'; +import { UnityOptions } from '../../command-options/unity-options.ts'; +import { YargsInstance } from '../../dependencies.ts'; +import { VersioningOptions } from '../../command-options/versioning-options.ts'; +import { BuildOptions } from '../../command-options/build-options.ts'; export class UnityBuildCommand extends CommandBase implements CommandInterface { public async execute(options): Promise { @@ -32,10 +36,9 @@ export class UnityBuildCommand extends CommandBase implements CommandInterface { } } - public async configureOptions(instance): Promise { - instance.option('buildName', { - description: 'Name of the build', - type: 'string', - }); + public async configureOptions(yargs: YargsInstance): Promise { + await UnityOptions.configure(yargs); + await VersioningOptions.configure(yargs); + await BuildOptions.configure(yargs); } } diff --git a/src/command/command-factory.ts b/src/command/command-factory.ts index 643b078d..f9122b28 100644 --- a/src/command/command-factory.ts +++ b/src/command/command-factory.ts @@ -2,7 +2,7 @@ import { NonExistentCommand } from './null/non-existent-command.ts'; import { UnityBuildCommand } from './build/unity-build-command.ts'; import { CommandInterface } from './command-interface.ts'; import { UnityRemoteBuildCommand } from './remote/unity-remote-build-command.ts'; -import { Engine } from '../model/engine.ts'; +import { Engine } from '../model/engine/engine.ts'; export class CommandFactory { constructor() {} diff --git a/src/core/logger/index.ts b/src/core/logger/index.ts index 2cbd417b..e72d611a 100644 --- a/src/core/logger/index.ts +++ b/src/core/logger/index.ts @@ -58,6 +58,7 @@ export const configureLogger = async (verbosity: Verbosity) => { // Verbosity window.log.verbosity = verbosity; + window.log.verbosityName = Verbosity[verbosity]; window.log.isQuiet = isQuiet; window.log.isVerbose = isVerbose; window.log.isVeryVerbose = isVeryVerbose; diff --git a/src/dependencies.ts b/src/dependencies.ts index 57c757ba..ea9e307b 100644 --- a/src/dependencies.ts +++ b/src/dependencies.ts @@ -20,8 +20,8 @@ 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'; -import yargs from 'https://deno.land/x/yargs@v17.4.0-deno/deno.ts'; -import * as yargsTypes from 'https://deno.land/x/yargs@v17.4.0-deno/deno-types.ts'; +import yargs from 'https://deno.land/x/yargs@v17.5.1-deno/deno.ts'; +import type { Arguments as YargsArguments } from 'https://deno.land/x/yargs@v17.5.1-deno/deno-types.ts'; // Internally managed packages import waitUntil from './module/wait-until.ts'; @@ -42,6 +42,9 @@ const __dirname = path.dirname(path.fromFileUrl(import.meta.url)); const { V1EnvVar, V1EnvVarSource, V1SecretKeySelector } = k8s; +type YargsInstance = yargs.Argv; + +export type { YargsArguments, YargsInstance }; export { __dirname, __filename, @@ -74,5 +77,4 @@ export { Writable, yaml, yargs, - yargsTypes, }; diff --git a/src/global.d.ts b/src/global.d.ts index ed926f3a..8d2500a5 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,16 +1,26 @@ import { Verbosity } from './core/logger/index.ts'; -let log: { - verbosity: Verbosity; - isQuiet: boolean; - isVerbose: boolean; - isVeryVerbose: boolean; - isMaxVerbose: boolean; - debug: (msg: any, ...args: any[]) => void; - info: (msg: any, ...args: any[]) => void; - warning: (msg: any, ...args: any[]) => void; - error: (msg: any, ...args: any[]) => void; -}; +declare global { + interface String { + dedent(indentedString: string): string; + } + + let log: { + verbosity: Verbosity; + isQuiet: boolean; + isVerbose: boolean; + isVeryVerbose: boolean; + isMaxVerbose: boolean; + debug: (msg: any, ...args: any[]) => void; + info: (msg: any, ...args: any[]) => void; + warning: (msg: any, ...args: any[]) => void; + error: (msg: any, ...args: any[]) => void; + }; +} + +declare interface String { + dedent(indentedString: string): string; +} declare interface Window { log: any; diff --git a/src/index.ts b/src/index.ts index 35b3346f..4f8a79ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,8 @@ class GameCI { if (!success) throw new Error(`${command.name} failed.`); } catch (error) { - log.error(error); + // eslint-disable-next-line no-console + console.error(error); Deno.exit(1); } } diff --git a/src/logic/unity/platform-setup/setup-mac.ts b/src/logic/unity/platform-setup/setup-mac.ts index 9b8dfe08..68cab036 100644 --- a/src/logic/unity/platform-setup/setup-mac.ts +++ b/src/logic/unity/platform-setup/setup-mac.ts @@ -1,6 +1,6 @@ import { Parameters } from '../../../model/index.ts'; import { fsSync as fs, getUnityChangeSet } from '../../../dependencies.ts'; -import System from '../../../model/system.ts'; +import System from '../../../model/system/system.ts'; class SetupMac { static unityHubPath = `"/Applications/Unity Hub.app/Contents/MacOS/Unity Hub"`; diff --git a/src/logic/unity/platform-setup/setup-windows.ts b/src/logic/unity/platform-setup/setup-windows.ts index 8bf50354..1d514475 100644 --- a/src/logic/unity/platform-setup/setup-windows.ts +++ b/src/logic/unity/platform-setup/setup-windows.ts @@ -1,7 +1,7 @@ import { fsSync as fs } from '../../../dependencies.ts'; import { Parameters } from '../../../model/index.ts'; import ValidateWindows from '../platform-validation/validate-windows.ts'; -import System from '../../../model/system.ts'; +import System from '../../../model/system/system.ts'; class SetupWindows { public static async setup(parameters: Parameters) { diff --git a/src/middleware/branch-detection/branch-detector.ts b/src/middleware/branch-detection/branch-detector.ts new file mode 100644 index 00000000..497dfea6 --- /dev/null +++ b/src/middleware/branch-detection/branch-detector.ts @@ -0,0 +1,33 @@ +import System from '../../model/system/system.ts'; + +export class BranchDetector { + public static async getCurrentBranch(projectPath) { + // GitHub pull request, GitHub non pull request + let branchName = this.headRef || this.ref?.slice(11); + + // Local + if (!branchName) { + const { status, output } = await System.shellRun('git branch --show-current', { cwd: projectPath }); + if (!status.success) throw new Error('did not expect "git branch --show-current"'); + branchName = output; + } + + return branchName; + } + + /** + * For pull requests we can reliably use GITHUB_HEAD_REF + * @deprecated + */ + private get headRef() { + return Deno.env.get('GITHUB_HEAD_REF'); + } + + /** + * For branches GITHUB_REF will have format `refs/heads/feature-branch-1` + * @deprecated + */ + private get ref() { + return Deno.env.get('GITHUB_REF'); + } +} diff --git a/src/middleware/branch-detection/index.ts b/src/middleware/branch-detection/index.ts new file mode 100644 index 00000000..c90e3adf --- /dev/null +++ b/src/middleware/branch-detection/index.ts @@ -0,0 +1,13 @@ +import System from '../../model/system/system.ts'; +import { BranchDetector } from './branch-detector.ts'; + +export const branchDetection = async (argv) => { + const { projectPath } = argv; + + const branch = await BranchDetector.getCurrentBranch(projectPath); + + // Todo - determine if we ever want to run the cli on a project that has no git repo. + if (!branch) throw new Error('Running GameCI CLI on a project without a git repository is not supported.'); + + argv.branch = branch; +}; diff --git a/src/model/android-versioning.ts b/src/middleware/build-versioning/android-build-version-generator.ts similarity index 64% rename from src/model/android-versioning.ts rename to src/middleware/build-versioning/android-build-version-generator.ts index d2cb4758..5831dc10 100644 --- a/src/model/android-versioning.ts +++ b/src/middleware/build-versioning/android-build-version-generator.ts @@ -1,15 +1,7 @@ -import { semver } from '../dependencies.ts'; +import { semver } from '../../dependencies.ts'; -export default class AndroidVersioning { - static determineVersionCode(version, inputVersionCode) { - if (!inputVersionCode) { - return AndroidVersioning.versionToVersionCode(version); - } - - return inputVersionCode; - } - - static versionToVersionCode(version) { +export default class AndroidBuildVersionGenerator { + public static determineVersionCode(version) { if (version === 'none') { log.info(`Versioning strategy is set to ${version}, so android version code should not be applied.`); @@ -37,10 +29,4 @@ export default class AndroidVersioning { return versionCode; } - - static determineSdkManagerParameters(targetSdkVersion) { - const parsedVersion = Number.parseInt(targetSdkVersion.slice(-2), 10); - - return Number.isNaN(parsedVersion) ? '' : `platforms;android-${parsedVersion}`; - } } diff --git a/src/model/versioning.ts b/src/middleware/build-versioning/build-version-generator.ts similarity index 74% rename from src/model/versioning.ts rename to src/middleware/build-versioning/build-version-generator.ts index 8cd943bc..8be221b3 100644 --- a/src/model/versioning.ts +++ b/src/middleware/build-versioning/build-version-generator.ts @@ -1,102 +1,33 @@ -import NotImplementedException from './error/not-implemented-exception.ts'; -import ValidationError from './error/validation-error.ts'; -import Input from './input.ts'; -import System from './system.ts'; -import { Action } from './index.ts'; +import NotImplementedException from '../../model/error/not-implemented-exception.ts'; +import Input from '../../model/input.ts'; +import System from '../../model/system/system.ts'; +import { Action } from '../../model/index.ts'; +import { VersioningStrategy } from '../../model/versioning/versioning-strategy.ts'; -export default class Versioning { - static get projectPath() { - return Input.projectPath; +export default class BuildVersionGenerator { + private readonly maxDiffLines: number = 60; + private readonly projectPath: string; + + constructor(projectPath, currentBranch) { + this.projectPath = projectPath; + this.currentBranch = currentBranch; } - static get strategies() { - return { None: 'None', Semantic: 'Semantic', Tag: 'Tag', Custom: 'Custom' }; - } - - static get grepCompatibleInputVersionRegex() { - return '^v?([0-9]+\\.)*[0-9]+.*'; - } - - /** - * Get the branch name of the (related) branch - */ - static async getCurrentBranch() { - // GitHub pull request, GitHub non pull request - let branchName = this.headRef || this.ref?.slice(11); - - // Local - if (!branchName) { - const { status, output } = await System.shellRun('git branch --show-current'); - if (!status.success) throw new Error('did not expect "git branch --show-current"'); - branchName = output; - } - - return branchName; - } - - /** - * For pull requests we can reliably use GITHUB_HEAD_REF - */ - static get headRef() { - return Deno.env.get('GITHUB_HEAD_REF'); - } - - /** - * For branches GITHUB_REF will have format `refs/heads/feature-branch-1` - */ - static get ref() { - return Deno.env.get('GITHUB_REF'); - } - - /** - * The commit SHA that triggered the workflow run. - */ - static get sha() { - return Deno.env.get('GITHUB_SHA'); - } - - /** - * Maximum number of lines to print when logging the git diff - */ - static get maxDiffLines() { - return 60; - } - - /** - * Regex to parse version description into separate fields - */ - static get descriptionRegex1() { - return /^v?([\d.]+)-(\d+)-g(\w+)-?(\w+)*/g; - } - - static get descriptionRegex2() { - return /^v?([\d.]+-\w+)-(\d+)-g(\w+)-?(\w+)*/g; - } - - static get descriptionRegex3() { - return /^v?([\d.]+-\w+\.\d+)-(\d+)-g(\w+)-?(\w+)*/g; - } - - static async determineBuildVersion(strategy: string, inputVersion: string, allowDirtyBuild: boolean) { - // Validate input - if (!Object.hasOwnProperty.call(this.strategies, strategy)) { - throw new ValidationError(`Versioning strategy should be one of ${Object.values(this.strategies).join(', ')}.`); - } - + public async determineBuildVersion(strategy: string, inputVersion: string, allowDirtyBuild: boolean) { log.info('Versioning strategy:', strategy); let version; switch (strategy) { - case this.strategies.None: + case VersioningStrategy.None: version = 'none'; break; - case this.strategies.Custom: + case VersioningStrategy.Custom: version = inputVersion; break; - case this.strategies.Semantic: + case VersioningStrategy.Semantic: version = await this.generateSemanticVersion(allowDirtyBuild); break; - case this.strategies.Tag: + case VersioningStrategy.Tag: version = await this.generateTagVersion(); break; default: @@ -108,6 +39,38 @@ export default class Versioning { return version; } + private get grepCompatibleInputVersionRegex() { + return '^v?([0-9]+\\.)*[0-9]+.*'; + } + + /** + * Get the branch name of the (related) branch + */ + private async getCurrentBranch() {} + + /** + * The commit SHA that triggered the workflow run. + * @deprecated + */ + private get sha() { + return Deno.env.get('GITHUB_SHA'); + } + + /** + * Regex to parse version description into separate fields + */ + private get descriptionRegex1() { + return /^v?([\d.]+)-(\d+)-g(\w+)-?(\w+)*/g; + } + + private get descriptionRegex2() { + return /^v?([\d.]+-\w+)-(\d+)-g(\w+)-?(\w+)*/g; + } + + private get descriptionRegex3() { + return /^v?([\d.]+-\w+\.\d+)-(\d+)-g(\w+)-?(\w+)*/g; + } + /** * Log up to maxDiffLines of the git diff. */ @@ -128,13 +91,13 @@ export default class Versioning { * * @See: https://semver.org/ */ - static async generateSemanticVersion(allowDirtyBuild) { + private async generateSemanticVersion(allowDirtyBuild) { if (await this.isShallow()) { await this.fetch(); } if ((await this.isDirty()) && !allowDirtyBuild) { - await Versioning.logDiff(); + await BuildVersionGenerator.logDiff(); throw new Error('Branch is dirty. Refusing to base semantic version on uncommitted changes'); } @@ -168,7 +131,7 @@ export default class Versioning { /** * Generate the proper version for unity based on an existing tag. */ - static async generateTagVersion() { + private async generateTagVersion() { let tag = await this.getTag(); if (tag.charAt(0) === 'v') { @@ -181,7 +144,7 @@ export default class Versioning { /** * Parses the versionDescription into their named parts. */ - static async parseSemanticVersion() { + private async parseSemanticVersion() { const description = await this.getVersionDescription(); try { @@ -225,7 +188,7 @@ export default class Versioning { /** * Returns whether the repository is shallow. */ - static async isShallow() { + private async isShallow() { const output = await this.git('rev-parse --is-shallow-repository'); return output !== 'false'; @@ -238,7 +201,7 @@ export default class Versioning { * * Note: `--all` should not be used, and would break fetching for push event. */ - static async fetch() { + private async fetch() { try { await this.git('fetch --unshallow'); } catch { @@ -255,7 +218,7 @@ export default class Versioning { * In this format v0.12 is the latest tag, 24 are the number of commits since, and gd2198ab * identifies the current commit. */ - static async getVersionDescription() { + private async getVersionDescription() { let commitIsh = ''; // In CI the repo is checked out in detached head mode. @@ -271,7 +234,7 @@ export default class Versioning { /** * Returns whether there are uncommitted changes that are not ignored. */ - static async isDirty() { + private async isDirty() { const output = await this.git('status --porcelain'); const isDirty = output !== ''; @@ -288,7 +251,7 @@ export default class Versioning { /** * Get the tag if there is one pointing at HEAD */ - static async getTag() { + private async getTag() { return await this.git('tag --points-at HEAD'); } @@ -297,7 +260,7 @@ export default class Versioning { * * Note: Currently this is run in all OSes, so the syntax must be cross-platform. */ - static async hasAnyVersionTags() { + private async hasAnyVersionTags() { const command = `git tag --list --merged HEAD | grep -E '${this.grepCompatibleInputVersionRegex}' | wc -l`; // Todo - make sure this cwd is actually passed in somehow @@ -318,7 +281,7 @@ export default class Versioning { * * Note: HEAD should not be used, as it may be detached, resulting in an additional count. */ - static async getTotalNumberOfCommits() { + private async getTotalNumberOfCommits() { const numberOfCommitsAsString = await this.git(`rev-list --count ${this.sha}`); return Number.parseInt(numberOfCommitsAsString, 10); @@ -327,7 +290,7 @@ export default class Versioning { /** * Run git in the specified project path */ - static async git(arguments_, options = {}) { + private async git(arguments_, options = {}) { const result = await System.run(`git ${arguments_}`, { cwd: this.projectPath, ...options }); log.warning(result); diff --git a/src/middleware/build-versioning/index.ts b/src/middleware/build-versioning/index.ts new file mode 100644 index 00000000..b49c7e39 --- /dev/null +++ b/src/middleware/build-versioning/index.ts @@ -0,0 +1,14 @@ +import BuildVersionGenerator from './build-version-generator.ts'; +import AndroidBuildVersionGenerator from './android-build-version-generator.ts'; + +export const buildVersioning = async (argv) => { + const { projectPath, versioningStrategy, version, allowDirtyBuild, androidVersionCode, buildVersion } = argv; + + const buildVersionGenerator = new BuildVersionGenerator(projectPath); + + argv.buildVersion = await buildVersionGenerator.determineBuildVersion(versioningStrategy, version, allowDirtyBuild); + + if (!androidVersionCode) { + argv.androidVersionCode = AndroidBuildVersionGenerator.determineVersionCode(buildVersion); + } +}; diff --git a/src/middleware/engine-detection/engine-detector.ts b/src/middleware/engine-detection/engine-detector.ts index 7f864827..da822232 100644 --- a/src/middleware/engine-detection/engine-detector.ts +++ b/src/middleware/engine-detection/engine-detector.ts @@ -1,15 +1,22 @@ -export class EngineDetector { - private projectPath: string; +import UnityVersionDetector from './unity-version-detector.ts'; - constructor(subCommands: string[], args: string[]) { - this.projectPath = subCommands[0] || args.projectPath || '.'; +export class EngineDetector { + private readonly projectPath: string; + + constructor(projectPath) { + this.projectPath = projectPath; } public async detect(): Promise<{ engine: string; engineVersion: string }> { - // Todo - detect and return real versions + if (UnityVersionDetector.isUnityProject(this.projectPath)) { + const engineVersion = await UnityVersionDetector.getUnityVersion(this.projectPath); + + return { engine: 'unity', engineVersion }; + } + return { - engine: 'unity', - engineVersion: '2020.1.0f1', + engine: 'unknown', + engineVersion: 'unknown', }; } } diff --git a/src/model/unity-versioning.test.ts b/src/middleware/engine-detection/unity-version-detector.test.ts similarity index 51% rename from src/model/unity-versioning.test.ts rename to src/middleware/engine-detection/unity-version-detector.test.ts index 2c863066..b8006df1 100644 --- a/src/model/unity-versioning.test.ts +++ b/src/middleware/engine-detection/unity-version-detector.test.ts @@ -1,35 +1,35 @@ -import UnityVersioning from './unity-versioning.ts'; +import UnityVersionDetector from './unity-version-detector.ts'; describe('Unity Versioning', () => { describe('parse', () => { it('throws for empty string', () => { - expect(() => UnityVersioning.parse('')).toThrow(Error); + expect(() => UnityVersionDetector.parse('')).toThrow(Error); }); it('parses from ProjectVersion.txt', () => { const projectVersionContents = `m_EditorVersion: 2019.2.11f1 m_EditorVersionWithRevision: 2019.2.11f1 (5f859a4cfee5)`; - expect(UnityVersioning.parse(projectVersionContents)).toBe('2019.2.11f1'); + expect(UnityVersionDetector.parse(projectVersionContents)).toBe('2019.2.11f1'); }); }); describe('read', () => { it('throws for invalid path', () => { - expect(() => UnityVersioning.read('')).toThrow(Error); + expect(() => UnityVersionDetector.read('')).toThrow(Error); }); it('reads from test-project', () => { - expect(UnityVersioning.read('./test-project')).toBe('2019.2.11f1'); + expect(UnityVersionDetector.read('./test-project')).toBe('2019.2.11f1'); }); }); describe('determineUnityVersion', () => { it('defaults to parsed version', () => { - expect(UnityVersioning.determineUnityVersion('./test-project', 'auto')).toBe('2019.2.11f1'); + expect(UnityVersionDetector.determineUnityVersion('./test-project', 'auto')).toBe('2019.2.11f1'); }); it('use specified unityVersion', () => { - expect(UnityVersioning.determineUnityVersion('./test-project', '1.2.3')).toBe('1.2.3'); + expect(UnityVersionDetector.determineUnityVersion('./test-project', '1.2.3')).toBe('1.2.3'); }); }); }); diff --git a/src/model/unity-versioning.ts b/src/middleware/engine-detection/unity-version-detector.ts similarity index 52% rename from src/model/unity-versioning.ts rename to src/middleware/engine-detection/unity-version-detector.ts index ef27cf3b..9a5094d7 100644 --- a/src/model/unity-versioning.ts +++ b/src/middleware/engine-detection/unity-version-detector.ts @@ -1,16 +1,22 @@ -import { fsSync as fs, path } from '../dependencies.ts'; +import { fsSync as fs, path } from '../../dependencies.ts'; -export default class UnityVersioning { +export default class UnityVersionDetector { static get versionPattern() { return /20\d{2}\.\d\.\w{3,4}|3/; } - static determineUnityVersion(projectPath, unityVersion) { - if (unityVersion === 'auto') { - return UnityVersioning.read(projectPath); - } + public static isUnityProject(projectPath) { + try { + UnityVersionDetector.read(projectPath); - return unityVersion; + return true; + } catch { + return false; + } + } + + static getUnityVersion(projectPath) { + return UnityVersionDetector.read(projectPath); } static read(projectPath) { @@ -19,11 +25,11 @@ export default class UnityVersioning { throw new Error(`Project settings file not found at "${filePath}". Have you correctly set the projectPath?`); } - return UnityVersioning.parse(Deno.readTextFileSync(filePath, 'utf8')); + return UnityVersionDetector.parse(Deno.readTextFileSync(filePath, 'utf8')); } static parse(projectVersionTxt) { - const matches = projectVersionTxt.match(UnityVersioning.versionPattern); + const matches = projectVersionTxt.match(UnityVersionDetector.versionPattern); if (!matches || matches.length === 0) { throw new Error(`Failed to parse version from "${projectVersionTxt}".`); } diff --git a/src/middleware/logger-verbosity/index.ts b/src/middleware/logger-verbosity/index.ts index c7a5081c..e021b8bc 100644 --- a/src/middleware/logger-verbosity/index.ts +++ b/src/middleware/logger-verbosity/index.ts @@ -17,4 +17,12 @@ export const configureLogger = async (argv) => { } await createLoggerAndSetVerbosity(verbosity); + + argv.logLevel = log.verbosity; + argv.logLevelName = log.verbosityName; + + argv.quiet = undefined; + argv.verbose = undefined; + argv.veryVerbose = undefined; + argv.maxVerbose = undefined; }; diff --git a/src/model/__mocks__/input.ts b/src/model/__mocks__/input.ts index 0e93a989..4a478b51 100644 --- a/src/model/__mocks__/input.ts +++ b/src/model/__mocks__/input.ts @@ -1,11 +1,11 @@ // Import this named export into your test file: -import Platform from '../platform.ts'; +import UnityTargetPlatform from '../unity/unity-target-platform.ts'; export const mockGetFromUser = jest.fn().mockResolvedValue({ editorVersion: '', - targetPlatform: Platform.types.Test, + targetPlatform: UnityTargetPlatform.Test, projectPath: '.', - buildName: Platform.types.Test, + buildName: UnityTargetPlatform.Test, buildsPath: 'build', buildMethod: undefined, buildVersion: '1.3.37', diff --git a/src/model/android-versioning.test.ts b/src/model/android-versioning.test.ts index 77d7f798..d09aac8d 100644 --- a/src/model/android-versioning.test.ts +++ b/src/model/android-versioning.test.ts @@ -1,41 +1,41 @@ -import AndroidVersioning from './android-versioning.ts'; +import AndroidBuildVersionGenerator from '../middleware/build-versioning/android-build-version-generator.ts'; describe('Android Versioning', () => { describe('versionToVersionCode', () => { it('defaults to 0 when versioning strategy is none', () => { - expect(AndroidVersioning.versionToVersionCode('none')).toBe(0); + expect(AndroidBuildVersionGenerator.versionToVersionCode('none')).toBe(0); }); it('defaults to 1 when version is not a valid semver', () => { - expect(AndroidVersioning.versionToVersionCode('abcd')).toBe(1); + expect(AndroidBuildVersionGenerator.versionToVersionCode('abcd')).toBe(1); }); it('returns a number', () => { - expect(AndroidVersioning.versionToVersionCode('123.456.789')).toBe(123_456_789); + expect(AndroidBuildVersionGenerator.versionToVersionCode('123.456.789')).toBe(123_456_789); }); it('throw when generated version code is too large', () => { - expect(() => AndroidVersioning.versionToVersionCode('2050.0.0')).toThrow(); + expect(() => AndroidBuildVersionGenerator.versionToVersionCode('2050.0.0')).toThrow(); }); }); describe('determineVersionCode', () => { it('defaults to parsed version', () => { - expect(AndroidVersioning.determineVersionCode('1.2.3', '')).toBe(1_002_003); + expect(AndroidBuildVersionGenerator.determineVersionCode('1.2.3', '')).toBe(1_002_003); }); it('use specified code', () => { - expect(AndroidVersioning.determineVersionCode('1.2.3', 2)).toBe(2); + expect(AndroidBuildVersionGenerator.determineVersionCode('1.2.3', 2)).toBe(2); }); }); describe('determineSdkManagerParameters', () => { it('defaults to blank', () => { - expect(AndroidVersioning.determineSdkManagerParameters('AndroidApiLevelAuto')).toBe(''); + expect(AndroidBuildVersionGenerator.determineSdkManagerParameters('AndroidApiLevelAuto')).toBe(''); }); it('uses the specified api level', () => { - expect(AndroidVersioning.determineSdkManagerParameters('AndroidApiLevel30')).toBe('platforms;android-30'); + expect(AndroidBuildVersionGenerator.determineSdkManagerParameters('AndroidApiLevel30')).toBe('platforms;android-30'); }); }); }); diff --git a/src/model/build-parameters.test.ts b/src/model/build-parameters.test.ts index 695a0fa5..238c9e36 100644 --- a/src/model/build-parameters.test.ts +++ b/src/model/build-parameters.test.ts @@ -1,21 +1,21 @@ -import Versioning from './versioning.ts'; -import UnityVersioning from './unity-versioning.ts'; -import AndroidVersioning from './android-versioning.ts'; +import BuildVersionGenerator from '../middleware/build-versioning/build-version-generator.ts'; +import UnityVersionDetector from '../middleware/engine-detection/unity-version-detector.ts'; +import AndroidBuildVersionGenerator from '../middleware/build-versioning/android-build-version-generator.ts'; import Parameters from './parameters.ts'; import Input from './input.ts'; -import Platform from './platform.ts'; +import UnityTargetPlatform from './unity/unity-target-platform.ts'; // Todo - Don't use process.env directly, that's what the input model class is for. const testLicense = '\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \nm0Db8UK+ktnOLJBtHybkfetpcKo=o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw=='; Deno.env.set('UNITY_LICENSE', testLicense); -const determineVersion = jest.spyOn(Versioning, 'determineBuildVersion').mockImplementation(async () => '1.3.37'); +const determineVersion = jest.spyOn(BuildVersionGenerator, 'determineBuildVersion').mockImplementation(async () => '1.3.37'); const determineUnityVersion = jest - .spyOn(UnityVersioning, 'determineUnityVersion') + .spyOn(UnityVersionDetector, 'determineUnityVersion') .mockImplementation(() => '2019.2.11f1'); const determineSdkManagerParameters = jest - .spyOn(AndroidVersioning, 'determineSdkManagerParameters') + .spyOn(AndroidBuildVersionGenerator, 'determineSdkManagerParameters') .mockImplementation(() => 'platforms;android-30'); afterEach(() => { @@ -88,7 +88,7 @@ describe('BuildParameters', () => { expect(Parameters.create()).resolves.toEqual(expect.objectContaining({ buildFile: mockValue })); }); - test.each([Platform.types.StandaloneWindows, Platform.types.StandaloneWindows64])( + test.each([UnityTargetPlatform.StandaloneWindows, UnityTargetPlatform.StandaloneWindows64])( 'appends exe for %s', async (targetPlatform) => { jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform); @@ -97,14 +97,14 @@ describe('BuildParameters', () => { }, ); - test.each([Platform.types.Android])('appends apk for %s', async (targetPlatform) => { + test.each([UnityTargetPlatform.Android])('appends apk for %s', async (targetPlatform) => { jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform); jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform); jest.spyOn(Input, 'androidAppBundle', 'get').mockReturnValue(false); expect(Parameters.create()).resolves.toEqual(expect.objectContaining({ buildFile: `${targetPlatform}.apk` })); }); - test.each([Platform.types.Android])('appends aab for %s', async (targetPlatform) => { + test.each([UnityTargetPlatform.Android])('appends aab for %s', async (targetPlatform) => { jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform); jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform); jest.spyOn(Input, 'androidAppBundle', 'get').mockReturnValue(true); diff --git a/src/model/cloud-runner/cloud-runner.test.ts b/src/model/cloud-runner/cloud-runner.test.ts index 154a8bcc..e09cb600 100644 --- a/src/model/cloud-runner/cloud-runner.test.ts +++ b/src/model/cloud-runner/cloud-runner.test.ts @@ -3,7 +3,7 @@ import CloudRunner from './cloud-runner.ts'; import Input from '../input.ts'; import { CloudRunnerStatics } from './cloud-runner-statics.ts'; import { TaskParameterSerializer } from './services/task-parameter-serializer.ts'; -import UnityVersioning from '../unity-versioning.ts'; +import UnityVersionDetector from '../../middleware/engine-detection/unity-version-detector.ts'; import { Cli } from '../cli/cli.ts'; import CloudRunnerLogger from './services/cloud-runner-logger.ts'; import { v4 as uuidv4 } from '../../../node_modules/uuid'; @@ -20,7 +20,7 @@ describe('Cloud Runner', () => { Cli.options = { versioning: 'None', projectPath: 'test-project', - unityVersion: UnityVersioning.read('test-project'), + engineVersion: UnityVersionDetector.read('test-project'), targetPlatform: 'StandaloneLinux64', customJob: ` - name: 'step 1' @@ -68,7 +68,7 @@ describe('Cloud Runner', () => { Cli.options = { versioning: 'None', projectPath: 'test-project', - unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + engineVersion: UnityVersionDetector.determineUnityVersion('test-project', UnityVersionDetector.read('test-project')), targetPlatform: 'StandaloneLinux64', cacheKey: `test-case-${uuidv4()}`, }; @@ -96,7 +96,7 @@ describe('Cloud Runner', () => { Cli.options = { versioning: 'None', projectPath: 'test-project', - unityVersion: UnityVersioning.read('test-project'), + engineVersion: UnityVersionDetector.read('test-project'), cloudRunnerCluster: 'local-system', targetPlatform: 'StandaloneLinux64', customJob: ` @@ -124,7 +124,7 @@ describe('Cloud Runner', () => { Cli.options = { versioning: 'None', projectPath: 'test-project', - unityVersion: UnityVersioning.read('test-project'), + engineVersion: UnityVersionDetector.read('test-project'), cloudRunnerCluster: 'test', targetPlatform: 'StandaloneLinux64', }; diff --git a/src/model/cloud-runner/remote-client/caching.test.ts b/src/model/cloud-runner/remote-client/caching.test.ts index f8ef9912..7f7d86c1 100644 --- a/src/model/cloud-runner/remote-client/caching.test.ts +++ b/src/model/cloud-runner/remote-client/caching.test.ts @@ -2,7 +2,7 @@ import { fs, uuid, path, __dirname } from '../../../dependencies.ts'; import Parameters from '../../parameters.ts'; import { Cli } from '../../cli/cli.ts'; import Input from '../../input.ts'; -import UnityVersioning from '../../unity-versioning.ts'; +import UnityVersionDetector from '../../../middleware/engine-detection/unity-version-detector.ts'; import CloudRunner from '../cloud-runner.ts'; import { CloudRunnerSystem } from '../services/cloud-runner-system/index.ts'; import { Caching } from './caching.ts'; @@ -16,7 +16,7 @@ describe('Cloud Runner Caching', () => { Cli.options = { versioning: 'None', projectPath: 'test-project', - unityVersion: UnityVersioning.read('test-project'), + engineVersion: UnityVersionDetector.read('test-project'), targetPlatform: 'StandaloneLinux64', cacheKey: `test-case-${uuid()}`, }; diff --git a/src/model/docker.ts b/src/model/docker.ts index 99c1ab37..2e64574d 100644 --- a/src/model/docker.ts +++ b/src/model/docker.ts @@ -1,6 +1,6 @@ import ImageEnvironmentFactory from './image-environment-factory.ts'; import { path, fsSync as fs } from '../dependencies.ts'; -import System from './system.ts'; +import System from './system/system.ts'; class Docker { static async run(image, parameters) { diff --git a/src/model/engine.ts b/src/model/engine/engine.ts similarity index 100% rename from src/model/engine.ts rename to src/model/engine/engine.ts diff --git a/src/model/image-tag.ts b/src/model/image-tag.ts index 5786ae32..309c2e9a 100644 --- a/src/model/image-tag.ts +++ b/src/model/image-tag.ts @@ -1,4 +1,4 @@ -import Platform from './platform.ts'; +import UnityTargetPlatform from './unity/unity-target-platform.ts'; import Parameters from './parameters.ts'; @@ -78,10 +78,10 @@ class ImageTag { // @see: https://docs.unity3d.com/ScriptReference/BuildTarget.html switch (platform) { - case Platform.types.StandaloneOSX: + case UnityTargetPlatform.StandaloneOSX: return mac; - case Platform.types.StandaloneWindows: - case Platform.types.StandaloneWindows64: + case UnityTargetPlatform.StandaloneWindows: + case UnityTargetPlatform.StandaloneWindows64: // Can only build windows-il2cpp on a windows based system if (process.platform === 'win32') { // Unity versions before 2019.3 do not support il2cpp @@ -94,7 +94,7 @@ class ImageTag { } return windows; - case Platform.types.StandaloneLinux64: { + case UnityTargetPlatform.StandaloneLinux64: { // Unity versions before 2019.3 do not support il2cpp if (major >= 2020 || (major === 2019 && minor >= 3)) { return linuxIl2cpp; @@ -102,45 +102,45 @@ class ImageTag { return linux; } - case Platform.types.iOS: + case UnityTargetPlatform.iOS: return ios; - case Platform.types.Android: + case UnityTargetPlatform.Android: return android; - case Platform.types.WebGL: + case UnityTargetPlatform.WebGL: return webgl; - case Platform.types.WSAPlayer: + case UnityTargetPlatform.WSAPlayer: if (process.platform !== 'win32') { throw new Error(`WSAPlayer can only be built on a windows base OS`); } return wsaPlayer; - case Platform.types.PS4: + case UnityTargetPlatform.PS4: return windows; - case Platform.types.XboxOne: + case UnityTargetPlatform.XboxOne: return windows; - case Platform.types.tvOS: + case UnityTargetPlatform.tvOS: if (process.platform !== 'win32') { throw new Error(`tvOS can only be built on a windows base OS`); } return tvos; - case Platform.types.Switch: + case UnityTargetPlatform.Switch: return windows; // Unsupported - case Platform.types.Lumin: + case UnityTargetPlatform.Lumin: return windows; - case Platform.types.BJM: + case UnityTargetPlatform.BJM: return windows; - case Platform.types.Stadia: + case UnityTargetPlatform.Stadia: return windows; - case Platform.types.Facebook: + case UnityTargetPlatform.Facebook: return facebook; - case Platform.types.NoTarget: + case UnityTargetPlatform.NoTarget: return generic; // Test specific - case Platform.types.Test: + case UnityTargetPlatform.Test: return generic; default: throw new Error(` diff --git a/src/model/index.ts b/src/model/index.ts index a2ae22a8..7c00b0f8 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -5,10 +5,10 @@ import Docker from './docker.ts'; import Input from './input.ts'; import ImageTag from './image-tag.ts'; import Output from './output.ts'; -import Platform from './platform.ts'; +import UnityTargetPlatform from './unity/unity-target-platform.ts'; import Project from './project.ts'; -import Unity from './unity.ts'; -import Versioning from './versioning.ts'; +import Unity from './unity/unity.ts'; +import BuildVersionGenerator from '../middleware/build-versioning/build-version-generator.ts'; import CloudRunner from './cloud-runner/cloud-runner.ts'; export { @@ -19,9 +19,9 @@ export { Input, ImageTag, Output, - Platform, + UnityTargetPlatform, Project, Unity, - Versioning, + BuildVersionGenerator, CloudRunner, }; diff --git a/src/model/input.test.ts b/src/model/input.test.ts index 7a0e36b6..931b15ed 100644 --- a/src/model/input.test.ts +++ b/src/model/input.test.ts @@ -1,22 +1,22 @@ import { core } from '../dependencies.ts'; import Input from './input.ts'; -import Platform from './platform.ts'; +import UnityTargetPlatform from './unity/unity-target-platform.ts'; afterEach(() => { jest.restoreAllMocks(); }); describe('Input', () => { - describe('unityVersion', () => { + describe('engineVersion', () => { it('returns the default value', () => { - expect(Input.unityVersion).toStrictEqual('auto'); + expect(Input.engineVersion).toStrictEqual('auto'); }); it('takes input from the users workflow', () => { const mockValue = '2020.4.99f9'; const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue); - expect(Input.unityVersion).toStrictEqual(mockValue); + expect(Input.engineVersion).toStrictEqual(mockValue); expect(spy).toHaveBeenCalledTimes(1); }); }); @@ -35,7 +35,7 @@ describe('Input', () => { describe('targetPlatform', () => { it('returns the default value', () => { - expect(Input.targetPlatform).toStrictEqual(Platform.default); + expect(Input.targetPlatform).toStrictEqual(UnityTargetPlatform.default); }); it('takes input from the users workflow', () => { diff --git a/src/model/input.ts b/src/model/input.ts index a45cdbb2..969e2852 100644 --- a/src/model/input.ts +++ b/src/model/input.ts @@ -1,7 +1,6 @@ import { fsSync as fs, path, core } from '../dependencies.ts'; import { Cli } from './cli/cli.ts'; import CloudRunnerQueryOverride from './cloud-runner/services/cloud-runner-query-override.ts'; -import Platform from './platform.ts'; import { CliArguments } from '../core/cli/cli-arguments.ts'; /** @@ -102,42 +101,10 @@ class Input { return this.get('GITHUB_RUN_NUMBER') || '0'; } - public get targetPlatform() { - return this.get('targetPlatform') || Platform.default; - } - - public get unityVersion() { - return this.get('unityVersion') || 'auto'; - } - - public get unityEmail() { - return this.get('unityEmail') || ''; - } - - public get unityPassword() { - return this.get('unityPassword') || ''; - } - - public get unityLicense() { - return this.get('unityLicense') || ''; - } - - public get unityLicenseFile() { - return this.get('unityLicenseFile') || ''; - } - public get unitySerial() { return this.get('unitySerial') || ''; } - public get usymUploadAuthToken() { - return this.get('usymUploadAuthToken') || ''; - } - - public get customImage() { - return this.get('customImage') || ''; - } - public get projectPath() { let input = this.get('projectPath'); @@ -156,10 +123,6 @@ class Input { return this.get('buildName'); } - public get buildsPath() { - return this.get('buildsPath') || 'build'; - } - public get buildMethod() { return this.get('buildMethod') || ''; // Processed in docker file } @@ -168,14 +131,6 @@ class Input { return this.get('customParameters') || ''; } - public get versioningStrategy() { - return this.get('versioning') || 'Semantic'; - } - - public get specifiedVersion() { - return this.get('version') || ''; - } - public get androidVersionCode() { return this.get('androidVersionCode'); } @@ -250,13 +205,6 @@ class Input { return this.get('chownFilesTo') || ''; } - public get allowDirtyBuild() { - const input = this.get('allowDirtyBuild'); - log.debug('input === ', input); - - return input || false === true; - } - public get postBuildSteps() { return this.get('postBuildSteps') || ''; } diff --git a/src/model/mac-builder.ts b/src/model/mac-builder.ts index 4e75f3a3..57360656 100644 --- a/src/model/mac-builder.ts +++ b/src/model/mac-builder.ts @@ -1,5 +1,5 @@ import { Parameters } from './parameters.ts'; -import System from './system.ts'; +import System from './system/system.ts'; class MacBuilder { public static async run(actionFolder) { diff --git a/src/model/parameters.ts b/src/model/parameters.ts index 1c21b2b2..f8ca1a97 100644 --- a/src/model/parameters.ts +++ b/src/model/parameters.ts @@ -1,9 +1,9 @@ import { default as getHomeDir } from 'https://deno.land/x/dir@1.5.1/home_dir/mod.ts'; -import AndroidVersioning from './android-versioning.ts'; +import AndroidBuildVersionGenerator from '../middleware/build-versioning/android-build-version-generator.ts'; import Input from './input.ts'; -import Platform from './platform.ts'; -import UnityVersioning from './unity-versioning.ts'; -import Versioning from './versioning.ts'; +import UnityTargetPlatform from './unity/unity-target-platform.ts'; +import UnityVersionDetector from '../middleware/engine-detection/unity-version-detector.ts'; +import BuildVersionGenerator from '../middleware/build-versioning/build-version-generator.ts'; import { GitRepoReader } from './input-readers/git-repo.ts'; import { CommandInterface } from '../command/command-interface.ts'; import { Environment } from '../core/env/environment.ts'; @@ -94,39 +94,13 @@ class Parameters { } public async parse(): Promise { - const cliStoragePath = `${getHomeDir()}/.game-ci`; - const targetPlatform = this.input.get('targetPlatform'); - const buildsPath = this.input.get('buildsPath'); - const projectPath = this.get('projectPath'); - const unityVersion = this.get('unityVersion'); - const versioningStrategy = this.get('versioningStrategy'); - const specifiedVersion = this.get('specifiedVersion'); - const allowDirtyBuild = this.get('allowDirtyBuild'); - const androidTargetSdkVersion = this.get('androidTargetSdkVersion'); - - const buildName = this.input.get('buildName') || targetPlatform; - const buildFile = Parameters.parseBuildFile(buildName, targetPlatform, this.get('androidAppBundle')); - const buildPath = `${buildsPath}/${targetPlatform}`; - const editorVersion = UnityVersioning.determineUnityVersion(projectPath, unityVersion); - const buildVersion = await Versioning.determineBuildVersion(versioningStrategy, specifiedVersion, allowDirtyBuild); - const androidVersionCode = AndroidVersioning.determineVersionCode(buildVersion, this.get('androidVersionCode')); - const androidSdkManagerParameters = AndroidVersioning.determineSdkManagerParameters(androidTargetSdkVersion); - const branch = (await Versioning.getCurrentBranch()) || (await GitRepoReader.GetBranch()); + const branch = (await BuildVersionGenerator.getCurrentBranch()) || (await GitRepoReader.GetBranch()); const parameters = { branch, - unityEmail: this.get('unityEmail'), - unityPassword: this.get('unityPassword'), - unityLicense: this.get('unityLicense'), - unityLicenseFile: this.get('unityLicenseFile'), unitySerial: this.getUnitySerial(), - cliStoragePath, - editorVersion, - customImage: this.get('customImage'), - usymUploadAuthToken: this.get('usymUploadAuthToken'), + editorVersion: engineVersion, runnerTempPath: this.env.get('RUNNER_TEMP'), - targetPlatform, - projectPath, buildName, buildPath, buildFile, @@ -156,18 +130,6 @@ class Parameters { }; } - static parseBuildFile(filename, platform, androidAppBundle) { - if (Platform.isWindows(platform)) { - return `${filename}.exe`; - } - - if (Platform.isAndroid(platform)) { - return androidAppBundle ? `${filename}.aab` : `${filename}.apk`; - } - - return filename; - } - private getUnitySerial() { let unitySerial = this.get('unitySerial'); diff --git a/src/model/platform.test.ts b/src/model/platform.test.ts deleted file mode 100644 index 263ea0a0..00000000 --- a/src/model/platform.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import Platform from './platform.ts'; - -describe('Platform', () => { - describe('default', () => { - it('does not throw', () => { - expect(() => Platform.default).not.toThrow(); - }); - - it('returns a string', () => { - expect(typeof Platform.default).toStrictEqual('string'); - }); - - it('returns a platform', () => { - expect(Object.values(Platform.types)).toContain(Platform.default); - }); - }); - - describe('isWindows', () => { - it('returns true for windows', () => { - expect(Platform.isWindows(Platform.types.StandaloneWindows64)).toStrictEqual(true); - }); - - it('returns false for MacOS', () => { - expect(Platform.isWindows(Platform.types.StandaloneOSX)).toStrictEqual(false); - }); - }); - - describe('isAndroid', () => { - it('returns true for Android', () => { - expect(Platform.isAndroid(Platform.types.Android)).toStrictEqual(true); - }); - - it('returns false for Windows', () => { - expect(Platform.isAndroid(Platform.types.StandaloneWindows64)).toStrictEqual(false); - }); - }); -}); diff --git a/src/model/platform.ts b/src/model/platform.ts deleted file mode 100644 index 2d8fe865..00000000 --- a/src/model/platform.ts +++ /dev/null @@ -1,53 +0,0 @@ -class Platform { - static get default() { - return Platform.types.StandaloneWindows64; - } - - static get types() { - return { - StandaloneOSX: 'StandaloneOSX', - StandaloneWindows: 'StandaloneWindows', - StandaloneWindows64: 'StandaloneWindows64', - StandaloneLinux64: 'StandaloneLinux64', - iOS: 'iOS', - Android: 'Android', - WebGL: 'WebGL', - WSAPlayer: 'WSAPlayer', - PS4: 'PS4', - XboxOne: 'XboxOne', - tvOS: 'tvOS', - Switch: 'Switch', - - // Unsupported - Lumin: 'Lumin', - BJM: 'BJM', - Stadia: 'Stadia', - Facebook: 'Facebook', - NoTarget: 'NoTarget', - - // Test specific - Test: 'Test', - }; - } - - static isWindows(platform) { - switch (platform) { - case Platform.types.StandaloneWindows: - case Platform.types.StandaloneWindows64: - return true; - default: - return false; - } - } - - static isAndroid(platform) { - switch (platform) { - case Platform.types.Android: - return true; - default: - return false; - } - } -} - -export default Platform; diff --git a/src/model/project.ts b/src/model/project.ts index a4d5e07d..b22f50bc 100644 --- a/src/model/project.ts +++ b/src/model/project.ts @@ -1,5 +1,5 @@ import Input from './input.ts'; -import Unity from './unity.ts'; +import Unity from './unity/unity.ts'; import Action from './action.ts'; class Project { diff --git a/src/model/system.integration.test.ts b/src/model/system/system.integration.test.ts similarity index 100% rename from src/model/system.integration.test.ts rename to src/model/system/system.integration.test.ts diff --git a/src/model/system.test.ts b/src/model/system/system.test.ts similarity index 100% rename from src/model/system.test.ts rename to src/model/system/system.test.ts diff --git a/src/model/system.ts b/src/model/system/system.ts similarity index 88% rename from src/model/system.ts rename to src/model/system/system.ts index f168594d..fcf5d75a 100644 --- a/src/model/system.ts +++ b/src/model/system/system.ts @@ -1,5 +1,5 @@ export interface RunOptions { - pwd: string; + cwd: string; attach: boolean; } @@ -14,12 +14,7 @@ class System { * * @throws {Error} if anything was output to stderr. */ - static async run(rawCommand: string, options: RunOptions = {}): Promise { - const { pwd } = options; - - let command = rawCommand; - if (pwd) command = `cd ${pwd} ; ${command}`; - + static async run(command: string, options: RunOptions = {}): Promise { const isWindows = Deno.build.os === 'windows'; const shellMethod = isWindows ? System.powershellRun : System.shellRun; @@ -28,14 +23,20 @@ class System { return shellMethod(command, options); } - static async shellRun(command: string, options: RunOptions = {}): Promise { - const { attach } = options; + static async shellRun(rawCommand: string, options: RunOptions = {}): Promise { + const { attach, cwd } = options; + + let command = rawCommand; + if (cwd) command = `cd ${cwd} ; ${command}`; return attach ? System.runAndAttach('sh', ['-c', command]) : System.runAndCapture('sh', ['-c', command]); } - static async powershellRun(command: string, options: RunOptions = {}): Promise { - const { attach } = options; + static async powershellRun(rawCommand: string, options: RunOptions = {}): Promise { + const { attach, cwd } = options; + + let command = rawCommand; + if (cwd) command = `cd ${cwd} ; ${command}`; return attach ? System.runAndAttach('powershell', [command]) : System.runAndCapture('powershell', [command]); } diff --git a/src/model/unity.ts b/src/model/unity.ts deleted file mode 100644 index 72f8e1ed..00000000 --- a/src/model/unity.ts +++ /dev/null @@ -1,7 +0,0 @@ -class Unity { - static get libraryFolder() { - return 'Library'; - } -} - -export default Unity; diff --git a/src/model/unity/unity-target-platform.test.ts b/src/model/unity/unity-target-platform.test.ts new file mode 100644 index 00000000..3880bf96 --- /dev/null +++ b/src/model/unity/unity-target-platform.test.ts @@ -0,0 +1,37 @@ +import UnityTargetPlatform from './unity-target-platform.ts'; + +describe('UnityTargetPlatform', () => { + describe('default', () => { + it('does not throw', () => { + expect(() => UnityTargetPlatform.default).not.toThrow(); + }); + + it('returns a string', () => { + expect(typeof UnityTargetPlatform.default).toStrictEqual('string'); + }); + + it('returns a platform', () => { + expect(Object.values(UnityTargetPlatform.types)).toContain(UnityTargetPlatform.default); + }); + }); + + describe('isWindows', () => { + it('returns true for windows', () => { + expect(UnityTargetPlatform.isWindows(UnityTargetPlatform.StandaloneWindows64)).toStrictEqual(true); + }); + + it('returns false for MacOS', () => { + expect(UnityTargetPlatform.isWindows(UnityTargetPlatform.StandaloneOSX)).toStrictEqual(false); + }); + }); + + describe('isAndroid', () => { + it('returns true for Android', () => { + expect(UnityTargetPlatform.isAndroid(UnityTargetPlatform.Android)).toStrictEqual(true); + }); + + it('returns false for Windows', () => { + expect(UnityTargetPlatform.isAndroid(UnityTargetPlatform.StandaloneWindows64)).toStrictEqual(false); + }); + }); +}); diff --git a/src/model/unity/unity-target-platform.ts b/src/model/unity/unity-target-platform.ts new file mode 100644 index 00000000..47797135 --- /dev/null +++ b/src/model/unity/unity-target-platform.ts @@ -0,0 +1,48 @@ +class UnityTargetPlatform { + public static readonly Android = 'Android'; + public static readonly iOS = 'iOS'; + public static readonly StandaloneLinux64 = 'StandaloneLinux64'; + public static readonly StandaloneOSX = 'StandaloneOSX'; + public static readonly StandaloneWindows = 'StandaloneWindows'; + public static readonly StandaloneWindows64 = 'StandaloneWindows64'; + public static readonly Switch = 'Switch'; + public static readonly tvOS = 'tvOS'; + public static readonly WebGL = 'WebGL'; + public static readonly WSAPlayer = 'WSAPlayer'; + public static readonly XboxOne = 'XboxOne'; + + // Unsupported + public static readonly Lumin = 'Lumin'; + public static readonly BJM = 'BJM'; + public static readonly Stadia = 'Stadia'; + public static readonly Facebook = 'Facebook'; + public static readonly NoTarget = 'NoTarget'; + + // Test specific + public static readonly Test = 'Test'; + + static get default() { + return UnityTargetPlatform.StandaloneWindows64; + } + + static isWindows(platform) { + switch (platform) { + case UnityTargetPlatform.StandaloneWindows: + case UnityTargetPlatform.StandaloneWindows64: + return true; + default: + return false; + } + } + + static isAndroid(platform) { + switch (platform) { + case UnityTargetPlatform.Android: + return true; + default: + return false; + } + } +} + +export default UnityTargetPlatform; diff --git a/src/model/unity/unity-target-platforms.ts b/src/model/unity/unity-target-platforms.ts new file mode 100644 index 00000000..8da1bdba --- /dev/null +++ b/src/model/unity/unity-target-platforms.ts @@ -0,0 +1,27 @@ +import { UnityTargetPlatform } from '../index.ts'; + +export class UnityTargetPlatforms { + public static readonly all = [ + UnityTargetPlatform.Android, + UnityTargetPlatform.iOS, + UnityTargetPlatform.StandaloneLinux64, + UnityTargetPlatform.StandaloneOSX, + UnityTargetPlatform.StandaloneWindows, + UnityTargetPlatform.StandaloneWindows64, + UnityTargetPlatform.Switch, + UnityTargetPlatform.tvOS, + UnityTargetPlatform.WebGL, + UnityTargetPlatform.WSAPlayer, + UnityTargetPlatform.XboxOne, + + // Unsupported + UnityTargetPlatform.Lumin, + UnityTargetPlatform.BJM, + UnityTargetPlatform.Stadia, + UnityTargetPlatform.Facebook, + UnityTargetPlatform.NoTarget, + + // Test specific + UnityTargetPlatform.Test, + ]; +} diff --git a/src/model/unity.test.ts b/src/model/unity/unity.test.ts similarity index 100% rename from src/model/unity.test.ts rename to src/model/unity/unity.test.ts diff --git a/src/model/unity/unity.ts b/src/model/unity/unity.ts new file mode 100644 index 00000000..8c0ea823 --- /dev/null +++ b/src/model/unity/unity.ts @@ -0,0 +1,21 @@ +import UnityTargetPlatform from './unity-target-platform.ts'; + +class Unity { + static get libraryFolder() { + return 'Library'; + } + + static determineBuildFileName(buildName, platform, androidAppBundle) { + if (UnityTargetPlatform.isWindows(platform)) { + return `${buildName}.exe`; + } + + if (UnityTargetPlatform.isAndroid(platform)) { + return androidAppBundle ? `${buildName}.aab` : `${buildName}.apk`; + } + + return buildName; + } +} + +export default Unity; diff --git a/src/model/versioning/versioning-strategies.ts b/src/model/versioning/versioning-strategies.ts new file mode 100644 index 00000000..f16e4cc5 --- /dev/null +++ b/src/model/versioning/versioning-strategies.ts @@ -0,0 +1,7 @@ +import { VersioningStrategy } from './versioning-strategy.ts'; + +export class VersioningStrategies { + public static get all() { + return [VersioningStrategy.None, VersioningStrategy.Semantic, VersioningStrategy.Tag, VersioningStrategy.Custom]; + } +} diff --git a/src/model/versioning/versioning-strategy.ts b/src/model/versioning/versioning-strategy.ts new file mode 100644 index 00000000..bd0023e6 --- /dev/null +++ b/src/model/versioning/versioning-strategy.ts @@ -0,0 +1,6 @@ +export class VersioningStrategy { + public static None: 'None'; + public static Semantic: 'Semantic'; + public static Tag: 'Tag'; + public static Custom: 'Custom'; +} diff --git a/src/model/versioning.test.ts b/src/model/versioning/versioning.test.ts similarity index 53% rename from src/model/versioning.test.ts rename to src/model/versioning/versioning.test.ts index bef68c63..cfb5acd4 100644 --- a/src/model/versioning.test.ts +++ b/src/model/versioning/versioning.test.ts @@ -1,8 +1,8 @@ import { core } from '../../dependencies.ts'; -import NotImplementedException from './error/not-implemented-exception.ts'; -import System from './system.ts'; -import Versioning from './versioning.ts'; -import { validVersionTagInputs, invalidVersionTagInputs } from './__data__/versions.ts'; +import NotImplementedException from '../error/not-implemented-exception.ts'; +import System from '../system/system.ts'; +import BuildVersionGenerator from '../../middleware/build-versioning/build-version-generator.ts'; +import { validVersionTagInputs, invalidVersionTagInputs } from '../__data__/versions.ts'; afterEach(() => { jest.restoreAllMocks(); @@ -11,27 +11,27 @@ afterEach(() => { describe('Versioning', () => { describe('strategies', () => { it('returns an object', () => { - expect(typeof Versioning.strategies).toStrictEqual('object'); + expect(typeof BuildVersionGenerator.strategies).toStrictEqual('object'); }); it('has items', () => { - expect(Object.values(Versioning.strategies).length).toBeGreaterThan(2); + expect(Object.values(BuildVersionGenerator.strategies).length).toBeGreaterThan(2); }); it('has an opt out option', () => { - expect(Versioning.strategies).toHaveProperty('None'); + expect(BuildVersionGenerator.strategies).toHaveProperty('None'); }); it('has the semantic option', () => { - expect(Versioning.strategies).toHaveProperty('Semantic'); + expect(BuildVersionGenerator.strategies).toHaveProperty('Semantic'); }); it('has a strategy for tags', () => { - expect(Versioning.strategies).toHaveProperty('Tag'); + expect(BuildVersionGenerator.strategies).toHaveProperty('Tag'); }); it('has an option that allows custom input', () => { - expect(Versioning.strategies).toHaveProperty('Custom'); + expect(BuildVersionGenerator.strategies).toHaveProperty('Custom'); }); }); @@ -39,7 +39,7 @@ describe('Versioning', () => { // eslint-disable-next-line unicorn/consistent-function-scoping const matchInputUsingGrep = async (input) => { const output = await System.run('sh', undefined, { - input: Buffer.from(`echo '${input}' | grep -E '${Versioning.grepCompatibleInputVersionRegex}'`), + input: Buffer.from(`echo '${input}' | grep -E '${BuildVersionGenerator.grepCompatibleInputVersionRegex}'`), silent: true, }); @@ -57,34 +57,34 @@ describe('Versioning', () => { describe('branch', () => { it('returns headRef when set', async () => { - const headReference = jest.spyOn(Versioning, 'headRef', 'get').mockReturnValue('feature-branch-1'); + const headReference = jest.spyOn(BuildVersionGenerator, 'headRef', 'get').mockReturnValue('feature-branch-1'); - await expect(Versioning.getCurrentBranch).resolves.toStrictEqual('feature-branch-1'); + await expect(BuildVersionGenerator.getCurrentBranch).resolves.toStrictEqual('feature-branch-1'); expect(headReference).toHaveBeenCalledTimes(1); }); it('returns part of Ref when set', () => { - jest.spyOn(Versioning, 'headRef', 'get').mockImplementation(); - const reference = jest.spyOn(Versioning, 'ref', 'get').mockReturnValue('refs/heads/feature-branch-2'); + jest.spyOn(BuildVersionGenerator, 'headRef', 'get').mockImplementation(); + const reference = jest.spyOn(BuildVersionGenerator, 'ref', 'get').mockReturnValue('refs/heads/feature-branch-2'); - await expect(Versioning.getCurrentBranch).resolves.toStrictEqual('feature-branch-2'); + await expect(BuildVersionGenerator.getCurrentBranch).resolves.toStrictEqual('feature-branch-2'); expect(reference).toHaveBeenCalledTimes(2); }); it('prefers headRef over ref when set', () => { - const headReference = jest.spyOn(Versioning, 'headRef', 'get').mockReturnValue('feature-branch-1'); - const reference = jest.spyOn(Versioning, 'ref', 'get').mockReturnValue('refs/heads/feature-2'); + const headReference = jest.spyOn(BuildVersionGenerator, 'headRef', 'get').mockReturnValue('feature-branch-1'); + const reference = jest.spyOn(BuildVersionGenerator, 'ref', 'get').mockReturnValue('refs/heads/feature-2'); - await expect(Versioning.getCurrentBranch).resolves.toStrictEqual('feature-branch-1'); + await expect(BuildVersionGenerator.getCurrentBranch).resolves.toStrictEqual('feature-branch-1'); expect(headReference).toHaveBeenCalledTimes(1); expect(reference).toHaveBeenCalledTimes(0); }); it('returns undefined when headRef and ref are not set', async () => { - const headReference = jest.spyOn(Versioning, 'headRef', 'get').mockImplementation(); - const reference = jest.spyOn(Versioning, 'ref', 'get').mockImplementation(); + const headReference = jest.spyOn(BuildVersionGenerator, 'headRef', 'get').mockImplementation(); + const reference = jest.spyOn(BuildVersionGenerator, 'ref', 'get').mockImplementation(); - await expect(Versioning.getCurrentBranch).resolves.not.toBeDefined(); + await expect(BuildVersionGenerator.getCurrentBranch).resolves.not.toBeDefined(); expect(headReference).toHaveBeenCalledTimes(1); expect(reference).toHaveBeenCalledTimes(1); @@ -93,23 +93,23 @@ describe('Versioning', () => { describe('headRef', () => { it('does not throw', () => { - expect(() => Versioning.headRef).not.toThrow(); + expect(() => BuildVersionGenerator.headRef).not.toThrow(); }); }); describe('ref', () => { it('does not throw', () => { - expect(() => Versioning.ref).not.toThrow(); + expect(() => BuildVersionGenerator.ref).not.toThrow(); }); }); describe('isDirtyAllowed', () => { it('does not throw', () => { - expect(() => Versioning.isDirtyAllowed).not.toThrow(); + expect(() => BuildVersionGenerator.isDirtyAllowed).not.toThrow(); }); it('returns false by default', () => { - expect(Versioning.isDirtyAllowed).toStrictEqual(false); + expect(BuildVersionGenerator.isDirtyAllowed).toStrictEqual(false); }); }); @@ -117,17 +117,17 @@ describe('Versioning', () => { it('calls git diff', async () => { // allowDirtyBuild: true jest.spyOn(core, 'getInput').mockReturnValue('true'); - jest.spyOn(Versioning, 'isShallow').mockResolvedValue(true); - jest.spyOn(Versioning, 'isDirty').mockResolvedValue(false); - jest.spyOn(Versioning, 'fetch').mockImplementation(); - jest.spyOn(Versioning, 'hasAnyVersionTags').mockResolvedValue(true); + jest.spyOn(BuildVersionGenerator, 'isShallow').mockResolvedValue(true); + jest.spyOn(BuildVersionGenerator, 'isDirty').mockResolvedValue(false); + jest.spyOn(BuildVersionGenerator, 'fetch').mockImplementation(); + jest.spyOn(BuildVersionGenerator, 'hasAnyVersionTags').mockResolvedValue(true); jest - .spyOn(Versioning, 'parseSemanticVersion') + .spyOn(BuildVersionGenerator, 'parseSemanticVersion') .mockResolvedValue({ match: '', tag: 'mocktag', commits: 'abcdef', hash: '75822BCAF' }); - const logDiffSpy = jest.spyOn(Versioning, 'logDiff'); + const logDiffSpy = jest.spyOn(BuildVersionGenerator, 'logDiff'); const gitSpy = jest.spyOn(System, 'run').mockImplementation(); - await Versioning.generateSemanticVersion(); + await BuildVersionGenerator.generateSemanticVersion(); expect(logDiffSpy).toHaveBeenCalledTimes(1); expect(gitSpy).toHaveBeenCalledTimes(1); @@ -140,39 +140,39 @@ describe('Versioning', () => { describe('descriptionRegex1', () => { it('is a valid regex', () => { - expect(Versioning.descriptionRegex1).toBeInstanceOf(RegExp); + expect(BuildVersionGenerator.descriptionRegex1).toBeInstanceOf(RegExp); }); test.each(['v1.1-1-g12345678', 'v0.1-2-g12345678', 'v0.0-500-gA9B6C3D0-dirty'])( 'is happy with valid %s', (description) => { - expect(Versioning.descriptionRegex1.test(description)).toBeTruthy(); + expect(BuildVersionGenerator.descriptionRegex1.test(description)).toBeTruthy(); }, ); test.each(['1.1-1-g12345678', '0.1-2-g12345678', '0.0-500-gA9B6C3D0-dirty'])( 'accepts valid semantic versions without v-prefix %s', (description) => { - expect(Versioning.descriptionRegex1.test(description)).toBeTruthy(); + expect(BuildVersionGenerator.descriptionRegex1.test(description)).toBeTruthy(); }, ); test.each(['v0', 'v0.1', 'v0.1.2', 'v0.1-2', 'v0.1-2-g'])('does not like %s', (description) => { - expect(Versioning.descriptionRegex1.test(description)).toBeFalsy(); + expect(BuildVersionGenerator.descriptionRegex1.test(description)).toBeFalsy(); // Also, never expect without the v to work for any of these cases. - expect(Versioning.descriptionRegex1.test(description?.slice(1))).toBeFalsy(); + expect(BuildVersionGenerator.descriptionRegex1.test(description?.slice(1))).toBeFalsy(); }); }); describe('determineBuildVersion', () => { test.each(['somethingRandom'])('throws for invalid strategy %s', async (strategy) => { - await expect(Versioning.determineBuildVersion(strategy, '')).rejects.toThrowErrorMatchingSnapshot(); + await expect(BuildVersionGenerator.determineBuildVersion(strategy, '')).rejects.toThrowErrorMatchingSnapshot(); }); describe('opt out strategy', () => { it("returns 'none'", async () => { - await expect(Versioning.determineBuildVersion('None', 'v1.0')).resolves.toMatchInlineSnapshot(`"none"`); + await expect(BuildVersionGenerator.determineBuildVersion('None', 'v1.0')).resolves.toMatchInlineSnapshot(`"none"`); }); }); @@ -180,25 +180,25 @@ describe('Versioning', () => { test.each(['v0.1', '1', 'CamelCase', 'dashed-version'])( 'returns the inputVersion for %s', async (inputVersion) => { - await expect(Versioning.determineBuildVersion('Custom', inputVersion)).resolves.toStrictEqual(inputVersion); + await expect(BuildVersionGenerator.determineBuildVersion('Custom', inputVersion)).resolves.toStrictEqual(inputVersion); }, ); }); describe('semantic strategy', () => { it('refers to generateSemanticVersion', async () => { - const generateSemanticVersion = jest.spyOn(Versioning, 'generateSemanticVersion').mockResolvedValue('1.3.37'); + const generateSemanticVersion = jest.spyOn(BuildVersionGenerator, 'generateSemanticVersion').mockResolvedValue('1.3.37'); - await expect(Versioning.determineBuildVersion('Semantic', '')).resolves.toStrictEqual('1.3.37'); + await expect(BuildVersionGenerator.determineBuildVersion('Semantic', '')).resolves.toStrictEqual('1.3.37'); expect(generateSemanticVersion).toHaveBeenCalledTimes(1); }); }); describe('tag strategy', () => { it('refers to generateTagVersion', async () => { - const generateTagVersion = jest.spyOn(Versioning, 'generateTagVersion').mockResolvedValue('0.1'); + const generateTagVersion = jest.spyOn(BuildVersionGenerator, 'generateTagVersion').mockResolvedValue('0.1'); - await expect(Versioning.determineBuildVersion('Tag', '')).resolves.toStrictEqual('0.1'); + await expect(BuildVersionGenerator.determineBuildVersion('Tag', '')).resolves.toStrictEqual('0.1'); expect(generateTagVersion).toHaveBeenCalledTimes(1); }); }); @@ -207,24 +207,24 @@ describe('Versioning', () => { it('throws a not implemented exception', async () => { const strategy = 'Test'; // @ts-ignore - jest.spyOn(Versioning, 'strategies', 'get').mockReturnValue({ [strategy]: strategy }); - await expect(Versioning.determineBuildVersion(strategy, '')).rejects.toThrowError(NotImplementedException); + jest.spyOn(BuildVersionGenerator, 'strategies', 'get').mockReturnValue({ [strategy]: strategy }); + await expect(BuildVersionGenerator.determineBuildVersion(strategy, '')).rejects.toThrowError(NotImplementedException); }); }); }); describe('generateTagVersion', () => { it('removes the v', async () => { - jest.spyOn(Versioning, 'getTag').mockResolvedValue('v1.3.37'); - await expect(Versioning.generateTagVersion()).resolves.toStrictEqual('1.3.37'); + jest.spyOn(BuildVersionGenerator, 'getTag').mockResolvedValue('v1.3.37'); + await expect(BuildVersionGenerator.generateTagVersion()).resolves.toStrictEqual('1.3.37'); }); }); describe('parseSemanticVersion', () => { it('returns the named parts', async () => { - jest.spyOn(Versioning, 'getVersionDescription').mockResolvedValue('v0.1-2-g12345678'); + jest.spyOn(BuildVersionGenerator, 'getVersionDescription').mockResolvedValue('v0.1-2-g12345678'); - await expect(Versioning.parseSemanticVersion()).resolves.toMatchObject({ + await expect(BuildVersionGenerator.parseSemanticVersion()).resolves.toMatchObject({ tag: '0.1', commits: '2', hash: '12345678', @@ -232,9 +232,9 @@ describe('Versioning', () => { }); it('throws when no match could be made', async () => { - jest.spyOn(Versioning, 'getVersionDescription').mockResolvedValue('no-match-can-be-made'); + jest.spyOn(BuildVersionGenerator, 'getVersionDescription').mockResolvedValue('no-match-can-be-made'); - await expect(Versioning.parseSemanticVersion()).toMatchObject({}); + await expect(BuildVersionGenerator.parseSemanticVersion()).toMatchObject({}); }); }); @@ -242,7 +242,7 @@ describe('Versioning', () => { it('returns the commands output', async () => { const runOutput = 'someValue'; jest.spyOn(System, 'run').mockResolvedValue(runOutput); - await expect(Versioning.getVersionDescription()).resolves.toStrictEqual(runOutput); + await expect(BuildVersionGenerator.getVersionDescription()).resolves.toStrictEqual(runOutput); }); }); @@ -250,13 +250,13 @@ describe('Versioning', () => { it('returns true when the repo is shallow', async () => { const runOutput = 'true\n'; jest.spyOn(System, 'run').mockResolvedValue(runOutput); - await expect(Versioning.isShallow()).resolves.toStrictEqual(true); + await expect(BuildVersionGenerator.isShallow()).resolves.toStrictEqual(true); }); it('returns false when the repo is not shallow', async () => { const runOutput = 'false\n'; jest.spyOn(System, 'run').mockResolvedValue(runOutput); - await expect(Versioning.isShallow()).resolves.toStrictEqual(false); + await expect(BuildVersionGenerator.isShallow()).resolves.toStrictEqual(false); }); }); @@ -264,14 +264,14 @@ describe('Versioning', () => { it('awaits the command', async () => { jest.spyOn(core, 'warning').mockImplementation(() => {}); jest.spyOn(System, 'run').mockImplementation(); - await expect(Versioning.fetch()).resolves.not.toThrow(); + await expect(BuildVersionGenerator.fetch()).resolves.not.toThrow(); }); it('falls back to the second strategy when the first fails', async () => { jest.spyOn(core, 'warning').mockImplementation(() => {}); const gitFetch = jest.spyOn(System, 'run').mockImplementation(); - await expect(Versioning.fetch()).resolves.not.toThrow(); + await expect(BuildVersionGenerator.fetch()).resolves.not.toThrow(); expect(gitFetch).toHaveBeenCalledTimes(1); }); }); @@ -280,35 +280,35 @@ describe('Versioning', () => { it('returns a proper version from description', async () => { jest.spyOn(System, 'run').mockImplementation(); jest.spyOn(core, 'info').mockImplementation(() => {}); - jest.spyOn(Versioning, 'isDirty').mockResolvedValue(false); - jest.spyOn(Versioning, 'hasAnyVersionTags').mockResolvedValue(true); - jest.spyOn(Versioning, 'getTotalNumberOfCommits').mockResolvedValue(2); - jest.spyOn(Versioning, 'parseSemanticVersion').mockResolvedValue({ + jest.spyOn(BuildVersionGenerator, 'isDirty').mockResolvedValue(false); + jest.spyOn(BuildVersionGenerator, 'hasAnyVersionTags').mockResolvedValue(true); + jest.spyOn(BuildVersionGenerator, 'getTotalNumberOfCommits').mockResolvedValue(2); + jest.spyOn(BuildVersionGenerator, 'parseSemanticVersion').mockResolvedValue({ match: '0.1-2-g1b345678', tag: '0.1', commits: '2', hash: '1b345678', }); - await expect(Versioning.generateSemanticVersion()).resolves.toStrictEqual('0.1.2'); + await expect(BuildVersionGenerator.generateSemanticVersion()).resolves.toStrictEqual('0.1.2'); }); it('throws when dirty', async () => { jest.spyOn(System, 'run').mockImplementation(); jest.spyOn(core, 'info').mockImplementation(() => {}); - jest.spyOn(Versioning, 'isDirty').mockResolvedValue(true); - await expect(Versioning.generateSemanticVersion()).rejects.toThrowError(); + jest.spyOn(BuildVersionGenerator, 'isDirty').mockResolvedValue(true); + await expect(BuildVersionGenerator.generateSemanticVersion()).rejects.toThrowError(); }); it('falls back to commits only, when no tags are present', async () => { const commits = Math.round(Math.random() * 10); jest.spyOn(System, 'run').mockImplementation(); jest.spyOn(core, 'info').mockImplementation(() => {}); - jest.spyOn(Versioning, 'isDirty').mockResolvedValue(false); - jest.spyOn(Versioning, 'hasAnyVersionTags').mockResolvedValue(false); - jest.spyOn(Versioning, 'getTotalNumberOfCommits').mockResolvedValue(commits); + jest.spyOn(BuildVersionGenerator, 'isDirty').mockResolvedValue(false); + jest.spyOn(BuildVersionGenerator, 'hasAnyVersionTags').mockResolvedValue(false); + jest.spyOn(BuildVersionGenerator, 'getTotalNumberOfCommits').mockResolvedValue(commits); - await expect(Versioning.generateSemanticVersion()).resolves.toStrictEqual(`0.0.${commits}`); + await expect(BuildVersionGenerator.generateSemanticVersion()).resolves.toStrictEqual(`0.0.${commits}`); }); }); @@ -316,13 +316,13 @@ describe('Versioning', () => { it('returns true when there are files listed', async () => { const runOutput = 'file.ext\nfile2.ext'; jest.spyOn(System, 'run').mockResolvedValue(runOutput); - await expect(Versioning.isDirty()).resolves.toStrictEqual(true); + await expect(BuildVersionGenerator.isDirty()).resolves.toStrictEqual(true); }); it('returns false when there is no output', async () => { const runOutput = ''; jest.spyOn(System, 'run').mockResolvedValue(runOutput); - await expect(Versioning.isDirty()).resolves.toStrictEqual(false); + await expect(BuildVersionGenerator.isDirty()).resolves.toStrictEqual(false); }); }); @@ -330,7 +330,7 @@ describe('Versioning', () => { it('returns the commands output', async () => { const runOutput = 'v1.0'; jest.spyOn(System, 'run').mockResolvedValue(runOutput); - await expect(Versioning.getTag()).resolves.toStrictEqual(runOutput); + await expect(BuildVersionGenerator.getTag()).resolves.toStrictEqual(runOutput); }); }); @@ -338,20 +338,20 @@ describe('Versioning', () => { it('returns false when the command returns 0', async () => { const runOutput = '0'; jest.spyOn(System, 'run').mockResolvedValue(runOutput); - await expect(Versioning.hasAnyVersionTags()).resolves.toStrictEqual(false); + await expect(BuildVersionGenerator.hasAnyVersionTags()).resolves.toStrictEqual(false); }); it('returns true when the command returns >= 0', async () => { const runOutput = '9'; jest.spyOn(System, 'run').mockResolvedValue(runOutput); - await expect(Versioning.hasAnyVersionTags()).resolves.toStrictEqual(true); + await expect(BuildVersionGenerator.hasAnyVersionTags()).resolves.toStrictEqual(true); }); }); describe('getTotalNumberOfCommits', () => { it('returns a number from the command', async () => { jest.spyOn(System, 'run').mockResolvedValue('9'); - await expect(Versioning.getTotalNumberOfCommits()).resolves.toStrictEqual(9); + await expect(BuildVersionGenerator.getTotalNumberOfCommits()).resolves.toStrictEqual(9); }); }); });