So I said at the end of my Hugo CI post that I wanted to set up a Unity build pipeline next. That was in 2020. Better late than never, right?

The reason this took so long (is that I’m “zerstreut”) is that Unity CI is genuinely annoying to set up compared to most other things. There’s a license activation step that is a MAJOR (and I mean MAJOR) PITA. But I finally got it working and it’s absolutely worth it, but… I cheated, sort of…

The secret weapon is GameCI. They maintain a set of Docker images with Unity pre-installed and a suite of GitHub Actions that wrap them. You don’t have to figure out how to install Unity in a headless environment yourself, which, trust me, you do not want to do yourself (especially on a windows 10 server behind a corpo-proxy, UNITY_NOPROXY anyone???).

The License Problem

Before you write a single line of workflow YAML you need to sort out licensing. Unity requires an activated license to build, even in a headless environment. For a personal license the process goes like this:

  1. Create a workflow that runs game-ci/unity-activate@v4 and uploads the resulting .alf (activation license file) as an artifact.
  2. Download that .alf, go to license.unity3d.com and exchange it for a .ulf (Unity license file).
  3. Copy the contents of the .ulf and paste it into a repository secret called UNITY_LICENSE.

Also add your Unity account credentials as secrets: UNITY_EMAIL and UNITY_PASSWORD. GameCI’s docs walk through this in more detail here.

If you have a Pro license it’s simpler, you just store your serial as UNITY_SERIAL alongside the email and password secrets and skip the .alf dance entirely. Lucky you.

Or rather not so lucky because they just announced another price increase. Really, in this economy?

Anyways…

The Workflow

With the secrets in place, here’s a workflow that runs your tests and then builds for WebGL:

name: CI

on: [push, pull_request]

jobs:
  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          lfs: true

      - uses: game-ci/unity-test-runner@v4
        env:
          UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
          UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
          UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
        with:
          projectPath: .
          githubToken: ${{ secrets.GITHUB_TOKEN }}

  build:
    name: Build
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
        with:
          lfs: true

      - uses: game-ci/unity-builder@v4
        env:
          UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
          UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
          UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
        with:
          targetPlatform: WebGL

      - uses: actions/upload-artifact@v4
        with:
          name: Build
          path: build

The needs: test on the build job means it only runs if the tests pass. Very sensible. The lfs: true on checkout is important if you store any assets in Git LFS, forget it and you’ll get placeholder files instead of your actual textures and audio, which makes for a creative but unintentional art direction.

targetPlatform can be swapped out for any platform GameCI supports: StandaloneWindows64, StandaloneLinux64, Android, iOS, etc. You can even fan out into a build matrix and produce all of them in parallel.

One big caveat though: IL2CPP builds are platform-native. If your project uses the IL2CPP scripting backend (which it should be, there are few reasons not to), the build machine has to be the actual target platform. A Linux runner can’t produce an IL2CPP Windows or iOS build because IL2CPP compiles to native code using the platform’s own toolchain: MSVC for Windows, Xcode for iOS, and so on. Cross-compilation just isn’t there. (Or is it???)

In practice this means:

  • StandaloneWindows64 with IL2CPP → runs-on: windows-latest
  • iOSruns-on: macos-latest
  • StandaloneLinux64 and WebGLruns-on: ubuntu-latest is fine
  • Android → Linux is fine too, the NDK handles it

So if you want a full multi-platform experience you’ll end up mixing runner types in your workflow. Windows and macOS runners on GitHub Actions are significantly more expensive in CI minutes than Linux, just something to keep in mind if you’re on a free plan and building on every push.

That’s It

Honestly once the license headache is out of the way the actual workflow is refreshingly simple. GameCI has done all the heavy lifting. Now every push to my repo runs the test suite automatically and I get build artifacts I can grab and test without having to touch Unity Editor.

There’s a big but, tho.

As nice as the workflow on GitHub is, getting this to work on GitLab with privately hosted runners behind a firewall and a proxy is pain, just unending pain. Especially if you need to pull this off bare-metal on Windows 10. Who needs docker, amirite?! If you’re in my shoes, remember: Unity is special and doesn’t respect NO_PROXY, like all the good kids. Unity needs it’s super duper individual UNITY_NOPROXY env var to tell it how it can reach the internet.

Regardless, automation is the way, always. My Factorio-brain demands it.

Thanks for reading 🎮