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.exein both PR validation and main-branch Build & Package, with results reported to Discord.
Table of Contents¶
- Architecture Overview
- Branch Strategy
- Pipeline Stages
- Automated Testing
- Version Stamping
- Discord Notifications
- Cook Configuration
- Related Systems
- Source References
- Recent Changes
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:
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)¶
| 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 |
Related Systems¶
- EGS Deployment - Epic Games Store upload and labeling
- Infrastructure Setup - Runner machine setup and configuration
- Testing - Test framework, writing tests, shared helpers
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 |