Infrastructure Setup
Summary: The build infrastructure runs on a single Windows 11 machine with a GitHub Actions self-hosted runner. This document covers machine setup, directory structure, required secrets, and scaling considerations.
Table of Contents
Machine Specs
| Component |
Value |
| OS |
Windows 11 Home |
| CPU |
i5-14400F (10 cores / 16 threads) |
| RAM |
16 GB |
| UE Version |
5.6 |
| .NET |
9.0.304 |
| Git |
2.45.1 + LFS 3.5.1 |
Directory Structure
C:\
├── Program Files\Epic Games\UE_5.6\ UE engine installation
├── Actions\ GitHub Actions runner
│ └── _work\ Runner workspace (builds happen here)
├── BuildArtifacts\ Packaged build output
│ ├── 18\ Build #18 (folder per run number)
│ │ └── Windows\ Packaged game files
│ ├── 19\
│ └── ... Last 5 builds kept, older auto-deleted
├── SharedDDC\ Derived Data Cache (shared across builds)
├── BPTCloudDir\ BuildPatchTool chunk storage
├── Tools\
│ └── BuildPatchTool\ Standalone BPT from Epic Dev Portal
│ └── Engine\Binaries\Win64\
│ └── BuildPatchTool.exe v1.7.3
└── GameServer\ Dev game server deployment (future)
└── ProjectEternal\
Windows Defender Exclusions
These paths should be excluded from real-time scanning for build performance:
| Path |
Why |
C:\Program Files\Epic Games\UE_5.6\ |
Engine reads thousands of files during compile |
C:\Actions\_work\ |
Build workspace with constant file I/O |
C:\SharedDDC\ |
DDC cache has many small files |
C:\BuildArtifacts\ |
Large packaged builds |
Runner Installation
Setup Steps
- Download Windows x64 runner from repository Settings > Actions > Runners > New
- Extract to
C:\Actions\
- Configure with labels:
./config.cmd --url https://github.com/oliwervik/Project-Eternal
--token <TOKEN>
--labels self-hosted,Windows,X64,UE5
--name "eternal-build-01"
--work C:\Actions\_work
- Install and start as Windows Service:
./svc.cmd install
./svc.cmd start
Runner Labels
| Label |
Purpose |
self-hosted |
Required by GitHub |
Windows |
OS identifier |
UE5 |
Marks machine as having UE installed |
Important Notes
| Consideration |
Detail |
| Windows 11 Home |
No Hyper-V or proper Remote Desktop |
| Must stay logged in |
Lock screen is fine, sign-out breaks the service |
| Remote access |
Use Tailscale or RustDesk for remote management |
GitHub Secrets
Required Secrets
| Secret |
Purpose |
Where to Find |
REPO_PAT |
Git checkout with LFS access |
GitHub Settings > Developer Settings > Tokens |
EGS_ORGANIZATION_ID |
Epic org identifier |
Dev Portal > Organization Settings |
EGS_PRODUCT_ID |
Product identifier |
Dev Portal > Product Settings |
EGS_ARTIFACT_ID |
Artifact identifier |
Dev Portal > Artifacts and Binaries |
EGS_CLIENT_ID |
BPT authentication |
Dev Portal > Product Settings > BPT Credentials |
EGS_CLIENT_SECRET |
BPT authentication |
Dev Portal > Product Settings > BPT Credentials |
EGS_SANDBOX_ID |
Sandbox for label operations |
Dev Portal > Product Settings > Sandboxes |
DISCORD_WEBHOOK_URL |
Build notifications |
Discord > Server Settings > Integrations > Webhooks |
Common Pitfalls
| Pitfall |
Prevention |
Typo in secret name (e.g., ESG_ vs EGS_) |
Verify with gh secret list |
| BPT credentials vs EOS credentials |
BPT has its own Client ID/Secret separate from EOS |
| Secret set as Variable instead of Secret |
Check both tabs in GitHub Settings |
RAM Constraints
| Operation |
Typical RAM Usage |
| C++ Compilation (UBT) |
8-12 GB |
| Cook (RunUAT) |
10-14 GB |
| Cook + Package combined |
Up to 14 GB peak |
16 GB RAM is tight. The machine will be slow during builds. Consider upgrading to 32 GB.
Disk Budget
| Item |
Approximate Size |
| UE 5.6 engine |
~60 GB |
| Project workspace |
~15 GB (with LFS) |
| Packaged build (each) |
~2-5 GB |
| 5 retained builds |
~10-25 GB |
| SharedDDC |
~10-30 GB (grows over time) |
| BPT cloud dir |
~5-10 GB |
| Total CI footprint |
~100-150 GB |
DDC (Derived Data Cache)
The SharedDDC at C:\SharedDDC\ persists across builds. First cook is slow (generates all derived data), subsequent cooks reuse cached data significantly faster.
Scaling Path
| Phase |
When |
What |
| 1 (current) |
Single machine |
One self-hosted runner, sequential builds |
| 2 |
2nd machine available |
Add runner with same labels, GitHub distributes jobs |
| 3 |
Need cloud burst |
Azure/AWS Windows VM as ephemeral runner |
| 4 |
5+ devs, frequent builds |
Migrate to Horde with UBA distributed compilation |
Re-enabling LFS Locking for a Team
Git LFS file-locking is currently OFF (solo dev — see CLAUDE.md › Git LFS File Locking). LFS storage stays on regardless; only the locking workflow is disabled. Re-enabling it for a multi-dev team means restoring 4 independent layers — miss one and files stay writable-but-unprotected or the editor still refuses to force checkout. Do not touch the filter=lfs entries in .gitattributes — those are storage, not locking.
The 4 Layers
| # |
Layer |
Where |
Scope |
Action to re-enable |
| 1 |
Server-side locks |
LFS server |
Shared |
Re-lock the files that should be protected (git lfs lock <path>). git lfs locks is the server-side source of truth for what is held. |
| 2 |
lockable attribute |
.gitattributes (committed) |
Shared |
Restore lockable on the [attr]lock macro (line 1). *.uasset/*.umap reference that lock macro, so both patterns become read-only-until-checked-out again. This is the team-facing layer. |
| 3 |
locksverify |
git config per-machine |
Per-machine |
Set lfs.<remote-url>/info/lfs.locksverify=true so pushes fail on edits to files locked by someone else. |
| 4 |
UE Git plugin |
Saved/Config/WindowsEditor/SourceControlSettings.ini |
Per-machine |
Set [GitSourceControl.GitSourceControlSettings] UsingGitLfsLocking=True. Saved/ is gitignored, so every machine sets this itself. The editor rewrites this file on exit — flip it while the editor is CLOSED. |
Clearing Read-Only Bits on Already-Checked-Out Files
Restoring the lockable attribute (layer 2) only affects future checkouts — files already on disk keep whatever read-only state they currently have. After re-enabling, sweep existing assets so their read-only bit matches the new locking regime (set read-only on unlocked files, writable on files you hold a lock for): iterate Content/**/*.uasset and *.umap and set the IsReadOnly attribute accordingly.
The same sweep is required when disabling locking. Removing the attribute did NOT clear the
bit on already-checked-out files: 3375 stale read-only assets survived the 2026-05-25 disable and
silently failed every package save/rename/redirector write until a full clear on 2026-07-07
(Get-ChildItem Content -Recurse -File | ? IsReadOnly | % { $_.IsReadOnly = $false }). Run that on
every machine after flipping locking off (see learning stale-readonly-flags-fail-bulk-asset-saves).
Verification
git lfs locks returns the expected held locks (server truth, not local cache).
- A fresh checkout of a
*.uasset lands read-only on disk.
- The editor shows the Check Out / lock workflow again.
- A push touching a file locked by another user is rejected.
Related: reference_lfs_locking_disable (session brain), project_asset_repo_health.
Recent Changes
| Date |
Change |
Impact |
| 2026-07-03 |
Add "Re-enabling LFS Locking for a Team" runbook (4 layers + read-only bit sweep) |
Document how to restore locking when the team grows |
| 2026-03-02 |
Created documentation |
Initial infrastructure docs |
| 2026-02-27 |
Runner operational |
Self-hosted runner building and uploading successfully |
| 2026-02-27 |
Standalone BPT installed |
C:\Tools\BuildPatchTool\ for EGS uploads |