Tutorials Modern Delivery Pipeline Chapter 6

Shipping the App Half: iOS, Android, Mac, and Windows Release Pipelines

DeliveryChapter 6 of the Modern Delivery Pipeline28 minJune 7, 2026Intermediate

Ch 5 shipped the web half, where a bad release is undone with wrangler rollback in thirty seconds. This chapter ships the app half — iOS, Android, Mac, and Windows — and the single most important thing to understand up front is what changes: you cannot instantly un-ship a native app. Once a build is in the App Store or installed on a user's machine, "rollback" is no longer a command; it's a strategy. Every best practice in this chapter exists because of that asymmetry.

The good news: all four targets are the same five-act play and the same six-stage spine from Ch 1. Learn the shape once and each platform is "the same thing, different store." And the Ch 4 signing identity — the whole reason we hardened the Mac mini — is what powers all of it.

The Same Spine, Four Endings

Every native release runs the same six stages; only the nouns change. This is the Ch 1 table again, now with the release detail filled in:

Stage🍎 iOS🤖 Android💻 Mac (direct)🪟 Windows
Buildxcodebuild archivegradle bundleReleasexcodebuild archivemsbuild / dotnet publish
SignDist cert + provisioning profileUpload key → Play App SigningDeveloper ID + notarizeAuthenticode (signtool)
Package.ipa.aab.dmg / .pkgMSIX / MSI
DistributeTestFlight → App StorePlay Console tracksR2 / your websitewinget / MS Store / site
UpdateApp Store auto-updatePlay auto-updateSparkle-style appcastMSIX / winget upgrade
"Rollback"Halt phased release; expedite a fixHalt staged rollout %Re-publish previous DMG + appcastRe-publish previous + winget

The first four stages are mechanical and well-trodden. The interesting rows are the last two — update and rollback — because that's where native diverges hardest from web, and where the rest of this chapter lives.

Loading diagram…

Figure 1 — One signed artifact, four store-shaped endings. The build and signing converge on the Ch 4 identity; the divergence is entirely in how each store distributes and updates. Signing (red) is the shared hard part — see Ship iOS Ch 1.

iOS → TestFlight → App Store

The iOS pipeline runs on the Mac mini (Apple-only). fastlane is the glue that wraps every step — the same Fastfile runs on a laptop or the runner (Ship iOS Ch 1):

# fastlane/Fastfile
lane :beta do
  match(type: "appstore")        # sync signing identity (Ch 4) — no certs on disk
  build_app(scheme: "MyApp")     # xcodebuild archive + export .ipa
  upload_to_testflight           # → beta testers, the progressive-delivery slice
end

The release shape is TestFlight first, then a phased App Store release. TestFlight is your "preview URL" — a real signed build in front of a limited audience before everyone. Then App Store Phased Release rolls an approved update out to existing users over seven days (1% → 2% → 5% → 10% → 20% → 50% → 100%), and you can pause it at any step if reports go bad. That pause is the closest thing iOS has to a rollback — it stops the bleed, but it doesn't un-install what already shipped.

Android → Play Console Tracks

Android builds on cheap Linux (Ch 3) — no Apple tax. The artifact is an Android App Bundle (.aab), and signing is usually delegated to Play App Signing (Google holds the app signing key; you hold an upload key). Distribution is through ascending tracks:

Loading diagram…

Figure 2 — Play tracks are environments for apps. The same artifact is promoted up the tracks (internal → closed → open → production), exactly the Ch 5 promotion idea — and production itself supports a staged rollout percentage you can halt, the Android twin of Cloudflare's traffic split.

Mac (Direct) → Notarize → DMG

This is the pipeline this project actually ships, so it's the most concrete. A direct-distribution Mac app (not on the Mac App Store) must be Developer ID-signed and notarized, or Gatekeeper blocks it on launch:

xcodebuild -scheme "Simple App Shipper" -configuration Release \
  -archivePath build/App.xcarchive archive
xcodebuild -exportArchive -archivePath build/App.xcarchive \
  -exportPath build/export -exportOptionsPlist scripts/exportOptions.plist   # Developer ID signing
xcrun notarytool submit "build/export/Simple App Shipper.app" \
  --keychain-profile "AC_PASSWORD" --wait                                     # Apple notarizes
xcrun stapler staple "build/export/Simple App Shipper.app"                    # attach the ticket
hdiutil create -volname "Simple App Shipper" -srcfolder build/export -ov -format UDZO build/App.dmg
xcrun stapler staple build/App.dmg
wrangler r2 object put simpleappshipper-releases/SimpleAppShipper.dmg --file build/App.dmg --remote

Distribution is a DMG on R2 behind releases.simpleappshipper.com; auto-update is a Sparkle-style appcast (an XML feed the app checks for new versions). "Rollback" here is genuinely simple compared to the stores: re-upload the previous DMG and point the appcast back at it — you control the whole channel, so you can revert the download in seconds (though already-updated users still need the next build).

Windows → Authenticode → MSIX

The Windows pipeline runs on a windows-latest runner (Ch 3). The signing concept mirrors macOS but with Authenticode:

dotnet publish -c Release                                  # or msbuild
signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 MyApp.exe   # Authenticode
makeappx pack /d .\publish /p MyApp.msix                   # package
signtool sign /fd SHA256 MyApp.msix                        # sign the package too

Two Windows-specific facts worth knowing:

The Hard Truth: You Can't wrangler rollback an App

Here's the asymmetry stated plainly, because it's the whole reason app releases need more discipline than web ones:

Loading diagram…

Figure 3 — Web reverts; apps can only stop-and-fix-forward. A shipped app lives on users' devices; you can stop new installs (halt the rollout) but can't reach back and un-install. This is why the app half leans so hard on catching problems before wide release.

Because there's no true rollback, your real safety nets are all about limiting and recovering rather than reverting:

Safety netWhat it does
Beta channel firstTestFlight / Play closed track / a beta appcast — catch it before the public ever sees it. The single most effective net.
Phased / staged rolloutExpose the new version to 1–10% of users; a bad build only reaches the slice before you halt.
Halt the rolloutStop new users getting the bad version — the "pause" button on iOS phased release / Android staged rollout.
Fix-forward + expediteShip a corrected build fast; request expedited App Store review for genuine emergencies.
Server-side kill switchGate risky features behind a remote flag your backend controls — so you can disable the bad behaviour without shipping a new build at all.

Signing Is Still the Hard Part

Across all four, the build is easy and the signing is where the pain lives (Ship iOS Ch 1 made this case for iOS; it generalises). Four platforms, four identities the Ch 4 cluster has to manage safely:

The Ch 4 rule holds for all of them: the identity lives on the hardened machine, is used per-job, and never sits in git or on a laptop.

What This Project Actually Does

Honest status: of the four, this project ships the Mac (direct) column for real, and the others are the general pattern.

The summary: the hardest native column — a notarized, directly-distributed Mac app with a rollback you actually control — is real here; the other three are the same spine pointed at a different store.

Mental Model — Three Sentences

  1. All four native targets are the same build/sign/package/distribute/update spine from Ch 1; only the store-shaped ending differs, and the Ch 4 signing identity is what powers every one of them.
  2. You cannot instantly un-ship a native app, so each store's phased/staged rollout is your blast-radius control and "halt the rollout" is your stop button — but a true revert doesn't exist, which is why beta channels and catching problems pre-release matter so much more than on the web.
  3. The app world's real rollback is a server-side kill switch: make risky features remotely toggleable so you can disable a broken one instantly without shipping a new build — web-speed recovery on a native app.

Try It Yourself (15 Minutes)

  1. Map your spine. For an app you ship (or want to), fill in the six-stage row for its platform. The "sign" and "rollback" cells are where the real work is.
  2. Find your beta channel. TestFlight, a Play closed track, or a beta appcast — which one stands between your next build and all your users? If the answer is "nothing," that's the net to build first.
  3. Locate a kill switch. Pick the riskiest feature in your app. Could you disable it from your backend without shipping an update? If not, that's the highest-leverage resilience change you can make.
  4. Check your signing hygiene. Where does your signing key live right now? If the answer is "committed in the repo" or "on my laptop," revisit Ch 4 — that's the one to fix before automating anything.

Where This Lands in the Series

Both halves now ship: the web reverts in seconds, the apps roll out in slices behind beta channels and kill switches. You can take a change from a keystroke on a MacBook Pro all the way to five production targets, safely.

One act of the Ch 1 play remains: Operate. Ch 7 is the safety net under everything — environments and secrets done right across all five targets, the observability that tells you a release is bad (web and app), and the rollback/kill-switch playbook that turns "it broke in production" into a non-event. It's the chapter that makes shipping fast and calm, and it closes the series.

Ch 5: Shipping the Web Half — Cloudflare Preview Deploys, Promotion & RollbackCh 7: After Deploy — Environments, Secrets, Observability & Rollback
Git + GitHubGit & GitHub Pro SeriesGit and GitHub practices for branches, pull requests, rebase, history repair, and team review.Ship iOSShip iOS Apps SeriesShipping workflows for iOS apps: signing, TestFlight, App Store Connect, CI, and release hygiene.Production WebProduction Web Apps SeriesProduction patterns for web apps: caching, rate limiting, webhooks, queues, cron jobs, and idempotency.

Ship your apps faster

When you're ready to publish your Swift app to the App Store, Simple App Shipper handles metadata, screenshots, TestFlight, and submissions — all in one place.

Try Simple App Shipper
5 free articles remainingSubscribe for unlimited access