Skip to main content
Version: v4 (current)

Build Reliability

Build reliability features harden CI builds against common failure modes: git corruption on persistent runners, Windows filesystem issues with cross-platform repositories, and build output management. All features are opt-in via action inputs and fail gracefully - a reliability check that encounters an error logs a warning rather than failing the build.

Git Integrity Checking

Self-hosted runners with persistent workspaces accumulate state between builds. Aborted jobs, disk errors, and concurrent git operations can leave the repository in a corrupted state. When this happens, the next build fails with cryptic git errors that are difficult to diagnose.

Git integrity checking catches corruption before it causes build failures.

What It Checks

The integrity check runs three validations in sequence:

  1. git fsck --no-dangling - Detects broken links, missing objects, and corrupt pack data in the local repository. The --no-dangling flag suppresses harmless warnings about unreachable objects that are normal in CI environments.

  2. Stale lock files - Scans the .git/ directory recursively for any files ending in .lock (index.lock, shallow.lock, config.lock, HEAD.lock, refs/**/*.lock). These are left behind by git processes that were killed mid-operation and prevent subsequent git commands from running. All lock files found are removed.

  3. Submodule backing stores - For each submodule declared in .gitmodules, validates that the .git file inside the submodule directory points to an existing backing store under .git/modules/. A broken backing store reference means the submodule's history is inaccessible, and any git operation inside it will fail.

Configuration

- uses: game-ci/unity-builder@v4
with:
gitIntegrityCheck: 'true'

Automatic Recovery

When gitAutoRecover is enabled (the default when gitIntegrityCheck is on) and corruption is detected, the service attempts recovery:

  1. Remove the corrupted .git directory entirely
  2. Re-initialize the repository with git init
  3. The checkout action completes the clone on the next step

This is a last-resort recovery. It works because the orchestrator's checkout step will re-populate the repository from the remote after re-initialization.

- uses: game-ci/unity-builder@v4
with:
gitIntegrityCheck: 'true'
gitAutoRecover: 'true' # this is the default when gitIntegrityCheck is enabled

To run integrity checks without automatic recovery (report-only mode), disable it explicitly:

- uses: game-ci/unity-builder@v4
with:
gitIntegrityCheck: 'true'
gitAutoRecover: 'false'

In report-only mode, detected corruption is logged as a warning and the build continues. This is useful for monitoring repository health without taking corrective action.

Reserved Filename Cleanup

Windows has reserved device names (CON, PRN, AUX, NUL, COM1COM9, LPT1LPT9) that cannot be used as filenames. When a git repository created on macOS or Linux contains files with these names, checking them out on Windows causes problems.

The Problem

Unity is particularly sensitive to reserved filenames. When the asset importer encounters a file named aux.meta, nul.png, or similar, it can enter an infinite reimport loop - detecting the file, failing to process it, detecting it again. This manifests as:

  • Unity hanging during asset import with no progress
  • 100% CPU usage from the asset import worker
  • Build jobs that run until they hit the timeout limit
  • Explorer crashes when navigating to affected directories

These files are valid on macOS and Linux, so they can easily enter a repository through cross-platform contributions.

Solution

- uses: game-ci/unity-builder@v4
with:
cleanReservedFilenames: 'true'

When enabled, the service scans the Assets/ directory tree before Unity processes the project. Any file or directory whose name (without extension) matches a reserved device name is removed. Each removal is logged as a warning so the source of the problematic files can be traced.

Reserved Names

The full list of reserved names checked (case-insensitive, with any file extension):

CON, PRN, AUX, NUL, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, LPT9

Examples of files that would be removed: aux.meta, nul.png, CON.txt, com1.asset.

Unity Process Cleanup

Persistent Windows runners can retain Unity processes after cancelled or crashed jobs. These stale processes can hold Library file handles, compete with the next Unity launch, or leave satellite processes running after the parent editor exits.

Enable unityProcessCleanup on Windows self-hosted runners to clean up stale Unity processes before the local build starts:

- uses: game-ci/unity-builder@v4
with:
unityProcessCleanup: 'true'

The cleanup is scoped to the current project path. It targets Unity.exe processes whose command line contains the current -projectPath, then cleans orphaned Unity satellite processes such as ShaderCompiler, PackageManager, ILPP, CrashHandler, and AutoQuitter. It also checks whether Unity Hub and Unity.Licensing.Client are already running without invoking Hub CLI commands.

This option is intended for persistent Windows hosts. It is usually unnecessary for fresh Linux containers where the operating system process table is discarded after each job.

Unity Failure Diagnostics

Provider plugins and custom wrappers can import Unity diagnostics from @game-ci/orchestrator when they need to route failures based on evidence from Editor.log:

import { UnityBuildDiagnosticsService, UnityRecoveryService } from '@game-ci/orchestrator';

const diagnostics = UnityBuildDiagnosticsService.analyzeRun({
exitCode,
runtimeSeconds,
logText: editorLog,
projectPath,
});

const decision = UnityRecoveryService.decide(
diagnostics,
UnityRecoveryService.createDefaultBudgets(),
);

The diagnostics service detects common Unity CI signals including native crash evidence, licensing startup failures, PackageCache GUID errors, immutable package asset corruption, exit 0 builds where the build method was not invoked, API updater runs, import completion, and Git LFS pointer DLLs.

Recovery decisions use independent retry budgets per failure type. A licensing retry, for example, does not consume the budget for PackageCache cleanup or crash recovery.

Selective Library Cleanup

Full Library deletion is expensive and should be a last resort. The reliability service exposes smaller cleanup operations for targeted recovery:

BuildReliabilityService.clearScriptAssemblies(projectPath);
BuildReliabilityService.clearBee(projectPath);
BuildReliabilityService.clearPackageCache(projectPath);
BuildReliabilityService.resetSourceAssetDatabase(projectPath);
BuildReliabilityService.removeAutoGeneratedAssets(projectPath, [
'Assets/Gemserk.SelectionHistory.asset',
'Assets/HitMeSettings.asset',
]);

Use these helpers based on log evidence:

SymptomPreferred recovery
CS0246 under Library/PackageCacheClear Library/PackageCache, retry
Could not restore immutable package assetClear Library/PackageCache, retry
Exit 0, build method not invokedReset SourceAssetDB and generated assets
Short exit -1 with licensing errorRetry after a delay, preserve Library
Native crash after import completedDelete Library only after crash evidence

Build Output Archival

Automatically archive build outputs after successful builds. Archives are organized per platform and managed with a count-based retention policy.

Configuration

- uses: game-ci/unity-builder@v4
with:
buildArchiveEnabled: 'true'
buildArchivePath: '/mnt/build-archives'
buildArchiveRetention: '5'

How It Works

  1. After a successful build, the build output directory is moved (or copied, if a cross-device move is not possible) to {archivePath}/{platform}/build-{timestamp}.
  2. Archives are organized by platform - each target platform gets its own subdirectory.
  3. The retention policy keeps the N most recent builds per platform. Older builds are automatically removed.

The archive path must be set when archival is enabled. This can be a local directory on the runner or a mounted network volume.

Retention Strategy

Retention is count-based: the buildArchiveRetention value specifies how many builds to keep per platform. When a new build is archived and the total exceeds the retention count, the oldest archives are removed.

  • Default retention: 3 builds per platform
  • Set a higher value for release branches where rollback capability is important
  • Archives are sorted by modification time, so the most recent builds are always retained

Archive Layout

/mnt/build-archives/
StandaloneLinux64/
build-2025-01-15T10-30-00-000Z/
build-2025-01-16T14-22-00-000Z/
build-2025-01-17T09-15-00-000Z/
StandaloneWindows64/
build-2025-01-15T10-45-00-000Z/
build-2025-01-16T14-35-00-000Z/

Git Environment Configuration

The reliability service configures a git environment variable automatically:

  • GIT_CONFIG_NOSYSTEM=1 - Bypasses the system-level git configuration file. This prevents corrupted or misconfigured system git configs on self-hosted runners from affecting builds.

This is applied automatically and does not require any configuration.

Inputs Reference

InputDescriptionDefault
gitIntegrityCheckRun git integrity checks before build'false'
gitAutoRecoverAttempt automatic recovery if corruption detected (requires gitIntegrityCheck)'true'
cleanReservedFilenamesRemove Windows reserved filenames from Assets/'false'
unityProcessCleanupClean stale Unity processes on Windows self-hosted runners'false'
buildArchiveEnabledArchive build output after successful build'false'
buildArchivePathPath to store build archives (required when archival is enabled)''
buildArchiveRetentionNumber of builds to retain per platform'3'

For self-hosted runners with persistent workspaces:

- uses: game-ci/unity-builder@v4
with:
gitIntegrityCheck: 'true'
gitAutoRecover: 'true'
cleanReservedFilenames: 'true'
unityProcessCleanup: 'true'
buildArchiveEnabled: 'true'
buildArchivePath: '/mnt/build-archives'
buildArchiveRetention: '5'

For ephemeral runners (GitHub-hosted or fresh containers), git integrity checking is less valuable since the workspace is created fresh each time. Reserved filename cleanup is still useful if the repository contains cross-platform contributions:

- uses: game-ci/unity-builder@v4
with:
cleanReservedFilenames: 'true'