Skip to content

chore: reclassify build-lifecycle capabilities back to plugins, keep only symbols #274

chore: reclassify build-lifecycle capabilities back to plugins, keep only symbols

chore: reclassify build-lifecycle capabilities back to plugins, keep only symbols #274

name: Engine Smoke Tests
on:
push:
branches: [main, 'release/**', 'feat/**']
pull_request:
branches: [main, 'release/**']
workflow_dispatch:
inputs:
unity-version:
description: 'Unity Editor version'
required: false
default: '2019.4.40f1'
godot-version:
description: 'Godot CI image tag'
required: false
default: '4.3'
unreal-image:
description: 'Custom Unreal Engine Docker image (optional, leave empty to skip UE test)'
required: false
default: ''
permissions:
contents: read
jobs:
# ─── Which engines does this PR actually touch? ────────────────
# These jobs pull multi-GB engine images (the Unity Windows pull alone
# runs ~6 minutes), so running all of them on every PR is the single
# biggest drag on review throughput. Anything touching the CLI core or
# the plugin interface is cross-cutting and still runs everything.
changes:
name: Detect changed engines
runs-on: ubuntu-latest
outputs:
core: ${{ steps.filter.outputs.core }}
unity: ${{ steps.filter.outputs.unity }}
godot: ${{ steps.filter.outputs.godot }}
unreal: ${{ steps.filter.outputs.unreal }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
core:
- 'src/cli.ts'
- 'src/cli-commands.ts'
- 'src/dependencies.ts'
- 'src/index.ts'
- 'src/plugin/**'
- 'src/model/docker.ts'
- 'src/model/system/**'
- 'src/command/command-factory.ts'
- 'package.json'
- '.github/workflows/engine-smoke-test.yml'
unity:
- 'src/logic/unity/**'
- 'src/model/unity/**'
- 'src/model/unity-*.ts'
- 'src/command/activate/**'
- 'src/command/**/unity-*.ts'
- 'src/command-options/unity-*.ts'
- 'src/plugin/builtin/unity-plugin.ts'
- 'src/middleware/engine-detection/unity-version-detector.ts'
- 'dist/**'
godot:
- 'src/plugin/builtin/godot-plugin.ts'
- 'src/**/godot*/**'
unreal:
- 'src/plugin/builtin/unreal-plugin.ts'
- 'src/**/unreal*/**'
# ─── Unity — detect + CLI-driven stub build ────────────────────
unity-build:
name: Unity stub build
needs: changes
if: needs.changes.outputs.core == 'true' || needs.changes.outputs.unity == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Test Unity engine detection
run: |
bun -e "
import { UnityVersionDetector } from './src/middleware/engine-detection/unity-version-detector.ts';
const v = UnityVersionDetector.getUnityVersion('test-project');
console.log('Detected Unity version:', v);
if (v !== '2019.4.40f1') throw new Error('Expected 2019.4.40f1, got ' + v);
"
- name: Build Unity CI stub
run: |
mkdir -p /tmp/unity-stub-ctx
cat > /tmp/unity-stub-ctx/unity-editor <<'SCRIPT'
#!/bin/bash
set -euo pipefail
echo "Unity Editor CI stub"
if printf '%s\n' "$*" | grep -q -- '-manualLicenseFile'; then
echo "LICENSE SYSTEM [CI Stub] Next license update check is after 2099-01-01T00:00:00"
exit 0
fi
BUILD_PATH=""
while [ "$#" -gt 0 ]; do
case "$1" in
-customBuildPath)
BUILD_PATH="$2"
shift 2
;;
*)
shift
;;
esac
done
if [ -n "$BUILD_PATH" ]; then
mkdir -p "$(dirname "$BUILD_PATH")"
echo "Unity CLI smoke artifact" > "$BUILD_PATH"
fi
cat <<'RESULTS'
###########################
# Build results #
###########################
Duration: 00:00:01
Warnings: 0
Errors: 0
Size: 1 bytes
RESULTS
exit 0
SCRIPT
chmod +x /tmp/unity-stub-ctx/unity-editor
cat > /tmp/unity-stub-ctx/Dockerfile <<'DOCKERFILE'
FROM alpine:3.19
RUN apk add --no-cache bash coreutils git
COPY unity-editor /usr/local/bin/unity-editor
RUN chmod +x /usr/local/bin/unity-editor
WORKDIR /github/workspace
DOCKERFILE
docker build -t game-ci/unity-editor-stub:latest -f /tmp/unity-stub-ctx/Dockerfile /tmp/unity-stub-ctx
- name: Run Unity CLI stub build
run: |
UNITY_VERSION="${{ github.event.inputs.unity-version || '2019.4.40f1' }}"
bun run src/index.ts build test-project \
--engineVersion "${UNITY_VERSION}" \
--targetPlatform StandaloneLinux64 \
--customImage game-ci/unity-editor-stub:latest \
--unityLicense ci-stub-license
- name: Verify build artifacts
run: |
# StandaloneLinux64 builds get a .x86_64 extension by default now
# (matching unity-builder - see game-ci/cli#70), unless
# --linux64RemoveExecutableExtension is passed.
test -f build/StandaloneLinux64/StandaloneLinux64.x86_64
grep -q "Unity CLI smoke artifact" build/StandaloneLinux64/StandaloneLinux64.x86_64
echo "Unity CLI stub build verified"
# ─── Godot — detect + build using third-party image ────────────
godot-build:
name: Godot build
needs: changes
if: needs.changes.outputs.core == 'true' || needs.changes.outputs.godot == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Test Godot engine detection
run: |
bun -e "
import { GodotVersionDetector } from './src/middleware/engine-detection/godot-version-detector.ts';
const v = GodotVersionDetector.getGodotVersion('test-projects/godot-minimal');
console.log('Detected Godot version:', v);
if (v !== '4.3') throw new Error('Expected 4.3, got ' + v);
"
- name: Export Godot project in container
run: |
GODOT_VERSION="${{ github.event.inputs.godot-version || '4.3' }}"
mkdir -p test-projects/godot-minimal/build
docker run --rm \
-v "${{ github.workspace }}/test-projects/godot-minimal:/project" \
-w /project \
"barichello/godot-ci:${GODOT_VERSION}" \
bash -c '
set -euo pipefail
echo "Godot $(godot --headless --version)"
mkdir -p build
if [ -f export_presets.cfg ]; then
if godot --headless --verbose --export-release "Linux/X11" build/game.x86_64 2>&1; then
echo "Godot export completed"
else
echo "Godot export unavailable for minimal project, running import validation instead"
godot --headless --import 2>&1
fi
else
echo "No export presets found, running import validation instead"
godot --headless --import 2>&1
fi
ls -la build/
'
- name: Verify output
run: |
if [ -f test-projects/godot-minimal/build/game.x86_64 ]; then
echo "Godot binary exported successfully"
else
echo "Godot project validated (import-only for minimal project)"
fi
# ─── UE — detect + stub build ─────────────────────────────────
unreal-build:
name: UE stub build
needs: changes
if: needs.changes.outputs.core == 'true' || needs.changes.outputs.unreal == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Test Unreal engine detection
run: |
bun -e "
import { UnrealProjectDetector } from './src/middleware/engine-detection/unreal-project-detector.ts';
const v = UnrealProjectDetector.getUnrealVersion('test-projects/unreal-minimal');
console.log('Detected UE version:', v);
if (v !== '5.4') throw new Error('Expected 5.4, got ' + v);
"
- name: Build UE CI stub
run: |
mkdir -p /tmp/ue-stub-ctx
cat > /tmp/ue-stub-ctx/RunUAT.sh <<'SCRIPT'
#!/bin/bash
echo "=== RunUAT.sh (CI Stub) ==="
PROJECT=""
ARCHIVE_DIR=""
for arg in "$@"; do
case "$arg" in
-project=*) PROJECT="${arg#-project=}" ;;
-archivedirectory=*) ARCHIVE_DIR="${arg#-archivedirectory=}" ;;
esac
done
if [ -n "$PROJECT" ] && [ -f "$PROJECT" ]; then
echo "Project: $PROJECT"
cat "$PROJECT"
fi
if [ -n "$ARCHIVE_DIR" ]; then
mkdir -p "$ARCHIVE_DIR"
echo "{\"engine\":\"unreal\",\"stub\":true}" > "$ARCHIVE_DIR/build-manifest.json"
fi
echo "=== Build OK ==="
SCRIPT
chmod +x /tmp/ue-stub-ctx/RunUAT.sh
cat > /tmp/ue-stub-ctx/Dockerfile <<'DOCKERFILE'
FROM alpine:3.19
RUN apk add --no-cache bash
RUN mkdir -p /home/ue4/UnrealEngine/Engine/Build/BatchFiles
COPY RunUAT.sh /home/ue4/UnrealEngine/Engine/Build/BatchFiles/RunUAT.sh
RUN chmod +x /home/ue4/UnrealEngine/Engine/Build/BatchFiles/RunUAT.sh
WORKDIR /build
DOCKERFILE
docker build -t game-ci/ue-stub:latest -f /tmp/ue-stub-ctx/Dockerfile /tmp/ue-stub-ctx
- name: Run UE stub build
run: |
docker run --rm \
-v "${{ github.workspace }}/test-projects/unreal-minimal:/build" \
-w /build \
game-ci/ue-stub:latest \
/home/ue4/UnrealEngine/Engine/Build/BatchFiles/RunUAT.sh \
BuildCookRun \
-project=/build/MinimalTest.uproject \
-targetplatform=Linux \
-clientconfig=Shipping \
-build -cook -stage -pak -archive \
-archivedirectory=/build/output
- name: Verify build artifacts
run: |
cat test-projects/unreal-minimal/output/build-manifest.json
bun -e "
const m = JSON.parse(require('fs').readFileSync('test-projects/unreal-minimal/output/build-manifest.json', 'utf-8'));
if (m.engine !== 'unreal') throw new Error('wrong engine');
console.log('UE stub build verified:', JSON.stringify(m));
"
# ─── Unreal Engine real image (manual only) ─────────────────────
unreal-smoke-test:
name: Unreal Engine smoke test
runs-on: ubuntu-latest
if: github.event.inputs.unreal-image != ''
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Pull and verify Unreal Engine container
run: |
UE_IMAGE="${{ github.event.inputs.unreal-image }}"
echo "Pulling: ${UE_IMAGE}"
docker pull "${UE_IMAGE}"
docker run --rm "${UE_IMAGE}" \
bash -c "echo 'UE container OK'"
# ─── Executable CLI Plugin Tests ────────────────────────────────
protocol-tests:
name: Executable CLI plugin tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Test CLI plugin system can load executable plugin
run: |
bun -e "
import { CliProtocolPlugin } from './src/plugin/cli-protocol-plugin.ts';
// Verify construction with executable path
const plugin = new CliProtocolPlugin({ providerExecutable: '/usr/bin/echo' });
console.log('CliProtocolPlugin constructed OK');
// Verify all execution methods required by the CLI exist
const methods = ['setupWorkflow', 'cleanupWorkflow', 'runTaskInWorkflow',
'garbageCollect', 'listResources', 'listWorkflow', 'watchWorkflow'];
for (const m of methods) {
if (typeof plugin[m] !== 'function') {
throw new Error('Missing method: ' + m);
}
}
console.log('All required CLI execution methods present');
"
- name: "Test PluginLoader handles executable: prefix"
run: |
bun -e "
import { PluginLoader } from './src/plugin/plugin-loader.ts';
import { PluginRegistry } from './src/plugin/plugin-registry.ts';
PluginRegistry.reset();
// Verify executable: prefix with a real binary on the system
try {
const plugin = await PluginLoader.load('executable:/usr/bin/echo');
console.log('Loaded executable plugin:', plugin.name);
console.log('Providers:', Object.keys(plugin.providers || {}));
const providers = PluginRegistry.getAvailableProviders();
console.log('Registered providers:', providers);
if (!providers.includes('cli-protocol')) {
throw new Error('cli-protocol provider not registered');
}
console.log('Executable plugin integration test PASSED');
} catch (e) {
console.error('Test failed:', e.message);
process.exit(1);
}
"
- name: Run CLI plugin tests
run: bun test src/plugin/cli-protocol-plugin.test.ts
- name: Run plugin loader tests
run: bun test src/plugin/plugin-loader.test.ts
# Aggregate gate. Branch protection should require THIS check rather
# than the individual engine jobs above - a skipped-but-required job
# blocks every path-scoped PR, which would defeat the filtering.
smoke-gate:
name: Engine smoke gate
needs: [unity-build, godot-build, unreal-build, protocol-tests]
if: always()
runs-on: ubuntu-latest
steps:
- name: Verify no engine smoke test failed
run: |
if [ "${{ contains(needs.*.result, 'failure') }}" = "true" ]; then
echo "An engine smoke test failed."
exit 1
fi
echo "All engine smoke tests that ran are green."