Skip to content
This repository was archived by the owner on Aug 14, 2026. It is now read-only.

Commit f26eccd

Browse files
authored
feat: introduce preflight tests framework with 13 built-in checks (#27)
* refactor: make orchestrator engine-agnostic at the typed surface Remove Unity-specific licensing fields from orchestrator's typed surfaces (BuildParameters, OrchestratorConfig, CLI input mapper, build/orchestrate yargs commands, cli-plugin adapter): - unitySerial - unityLicensingServer - skipActivation These were vestigial — BuildParameters.create() hardcoded them to empty strings, no orchestrator service read them for logic. They existed only to mirror unity-builder's BuildParameters shape, which is exactly the boundary violation: orchestrator's domain is dispatch + providers, not engine-specific licensing. The plugin contract (coreParams: Record<string, any>) is already opaque and engine-agnostic. The host (unity-builder today, @game-ci/cli in the future) passes its full BuildParameters object through; orchestrator reads only generic build context (targetPlatform, projectPath, etc.) and its plugin-owned config from env/inputs. Engine-specific keys ride in the dict untouched. No companion change needed in unity-builder: it continues to construct its own BuildParameters with whatever fields it wants and pass it as coreParams. The dict's index signature accepts everything. Documentation: - Tracking issue #25 lays out the full architecture, today/future state, and migration runway. - Code comments in plugin-lifecycle.ts, interfaces.ts, build-parameters.ts, build.ts, orchestrate.ts, input-mapper.ts, build-parameters-adapter.ts reference the issue and explain the boundary intent so the next contributor understands why these fields are not (and must not be) declared here. Out of scope (separate cleanups, noted in tracking issue): - cacheUnityInstallationOnMac / unityHubVersionOnMac in input-mapper (Mac runtime install caching, more entangled) - task-parameter-serializer.ts UNITY_SERIAL well-known-secret list (well-known-secrets generalization) - activate CLI command (Unity-specific legacy helper, leave as-is) Refs game-ci/unity-builder#739 and game-ci/unity-builder#838 (the user- facing fix that motivated this boundary cleanup). * feat: introduce preflight tests framework with 13 built-in checks Preflight tests are fast, no-engine validation gates that run before expensive build dispatch and fail-fast on the first failure. This contrasts with the existing test-workflow engine, which fails-forward to surface every failure for maximum feedback per run. Users can compose a preflight suite by referencing built-in check IDs as strings, defining custom checks inline, or mixing both. The 13 built-in checks cover the common preflight surface area (pipeline contract, runner health, build/submodule profile validation, LFS health, config validation, script integrity, health-test discovery, C# heuristics, cross-profile compile, and PreUnityJob dry-run). New surfaces: - services/preflight (types, registry, service, barrel) - CLI: game-ci preflight [--suite|--list|--check] - BuildParameters.preflightSuite + action.yml input - BuildAutomationWorkflow runs preflight between pre-build hooks and build dispatch; failure aborts the build with a clear error - examples/preflight-suite.yml showing built-in + custom usage
1 parent 3c52cf8 commit f26eccd

11 files changed

Lines changed: 835 additions & 0 deletions

File tree

action.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,17 @@ inputs:
605605
required: false
606606
default: ''
607607

608+
# ─── Preflight ─────────────────────────────────────────────────────
609+
preflightSuite:
610+
required: false
611+
default: ''
612+
description: >
613+
Path to YAML preflight suite definition file. Preflight runs fast,
614+
no-engine validation checks before the build dispatch and fails fast
615+
on the first failure. Set to "default" to run the built-in fallback
616+
suite when no .game-ci/preflight-suite.yml exists. Empty disables
617+
preflight entirely.
618+
608619
# ─── Test workflow ─────────────────────────────────────────────────
609620
testSuitePath:
610621
required: false
@@ -900,6 +911,7 @@ runs:
900911
INPUT_ARTIFACTCOMPRESSION: ${{ inputs.artifactCompression }}
901912
INPUT_ARTIFACTRETENTIONDAYS: ${{ inputs.artifactRetentionDays }}
902913
INPUT_ARTIFACTCUSTOMTYPES: ${{ inputs.artifactCustomTypes }}
914+
INPUT_PREFLIGHTSUITE: ${{ inputs.preflightSuite }}
903915
INPUT_TESTSUITEPATH: ${{ inputs.testSuitePath }}
904916
INPUT_TESTSUITEEVENT: ${{ inputs.testSuiteEvent }}
905917
INPUT_TESTTAXONOMYPATH: ${{ inputs.testTaxonomyPath }}

examples/preflight-suite.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Example preflight suite -- fast no-engine validation gates that run BEFORE
2+
# build dispatch and fail-fast on the first failure.
3+
#
4+
# Drop a copy into your repo at .game-ci/preflight-suite.yml or pass a custom
5+
# path via:
6+
# - game-ci preflight --suite path/to/suite.yml (CLI)
7+
# - preflightSuite: path/to/suite.yml (action input)
8+
#
9+
# Two ways to add a check:
10+
# - String reference: a built-in check ID, e.g. "runner-health"
11+
# - Inline object: full custom check definition
12+
#
13+
# Run `game-ci preflight --list` to see all built-in check IDs.
14+
name: Default Preflight Suite
15+
description: Fast validation gates before the build
16+
17+
checks:
18+
# Built-in checks -- reference by ID
19+
- runner-health
20+
- build-profiles
21+
- lfs-health
22+
- config-validation
23+
- script-integrity
24+
25+
# Scoped built-in (only runs when matching files changed)
26+
- csharp-heuristics-changed
27+
28+
# Custom inline check
29+
- id: custom-asset-check
30+
name: Verify Critical Assets
31+
description: Ensures required asset bundles exist before build dispatch.
32+
category: integrity
33+
command: 'test -f Assets/critical-bundle.asset'
34+
timeout: 30
35+
36+
# Custom inline check with platform restriction
37+
- id: windows-only-tool-probe
38+
name: Windows Toolchain Probe
39+
description: Verifies Windows-specific build tooling is available.
40+
category: environment
41+
command: 'where msbuild'
42+
timeout: 15
43+
platforms:
44+
- win32

src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import statusCommand from './cli/commands/status';
1010
import versionCommand from './cli/commands/version';
1111
import updateCommand from './cli/commands/update';
1212
import initCommand from './cli/commands/init';
13+
import preflightCommand from './cli/commands/preflight';
1314
import * as core from '@actions/core';
1415

1516
const cli = yargs(hideBin(process.argv))
@@ -23,6 +24,7 @@ const cli = yargs(hideBin(process.argv))
2324
.command(versionCommand)
2425
.command(updateCommand)
2526
.command(initCommand)
27+
.command(preflightCommand)
2628
.demandCommand(1, 'You must specify a command. Run game-ci --help for available commands.')
2729
.strict()
2830
.alias('h', 'help')

src/cli/commands/preflight.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import type { CommandModule } from 'yargs';
2+
import * as core from '@actions/core';
3+
import {
4+
PreflightService,
5+
listBuiltInChecks,
6+
builtInChecks,
7+
} from '../../model/orchestrator/services/preflight';
8+
9+
interface PreflightArguments {
10+
suite?: string;
11+
list?: boolean;
12+
check?: string;
13+
}
14+
15+
const preflightCommand: CommandModule<object, PreflightArguments> = {
16+
command: 'preflight',
17+
describe: 'Run fast preflight validation checks before a build',
18+
builder: (yargs) => {
19+
return yargs
20+
.option('suite', {
21+
type: 'string',
22+
description:
23+
'Path to a preflight suite YAML. Defaults to .game-ci/preflight-suite.yml when omitted.',
24+
})
25+
.option('list', {
26+
type: 'boolean',
27+
description: 'List all built-in preflight checks and exit',
28+
default: false,
29+
})
30+
.option('check', {
31+
type: 'string',
32+
description: 'Run a single check by ID (built-in or defined in the suite)',
33+
})
34+
.example('game-ci preflight', 'Run the default preflight suite')
35+
.example('game-ci preflight --suite ./custom-suite.yml', 'Run a specific suite file')
36+
.example('game-ci preflight --list', 'List built-in preflight checks')
37+
.example('game-ci preflight --check runner-health', 'Run a single built-in check') as any;
38+
},
39+
handler: async (cliArguments) => {
40+
try {
41+
if (cliArguments.list) {
42+
printBuiltInList();
43+
return;
44+
}
45+
46+
if (cliArguments.check) {
47+
await runSingleCheck(cliArguments.check, cliArguments.suite);
48+
return;
49+
}
50+
51+
const suite = PreflightService.loadSuite(cliArguments.suite);
52+
core.info(`Running preflight suite: ${suite.name}`);
53+
54+
const results = await PreflightService.executeSuite(suite);
55+
PreflightService.reportResults(results);
56+
57+
if (!results.passed) {
58+
core.setFailed(`Preflight suite '${suite.name}' failed.`);
59+
process.exit(1);
60+
}
61+
} catch (error: any) {
62+
core.setFailed(`Preflight failed: ${error.message}`);
63+
throw error;
64+
}
65+
},
66+
};
67+
68+
function printBuiltInList(): void {
69+
const checks = listBuiltInChecks();
70+
core.info(`Built-in preflight checks (${checks.length}):\n`);
71+
core.info('| ID | Category | Name |');
72+
core.info('|----|----------|------|');
73+
for (const check of checks) {
74+
core.info(`| ${check.id} | ${check.category} | ${check.name} |`);
75+
}
76+
core.info('\nReference any of these IDs as a string entry in a suite file.');
77+
}
78+
79+
async function runSingleCheck(checkId: string, suitePath?: string): Promise<void> {
80+
// First, try the built-in registry. If not found, fall back to the suite
81+
// file (the user may be running a custom check defined inline there).
82+
let check = builtInChecks.get(checkId);
83+
84+
if (!check && suitePath) {
85+
const suite = PreflightService.loadSuite(suitePath);
86+
const resolved = PreflightService.resolveChecks(suite);
87+
check = resolved.find((c) => c.id === checkId);
88+
}
89+
90+
if (!check) {
91+
throw new Error(
92+
`Check '${checkId}' not found. Use 'game-ci preflight --list' to see built-in checks, ` +
93+
`or pass --suite <path> if the check is defined in a custom suite.`,
94+
);
95+
}
96+
97+
const result = await PreflightService.executeCheck(check);
98+
PreflightService.reportResults({
99+
passed: result.passed,
100+
results: [result],
101+
duration: result.duration,
102+
failedAt: result.passed ? undefined : 0,
103+
});
104+
105+
if (!result.passed) {
106+
core.setFailed(`Check '${checkId}' failed.`);
107+
process.exit(1);
108+
}
109+
}
110+
111+
export default preflightCommand;

src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@ export type {
6161
UnityRecoveryDecision,
6262
} from './model/orchestrator/services/reliability';
6363
export { TestWorkflowService } from './model/orchestrator/services/test-workflow';
64+
export {
65+
PreflightService,
66+
builtInChecks,
67+
listBuiltInCheckIds,
68+
listBuiltInChecks,
69+
} from './model/orchestrator/services/preflight';
70+
export type {
71+
PreflightCheck,
72+
PreflightCategory,
73+
PreflightScope,
74+
PreflightSuiteDefinition,
75+
PreflightResult,
76+
PreflightSuiteResult,
77+
} from './model/orchestrator/services/preflight';
6478
export { HotRunnerService } from './model/orchestrator/services/hot-runner';
6579
export { OutputService } from './model/orchestrator/services/output/output-service';
6680
export { OutputTypeRegistry } from './model/orchestrator/services/output/output-type-registry';

src/model/build-parameters.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@ class BuildParameters {
157157
// ── test workflow ───────────────────────────────────────────────────
158158
testSuitePath!: string;
159159

160+
// ── preflight ───────────────────────────────────────────────────────
161+
// Path to the preflight suite YAML. Empty string disables preflight.
162+
// The literal string 'default' runs the built-in fallback suite when
163+
// no .game-ci/preflight-suite.yml exists.
164+
preflightSuite!: string;
165+
160166
// ── artifact / output ───────────────────────────────────────────────
161167
artifactCustomTypes!: string;
162168
artifactOutputTypes!: string;
@@ -249,6 +255,7 @@ class BuildParameters {
249255
p.gitPrivateToken = Input.getInput('gitPrivateToken') || process.env.GIT_PRIVATE_TOKEN || '';
250256
p.engine = Input.getInput('engine') || 'unity';
251257
p.enginePlugin = Input.getInput('enginePlugin') || '';
258+
p.preflightSuite = Input.getInput('preflightSuite') || '';
252259

253260
// Initialize the engine plugin (Unity is built-in, others require enginePlugin source)
254261
initEngine(p.engine, p.enginePlugin || undefined);

0 commit comments

Comments
 (0)