Every time I join a Unity project mid-flight, the CI conversation starts in the same place. Someone wants play-mode test automation. It is always framed as the responsible engineering thing to do, and it is always the fifth most useful thing available.
Because underneath that conversation, the build usually exists on exactly one laptop. Right editor version, right Android SDK, a provisioning profile, a keystore in a folder its owner would struggle to find again, and eight months of small manual fixes living only in one person's memory. Nobody logs that as a risk, because from the inside it does not feel like one. Builds come out. It works, right up until that person is on a flight, or the client wants a build on a Friday night, or they leave.
I have shipped Unity to Windows, Android, iOS and standalone headsets, built the CI/CD for a cross-platform C# SDK at Geonode, and managed App Store and Play Console releases as CTO at RAQTS. The order that has actually worked for me is: build somewhere nobody uses, make every artifact identify itself, get signing keys off laptops, automate delivery, and only then write tests. Most teams do that list backwards, and the backwards version feels rigorous while producing very little.
Unity is genuinely harder to put on CI than a web app
This is worth being specific about, because "just add CI" is advice written by people whose builds are a checkout, an install and a bundler.
The Unity editor is a licensed application, so your build agent has to activate a licence and release it again. That is not a footnote, it is a live source of flaky failures that have nothing to do with your code. The Library folder is a multi-gigabyte derived cache that takes tens of minutes to regenerate, so a cold build and a warm build are different animals with different failure modes. Asset import is not fully deterministic across editor versions, so "same commit, different Unity patch release" is a real category of bug. And underneath it all sit the platform toolchains: an Android SDK and NDK pinned to versions Unity tolerates, an Xcode version Apple has not deprecated yet, and a Windows toolchain for desktop.
At Geonode I built the Repocket C# SDK for Windows, Android and iOS and set up the Unity and Windows pipelines around it. The lesson was blunt. Three platforms compiling on one developer's machine is not three platforms working. Until every target builds on a machine nobody has touched by hand, you do not know your dependencies. You know what your laptop happens to already have, which is a different and much less useful fact.
Step one: build on a machine nobody uses
The first pipeline you write should do one thing. Check out the commit, build the player, drop the artifact somewhere with the commit hash in its name.
No tests. No distribution. No platform matrix.
The value is not quality assurance, which I think is exactly why rigorous people skip it. That pipeline is an executing, always-current description of your build requirements. Every undocumented step gets flushed out in the first week: the SDK version nobody wrote down, the plugin someone copied into Plugins by hand, the define symbol set in the editor and never committed, the API key in a local file git never saw. Each is a landmine under the project, and a red pipeline finds all of them in an afternoon. That one guarantee has been worth more to me than any test suite I wrote in the six months after.
Step two: make every build say what it is
This is the cheapest high-value item on the list and it gets skipped constantly.
Every build should carry an identity you can read without a debugger: a version, a build number that increments on its own, and the short commit hash. Put it on a debug screen, in the log, in a settings corner, anywhere readable in ten seconds. Then have CI stamp it, because a version number a human edits is a version number that is sometimes wrong.
// Generated by CI before the build. Never hand edited.
public static class BuildInfo
{
public const string Version = "1.4.2";
public const string Build = "318";
public const string Commit = "a91c40e";
}
I learned to care about this in XR rather than in a normal app. Standalone headsets pass between hands with no install history a developer can see, so when someone in the field reports behaviour you cannot reproduce, the only question that matters is which build they were holding. Without an on-screen hash, that device could be running any of the last six.
The same rule holds on a phone. A bug report against "the TestFlight build" is a guess. A bug report against 1.4.2 (318) a91c40e is evidence.
Step three: get signing off laptops
Keystores, provisioning profiles and store API keys belong in the CI secret store, and the pipeline should be the only thing that touches them for release builds.
The obvious reason is availability. If one machine can sign an Android release, that machine is a single point of failure for shipping at all, and an Android keystore is not regenerable. Lose it and you cannot update the app under the same listing, ever.
The less obvious reason matters more to me. Once signing is a CI job, "who can publish" becomes a permission you granted rather than a fact about who has which files on which laptop. Release authority should be deliberate, and it usually is not.
Step four: delivery, not just building
An artifact in a storage bucket is not a release. The pipeline is not finished until the build reaches whoever has to look at it: TestFlight or a Play internal track, a signed installer for desktop, a direct install path for headsets.
The reason to automate this is feedback cycle time. When getting a build to a client is a fifteen minute chore, it happens once a week and every review round costs a day of calendar time. When it happens on merge, the client sees Tuesday's work on Tuesday. At RAQTS, where a Unity interactive platform, mobile apps and a web portal all had to move together, that gap decided whether a release was one coordinated thing or three things quietly drifting apart.
Step five, finally: tests
In Unity, the automated checks with the best return are narrower than people expect. Compile every target, so a change that breaks the iOS build is caught on the pull request instead of on release day. Unit test the pure C# where the actual logic lives, which is the same discipline as keeping a shared core free of platform knowledge. Both are cheap and pay every week.
Full play-mode automation is a much larger investment with much slower returns, and I would not reach for it before the four steps above exist. A team that builds reliably, identifies every artifact, signs from CI and delivers automatically is in better shape than a team with a green test suite and one laptop that knows how to build.
What I deliberately do not automate
Not every branch gets a full build. Unity builds are slow and, on hosted runners, not free. Compile checks on pull requests, full multi-platform builds on main and on tags. I keep the Library cache warm between runs, because a cold agent is a twenty minute tax on every job, and I run one scheduled clean build so cache drift never quietly becomes an undeclared dependency.
The store is a queue, not a deploy step
The part that surprises web-first teams is that publishing is not the end of the pipeline. It is a queue owned by someone else.
Review takes as long as it takes. Once a build is live you cannot hot fix it, and users update on their own schedule, so old clients keep talking to your backend long after you stopped thinking about them. Hence staged rollouts so a bad build reaches one percent instead of everyone, a backend that stays kind to older clients, and a rollback you have rehearsed rather than improvised.
That release discipline is the piece I carried furthest out of Unity. I ship changes to live AI voice agents the same way now. No store review, same underlying truth: the change is live in front of real users, so it goes out small, into the quietest traffic, with a reversal already practised.
The test to run today
Ask whether anyone other than the usual person could produce a signed, shippable build today, from a clean checkout, without asking them a question. If the answer is no, that is the work. Not coverage, not a nicer branching model.
The through line from a Unity project to an AI agent in production has been the same for me every time. The interesting engineering is rarely the part users see. It is whether you can change it safely, know exactly what you shipped, and put it back.
This is an adapted version of a longer piece on my site, where I write about shipping cross-platform products across Unity, XR, mobile and AI agents.
Top comments (0)