Skip to content

CI/CD Pipeline

Summary: Project Eternal uses GitHub Actions with a self-hosted Windows runner for automated builds, testing, cooking, packaging, and distribution to the Epic Games Store. The pipeline supports three branch tiers with different behavior per tier. Automated unit and integration tests run via UnrealEditor-Cmd.exe in both PR validation and main-branch Build & Package, with results reported to Discord.

Table of Contents


Architecture Overview

┌──────────────────────────────────────────────────────────────────┐
│                        GitHub Repository                         │
│                                                                  │
│   main ──push──▶ Build + Unit & Integration Tests + Cook/Package │
│   dev  ──push──▶ Build + Upload to EGS (Live label) + Steam      │
│   release/* ────▶ Build (Shipping) + Upload to EGS + Steam       │
│                                                                  │
│   PR to main ──▶ Style + Compile + Unit & Integration Tests       │
└──────────────────────┬───────────────────────────────────────────┘
                       │ webhook trigger
                       ▼
┌──────────────────────────────────────────────────────────────────┐
│                    Self-Hosted Runner (Windows 11)                │
│                                                                  │
│   ┌─────────────┐   ┌──────────────┐   ┌────────────────────┐   │
│   │  Compile     │──▶│ Cook+Package │──▶│ Upload EGS         │   │
│   │  (UBT)      │   │ (RunUAT)     │   │ (BuildPatchTool)   │   │
│   └─────────────┘   └──────────────┘   └────────────────────┘   │
│                                                                  │
│   Local Dirs:                                                    │
│     C:\BuildArtifacts\   (packaged builds, last 5 kept)          │
│     C:\SharedDDC\        (Derived Data Cache, speeds up cooks)   │
│     C:\BPTCloudDir\      (BPT chunk storage for uploads)         │
└──────────────────────────────────────────────────────────────────┘

Why Self-Hosted Runner?

Consideration Decision
Already on GitHub Zero additional cost, native webhook triggers
Single machine GitHub Actions runner is lightweight (~50 MB)
UE builds need full engine Cloud runners can't have UE installed
Familiar YAML config Same syntax as existing workflows
Scalable later Add more machines with same labels

Why Not Horde/Jenkins/Buildkite?

Alternative Why Not (For Now)
Horde Requires MongoDB, Redis, separate server process — overkill for one machine
Jenkins Extra infrastructure to maintain with no advantage for single-machine setup
Buildkite Additional cost and complexity for same result

Branch Strategy

Branch Build Config Triggers On EGS Upload EGS Label Steam Upload Purpose
main Development Push (Source, Content, Config, Plugins) No — No Validate builds compile, pass unit + integration tests, and cook
dev Development Push (same paths) Yes Live Yes (development branch) Active dev testing build on EGS + Steam
release/* Shipping Push (same paths) Yes Live Yes (development branch) Release candidate builds

The two upload jobs (upload-egs, upload-steam) run in parallel after build; each can also be forced on a manual workflow_dispatch via the upload_egs / upload_steam inputs. Steam specifics (SteamPipe VDF, cached-login runner setup, the standalone upload-steam.yml) live in Steam Deployment.

Path Filters

Builds only trigger when changes touch relevant files:

Path Why
Source/** C++ code changes
Content/** Asset changes
Config/** Configuration changes
*.uproject Project file changes
Plugins/** Plugin changes

Concurrency

One build per branch at a time. New pushes cancel in-progress builds on the same branch:

concurrency:
  group: build-${{ github.ref }}
  cancel-in-progress: true


Pipeline Stages

Build Job (all branches)

Disk Check ──▶ Checkout ──▶ Version Stamp ──▶ Discord Start ──▶ Compile ──▶ [main only: Editor Compile ──▶ Unit Tests ──▶ Integration Tests] ──▶ Cook+Package ──▶ Cleanup ──▶ Discord Result
Stage Tool Timeout Details
Disk Check PowerShell — Fails if < 50 GB free on C:
Checkout actions/checkout@v4 — LFS + submodules, shallow clone
Version Stamp PowerShell — Writes version into DefaultGame.ini
Compile UnrealBuildTool (UBT) 180 min total C++ compilation via dotnet UBT
Editor Compile (main only) UBT — ProjectEternalEditor target for tests; -NoHotReloadFromIDE to bypass Live Coding lock
Unit Tests (main only) UnrealEditor-Cmd.exe — Same filter as PR validation; fails loudly if 0 tests match
Integration Tests (main only) UnrealEditor-Cmd.exe — Starts/stops local eternal-server around ProjectEternal.Integration
Cook + Package RunUAT — BuildCookRun with pak, compression
Cleanup PowerShell — Removes builds beyond last 5

Upload Jobs (dev and release/* only)

upload-egs:   Upload Binary (BPT) ──▶ Label Binary (BPT) ──▶ Discord Notify
upload-steam: Upload via steamcmd (SteamPipe VDF) ──▶ Discord Notify      (parallel to upload-egs)
Stage Tool Details
Upload (EGS) BuildPatchTool UploadBinary Pushes packaged build to EGS
Label (EGS) BuildPatchTool LabelBinary Marks build as Live (active)
Upload (Steam) steamcmd Generated app_build VDF; lands on the non-default development Steam branch — see Steam Deployment
Notify Discord webhook One notification per upload job

PR Validation Job (PRs to main)

Trigger paths include Content/** and .github/workflows/** — content-only PRs run the full job (the content-validation step is why).

Checkout ──▶ Style Check ──▶ Compile (Editor) ──▶ Unit Tests ──▶ Content Validation ──▶ Integration Tests ──▶ Discord Result
Stage Tool Details
Style Check clang-format --dry-run --Werror on changed C++ files
Compile UBT Editor target (ProjectEternalEditor)
Unit Tests UnrealEditor-Cmd.exe Runs ProjectEternal.Persistence+Unit+Content+Editor filters via -NullRHI -unattended
Content Validation UnrealEditor-Cmd.exe -run=EternalValidation over /Game/DataAssets,/Game/Gameplay/GameplayAbilities,/Game/DataTables; echoes Saved/Validation/ContentValidation.md into the log; non-zero exit fails the PR — see Content Validation
Integration Tests UnrealEditor-Cmd.exe Starts local eternal-server, runs ProjectEternal.Integration, stops server
Discord Webhook Style, compile, and test results (pass/fail counts)

Tests run after compile because they require the Editor build. If compile fails, tests are skipped. Discord notification always fires (success or failure).

A separate lightweight workflow, docs-link-check.yml (ubuntu, no engine), runs Tools/check_doc_links.ps1 on Documentation/** and Source/** changes — it validates doc links and fails on Source/... citations pointing at files that no longer exist.


Automated Testing

What Runs in CI

Unit and integration tests run in both PR validation and main-branch Build & Package. Integration tests need a backend server; the workflows start a local eternal-server on the runner (port 5065) for the duration of the run.

Filter Runs In CI Notes
ProjectEternal.Persistence + Unit + Content + Editor Yes (PR validation + main builds) No external dependencies
ProjectEternal.Integration Yes (PR validation + main builds) eternal-server started/stopped by the workflow

How Tests Execute

UnrealEditor-Cmd.exe <project> -ExecCmds="Automation RunTests ProjectEternal.Persistence+ProjectEternal.Unit+ProjectEternal.Content+ProjectEternal.Editor;Quit" -NullRHI -unattended

The workflow parses the UE log file for test results (Result={Success} / Result={Fail}) and reports counts to Discord and the GitHub step summary. If the filter matches zero tests, the step fails loudly instead of silently passing.

Where Tests Run

Tests require UnrealEditor-Cmd.exe, so both workflows compile the Editor target first. In build-and-package.yml, the Editor compile and test steps run on main pushes only (dev/release builds skip them). PR validation still gates merges; the main-branch run catches anything that landed differently after merge.

Adding New Test Areas

When new test spec files are added outside the current filters, update the test filter in both pr-validation.yml and build-and-package.yml (they use the same +-separated filter list):

-ExecCmds="Automation RunTests ProjectEternal.Persistence+ProjectEternal.Unit+ProjectEternal.Content+ProjectEternal.Editor;Quit"

Local Perf / Cook Harness

Two scripts reproduce CI-adjacent work on a dev machine; neither runs in a workflow.

Script Purpose
Tools/cook_local.ps1 Local cook/stage/package for devkit-style deployment (e.g. a Steam Deck spike). Mirrors the CI BuildCookRun invocation in build-and-package.yml minus the runner-specific memory clamps. Close the editor first — the cook commandlet needs the module binaries and Live Coding holds a lock. -EnginePath, -Config, -ArchiveDir, -SkipBuild
Tools/perf_benchmark.ps1 Automated GPU/CPU benchmark: launches UnrealEditor.exe -game into L_Dungeon, lets Eternal.Perf.Run gate on generation / warm-up / CSV capture, then reduces Saved/Profiling/CSV into a per-config median/p95 table. Moves SaveGames aside for determinism (fresh save = identical layout) and restores it even on Ctrl+C. -Only, -Warmup, -Capture, -EnginePath
Tools/deck_perf.ps1 Steam Deck per-room benchmark: rsync-pushes the packaged build to a devkit-paired Deck (-Push), drives Eternal.Perf.Tour over SSH via -CmdLineFile + steam-devkit-rpc run-game, pulls Saved/PerfBench/<RunID>/ and reduces it to a config × room table with GPU/* pass columns. -Only, -Rooms, -Discover, -Settle, -Capture, -Deck. See Documentation/07_AI_World/06_Enemy_Performance.md §Benchmark Harness

Version Stamping

Build versions follow the format: YYYY.MM.DD-<run_number>-<short_sha>

Example: 2026.02.27-18-1a087dc9

Component Source Example
Date Build timestamp 2026.02.27
Run Number GitHub Actions run number 18
Short SHA First 8 chars of commit hash 1a087dc9

The version is stamped into Config/DefaultGame.ini at build time by replacing the ProjectVersion= line.


Discord Notifications

All pipeline stages send Discord notifications via webhook:

Event Color Content
Build Started Blue (3447003) Branch, commit message, config, EGS label
Build Succeeded Green (3066993) Branch, version, compile time, cook time, package size
Build Failed Red (15158332) Branch, version, which stage failed
EGS Upload Result Green/Red Version, branch, label
PR Validation Passed Green (3066993) PR title, branch, style/compile/test results (pass/fail counts)
PR Validation Failed Red (15158332) PR title, branch, which check failed, test failure count

Cook Configuration

Maps Cooked

Currently only two maps are included in the cook:

Map Path
Main Menu /Game/Maps/L_MainMenu
Default /Game/Maps/L_Default

Configured via -Map= flag in the RunUAT command.

Cook Settings

Setting Value Location Purpose
CookContentMissingSeverity Warning Config/DefaultEditor.ini Demotes editor-only asset reference errors to warnings
-SkipCookingEditorContent flag Workflow YAML Excludes editor-only content from cook
-pak -compressed flags Workflow YAML Packages into compressed .pak files

Known Cook Issues

Issue Status Workaround
UsdAssets materials reference editor-only textures Suppressed via CookContentMissingSeverity Replace editor-only texture references in materials
GameplayTag CDO crash during cook Fixed RequestGameplayTag(name, false) in ability constructors


Source References

File Purpose
.github/workflows/build-and-package.yml Main build + upload pipeline
.github/workflows/upload-egs.yml Standalone EGS upload (manual trigger)
.github/workflows/upload-steam.yml Standalone Steam upload of an existing build (manual trigger)
.github/workflows/deploy-docs.yml Publishes Documentation/** as an mkdocs site to Cloudflare Pages on push to main (GitHub-hosted, LFS-free checkout)
.github/workflows/pr-validation.yml PR style check + compile + unit tests + content validation
.github/workflows/docs-link-check.yml Doc link + Source/ citation checker (Tools/check_doc_links.ps1)
.github/workflows/deploy-server.yml Game server deployment

Repo Rot Watch (stale learnings / doc-symbol drift / unarchived plans) is not a GitHub workflow anymore: it runs as a Claude Code cloud routine every Monday 06:00 UTC and files a GitHub Issue with verified findings. | Config/DefaultEditor.ini | Cook settings (CookContentMissingSeverity) | | Config/DefaultGame.ini | Version stamping target, MapsToCook |


Recent Changes

Date Change Impact
2026-08-20 Documented the upload-steam job (parallel to upload-egs), the deploy-docs / garden / upload-steam workflows, the three-root validation path list, and the local cook/perf harness scripts Doc matched an EGS-only pipeline; Steam has shipped alongside it and the docs site + garden workflows were unlisted
2026-07-08 Content validation step in PR validation + Content/** trigger paths (B-1/B-2); docs-link-check gains Source/** trigger + citation pass (B-9) Content-only PRs now gated; broken data assets and stranded doc citations fail CI
2026-07-03 Documented unit + integration tests in Build & Package (main) as well as PR validation Doc now matches build-and-package.yml / pr-validation.yml; test filters expanded to Persistence+Unit+Content+Editor and Integration
2026-03-02 Added automated unit tests to PR validation Tests gate PRs to main, results reported to Discord
2026-03-02 Created documentation Initial DevOps docs
2026-02-27 Pipeline fully operational Builds compile, cook, package, upload to EGS
2026-02-27 Fixed EGS labels Only Live and Archive are valid BPT labels
2026-02-27 Added CookContentMissingSeverity Unblocked cook by demoting editor-only asset errors
2026-02-27 Fixed GameplayTag CDO crash RequestGameplayTag(name, false) for cook safety
2026-02-27 Branch strategy implemented main=build only, dev=build+upload, release=Shipping+upload