In Part 1 I laid out the Jenkins-as-a-Code setup — JCasC, Job DSL, ephemeral workers, Packer images, all of it. And I said macOS workers were their own special kind of hell that deserved a separate post. This is that post.

If you've never had to run macOS builds in CI, the short version is: everything that's easy on Linux is hard on macOS, and everything that's hard on Linux is also hard on macOS but in a different way. Apple's licensing rules, the fact that you can't just spin up a Mac in AWS the same way you spin up Ubuntu, the keychain weirdness, the signing tooling, the Xcode versions — all of it adds up. And the answer at most companies is "we have a few Mac minis under someone's desk that everybody SSHes into". Which works until it doesn't.

I wanted the same story I had for Linux and Windows — born for one build, destroyed after, byte-identical state every time — but on macOS. Took me a while to get there. Here's what actually worked.

Why macOS is hard in the first place

Before getting into solutions, it helps to name the problems, because they explain why the architecture ends up looking weird.