The Path That Sends a Build to the App Store

In Part 1 we created the MAS build target, and in Part 2 we created the configuration files and code branching that separate the two channels. The FocusTimer MAS target is now in a shape that can be put on the App Store.

In this final part, we’ll set up the path for uploading that build to App Store Connect, and cover how to verify the two channels so they stay unbroken going forward, wrapping up the series.

As in Parts 1 and 2, FocusTimer, com.example.FocusTimer.mas, the Team ID ABCDE12345, and so on are all example values.

Step 1 — The ExportOptions-MAS.plist for Upload

In the direct distribution series, we noted that when exporting an archive for distribution, the xcodebuild -exportArchive command reads an ExportOptions.plist. The MAS channel needs a separate export configuration file.

Create an ExportOptions-MAS.plist in the project’s release directory.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>app-store</string>
    <key>destination</key>
    <string>upload</string>
    <key>signingStyle</key>
    <string>automatic</string>
    <key>teamID</key>
    <string>ABCDE12345</string>
</dict>
</plist>

Here’s what each key means.

  • method = app-store — Means this build is exported for the Mac App Store channel. This contrasts with the direct distribution channel’s developer-id.
  • destination = upload — Has xcodebuild -exportArchive upload the exported artifact straight to App Store Connect. There’s no need to go through Transporter or a separate upload tool as in the past.
  • signingStyle = automatic — Xcode automatically matches the Mac App Store provisioning profile and the Apple Distribution certificate.
  • teamID — Your Apple Developer Team ID.

Once you create this file, you reuse it for every MAS release. Leave the direct distribution ExportOptions.plist as is, and keep this file alongside it.

Step 2 — Register the App Record in App Store Connect

To actually submit the app for review, there must be an app record in App Store Connect. A record is the container that holds all the information shown in the store — the app’s name, description, screenshots, price, and so on.

Creating the record can wait until just before your actual first submission. At the one-time setup stage, it’s enough to know in advance “which items to decide and how.” In particular, the Primary Language below is hard to undo once set, so decide it carefully ahead of time.

In App Store Connect → My Apps+New App, enter the following.

  • Platform — Select macOS
  • Primary Language — ⚠️ The item to decide most carefully. Once set, it’s very hard to change through self-service. If you plan to release the app in multiple countries, it’s common to set English (U.S.) as the primary language, because the primary language is “the baseline language shown when there’s no translation for a particular country.”
  • App Name — The name in the primary language (e.g., FocusTimer). Names for other languages are added later separately as per-language localizations. For example, you can make Korean users see a Korean name and Japanese users see a Japanese name.
  • Bundle ID — Select com.example.FocusTimer.mas from the dropdown. That’s the ID you registered in Part 1. Don’t confuse it with the direct distribution Bundle ID (com.example.FocusTimer) — it must be the one with .mas appended.
  • SKU — An internal identifier string used only by you. It isn’t exposed in the store, so you can choose it freely (e.g., focustimer-mas-001).

The app category must match the value you put in LSApplicationCategoryType in the Info.plist in Part 2. If you put public.app-category.productivity (Productivity) in the plist, you must also choose the “Productivity” category in App Store Connect so a mismatch warning doesn’t appear during review.

Step 3 — Verifying Both Channel Builds

You now have a single codebase with two build targets. This comes with a maintenance cost. If you normally only build the direct distribution channel, you won’t notice if the MAS target has quietly broken. For instance, you add new code but forget the #if canImport(Sparkle) branch, and only the MAS build fails to compile.

The most reliable way to prevent this is to archive the MAS target as well every time you make a direct distribution release. If the build succeeds, it means the branching of the two channels is intact.

xcodebuild -project FocusTimer.xcodeproj -scheme "FocusTimer MAS" \
  -configuration Release \
  -destination 'platform=macOS' \
  -archivePath build/FocusTimer-MAS.xcarchive \
  archive

If you see the following at the end of the output, the MAS branching is fine.

** ARCHIVE SUCCEEDED **

I recommend slotting this command into the last step of your release automation script so it runs automatically on every release. Even when you aren’t actually uploading the MAS build to the App Store, just confirming that the archive succeeds is enough to guarantee that the two channels’ branching hasn’t broken.

Series Recap — MAS Release One-Time Setup Complete

The three-part one-time setup for releasing to the Mac App Store is now fully done. You now have:

  • (Part 1) A MAS-only Bundle ID + the duplicated FocusTimer MAS build target
  • (Part 2) MAS-only entitlements and Info.plist + build settings + #if canImport(Sparkle) code branching
  • (Part 3) An ExportOptions-MAS.plist for upload + how to register an App Store Connect record + verification of both channel builds

With this, a single macOS app now has both the direct distribution (Developer ID) and Mac App Store channels. To restate the core structure:

  • Share the same codebase, but split it into two build targets.
  • The direct distribution target includes Sparkle, and the MAS target excludes it.
  • The difference between the two is expressed through separate entitlements, Info.plist, and ExportOptions files and #if canImport(Sparkle) code branching.

The initial setup takes a fair amount of effort, but this too is a structure that keeps getting reused once it’s in place. From then on, when you make a direct distribution release, you just archive the MAS build alongside it and confirm the branching is still alive. The actual App Store submission (screenshots, descriptions, handling review) is separate work done on top of this one-time setup, and it’s a topic for another post.

References