macOSFastlaneCI/CDDistributionNotarization

macOS CI/CD with Fastlane (part 1): distribution outside the App Store

Distributing a macOS app outside the Mac App Store looks like a solved problem —until you sit down to do it. Signing isn’t enough, notarization has its own flow, the ticket has to be stapled to the binary, and all of that has to be orchestrated every time you ship a new version. Without a pipeline you’re the one running it manually on every release.

This five-part series builds that pipeline with Fastlane, running locally on my Mac and uploading the final binary to GitHub Releases. The reference app is pokedex-macos, a minimal SwiftUI Pokédex: deliberately small, so the focus stays on the distribution steps, not the logic.

What we’re building

A full flow that, with a single command, does all of this:

bundle exec fastlane release version:1.0.1
  1. Bumps the version and build numbers.
  2. Compiles the app in Release configuration.
  3. Signs it with a Developer ID Application certificate.
  4. Sends it to Apple for notarization.
  5. Staples the notarization ticket onto the .app.
  6. Creates a DMG.
  7. Tags, pushes, and publishes a GitHub Release with the DMG as an asset.

At the end of the command you have a public URL anyone can download, double-click, and use with no warnings.

Why distribute outside the Mac App Store

The Mac App Store solves a lot of things —at the cost of others. Apple requires strict sandboxing, reviews every version, blocks certain system APIs, and takes a cut of revenue. For plenty of apps that’s fine. But there are cases where it doesn’t fit:

  • Developer tools that need to reach into other processes or arbitrary directories.
  • Private betas or internal distribution.
  • Small utilities that don’t justify the review cycle.
  • Apps that rely on APIs the sandbox blocks.

In any of those cases the route is Developer ID + notarization. You host the binary wherever you like (your site, S3, GitHub Releases), and Apple validates remotely that the app is yours and isn’t malicious.

Gatekeeper — the gate keeper

macOS ships a component called Gatekeeper that decides whether an app downloaded from the internet can launch. When you download a .dmg through the browser, macOS attaches an extended attribute called com.apple.quarantine. The first time the user opens the app, Gatekeeper looks at that attribute and asks three questions:

  1. Is it signed with a valid Developer ID certificate?
  2. Does it have a notarization ticket from Apple?
  3. Does the ticket match what’s actually in the binary?

If all three answers are yes, the app opens without friction. If any one is missing, the user sees a dialog asking for permission and —it gets worse with each macOS release— the only way out is System Settings → Privacy & Security → “Open Anyway”. Most people close the dialog and never come back.

The three steps Apple demands

The pipeline revolves around three acts:

Signing. Signing means applying your cryptographic identity to the binary using a Developer ID Application certificate (more on this certificate, and how it’s different from Mac App Distribution, in part 2). The tool is codesign, though we almost never call it by hand —Xcode does, and Fastlane sits on top of Xcode.

Notarization. The signed app is sent to Apple through notarytool. On the other side an automated pass runs —malware checks, suspicious patterns, misuse of private APIs— and a couple of minutes later you get a verdict. If it passes, Apple issues a “ticket” tied to that specific build.

Stapling. The ticket lives on Apple’s servers, but Gatekeeper also needs to work offline. That’s why stapler staples the ticket into the .app itself. That way, even if the user opens the app with no network, macOS can verify everything locally.

All three are required on macOS 10.15 and up. Skipping one works on your Mac (where the certificate is already in the Keychain), but it ruins the experience for anyone else who downloads it.

Where Fastlane comes in

None of this was invented by Fastlane. Every tool —codesign, xcodebuild, notarytool, stapler, hdiutil— ships with macOS and Xcode. You can assemble the same pipeline as a 200-line bash script and it works.

What Fastlane adds is declaring the pipeline as code. Instead of a run of commands with error branches, you have a Fastfile with two lanes:

lane :qa do
  # internal build, signed but not notarized
end

lane :release do |options|
  # build + sign + notarize + staple + DMG + GitHub Release
end

Every Fastlane action (build_mac_app, notarize, set_github_release) is a thin wrapper over the official tools. Fastlane doesn’t hide what happens —it orders it. When something breaks, the output tells you the exact command that failed and with which arguments, so you can reproduce it by hand if you need to.

And for local pipelines like this one, it has a concrete advantage: credentials live in a .env that Fastlane loads on its own, so the Fastfile stays clean and there are no hardcoded tokens anywhere.

What comes next

Five parts total:

  1. Overview — this one. What Apple demands and why orchestrating it is worth it.
  2. Setup — the Developer ID Application certificate (and its critical distinction from Mac App Distribution), the App Store Connect API Key, the GitHub PAT, and how the .env ends up.
  3. Notarization in depth — what codesign actually signs, the hardened runtime, entitlements for Developer ID, notarytool with an API Key, and why stapling matters.
  4. Auto-versioning and lanes — how Fastlane handles MARKETING_VERSION and CURRENT_PROJECT_VERSION, the difference between qa and release, and the skip_publish mode for rehearsals.
  5. GitHub Releases distribution — the final step of the pipeline: DMG, tag, push, and publish via set_github_release.

By the end I have a bundle exec fastlane release version:x.y.z that goes from code to public download without touching anything else. Part 2 kicks off the setup.