Plan: Mac (Catalyst) via TestFlight

Status: planned 2026-09-06, blocked on the holder’s Mac signing materials. Tracked in the backlog under Publishing.

Context

The iOS build 1.0.1 (3) is in TestFlight (app-only; widgets are disabled until the corrected widget profile arrives). The Mac build goes through the same App Store Connect record but follows Mac App Store rules: App Sandbox is mandatory, the app needs Mac App Store provisioning profiles, and the .pkg needs a Mac Installer Distribution certificate. Decision: keep the App Group in the Mac build.

Key finding (Apple Forums, DTS, macOS 15 beta 4+): a Mac Catalyst app should keep the iOS-style group name group.*, authorized by the embedded provisioning profile. The macOS 15 prompt “wants to access data from other apps” does not appear when the group is in the profile or the build comes from the Mac App Store. So WidgetDataConstants.appGroupIdentifier = "group.tools.minuta.apps" stays and no App Group code changes. Section 8 of the TestFlight guide (Team-ID-prefixed group) is outdated and must be rewritten.

The only code change: a sandbox guard for the CLI discovery pointer. StorageLocationManager.publishResolvedStoragePath() (Minuta/Sources/Services/StorageLocationManager.swift) writes <realHome>/Library/Application Support/minuta/storage-path.txt through CLIConfigService.writeStoragePath (Shared/Sources/MinutaShared/Services/CLIConfigService.swift); under App Sandbox that path is outside the container and the write is denied. There is no guard today, only a caught error.

Verified locally: xcodebuild -help documents the installerSigningCertificate export option (manual signing, selector "Mac Installer Distribution").

Needed from the Account Holder (blocks the archive)

Add a “Mac” section to TESTFLIGHT-HOLDER.md:

  1. Identifiers > App ID tools.minuta.app > enable the Mac Catalyst capability (same for tools.minuta.app.widgets for later). App Groups is already on.
  2. Certificates > + > Mac Installer Distribution (CSR on the holder’s Mac, as before) > export as .p12 with the private key; password through a separate channel.
  3. Profiles > + > Distribution > Mac App Store Connect, platform Mac Catalyst, App ID tools.minuta.app, the existing Apple Distribution certificate > name exactly tools.minuta.app MacAppStore. Later the same for tools.minuta.app.widgets MacAppStore.
  4. Send: the Mac Installer .p12 + password, the .provisionprofile.

In App Store Connect (Admin): app Minuta > add the macOS platform.

Project changes (can be done before the materials arrive)

  1. Minuta/TestFlight/Minuta.macOS.entitlements (new, committed, no secrets):

    • com.apple.security.app-sandbox = true
    • com.apple.security.files.user-selected.read-write = true
    • com.apple.security.files.bookmarks.app-scope = true — without it bookmarkData(options: .withSecurityScope) in StorageLocationManager.saveBookmark fails under sandbox
    • com.apple.security.application-groups = [group.tools.minuta.apps]

    No network entitlements: the app is offline and the TestFlight xcconfig drops MANUAL_DISTRIBUTION, so the update check is not compiled in.

  2. Sandbox guard in StorageLocationManager.publishResolvedStoragePath(): skip publisher.writeStoragePath(url) when the process is sandboxed (ProcessInfo.processInfo.environment["APP_SANDBOX_CONTAINER_ID"] != nil) and log that the CLI pointer is not published. Keep writeVersionMarker (it writes inside the storage folder). Existing StorageLocationManager tests with an injected publisher are unaffected.

  3. Minuta/TestFlight-mac.xcconfig (untracked; add to .gitignore next to TestFlight.xcconfig). In a Catalyst archive MinutaTray is really built (embedded), and an [sdk=macosx*] condition cannot separate it from the app (both use the macosx SDK). Select profile and entitlements by target name through nested substitution:

    CODE_SIGN_STYLE = Manual
    DEVELOPMENT_TEAM = HOLDER_TEAM_ID
    CODE_SIGN_IDENTITY = Apple Distribution
    
    PROFILE_Minuta = tools.minuta.app MacAppStore
    PROFILE_MinutaWidgets = tools.minuta.app.widgets MacAppStore
    PROVISIONING_PROFILE_SPECIFIER = $(PROFILE_$(TARGET_NAME))
    
    ENTITLEMENTS_Minuta = TestFlight/Minuta.macOS.entitlements
    ENTITLEMENTS_MinutaWidgets = TestFlight/MinutaWidgets.macOS.entitlements
    CODE_SIGN_ENTITLEMENTS = $(ENTITLEMENTS_$(TARGET_NAME))
    
    SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited)

    For MinutaTray both variables are undefined, so it is signed with Apple Distribution without a profile or entitlements (a bundle inside a sandboxed app needs neither). Check the substitution before archiving:

    xcodebuild -showBuildSettings -scheme MinutaArchive \
      -destination 'generic/platform=macOS,variant=Mac Catalyst' \
      -xcconfig TestFlight-mac.xcconfig | grep -E 'PROVISIONING_PROFILE_SPECIFIER|CODE_SIGN_ENTITLEMENTS'
  4. Scheme: reuse MinutaArchive (Tray comes in as a dependency, tests are excluded).

  5. Version: keep MARKETING_VERSION 1.0.1 / CURRENT_PROJECT_VERSION 3; build numbers are unique per platform in App Store Connect. Bump to 4 only on “Build already exists”.

Archive, export, upload (after the materials)

  1. Import the Mac Installer .p12 (Keychain Access). Check: security find-identity -v | grep "Mac Installer" shows 3rd Party Mac Developer Installer: <holder> (HOLDER_TEAM_ID).

  2. Install the profile as ~/Library/Developer/Xcode/UserData/Provisioning Profiles/<UUID>.provisionprofile (UUID via security cms -D | plutil -extract UUID). Confirm Platform contains OSX and the entitlements list group.tools.minuta.apps.

  3. Archive:

    cd Minuta && xcodebuild clean archive -project Minuta.xcodeproj -scheme MinutaArchive \
      -configuration Release -destination 'generic/platform=macOS,variant=Mac Catalyst' \
      -archivePath ../build/testflight/Minuta-mac.xcarchive -xcconfig TestFlight-mac.xcconfig
  4. Before exporting: codesign -d --entitlements - Minuta.app shows app-sandbox, files.user-selected.read-write, bookmarks.app-scope, application-groups; Contents/PlugIns/MinutaTray.bundle is present and signed by the holder’s team.

  5. ExportOptions-mac.plist: method app-store-connect, destination upload, teamID HOLDER_TEAM_ID, signingStyle manual, signingCertificate Apple Distribution, installerSigningCertificate Mac Installer Distribution, provisioningProfiles { tools.minuta.app: "tools.minuta.app MacAppStore" }.

  6. xcodebuild -exportArchive ... until Upload succeeded. Typical rejections: ITMS-90296 (sandbox) > entitlements above; ITMS-90242 (category) > LSApplicationCategoryType is already in Info.plist; “Mac Installer certificate not found” > step 1.

  7. App Store Connect > TestFlight > the macOS build > internal group; install through the TestFlight app on the Mac.

Docs and backlog

  • TESTFLIGHT-ORG.md section 8 and the TestFlight guide section 8: rewrite to the real procedure — group.* stays (profile authorization on macOS 15+), the full entitlements list, TestFlight-mac.xcconfig with the TARGET_NAME indirection, the archive command, ExportOptions with installerSigningCertificate, the sandbox guard for the CLI pointer. Add a troubleshooting row for the “access data from other apps” prompt (group missing from the profile > recreate the profile).
  • TESTFLIGHT-HOLDER.md: new “Mac” section (steps above).
  • CLAUDE.md: the Mac archive uses MinutaArchive + TestFlight-mac.xcconfig + Minuta/TestFlight/Minuta.macOS.entitlements; a Mac App Store build does not publish storage-path.txt (sandbox), the CLI falls back to ~/Documents/Minuta or --storage.
  • Known behavior to document: under sandbox the default ~/Documents/Minuta is the container’s Documents; Homebrew users moving to the App Store build pick their folder in Settings (bookmarks work).
  • Follow-up: CLI discovery pointer under App Sandbox — write the pointer into the group container ~/Library/Group Containers/group.tools.minuta.apps/ and add that path to StorageResolver (Shared/Sources/MinutaCLI/Runtime/StorageResolver.swift).

Verification

  1. cd Shared && swift test and xcodebuild test -scheme Minuta -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:MinutaTestsStorageLocationManager tests green.
  2. The personal-team Catalyst Debug build still works: ./scripts/run-mac.sh (the Mac entitlements/xcconfig do not touch regular builds; the guard is inert without APP_SANDBOX_CONTAINER_ID).
  3. -showBuildSettings with TestFlight-mac.xcconfig: Minuta gets the MacAppStore profile and TestFlight/Minuta.macOS.entitlements; MinutaTray gets both empty.
  4. After the materials: ARCHIVE SUCCEEDED, codesign shows sandbox + group entitlements and the holder’s TeamIdentifier, export Upload succeeded, the macOS build shows in TestFlight, installs on a Mac, and launches with no “access data from other apps” prompt; choosing a storage folder in Settings persists across relaunch.
  5. git status: TestFlight-mac.xcconfig is ignored.