Skip to content
← Blog

From Copy-Paste Build Scripts to Unity CLI: A Practical Guide to Streamlining Game CI/CD

Replace your sprawling Unity build scripts with the new unified CLI. See concrete before-and-after CI/CD examples for GitHub Actions, GitLab CI, and multi-platform pipelines.

7 min readSimon-Daniel März
From Copy-Paste Build Scripts to Unity CLI: A Practical Guide to Streamlining Game CI/CDGenerated with the help of AI

Every Unity studio has that one build script. A 400-line bash file, passed between three developers over two years, edited in a hurry before a milestone. Nobody fully understands what every flag does, but everyone is afraid to delete it. Multiply that across platforms (Windows, macOS, Android, iOS, WebGL) and Unity versions, and you get a maintenance burden that quietly eats 10-20% of every sprint.

Unity recently introduced a unified CLI that replaces that sprawl with a single command-line interface. It manages Unity editor installations, runs tests, and executes builds, all through one tool that works on any CI provider. If your team has been duct-taping custom scripts together for years, this changes the math on build automation.

The Problem: Why Unity CI/CD Scripts Spiral Out of Control

Unity game builds differ from typical software CI/CD in three ways that make scripting painful:

1. Multiple platforms from one project. A single game might need builds for Windows (64-bit), macOS (Intel + Apple Silicon), Android (APK + AAB), iOS, and WebGL. Each platform has its own build target enum, its own output format, and its own post-processing quirks (code signing on macOS, APK signing on Android, Xcode project generation for iOS).

2. Unity version pinning. Different branches often need different Unity versions. Managing installs, downloading the right version, applying the right modules (Android Build Support, iOS Build Support, Linux Build Support), is non-trivial. DIY scripts tend to hard-code paths and version numbers that break when anything changes.

3. Layout testing is interleaved. Edit-mode tests, play-mode tests, and integration tests each require different invocation flags. Studios often end up with separate test scripts for each category, each with its own error handling.

A hypothetical mid-size studio might maintain a script structure like this:

ci/
├── install_unity.sh          # Download & configure editor
├── build_windows.bat         # Windows standalone build
├── build_mac.sh              # macOS build + code signing
├── build_android.sh          # Android APK + AAB
├── run_editmode_tests.sh     # Unit & edit-mode tests
├── run_playmode_tests.sh     # Play-mode integration tests
├── deploy_itchio.sh          # Upload to itch.io
└── common_variables.env      # Paths, version numbers, signing keys

Six to eight files, touching three different shell interpreters, maintained by whoever last touched CI. When Unity updates its API or a platform module changes, the team has to hunt through every script.

What Unity CLI Actually Does

The new Unity CLI consolidates that entire surface area into a single tool with three core capabilities:

Installation management. Specify the Unity version and required modules; the CLI fetches and configures the editor. No more manual downloads or version-checking logic in custom scripts.

Test execution. Run edit-mode tests, play-mode tests, or both, with a single command. The CLI handles test runner flags, output formats, and exit codes that CI systems already understand.

Build execution. Trigger builds for any supported platform target. The CLI accepts the build target, output path, and relevant options, without requiring a custom C# build script unless your pipeline genuinely needs one.

The critical design point: it is provider-agnostic. The same CLI commands work in GitHub Actions, GitLab CI, Jenkins, TeamCity, Bitbucket Pipelines, or a developer's local terminal. This means your build logic lives in one place, not duplicated across platform-specific YAML blocks and shell scripts.

Before and After: A Worked Example

Let us look at what a multi-platform CI pipeline looks like with and without the Unity CLI. The following examples are hypothetical, illustrating the pattern, not reproducing exact CLI syntax that may change as the tool matures.

Before: Custom Scripts

A typical GitHub Actions workflow calling custom scripts might look like this:

# .github/workflows/build.yml, OLD APPROACH (hypothetical)
name: Build All Platforms

on: push

jobs:
  build-windows:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install Unity
        run: ./ci/install_unity.sh 2022.3.20f1 "Windows"
      - name: Build
        run: |
          ./ci/build_windows.sh \
            --project-path . \
            --output ./builds/windows/game.exe
      - uses: actions/upload-artifact@v4
        with:
          name: windows-build
          path: ./builds/windows/

  build-android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install Unity
        run: ./ci/install_unity.sh 2022.3.20f1 "Android"
      - name: Build APK
        run: |
          ./ci/build_android.sh \
            --project-path . \
            --keystore ${{ secrets.ANDROID_KEYSTORE }} \
            --output ./builds/android/game.apk
      - uses: actions/upload-artifact@v4
        with:
          name: android-build
          path: ./builds/android/

Every step depends on a custom script that wraps Unity's -batchmode -executeMethod invocation. If the project path changes, every script breaks. If you add Windows 64-bit as a separate target, you need another script.

After: Unity CLI

The same pipeline using Unity CLI collapses installation, testing, and building into fewer, clearer steps:

# .github/workflows/build.yml, UNITY CLI APPROACH (hypothetical syntax)
name: Build All Platforms

on: push

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install Unity 2022.3
        run: unity-cli install 2022.3.20f1 --modules Android,Windows
      - name: Run Edit Mode Tests
        run: unity-cli test --mode EditMode --output test-results.xml
      - uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: test-results.xml

  build-windows:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - name: Install Unity 2022.3
        run: unity-cli install 2022.3.20f1 --modules Windows
      - name: Build Windows
        run: unity-cli build --target StandaloneWindows64 --output ./builds/windows/
      - uses: actions/upload-artifact@v4
        with:
          name: windows-build
          path: ./builds/windows/

  build-android:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - name: Install Unity 2022.3
        run: unity-cli install 2022.3.20f1 --modules Android
      - name: Build Android
        run: unity-cli build --target Android --output ./builds/android/
      - uses: actions/upload-artifact@v4
        with:
          name: android-build
          path: ./builds/android/

The differences are structural, not cosmetic:

  • No platform-specific shell scripts. Build logic is expressed declaratively in the pipeline config.
  • Centralized version management. Changing the Unity version means editing one string in each job, not hunting through shell scripts for hardcoded paths.
  • Standard test integration. Test results appear as structured output that CI providers can parse natively, no wrapper scripts to convert Unity's log output into JUnit XML.

Adapting to GitLab CI

The same principle applies across providers. Here is what the pipeline might look like in .gitlab-ci.yml:

# .gitlab-ci.yml, UNITY CLI APPROACH (hypothetical syntax)
stages:
  - test
  - build

variables:
  UNITY_VERSION: "2022.3.20f1"

unit-tests:
  stage: test
  image: unityci/editor:ubuntu-2022.3.20f1-linux-il2cpp
  script:
    - unity-cli install $UNITY_VERSION --modules Linux
    - unity-cli test --mode EditMode --output test-results.xml
  artifacts:
    reports:
      junit: test-results.xml

build-windows:
  stage: build
  script:
    - unity-cli install $UNITY_VERSION --modules Windows
    - unity-cli build --target StandaloneWindows64 --output ./builds/windows/
  artifacts:
    paths:
      - ./builds/windows/

build-android:
  stage: build
  script:
    - unity-cli install $UNITY_VERSION --modules Android
    - unity-cli build --target Android --output ./builds/android/
  artifacts:
    paths:
      - ./builds/android/

The pattern is the same: declare what you need, let the CLI handle how. Whether your CI runs on GitLab's shared runners or self-hosted machines on custom game backend infrastructure, the commands do not change.

What This Means for Live Game Services

For studios running live games with frequent content updates, CI/CD is not a one-time setup, it is an ongoing operational cost. Every new platform, every Unity upgrade, every SDK integration that touches the build pipeline creates a potential failure point.

Consider a live-service game that ships weekly content updates across three platforms (Windows, Android, iOS). Before Unity CLI, a hypothetical pipeline might require:

  • 3 platform-specific build scripts (~150 lines each)
  • 1 shell script for Unity version installation (~120 lines)
  • 1 test runner wrapper (~80 lines)
  • Shared configuration spread across .env files and CI provider variables

That is roughly 650 lines of custom script just to produce three builds. When a developer modifies a build script and breaks the Android pipeline at 2 AM before a content drop, the root cause is usually buried in platform-specific logic that only one person understands.

Unity CLI pushes that complexity into a maintained tool. Your CI configuration becomes a specification (what to build) rather than implementation (how to invoke the Unity editor in batch mode). The latter is exactly the kind of code that rots, it works until it does not, and when it breaks, nobody knows why.

Best Practices for Adopting Unity CLI in Your Pipeline

1. Start with test automation, not builds. Wire up edit-mode and play-mode tests first. Tests give you a quick feedback loop to validate that the CLI integration works, and you win better coverage as a side effect. Builds are the higher-value target, but tests are the safer starting point.

2. Pin your Unity version explicitly in CI config. Even though the CLI manages installations, your CI config should declare the version as a variable (like UNITY_VERSION in the GitLab example above). When you upgrade, you change it in one place and the whole pipeline shifts together.

3. Keep build targets as parameters, not hard-coded values. Use matrix builds (GitHub Actions) or parallel jobs (GitLab CI) driven by a list of targets. Adding a new platform, say, Linux, should mean adding one entry to the matrix, not writing a new script.

4. Add build verification after every CLI-invoked build. Check that the output artifact exists and has a reasonable file size. A zero-byte build output means the CLI command silently failed upstream. Catch it immediately, not when QA reports the build is missing.

5. Cache the Unity installation in CI. Downloading and installing Unity with all platform modules can take 10-15 minutes. Use your CI provider's caching mechanism to store the installed editor between runs. A cache hit drops that step to under a minute.

Where ProjectMakers Fits

Adopting Unity CI/CD automation is one of those tasks that is straightforward in principle and fiddly in practice, especially when your project has custom build steps, platform-specific players, or inherited scripts from previous team members. Studios that would rather focus on game content than pipeline plumbing often bring in a game development partner to set up, document, and migrate the pipeline once, then hand over a system the internal team can maintain independently.

Whether your team handles it in-house or with outside help, the direction is clear: custom shell scripts wrapping Unity's batch mode are becoming legacy tooling. If you have an existing project with multiple build targets and a CI/CD pipeline that nobody fully trusts, try replacing one platform's build script with the new CLI as your next experiment. Start with the platform that breaks most often, you will know fast whether the CLI covers your needs.


Source: CICD Made Easier with Unity CLI

Continue in this topic

Software products