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 (Dev sandbox, Live label) │
│   release/* ────▶ Build (Shipping) + Upload to EGS (Live label)  │
│                                                                  │
│   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 Purpose
main Development Push (Source, Content, Config, Plugins) No Validate builds compile, pass unit + integration tests, and cook
dev Development Push (same paths) Yes Live Active dev testing build on EGS
release/* Shipping Push (same paths) Yes Live Release candidate builds

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 Job (dev and release/* only)

Upload Binary (BPT) ──▶ Label Binary (BPT) ──▶ Discord Notify
Stage Tool Details
Upload BuildPatchTool UploadBinary Pushes packaged build to EGS
Label BuildPatchTool LabelBinary Marks build as Live (active)
Notify Discord webhook Upload status notification

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; 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"

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
Hair material M_HairSheet_Master2 editor-only reference Same as above Same fix needed
L_DemoBlockout has broken references Excluded from cook Fix or remove the map
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/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
Config/DefaultEditor.ini Cook settings (CookContentMissingSeverity)
Config/DefaultGame.ini Version stamping target, MapsToCook

Recent Changes

Date Change Impact
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