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

Commit 2d91e20

Browse files
authored
Add injected Unity test filters and preset-based suites (#28)
* 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 * Add injected Unity test filters and preset-based suites
1 parent f26eccd commit 2d91e20

12 files changed

Lines changed: 717 additions & 108 deletions

action.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,18 @@ inputs:
624624
testSuiteEvent:
625625
required: false
626626
default: ''
627+
testFilterRefs:
628+
required: false
629+
default: ''
630+
description: 'Comma-separated suite filter preset names to inject into every test run.'
631+
testFilterInjection:
632+
required: false
633+
default: ''
634+
description: 'Inline YAML or JSON filter overlay injected into every test run. Supports refs, filters, and filterSets.'
635+
testFilterInjectionPath:
636+
required: false
637+
default: ''
638+
description: 'Path to a YAML or JSON test filter overlay file injected into every test run.'
627639
testTaxonomyPath:
628640
required: false
629641
default: ''
@@ -914,6 +926,9 @@ runs:
914926
INPUT_PREFLIGHTSUITE: ${{ inputs.preflightSuite }}
915927
INPUT_TESTSUITEPATH: ${{ inputs.testSuitePath }}
916928
INPUT_TESTSUITEEVENT: ${{ inputs.testSuiteEvent }}
929+
INPUT_TESTFILTERREFS: ${{ inputs.testFilterRefs }}
930+
INPUT_TESTFILTERINJECTION: ${{ inputs.testFilterInjection }}
931+
INPUT_TESTFILTERINJECTIONPATH: ${{ inputs.testFilterInjectionPath }}
917932
INPUT_TESTTAXONOMYPATH: ${{ inputs.testTaxonomyPath }}
918933
INPUT_TESTRESULTFORMAT: ${{ inputs.testResultFormat }}
919934
INPUT_TESTRESULTPATH: ${{ inputs.testResultPath }}

src/cli-plugin/build-parameters-adapter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ export function createBuildParametersFromCliOptions(options: Record<string, any>
157157

158158
// ── test workflow ─────────────────────────────────────────────────
159159
bp.testSuitePath = options.testSuitePath || '';
160+
bp.testFilterRefs = options.testFilterRefs || '';
161+
bp.testFilterInjection = options.testFilterInjection || '';
162+
bp.testFilterInjectionPath = options.testFilterInjectionPath || '';
160163

161164
// ── artifact / output ─────────────────────────────────────────────
162165
bp.artifactCustomTypes = options.artifactCustomTypes || '';

src/model/build-parameters.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ class BuildParameters {
156156

157157
// ── test workflow ───────────────────────────────────────────────────
158158
testSuitePath!: string;
159+
testFilterRefs!: string;
160+
testFilterInjection!: string;
161+
testFilterInjectionPath!: string;
159162

160163
// ── preflight ───────────────────────────────────────────────────────
161164
// Path to the preflight suite YAML. Empty string disables preflight.
@@ -256,6 +259,10 @@ class BuildParameters {
256259
p.engine = Input.getInput('engine') || 'unity';
257260
p.enginePlugin = Input.getInput('enginePlugin') || '';
258261
p.preflightSuite = Input.getInput('preflightSuite') || '';
262+
p.testSuitePath = Input.getInput('testSuitePath') || '';
263+
p.testFilterRefs = Input.getInput('testFilterRefs') || '';
264+
p.testFilterInjection = Input.getInput('testFilterInjection') || '';
265+
p.testFilterInjectionPath = Input.getInput('testFilterInjectionPath') || '';
259266

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

src/model/orchestrator/services/test-workflow/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
export { TestSuiteParser } from './test-suite-parser';
2+
export { TestFilterResolutionService } from './test-filter-resolution-service';
23
export { TaxonomyFilterService } from './taxonomy-filter-service';
34
export { TestResultReporter } from './test-result-reporter';
45
export { TestWorkflowService } from './test-workflow-service';
56
export {
7+
LegacyTaxonomyFilters,
8+
ResolvedTestFilter,
9+
TestCategoryFilterDefinition,
10+
TestFilterDefinition,
11+
TestFilterInjectionDefinition,
12+
TestNameFilterDefinition,
613
TestSuiteDefinition,
714
TestRunDefinition,
815
TaxonomyDimension,

src/model/orchestrator/services/test-workflow/taxonomy-filter-service.ts

Lines changed: 25 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import fs from 'node:fs';
22
import YAML from 'yaml';
3-
import { TaxonomyDimension, TaxonomyDefinition } from './test-workflow-types';
3+
import { ResolvedTestFilter, TaxonomyDimension, TaxonomyDefinition } from './test-workflow-types';
44

55
/**
6-
* Manages test taxonomy dimensions and builds filter arguments for
7-
* the Unity test runner CLI. Supports comma-separated value lists,
8-
* regex patterns (/pattern/), and hierarchical dot-notation matching.
6+
* Manages taxonomy dimensions and compiles resolved category/name filters into
7+
* Unity test runner CLI arguments.
98
*/
109
export class TaxonomyFilterService {
1110
/**
@@ -57,71 +56,36 @@ export class TaxonomyFilterService {
5756
}
5857

5958
/**
60-
* Convert a filter map to Unity test runner CLI args (--testFilter).
59+
* Convert resolved orchestrator filters to Unity CLI args.
6160
*
62-
* Each filter dimension becomes a category expression. Multiple values in one
63-
* dimension are OR'd; multiple dimensions are AND'd. The result is a single
64-
* --testFilter string suitable for passing to Unity's test runner CLI.
65-
*
66-
* Regex patterns (values wrapped in /.../) are converted to category regex
67-
* expressions supported by the Unity test runner.
61+
* Category filters are emitted via `-testCategory`, which Unity documents as
62+
* the category-selection argument. Test name / regex filters are emitted via
63+
* `-testFilter`. Negated entries are prefixed with `!`.
6864
*/
69-
static buildFilterArgs(filters: Record<string, string>): string {
70-
if (!filters || Object.keys(filters).length === 0) {
71-
return '';
65+
static buildFilterArgs(filter: ResolvedTestFilter): string[] {
66+
if (!filter) {
67+
return [];
7268
}
7369

74-
const categoryExpressions: string[] = [];
75-
76-
for (const [dimension, valueSpec] of Object.entries(filters)) {
77-
const expression = TaxonomyFilterService.buildDimensionExpression(dimension, valueSpec);
78-
if (expression) {
79-
categoryExpressions.push(expression);
80-
}
81-
}
82-
83-
if (categoryExpressions.length === 0) {
84-
return '';
85-
}
86-
87-
// Unity test runner uses --testFilter with category expressions
88-
// Multiple dimensions are AND'd by joining with ';'
89-
const filterString = categoryExpressions.join(';');
90-
return `--testFilter "${filterString}"`;
91-
}
92-
93-
/**
94-
* Build a filter expression for a single taxonomy dimension.
95-
*/
96-
private static buildDimensionExpression(dimension: string, valueSpec: string): string {
97-
if (!valueSpec || valueSpec.trim() === '') {
98-
return '';
99-
}
100-
101-
const trimmed = valueSpec.trim();
102-
103-
// Check if the value is a regex pattern: /pattern/
104-
if (trimmed.startsWith('/') && trimmed.endsWith('/') && trimmed.length > 2) {
105-
const pattern = trimmed.slice(1, -1);
106-
return `${dimension}=~${pattern}`;
107-
}
108-
109-
// Comma-separated values: OR'd together
110-
const values = trimmed
111-
.split(',')
112-
.map((v) => v.trim())
113-
.filter((v) => v.length > 0);
114-
115-
if (values.length === 0) {
116-
return '';
70+
const args: string[] = [];
71+
const categoryTokens = [
72+
...filter.categories.include,
73+
...filter.categories.exclude.map((value) => `!${value}`),
74+
];
75+
const nameTokens = [
76+
...filter.names.include,
77+
...filter.names.exclude.map((value) => `!${value}`),
78+
];
79+
80+
if (categoryTokens.length > 0) {
81+
args.push(`-testCategory "${categoryTokens.join(';')}"`);
11782
}
11883

119-
if (values.length === 1) {
120-
return `${dimension}=${values[0]}`;
84+
if (nameTokens.length > 0) {
85+
args.push(`-testFilter "${nameTokens.join(';')}"`);
12186
}
12287

123-
// Multiple values: use pipe-separated OR syntax
124-
return `${dimension}=${values.join('|')}`;
88+
return args;
12589
}
12690

12791
/**

0 commit comments

Comments
 (0)