Unity's New CLI: What Changes for CI/CD and Testing
Unity shipped a standalone CLI this week. Here's what it replaces from our build pipeline, and what we've already put to use.
TL;DR
- Unity released a standalone CLI two days ago (July 20). It's a single
unitybinary that installs editors, manages projects and auth, and drives builds from the terminal, no Unity Hub GUI required. - It replaces the old
-batchmode -executeMethoddance we've used for years in GitHub Actions, Bitbucket Pipelines, and GitLab CI, plus the slower Unity Hub headless install path. - Command output is structured (JSON or TSV) with real exit codes, instead of scraping a log file for a magic string.
- An experimental companion package,
com.unity.pipeline, lets you register your own commands with a[CliCommand]attribute and call them, or arbitrary C#, against an Editor that's already running, with no recompile. - We've already wired it into our build pipeline, our test runs, and a custom screenshot command. Unity is also pitching this heavily as infrastructure for AI agents; that's not the angle this post takes, and it's genuinely useful without it.
Unity shipped a standalone CLI two days ago. I spent the time since running it against our actual build setup instead of just the demo, and there's enough here worth writing down.
How we used to do this
For as long as I can remember, driving Unity from a script meant the Editor's command-line arguments: -batchmode -quit -projectPath, and if you wanted anything beyond a default build, -executeMethod pointing at a static method in your project. A typical CI build call looked something like this:
Unity -batchmode -quit -nographics \
-projectPath "$PWD" \
-buildTarget Android \
-executeMethod BuildScript.PerformBuild \
-logFile -
That worked, but every piece of it was a workaround. There's no structured result, just a log stream, so your CI script greps for a string like "Build succeeded" or checks whether a specific file exists afterward. Exit codes were inconsistent enough across platforms that most pipelines we've built double-check with a log grep anyway, on top of the exit code. Running tests meant the same pattern with different flags:
Unity -batchmode -quit -projectPath "$PWD" \
-runTests -testPlatform EditMode \
-testResults results.xml \
-logFile -
which at least produced NUnit XML that CI test reporters could parse, so testing was always the least painful part of this. Custom commands were the worst of the three: every one needed a static method baked into the project ahead of time, and every single invocation cost a full Editor boot plus a domain reload, often a minute or more before your actual code ran. If you wanted to run five small checks, you paid that startup cost five times.
We've run this workflow across GitHub Actions, Bitbucket Pipelines, and GitLab CI on different projects over the years, and the shape barely changes platform to platform. All three need a self-hosted runner with a licensed Unity install (none of them offer Unity in their own shared compute), and the YAML wrapper around the batchmode call is nearly identical whichever one you're on. The actual pain was never the CI platform, it was always what happened once Unity itself started up. We've written before about the platform side of this decision, and about the Gradle build failures that show up once the build itself is running, and the log-scraping habit is the same root cause in both places: no structured way to ask Unity what happened.
What the new Unity CLI actually is
The CLI ships as a single self-contained binary, distributed through Unity's own install scripts for now, with standard package manager support (brew, winget, apt) coming later. It adds itself to your PATH on install, and you update it in place with unity upgrade, so a build agent needs nothing else on the machine to get started.
The practical difference shows up immediately: because it's a native binary rather than a wrapper around the Editor, it starts in a fraction of the time the old Unity Hub headless path took. That gap is small per call, but a CI pipeline or a script makes dozens of calls a day, and it adds up fast.
Installing an editor with the modules you need is one line:
unity install 6000.2.10f1 -m android ios webgl
From there the commands read the way you'd expect: unity editors lists what's installed, unity open launches a project with the right editor already resolved, unity auth login signs you in, and unity modules list 6000.2.10f1 tells you which module IDs a given version actually has.
Built for automation
This is the part that actually fixes our old CI setup. Every command can emit structured output instead of a log stream:
unity editors --format json
unity editors --format tsv
Errors go to stderr, results go to stdout, and exit codes follow a fixed contract: 0 for success, 1 for an error, 130 if it was cancelled. That's a script's entire job made simpler, no more grepping a log file for a string that might change wording in the next Unity release.
Unattended installs skip every prompt:
unity install 6000.2.10f1 -m android ios --accept-eula --yes
and on a build agent, authentication happens through a service account and environment variables, no browser and no manual step. unity doctor diagnoses environment, credential, and configuration problems, which is the kind of thing that used to eat half an afternoon of a new contractor's onboarding.
Talking to a running Editor
Managing installs is only half of it. An experimental companion package, com.unity.pipeline, lets a running Editor receive commands from the CLI, entirely on your own machine, on Unity 6.0 LTS and newer. Adding it to a project is one command:
unity pipeline install
Once it's there, any static method in your project becomes a terminal-callable command by adding a [CliCommand] attribute, with [CliArg] on its parameters. Here's a small one we added for our own use, a screenshot command:
using Unity.Pipeline.Commands;
using UnityEngine;
public static class DevPipelineCommands
{
[CliCommand("screenshot", "Capture the Game view and save it to disk")]
public static string Screenshot(
[CliArg("path", "Where to save the PNG", Required = true)] string path)
{
ScreenCapture.CaptureScreenshot(path);
return path;
}
}
No registration step; the package finds it. unity command screenshot --path Screenshots/build-42.png runs it against the connected Editor and hands back the result. There's also unity command eval, which compiles and runs an arbitrary C# expression against a live Editor with no recompile:
unity command eval "return Application.version;"
It's gated behind a security token for obvious reasons, and the same mechanism can target a running development build instead of the Editor with a --runtime flag.
Unity's own announcement leans hard on all of this being the foundation for AI agents operating the Editor directly. I'm leaving that part alone here. Not everyone building games wants an agent driving their Editor, and the automation and the fast local REPL are worth having regardless of who or what is calling them.
What we're actually using it for
CI/CD. The batchmode calls in our GitHub Actions and GitLab CI jobs are being replaced with unity install plus a [CliCommand] wrapping our existing build method. The build agent no longer needs a pre-baked Unity install; the pipeline installs exactly the version and modules it needs and reports back in JSON, which our existing pipeline scripts can consume directly instead of parsing log output.
TDD. We wrapped Unity's TestRunnerAPI in our own [CliCommand("run-tests", ...)]. The difference isn't the test framework, it's that the Editor stays open between runs instead of cold-starting for every batchmode invocation. For an actual test-driven loop, where you want results back in seconds after a small change, that's the difference between it being practical and not.
Automatic screenshots. The screenshot command above runs against a connected Editor or a live dev build, so we can capture a consistent set of reference images per scene on every build, for visual regression checks, without a human opening the project and clicking through it.
Other places this is worth trying
A few other uses worth considering, even if we haven't built all of them ourselves yet:
- Multi-platform build matrices where each job installs only the editor version and modules it needs, instead of maintaining a separate Docker image per Unity version.
unity doctoras a first step in onboarding a new machine or a contractor, instead of a checklist of manual Unity Hub steps.- Smoke-testing a nightly dev build by hitting a
--runtimecommand after it launches, instead of a person opening the build to confirm it didn't crash on startup. - Auditing which Unity versions and modules are actually installed across a small team's machines with
unity editors, before a version mismatch causes a "works on my machine" bug.
Where to start
Unity's announcement post has the background and a short demo video; the CLI documentation has the full command reference. If you're untangling a Unity build pipeline or setting one up from scratch, that's the kind of CI/CD and DevOps work we do alongside Unity engineering day to day.