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
67 changes: 67 additions & 0 deletions src/command-options/unity-logs-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { YargsInstance } from '../dependencies.ts';
import { IOptions } from './options-interface.ts';

/**
* Unity diagnostic log collection options.
*
* Mirrors the orchestrator's `collectUnityLogs` / `streamUnityLogs` inputs so
* the same flags work whether the user runs `game-ci build` directly against
* Docker or `game-ci orchestrate` against a remote provider.
*
* The orchestrator package owns the canonical path registry; the CLI gathers
* the most commonly requested files (Editor.log, Unity.Licensing.Client.log,
* Unity.Entitlements.Audit.log, services-config.json) from the host.
*/
export class UnityLogsOptions implements IOptions {
public static configure(yargs: YargsInstance): void {
yargs
.option('collectUnityLogs', {
description: String.dedent`
Collect Unity-internal logs (Editor.log, Unity.Licensing.Client.log,
Unity.Entitlements.Audit.log, services-config.json, …) into the
workspace after the build so they can be uploaded as artifacts.
Useful when Unity support requests these files.`,
type: 'boolean',
demandOption: false,
default: false,
})
.option('collectUnityLogsOnSuccess', {
description: 'Also collect Unity logs on successful builds (default true).',
type: 'boolean',
demandOption: false,
default: true,
})
.option('unityLogCategories', {
description: String.dedent`
Comma-separated subset of categories to collect. Default = all
non-sensitive categories. See orchestrator docs for the full list.`,
type: 'string',
demandOption: false,
default: '',
})
.option('unityLogsIncludeSensitive', {
description: 'Include sensitive categories like license-file. Off by default.',
type: 'boolean',
demandOption: false,
default: false,
})
.option('unityLogsOutputDir', {
description: 'Override the directory for collected Unity logs. Default <workspace>/Logs/UnityDiagnostics.',
type: 'string',
demandOption: false,
default: '',
})
.option('streamUnityLogs', {
description: 'Live-tail Unity log files during the build and forward each line to stdout.',
type: 'boolean',
demandOption: false,
default: false,
})
.option('streamUnityLogPaths', {
description: 'Comma-separated files to live-tail. Defaults to <project>/Builds/Logs/Editor.log.',
type: 'string',
demandOption: false,
default: '',
});
}
}
63 changes: 59 additions & 4 deletions src/command/build/unity-build-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { CommandInterface } from '../command-interface.ts';
import { CacheValidation, Docker, RunnerImageTag, Output } from '../../model/index.ts';
import { PlatformSetup } from '../../model/platform-setup.ts';
import { MacBuilder } from '../../model/mac-builder.ts';
import { UnityLogs } from '../../model/unity-logs.ts';
import { path } from '../../dependencies.ts';
import { CommandBase } from '../command-base.ts';
import { UnityOptions } from '../../command-options/unity-options.ts';
import type { YargsInstance, Options } from '../../dependencies.ts';
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 { UnityLogsOptions } from '../../command-options/unity-logs-options.ts';
import { PlatformValidation } from '../../logic/unity/platform-validation/platform-validation.ts';
import { ProjectOptions } from '../../command-options/project-options.ts';

Expand All @@ -22,12 +25,63 @@ export class UnityBuildCommand extends CommandBase implements CommandInterface {
if (log.isVerbose) log.debug('Using image:', image);

await PlatformSetup.setup(options);
if (hostPlatform === 'darwin') {
await MacBuilder.run(options);
} else {
await Docker.run(image.toString(), options);

let stopTail: (() => void) | undefined;
if (options.streamUnityLogs) {
const projectDir: string =
(options as any).projectPath || (options as any).workspace || process.cwd();
const explicit = String(options.streamUnityLogPaths || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
const defaults = [path.join(projectDir, 'Builds', 'Logs', 'Editor.log')];
stopTail = UnityLogs.streamFiles(explicit.length > 0 ? explicit : defaults);
log.info('[UnityLogs] Live log streaming started');
}

let buildError: unknown;
let buildSucceeded = true;
try {
if (hostPlatform === 'darwin') {
await MacBuilder.run(options);
} else {
await Docker.run(image.toString(), options);
}
} catch (error) {
buildError = error;
buildSucceeded = false;
} finally {
if (stopTail) {
try {
stopTail();
} catch {
// ignore
}
}
}

if (
options.collectUnityLogs &&
(!buildSucceeded || options.collectUnityLogsOnSuccess !== false)
) {
try {
const projectDir: string =
(options as any).projectPath || (options as any).workspace || process.cwd();
const workspace: string = (options as any).workspace || projectDir;
UnityLogs.collect({
workspace,
projectPath: projectDir,
outputDir: options.unityLogsOutputDir || undefined,
categories: UnityLogs.parseCategories(options.unityLogCategories),
includeSensitive: !!options.unityLogsIncludeSensitive,
});
} catch (collectError: any) {
log.warning(`[UnityLogs] collection failed: ${collectError.message}`);
}
}

if (buildError) throw buildError;

await Output.setBuildVersion(options.buildVersion);
await Output.setAndroidVersionCode(options.androidVersionCode);

Expand All @@ -40,5 +94,6 @@ export class UnityBuildCommand extends CommandBase implements CommandInterface {
await VersioningOptions.configure(yargs);
await BuildOptions.configure(yargs);
await AndroidOptions.configure(yargs);
await UnityLogsOptions.configure(yargs);
}
}
112 changes: 112 additions & 0 deletions src/command/logs/unity-logs-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { YargsArguments, YargsInstance } from '../../dependencies.ts';
import { CommandBase } from '../command-base.ts';
import { CommandInterface } from '../command-interface.ts';
import { UnityLogs } from '../../model/unity-logs.ts';
import { UnityLogsOptions } from '../../command-options/unity-logs-options.ts';

/**
* Top-level dispatch for `game-ci logs <subcommand>`.
*
* Subcommands:
* collect - run the Unity log collector against the local host (or a path)
* tail - live-tail one or more Unity log files
* pull - (stub) collect from a remote runner via the provider interface
* fetch - (stub) fetch logs persisted from a past orchestrator build
*/
export class UnityLogsCommand extends CommandBase implements CommandInterface {
private readonly subCommand: string;

constructor(commandName: string, subCommand: string) {
super(commandName);
this.subCommand = subCommand || 'collect';
}

public async execute(options: YargsArguments): Promise<boolean> {
switch (this.subCommand) {
case 'collect':
return this.runCollect(options);
case 'tail':
return this.runTail(options);
case 'pull':
log.warning(
'[logs pull] Remote provider pulls are not implemented yet. ' +
'For now, ssh into the runner and run `game-ci logs collect` directly. ' +
'Tracking: https://github.com/game-ci/orchestrator/issues',
);
return false;
case 'fetch':
log.warning(
'[logs fetch] Retroactive fetch from past orchestrator builds is not implemented yet. ' +
'For now, the diagnostic bundle is uploaded as a build artifact when collectUnityLogs=true. ' +
'Tracking: https://github.com/game-ci/orchestrator/issues',
);
return false;
default:
log.error(
`Unknown logs subcommand: ${this.subCommand}. ` +
`Valid subcommands: collect, tail, pull, fetch.`,
);
return false;
}
}

public async configureOptions(yargs: YargsInstance): Promise<void> {
yargs
.option('projectPath', {
description: 'Path to the Unity project (defaults to cwd).',
type: 'string',
demandOption: false,
default: '',
})
.option('workspace', {
description: 'Workspace root (defaults to projectPath or cwd).',
type: 'string',
demandOption: false,
default: '',
});
await UnityLogsOptions.configure(yargs);
}

private async runCollect(options: YargsArguments): Promise<boolean> {
const workspace = (options.workspace as string) || (options.projectPath as string) || process.cwd();
const projectPath = (options.projectPath as string) || workspace;
const result = UnityLogs.collect({
workspace,
projectPath,
outputDir: (options.unityLogsOutputDir as string) || undefined,
categories: UnityLogs.parseCategories(options.unityLogCategories as string | undefined),
includeSensitive: !!options.unityLogsIncludeSensitive,
});

log.info(`[logs collect] Collected ${result.collected.length} item(s) → ${result.outputDir}`);
if (result.missing.length > 0) {
log.info(`[logs collect] Missing categories on this host: ${result.missing.join(', ')}`);
}
return false;
}

private async runTail(options: YargsArguments): Promise<boolean> {
const projectPath =
(options.projectPath as string) || (options.workspace as string) || process.cwd();
const explicit = String(options.streamUnityLogPaths || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
const defaults = [`${projectPath}/Builds/Logs/Editor.log`];
const files = explicit.length > 0 ? explicit : defaults;

log.info(`[logs tail] Tailing ${files.length} file(s) — Ctrl+C to stop`);
const stop = UnityLogs.streamFiles(files);

const onSignal = (): void => {
stop();
process.exit(0);
};
process.on('SIGINT', onSignal);
process.on('SIGTERM', onSignal);

return new Promise<boolean>(() => {
// Resolves only on signal; tail runs until interrupted.
});
}
}
Loading
Loading