Speed Up Unity in GitHub Actions
Last updated
To speed up Unity in GitHub Actions, cache the Library folder with actions/cache, keep one cache per build target, fetch Git LFS files only when needed, and run each platform as its own job with GameCI's unity-builder. The Library folder holds imported assets, and rebuilding it from scratch is usually the longest step in a Unity CI run.
This guide uses GameCI, the open-source Unity CI toolkit. For the general approach, see how to speed up GitHub Actions.
Why Unity CI is slow
A clean Unity build does four expensive things: pull a Docker image of the editor (several gigabytes), activate a license, import every asset into Library, then compile scripts and build the player. Import is the big one for projects with many textures, models and audio files. The editor image pull is the second. Neither is fixed by a faster compiler; both are fixed by caches and by doing less per job.
The Library cache
GameCI's builder docs show a workflow that caches Library with actions/cache. The key includes the target platform and a hash of the project content, with a restore-keys prefix for the platform:
- uses: actions/cache@v6
with:
path: Library
key: Library-${{ matrix.targetPlatform }}-${{ hashFiles('Assets/**', 'Packages/**', 'ProjectSettings/**') }}
restore-keys: |
Library-${{ matrix.targetPlatform }}-
Three points from that design:
- Platform in the key. Each build target (Android, iOS, WebGL, StandaloneLinux64) imports assets differently. A shared cache would be thrashed by the switch.
- Restore keys do the real work. Cache entries are immutable, so an exact hit is rare once any asset changes. The prefix match restores the closest previous
Library, and Unity re-imports only what changed. - Hash cost.
hashFilesoverAssets/**reads every file. On a large project with many gigabytes of assets, hashing itself takes a while. A cheaper key usesProjectSettings/ProjectVersion.txtandPackages/packages-lock.jsonplusgithub.shafor saving, with the prefix as the restore key.
Library caches are big, often 1 GB or more per platform, and GitHub caps total cache storage per repository, so see GitHub Actions cache limits. With three platforms and PR traffic, you can evict useful entries quickly. Save the cache only on main with actions/cache/save, restore it everywhere with actions/cache/restore.
License activation
GameCI documents two paths, using repository secrets:
- Personal license. Activate once through the GameCI activation workflow or locally, then store
UNITY_LICENSE(the contents of the.ulffile),UNITY_EMAILandUNITY_PASSWORD. - Professional (Plus or Pro). Store
UNITY_SERIAL,UNITY_EMAILandUNITY_PASSWORD. A pro seat is used while the job runs; GameCI includes a return-license step in the builder and test runner. - License server. If you run a floating license server, the
unityLicensingServerinput points the build at it.
GameCI says it does not acquire or store your credentials. Unity's licensing rules change, so check Unity's current terms for CI use before you rely on a personal license for a team. Parallel jobs on a pro serial may each need a seat.
Git LFS and checkout
Unity projects often store textures, models and audio in Git LFS. The GameCI example checks out with lfs: true; without it, files are pointers and the build breaks. Fetch with care:
- uses: actions/checkout@v7
with:
lfs: true
actions/checkout has lfs set to false by default and fetch-depth set to 1, so the checkout is shallow. The GameCI sample uses fetch-depth: 0 for full history, which you only need if your versioning reads Git history (for example, to compute a version from tags). Otherwise keep the default; a shallow checkout is faster. LFS downloads count against your LFS bandwidth quota, which is a separate limit from Actions. Cache the LFS objects (.git/lfs) keyed on a hash of .lfs-assets-id if bandwidth or time hurts, or use a service with local Git caching.
One job per platform
A matrix gives each target its own job, cache and machine:
name: build
on:
push:
branches: [main]
pull_request:
jobs:
build:
name: Build ${{ matrix.targetPlatform }}
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
targetPlatform:
- StandaloneLinux64
- WebGL
- Android
steps:
- uses: actions/checkout@v7
with:
lfs: true
- uses: actions/cache@v6
with:
path: Library
key: Library-${{ matrix.targetPlatform }}-${{ hashFiles('Assets/**', 'Packages/**', 'ProjectSettings/**') }}
restore-keys: |
Library-${{ matrix.targetPlatform }}-
- uses: game-ci/unity-builder@v4
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
targetPlatform: ${{ matrix.targetPlatform }}
- uses: actions/upload-artifact@v7
with:
name: Build-${{ matrix.targetPlatform }}
path: build/${{ matrix.targetPlatform }}
The builder's unityVersion input defaults to auto, which reads ProjectSettings/ProjectVersion.txt. The GameCI docs use game-ci/unity-builder@v4 in this example and mention v6 in other sections, so confirm the current tag in the action's releases before you copy this. timeout-minutes matters here: a hung license step can burn an hour.
Platform notes: Linux runners build Standalone Linux, WebGL and Android. Windows and macOS targets need their own runners, and iOS needs macOS, which costs more per minute; see macOS runners for iOS CI.
Tests
Run EditMode tests (fast, no player) on every PR with GameCI's test runner, and PlayMode tests less often. The test runner also uses the Library cache; give it the same cache step so tests do not trigger a fresh import.
What more CPU cores buy you
Asset import and script compilation use several cores, and Unity's IL2CPP builds are heavy, so a larger machine helps. Disk speed and RAM matter as well, since Library is big. We have no Unity row in our benchmarks, so we do not quote a speedup. If you test it, note that the editor image is large; fresh VMs pull it each run unless the machine keeps a Docker cache. compiler.dev's runners come with Docker layer caches ready, and the change is one line, runs-on: [self-hosted, Linux, compiler-ci]. See pricing and compare, and read Docker layer caching in CI for how image pulls behave. Also see our page on game QA testing.
FAQ
What should I cache for Unity in GitHub Actions?
The Library folder, with the target platform in the key and a platform prefix as the restore key. Do not share one cache across platforms.
Do I need Git LFS for Unity CI?
If your repo stores assets in LFS, yes. Set lfs: true on checkout, or builds see pointer files instead of assets.
Can I use a free Unity license in CI?
GameCI supports personal licenses through the UNITY_LICENSE, UNITY_EMAIL and UNITY_PASSWORD secrets. Check Unity's terms for your use.
Why does my Unity cache never hit?
Immutable entries and a key that hashes every asset mean exact hits are rare. Rely on the restore-keys prefix, and make sure it matches the platform.
Related
- Add a CI Speed Badge to Your READMEShow how fast your CI is with a README badge: the median GitHub Actions run time for your repository, updated automatically. Free for public repos.
- Speed Up .NET in GitHub ActionsSpeed up .NET builds in GitHub Actions: NuGet caching with setup-dotnet, --no-restore, --no-build, parallel dotnet test, and SQL Server services.
- Speed Up Bazel in GitHub ActionsSpeed up Bazel in GitHub Actions with setup-bazel, a disk cache, repository cache, a remote cache, and the right --jobs setting. Working YAML.
- Speed Up Django in GitHub ActionsSpeed up Django tests in GitHub Actions with pip or uv caching, pytest-xdist, --reuse-db, a Postgres service, and test splitting. Working YAML.