Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src/cli-commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { YargsInstance } from './dependencies.ts';
import { ProjectOptions } from './command-options/project-options.ts';

/**
* Register commands
*
* Bare minimum needed to register the commands. Any specific configuration (e.g. middleware) should be done from within
* the command itself through `CommandInterface.configureOptions`.
*
* @param yargs the global yargs instance
* @param register a function that registers the command. must be called from within the builder of each command.
*/
export class CliCommands {
private readonly yargs: YargsInstance;
private readonly register: (yargs: YargsInstance) => void;

constructor(yargs: YargsInstance, register: (yargs: YargsInstance) => void) {
this.yargs = yargs;
this.register = register;
}

public async registerAll() {
await this.configCommand();
await this.testCommand();
await this.buildCommand();
await this.remoteCommands();
}

private async configCommand() {
await this.yargs.command('config', 'GameCI CLI configuration', async (yargs: YargsInstance) => {
yargs.command('open', 'Opens the CLI configuration folder', async (yargs: YargsInstance) => {});
this.register(yargs);
});
}

private async testCommand() {
await this.yargs.command('test [projectPath]', 'Runs the tests of a given project', async (yargs) => {
ProjectOptions.preConfigure(yargs);
this.register(yargs);
});
}

private async buildCommand() {
await this.yargs.command('build [projectPath]', 'Builds a given project', async (yargs) => {
ProjectOptions.preConfigure(yargs);
this.register(yargs);
});
}

private async remoteCommands() {
await this.yargs.command('remote', 'Schedule jobs to be run remotely, in the cloud', async (yargs) => {
yargs
.command('build [projectPath]', 'Schedule a build to be run remotely', async (yargs) => {
ProjectOptions.preConfigure(yargs);
this.register(yargs);
})
.command('otherSubCommand', 'Other sub command', async (yargs) => {
// Todo - implement all subcommands
ProjectOptions.preConfigure(yargs);
this.register(yargs);
});
});
}
}
105 changes: 30 additions & 75 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import { yaml, yargs, YargsInstance, YargsArguments, getHomeDir, __dirname, path
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 { ProjectOptions } from './command-options/project-options.ts';
import { UnityBuildCommand } from './command/build/unity-build-command.ts';
import { NonExistentCommand } from './command/null/non-existent-command.ts';
import { CliCommands } from './cli-commands.ts';

export class Cli {
private readonly yargs: YargsInstance;
Expand All @@ -25,31 +24,45 @@ export class Cli {
this.currentWorkDir = cwd;
this.isRunningLocally = !Boolean(Deno.env.get('CI'));
this.hostPlatform = process.platform;
this.command = new NonExistentCommand('non-existent');

// Todo make these variables portable when generating the cli binary
this.cliPath = __dirname;
this.cliDistPath = path.join(path.dirname(__dirname), 'dist');
}

public async setup() {
await this.configureLogger();
await this.configureGlobalSettings();
await this.configureGlobalOptions();
}

public async registerCommands() {
const register = (yargs) => yargs.middleware([this.registerCommand.bind(this)]);
await new CliCommands(this.yargs, register).registerAll();
}

public async registerSchemaForChosenCommand() {
await this.yargs.parseAsync();
await this.command.configureOptions(this.yargs);
}

public async validateAndParseArguments() {
await this.registerConfigCommand();
await this.registerBuildCommand();
await this.registerRemoteCommand();
// Parsing may happen many times before this point as well.
const options = await this.finalParse();

if (log.isVeryVerbose) {
console.log('cliPath', this.cliPath);
console.log('distPath', this.cliDistPath);
}

await this.parse();

return {
command: this.command,
options: this.options,
options,
};
}

public async configureLogger() {
private async configureLogger() {
await this.yargs
.options('quiet', {
alias: 'q',
Expand Down Expand Up @@ -84,7 +97,7 @@ export class Cli {
.parseAsync();
}

public async configureGlobalSettings() {
protected async configureGlobalSettings() {
const defaultCanonicalPath = `${this.cliStorageCanonicalPath}/${this.configFileName}`;
const defaultAbsolutePath = `${this.cliStorageAbsolutePath}/${this.configFileName}`;

Expand All @@ -109,7 +122,7 @@ export class Cli {
// this.yargs.env();
}

public async configureGlobalOptions() {
protected async configureGlobalOptions() {
this.yargs
.config('config', `default: .game-ci.yml`, async (override: string) => {
// Todo - remove hardcoded. Yargs override seems to be bugged though.
Expand All @@ -125,85 +138,27 @@ export class Cli {
.default('hostPlatform', this.hostPlatform);
}

private async registerBuildCommand() {
this.yargs.command('build [projectPath]', 'Builds a project that you want to build', async (yargs) => {
ProjectOptions.configure(yargs);

yargs
// Todo - remove these lines with release 3.0.0
.option('unityVersion', {
describe: 'Override the engine version to be used',
type: 'string',
default: '',
})
.deprecateOption('unityVersion', 'This parameter will be removed. Use engineVersion instead')
.middleware(
[
async (args) => {
if (!args.unityVersion || args.unityVersion === 'auto' || args.engine !== Engine.unity) return;

args.engineVersion = args.unityVersion;
args.unityVersion = undefined;
},
],
true,
)

// End todo
.middleware([async (args) => this.registerCommand(args, yargs)]);

// Todo - remove hardcoded command - ideally this lives inside middleware
// however, middleware runs while args are being parsed, which means
// that we can not add new options to the command during middleware time.
await new UnityBuildCommand('test').configureOptions(yargs);
});
}

private registerRemoteCommand() {
this.yargs.command('remote', 'Schedule jobs to be run remotely, in the cloud', async (yargs) => {
yargs
.command('build [projectPath]', 'Schedule a build to be run remotely', async (yargs) => {
ProjectOptions.configure(yargs);
yargs.middleware([async (args) => this.registerCommand(args, yargs)]);
})
.command('otherSubCommand', 'Other sub command', async (yargs) => {
// Todo - implement all subcommands
yargs.middleware([async (args) => this.registerCommand(args, yargs)]);
});
});
}

private async registerConfigCommand() {
this.yargs.command('config', 'GameCI CLI configuration', async (yargs: YargsInstance) => {
yargs
.command('open', 'Opens the CLI configuration folder', async (yargs: YargsInstance) => {})
.middleware([async (args) => this.registerCommand(args, yargs)]);
});
}

private async registerCommand(args: YargsArguments, yargs: YargsInstance) {
private async registerCommand(args: YargsArguments) {
const { engine, engineVersion, _: command } = args;
this.command = new CommandFactory().selectEngine(engine, engineVersion).createCommand(command);

await this.command.configureOptions(yargs);
}

private async parse() {
protected async finalParse() {
const { _, $0, ...options } = await this.yargs.parseAsync();

if (log.isVeryVerbose) log.info('parsed:', _, $0, options);

this.options = options;
return options;
}

private static handleFailure(message: string, error: Error, yargs: YargsInstance) {
protected static handleFailure(message: string, error: Error, yargs: YargsInstance) {
if (error) throw error;

log.warning(message);
Deno.exit(1);
}

private async loadConfig(configPath: string) {
protected async loadConfig(configPath: string) {
try {
let configFile = await Deno.readTextFile(configPath);

Expand Down
3 changes: 2 additions & 1 deletion src/command-options/android-options.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { YargsInstance, YargsArguments } from '../dependencies.ts';
import { IOptions } from './options-interface.ts';

export class AndroidOptions {
export class AndroidOptions implements IOptions {
public static configure(yargs: YargsInstance): void {
yargs
.option('androidAppBundle', {
Expand Down
3 changes: 2 additions & 1 deletion src/command-options/build-options.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { YargsArguments, YargsInstance } from '../dependencies.ts';
import UnityTargetPlatform from '../model/unity/target-platform/unity-target-platform.ts';
import { IOptions } from './options-interface.ts';

export class BuildOptions {
export class BuildOptions implements IOptions {
public static configure(yargs: YargsInstance): void {
yargs
.demandOption('targetPlatform', 'Target platform is mandatory for builds')
Expand Down
5 changes: 5 additions & 0 deletions src/command-options/options-interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { YargsInstance } from '../dependencies.ts';

export class IOptions {
static configure: (yargs: YargsInstance) => Promise<void> | void;
}
20 changes: 16 additions & 4 deletions src/command-options/project-options.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
import { getHomeDir, YargsInstance } from '../dependencies.ts';
import { engineDetection } from '../middleware/engine-detection/index.ts';
import { vcsDetection } from '../middleware/vcs-detection/index.ts';
import { IOptions } from './options-interface.ts';

export class ProjectOptions {
public static configure(yargs: YargsInstance): void {
export class ProjectOptions implements IOptions {
/**
* Configuration, used by middleware to detect the engine and VCS.
*
* Note: keep this method to a minimum, as it is processed before showing the help message.
*/
public static preConfigure(yargs: YargsInstance): void {
yargs
.positional('projectPath', {
describe: 'Path to the project',
type: 'string',
demandOption: false,
default: '.',
})
.coerce('projectPath', async (arg) => {
return arg.replace(/^~/, getHomeDir()).replace(/\/$/, '');
.coerce('projectPath', async (arg: string) => {
const homeDir = getHomeDir();

if (homeDir === null) throw new Error('Could not determine home directory');

return arg.replace(/^~/, homeDir).replace(/\/$/, '');
})
.default('engine', '')
.default('engineVersion', '')
.middleware([engineDetection], true)
.default('branch', '')
.middleware([vcsDetection], true);
}

public static async configure(yargs: YargsInstance): Promise<void> {}
}
5 changes: 3 additions & 2 deletions src/command-options/remote-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { GitRepoReader } from '../model/input-readers/git-repo.ts';
import { GithubCliReader } from '../model/input-readers/github-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 { IOptions } from './options-interface.ts';

export class RemoteOptions {
public static configure(yargs: YargsInstance): void {
export class RemoteOptions implements IOptions {
public static async configure(yargs: YargsInstance): Promise<void> {
// const cloudRunnerCluster = Cli.isCliMode
// ? this.input.getInput('cloudRunnerCluster') || 'aws'
// : this.input.getInput('cloudRunnerCluster') || 'local';
Expand Down
9 changes: 6 additions & 3 deletions src/command-options/unity-options.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { YargsInstance } from '../dependencies.ts';
import UnityTargetPlatform from '../model/unity/target-platform/unity-target-platform.ts';
import { UnityTargetPlatforms } from '../model/unity/target-platform/unity-target-platforms.ts';
import { IOptions } from './options-interface.ts';

export class UnityOptions {
public static configure = async (yargs: YargsInstance) => {
export class UnityOptions implements IOptions {
public static configure = async (yargs: YargsInstance): Promise<void> => {
yargs
.option('targetPlatform', {
alias: 't',
Expand Down Expand Up @@ -42,7 +43,9 @@ export class UnityOptions {
default: '',
},
})
.coerce('unityLicense', async (arg) => {
.coerce('unityLicense', async (arg: string) => {
if (arg.endsWith('.alf')) throw new Error('Unity License File (.ulf) expected, but got .alf');

return arg.endsWith('.ulf') ? Deno.readTextFile(arg, { encoding: 'utf8' }) : arg;
})
.option('customImage', {
Expand Down
5 changes: 3 additions & 2 deletions src/command-options/versioning-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ 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';
import { IOptions } from './options-interface.ts';

export class VersioningOptions {
public static async configure(yargs: YargsInstance): void {
export class VersioningOptions implements IOptions {
public static async configure(yargs: YargsInstance): Promise<void> {
yargs
.option('versioningStrategy', {
description: 'Versioning strategy',
Expand Down
4 changes: 3 additions & 1 deletion src/command/build/unity-build-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { VersioningOptions } from '../../command-options/versioning-options.ts';
import { BuildOptions } from '../../command-options/build-options.ts';
import { AndroidOptions } from '../../command-options/android-options.ts';
import { PlatformValidation } from '../../logic/unity/platform-validation/platform-validation.ts';
import { ProjectOptions } from '../../command-options/project-options.ts';

export class UnityBuildCommand extends CommandBase implements CommandInterface {
public async execute(options: YargsArguments): Promise<boolean> {
Expand All @@ -19,7 +20,7 @@ export class UnityBuildCommand extends CommandBase implements CommandInterface {
CacheValidation.verify(options);

const baseImage = new RunnerImageTag(options);
log.debug('Using image:', baseImage);
if (log.isVerbose) log.debug('Using image:', baseImage);
//
// await PlatformSetup.setup(parameters, actionFolder);
// if (env.getOS() === 'darwin') {
Expand All @@ -35,6 +36,7 @@ export class UnityBuildCommand extends CommandBase implements CommandInterface {
}

public async configureOptions(yargs: YargsInstance): Promise<void> {
await ProjectOptions.configure(yargs);
await UnityOptions.configure(yargs);
await VersioningOptions.configure(yargs);
await BuildOptions.configure(yargs);
Expand Down
6 changes: 4 additions & 2 deletions src/command/remote/unity-remote-build-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import CloudRunnerBuildGuid from '../../model/cloud-runner/services/cloud-runner
import { GithubCliReader } from '../../model/input-readers/github-cli.ts';
import { CommandBase } from '../command-base.ts';
import { RemoteOptions } from '../../command-options/remote-options.ts';
import { ProjectOptions } from '../../command-options/project-options.ts';

// Todo - Verify this entire flow
export class UnityRemoteBuildCommand extends CommandBase implements CommandInterface {
Expand All @@ -27,7 +28,8 @@ export class UnityRemoteBuildCommand extends CommandBase implements CommandInter
return false;
}

configureOptions(yargs: YargsInstance): Promise<void> {
RemoteOptions.configure(yargs);
public async configureOptions(yargs: YargsInstance): Promise<void> {
await ProjectOptions.configure(yargs);
await RemoteOptions.configure(yargs);
}
}
Loading