Shipping Greenhouse's release pipeline on a security camera's Mac Mini
Greenhouse needed a release pipeline: push a tag, get a signed .dmg. The Tauri docs make this look like a five-minute job - add a GitHub Actions workflow, point it at macos-latest, done. Except I don’t run GitHub-hosted runners here. Everything in this homelab runs on hardware I own, including CI.
That single constraint turned a five-minute job into a real infrastructure project, and along the way into a genuine debugging story.
Finding a Mac that wasn’t doing anything
Building a macOS app needs an actual Mac. I already had one server-side: a Mac Mini running Frigate, my security camera’s object-detection NVR. Before touching it I checked its actual load, not just assumed it had headroom - and it didn’t. Sustained 75-80% CPU across all 8 cores, continuously, from real-time detection work. Adding a Rust compiler on top of that is a real resource fight, not a hypothetical one.
I didn’t have a second idle Mac sitting around, so I made a deliberate tradeoff instead of pretending the contention wasn’t there: registered the build runner on the same box, but capped its scheduling priority (nice plus a background process type) so it yields CPU to the camera system under load. Releases are rare, tag-triggered events - occasionally slower builds are a fine price for not buying new hardware.
The build that took sixteen minutes to fail
First real test run. I pushed a tag, watched it pick up the job, watched cargo compile Tauri’s entire dependency tree from a cold cache - tauri, wry, rusqlite bundled from C source, the works. Ten minutes of legitimate compiling. Then it got to bundling the actual .dmg file and just sat there. Six minutes later: Finder got an error: AppleEvent timed out.
Turns out Tauri’s DMG bundler doesn’t just zip files into a disk image - it drives Finder through AppleScript to lay out where the app icon and Applications-folder shortcut should sit in the installer window. That requires a one-time permission grant, the kind of dialog macOS pops up asking “Allow Terminal to control Finder?” On a machine nobody is sitting in front of, there’s no one to click Allow. The dialog either never renders or times out unanswered, and the whole build fails.
I reproduced the exact same error on my own interactive machine first, specifically to rule out “no GUI session” as the cause - and it still failed, even with an active Finder and a real login. That told me this wasn’t about headlessness at all. It was a one-time consent gate, not a fundamental capability gap.
The fix that didn’t fix it
The documented remedy is setting CI=true, which tells Tauri’s bundler to add a flag that skips the AppleScript step entirely. I set it. It didn’t work. Same timeout, same failure, sixteen minutes in.
Rather than guess again, I went and actually read the source - both the Rust bundler code and the JavaScript GitHub Action that wraps it, not blog posts describing them secondhand. The bundler’s logic was exactly what I expected: skip the AppleScript step when CI is true. But the Action I was using to drive the whole build had its own opinion. It sets a second, more specific variable that silently overrides my first one, specifically so that builds on GitHub’s own hosted Mac runners - which do have working Automation permissions - keep their nicer icon layout. Reasonable default for the common case. Exactly wrong for mine.
I verified the actual fix the boring, reliable way before trusting it in CI: SSH’d into the build machine, ran the identical build command by hand with both variables set correctly, and watched a real, valid, signed .dmg come out the other end. Only after that did I push a second test tag through the real pipeline. Sixteen minutes later: a green build, a draft GitHub release, and a working installer, produced by a Mac Mini that spends the other 99% of its time watching my backyard for raccoons.
What I’d tell someone hitting this
If your Tauri macOS build times out on the .dmg step with an AppleEvent error, and you’re already building through tauri-apps/tauri-action, setting CI=true alone won’t save you. You need to explicitly unset TAURI_BUNDLER_DMG_IGNORE_CI too. I found an existing note in my own knowledge base from an earlier, smaller version of this exact problem - written months ago, before I’d found the real fix - that just said “build without a DMG, or do it on a real desktop.” It wasn’t wrong, exactly. It just stopped one layer short of the actual answer. I went back and corrected it rather than write a new note next to the old, incomplete one.
Related reading
Adding a native macOS menu to Greenhouse broke Cmd+Q, and my test harness couldn't press Cmd+N either
set_menu replaces the default menu, it doesn't extend it - taking Quit, Hide, and every Edit shortcut with it. Then the WebDriver actions endpoint turned out unable to press a key.
Teaching a project manager to recognize a Logic Pro session
.logicx files aren't files, they're bundles - and the filter hiding .DS_Store junk was hiding every DAW project too. Plus the silent no-op click that fooled a retry loop.
The fix I talked myself out of
A 'don't build this' recommendation built on two unverified assumptions, and the one-line Tauri activation-policy fix that was sitting there the whole time.