Plan: Mac distribution under the holder’s team
Status: materials received 2026-10-01; 2026-10-02 the notarized Homebrew release 1.0.3 / CLI 0.1.2 shipped (cask postflight removed) and iOS 1.0.3 (5) with widgets was uploaded to TestFlight; the Mac Installer .p12 arrived later the same day and the Mac App Store build 1.0.4 (6) was uploaded to TestFlight (installerSigningCertificate needs the full identity name, see ExportOptions-mac.plist). Supersedes 917-plan-mac-testflight. Tracked in the backlog under Publishing.
Context
Two Mac channels, one holder team (G86VRDBMDK, Individual membership):
- Homebrew / direct download. The shipped
Minuta.appis signed with the free personal team’sApple Developmentcertificate (UKMJ95C96B), carries theget-task-allowdebug entitlement, andspctl -a -t execrejects it. The cask hides this by strippingcom.apple.quarantineinpostflight; the direct download needs right-click > Open. The CLI tarball is ad-hoc signed. The fix is a Developer ID Application certificate plus Apple notarization. The holder’sApple Distributioncertificate cannot do this (App Store only), and only the Account Holder can create a Developer ID certificate. - Mac App Store / TestFlight. A sandboxed Catalyst archive uploaded to the same App Store Connect record with the macOS platform added: Mac App Store profile, Mac Installer Distribution certificate, a per-target Mac xcconfig, and a sandbox guard for the CLI discovery pointer (from plan 917).
- Widgets in TestFlight. The holder’s profile
tools.minuta.app.widgets AppStorewas issued for App IDtools.minuta.app.widget(missing the trailing “s”), so the widget dependency is commented out inMinuta/project.ymland theMinutaArchivescheme.
All three need materials from the holder, so they share one request (TESTFLIGHT-HOLDER.md) and one plan. Everything on our side is done; the release runs happen after the materials arrive. The App Store Connect API key used for notarization is also the prerequisite for “upload without an Xcode login” in the publishing-automation backlog item.
Needed from the Account Holder
The request was sent to the holder as TESTFLIGHT-HOLDER.md (Russian). Since the materials arrived that file only records what was received and one optional item, so the steps are listed here:
- Widgets. Register App ID
tools.minuta.app.widgets(explicit, App Groups capability withgroup.tools.minuta.apps) if absent, delete the profile bound to the misspelled App ID, recreate an App Store Connect / iOS profile for the right App ID named exactlytools.minuta.app.widgets AppStore. - Developer ID Application certificate (Account Holder only): CSR on the holder’s Mac, Certificates > + > Developer ID Application, install, export
.p12with the private key. Valid 5 years. - App Store Connect API access: Users and Access > Integrations > App Store Connect API > Request Access (Account Holder only). After that an Admin creates a Team key (role Developer) for
notarytool; alternatively the holder creates and sendsAuthKey_<KEYID>.p8+ Key ID + Issuer ID. An app-specific password is the fallback, not recommended. - Mac App Store: Mac Catalyst capability on both App IDs; Mac Installer Distribution certificate exported as
.p12; Mac App Store Connect / Mac Catalyst profiles named exactlytools.minuta.app MacAppStoreandtools.minuta.app.widgets MacAppStorewith the existing Apple Distribution certificate. - Send: the widgets
.mobileprovision, Developer ID.p12+ password, API access confirmation or.p8+ Key ID + Issuer ID, Mac Installer.p12+ password, both.provisionprofilefiles. Passwords through a different channel than the files.
On our side (Admin): App Store Connect > Minuta > add the macOS platform.
Developer ID build for Homebrew
Minuta/DeveloperID.xcconfig (untracked, in .gitignore)
// Developer ID build for Homebrew / direct download. Holder's Individual team.
// Not committed (.gitignore). Personal-team defaults stay in project.yml.
CODE_SIGN_STYLE = Manual
DEVELOPMENT_TEAM = G86VRDBMDK
CODE_SIGN_IDENTITY = Developer ID Application
// No profile: Developer ID needs one only for profile-backed entitlements
// (iCloud, push, App Groups); ours are empty. The empty specifier also lets
// the same xcconfig apply to MinutaTray (a macOS bundle rejects a manual profile).
PROVISIONING_PROFILE_SPECIFIER =
ENABLE_HARDENED_RUNTIME = YES
OTHER_CODE_SIGN_FLAGS = --timestamp
// Xcode injects com.apple.security.get-task-allow from its base entitlements
// even with this identity on Catalyst; notarization rejects it (verified 2026-10-02).
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO - No
CODE_SIGN_ENTITLEMENTS: the committedSources/Minuta.entitlementsis an empty dict andMinutaTrayhas none. No App Sandbox: not required for Developer ID, and it would break the CLI pointer write. SWIFT_ACTIVE_COMPILATION_CONDITIONSis not overridden, soMANUAL_DISTRIBUTION(the opt-in update check) stays in the Homebrew build.CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NOis required: the first run on 2026-10-02 produced a correct Developer ID signature but still carriedget-task-allow, and the script’s guard stopped before submission.project.ymlis untouched; dev builds keep the personal team. Thebuildaction is kept (noarchive+-exportArchive):-xcconfigapplies to every target built in the invocation, including the implicitMinutaTraydependency, so Xcode signs the plugin and then the app inside-out with one identity and no--deepre-sign is needed.
scripts/release-brew.sh
- Switch:
NOTARIZE=1whenMINUTA_NOTARIZEis not0andMinuta/DeveloperID.xcconfigexists. The signing identity is resolved fromsecurity find-identity -v -p codesigningfiltered byDeveloper ID Application:and the team from the xcconfig (the keychain can hold Developer ID identities of other teams). Thenotarytoolkeychain profile (NOTARY_PROFILE, defaultminuta-notary) must answernotarytool history. Otherwise a loud warning and the old path: personal-team app + ad-hoc CLI, not for upload. notarize <zip>:xcrun notarytool submit --keychain-profile <profile> --wait; on anything butstatus: Acceptedit printsnotarytool log <id>(names the offending file) and exits 1.- CLI:
codesign --force --options runtime --timestamp --sign "<identity>",codesign --verify --strict, the bare binary is zipped withditto -c -k(notarytool accepts zip/dmg/pkg, not tar.gz), notarized, and the tarball is built from the signed binary. A bare Mach-O cannot be stapled; Gatekeeper fetches the ticket online. - App:
xcodebuild ... -xcconfig DeveloperID.xcconfig build, then guards (noget-task-allow,codesign --verify --deep --strict,Authority=Developer ID Applicationon the app and onContents/PlugIns/MinutaTray.bundle), a throwaway zip fornotarytool,xcrun stapler staple Minuta.app,stapler validate,spctl -a -t exec -vv(expectNotarized Developer ID), and only then the published zip. scripts/upload-brew-artifacts.shis unchanged.
Local one-time setup (after the materials)
security import DeveloperID.p12 -k ~/Library/Keychains/login.keychain-db -P '<pw>' -T /usr/bin/codesign -T /usr/bin/security
security find-identity -v -p codesigning | grep 'Developer ID Application: .* (G86VRDBMDK)'
mkdir -p ~/.appstoreconnect/private_keys && mv AuthKey_<KEYID>.p8 ~/.appstoreconnect/private_keys/
xcrun notarytool store-credentials minuta-notary \
--key ~/.appstoreconnect/private_keys/AuthKey_<KEYID>.p8 --key-id <KEYID> --issuer <ISSUER-UUID>
xcrun notarytool history --keychain-profile minuta-notary Then create Minuta/DeveloperID.xcconfig from the template above and run ./scripts/release-brew.sh, ./scripts/upload-brew-artifacts.sh.
Future releases run all channels (Homebrew artifacts and tap, TestFlight iOS, Mac App Store, site) through scripts/release.sh: Release.
Tap and site
With the first notarized release, in the same tap commit as the sha bump:
Casks/minuta.rb: remove thepostflightblock that runsxattr -dr com.apple.quarantine; bumpversion/sha256.Formula/minuta.rb:url/sha256bump.README.md: replace the Gatekeeper note with one sentence (Developer ID signed and notarized).
The downloads page and the CLI reference already describe the notarized state; the xattr command stays there as troubleshooting for builds before the notarized one.
Mac App Store build for TestFlight
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.
Project changes (done)
Minuta/TestFlight/Minuta.macOS.entitlements(committed):com.apple.security.app-sandbox,files.user-selected.read-write,files.bookmarks.app-scope(without itbookmarkData(options: .withSecurityScope)inStorageLocationManager.saveBookmarkfails under sandbox),application-groups = [group.tools.minuta.apps]. No network entitlements: the app is offline and the Mac xcconfig dropsMANUAL_DISTRIBUTION.MinutaWidgets.macOS.entitlements: sandbox + App Group, for when Mac widgets are enabled.Sandbox guard in
StorageLocationManager.publishResolvedStoragePath(): whenAPP_SANDBOX_CONTAINER_IDis in the environment the CLI pointer write to<realHome>/Library/Application Support/minuta/storage-path.txtis skipped and logged; the version marker (inside the storage folder) is still written. The environment is injectable throughinit(environment:); covered byStorageLocationManagerTests.testSandboxedLaunchSkipsPointerWriteButWritesMarker.Minuta/ExportOptions-mac.plist(committed, no secrets):method app-store-connect,destination upload,teamID G86VRDBMDK,signingStyle manual,signingCertificate Apple Distribution,installerSigningCertificate Mac Installer Distribution,provisioningProfiles { tools.minuta.app: "tools.minuta.app MacAppStore" }.Minuta/TestFlight-mac.xcconfig(untracked, in.gitignore). In a Catalyst archiveMinutaTrayis really built and embedded, and an[sdk=macosx*]condition cannot separate it from the app (both use the macosx SDK). Profile and entitlements are selected by target name through nested substitution; forMinutaTrayboth variables are undefined, so it is signed with Apple Distribution without a profile or entitlements (a plugin inside a sandboxed app needs neither):CODE_SIGN_STYLE = Manual DEVELOPMENT_TEAM = G86VRDBMDK CODE_SIGN_IDENTITY = Apple Distribution PROFILE_Minuta = tools.minuta.app MacAppStore // iOS profile on purpose: the Catalyst archive compiles the iOS-only widget target PROFILE_MinutaWidgets = tools.minuta.app.widgets AppStore PROVISIONING_PROFILE_SPECIFIER = $(PROFILE_$(TARGET_NAME)) ENTITLEMENTS_Minuta = TestFlight/Minuta.macOS.entitlements ENTITLEMENTS_MinutaWidgets = TestFlight/MinutaWidgets.entitlements CODE_SIGN_ENTITLEMENTS = $(ENTITLEMENTS_$(TARGET_NAME)) // Mac App Store build: no update check against the site. An empty value is // required: $(inherited) resolves to the target-level "$(inherited) MANUAL_DISTRIBUTION" // and keeps the flag (verified with -showBuildSettings, 2026-09-08). SWIFT_ACTIVE_COMPILATION_CONDITIONS =Check the substitution before archiving:
cd Minuta && xcodebuild -showBuildSettings -scheme MinutaArchive \ -destination 'generic/platform=macOS,variant=Mac Catalyst' \ -xcconfig TestFlight-mac.xcconfig | grep -E 'PROVISIONING_PROFILE_SPECIFIER|CODE_SIGN_ENTITLEMENTS'Scheme:
MinutaArchive(Tray comes in as a dependency, tests are excluded). Version:MARKETING_VERSIONstays; build numbers are unique per platform in App Store Connect, bumpCURRENT_PROJECT_VERSIONonly on “Build already exists”.
Archive, export, upload (after the materials)
Import the Mac Installer
.p12;security find-identity -v | grep "Mac Developer Installer"shows3rd Party Mac Developer Installer: <holder> (G86VRDBMDK).Install the profiles as
~/Library/Developer/Xcode/UserData/Provisioning Profiles/<UUID>.provisionprofile(UUID viasecurity cms -D -i <file> | plutil -extract UUID raw -). ConfirmPlatformcontainsOSXand the entitlements listgroup.tools.minuta.apps.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.xcconfigBefore exporting:
codesign -d --entitlements :- <archive>/Products/Applications/Minuta.appshowsapp-sandbox,files.user-selected.read-write,files.bookmarks.app-scope,application-groups;Contents/PlugIns/MinutaTray.bundleis present and signed by the holder’s team.xcodebuild -exportArchive -archivePath ../build/testflight/Minuta-mac.xcarchive -exportOptionsPlist ExportOptions-mac.plist -exportPath ../build/testflight/macuntilUpload succeeded. Typical rejections: ITMS-90296 (sandbox) > entitlements above; ITMS-90242 (category) >LSApplicationCategoryTypeis already in Info.plist; “Mac Installer certificate not found” > step 1.App Store Connect > TestFlight > the macOS build > internal group; install through the TestFlight app on the Mac.
Known behaviour: under sandbox the default ~/Documents/Minuta is the container’s Documents. Users moving from the Homebrew build to the App Store build pick their real folder in Settings (security-scoped bookmarks work). Follow-up in the backlog: write the CLI discovery pointer into the group container ~/Library/Group Containers/group.tools.minuta.apps/ and add that path to StorageResolver.
Widgets in TestFlight (after the corrected profile)
- Install the new
tools.minuta.app.widgets AppStore.mobileprovision;security cms -D -i <file> | grep -A1 application-identifiermust end in.widgets. - Uncomment the
MinutaWidgetsdependency inMinuta/project.ymlandMinutaWidgets: [archive]in theMinutaArchivescheme,xcodegen generate. - Archive iOS with
TestFlight.xcconfigas in the TestFlight guide. MinutaWidgetsisplatformFilter: iOS, so neither Mac build changes. Verify that./scripts/release-brew.shstill succeeds with the widget target back in the scheme (Catalyst filters it out; the original reason for commenting it out was the personal team failing to sign it in Release).
Execution log (2026-10-02)
- Keychain:
Developer ID Application: <holder> (G86VRDBMDK)imported from the holder’s.p12(valid to 2031-09-17) plus the Apple Developer ID G2 intermediate. Profiles installed under~/Library/Developer/Xcode/UserData/Provisioning Profiles/:tools.minuta.app AppStore(caaad332-8fe4-46b9-9bf8-e0be6c315624, reissued),tools.minuta.app.widgets AppStore(ee47b42f-5cae-4ba6-9e6a-bb9234077ba6),tools.minuta.app MacAppStore(ec546bd4-4d8e-4927-b29f-af44008445f6),tools.minuta.app.widgets MacAppStore(a3e77764-72c1-427e-96d5-6a17142ab1f5); the two old iOS profiles were removed. - notarytool: the App Store Connect Team Keys page errors out for the Admin account (“An error has occurred. Try again later.”), so
minuta-notaryis an Apple ID + app-specific password profile (--apple-id --team-id G86VRDBMDK --password), stored from the untracked root.env. Issuer ID for a future API key:8b60b2ec-8676-41fa-92ca-820560cf3044. - First
release-brew.shrun stopped at the guard:get-task-allowwas still injected ->CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NOadded to the xcconfig template. Second run tripped the script’s owncodesign | grep -qcheck underpipefail(SIGPIPE) although the signature was correct -> the checks now capture output first. Third run: CLI and appstatus: Accepted, app stapled,spctlaccepted source=Notarized Developer ID. Each run re-notarizes the CLI (one extra round trip per retry). spctl -a -t execon the bare CLI binary answersrejected (the code is valid but does not seem to be an app); that is spctl’s standard reply for a non-bundle Mach-O, and the quarantined binary runs without a prompt.- Tap commit
c840b23(notarized 1.0.3): cask 1.0.3 withoutpostflight, formula 0.1.2; site deployed,version.jsonadvertises 1.0.3.brew inforesolves cask 1.0.3 / formula 0.1.2; Homebrew 7 refuses to install from a third-party tap untilbrew trust ramil-k/minutais run once (new in brew 7, see the install docs). - iOS:
TestFlight.xcconfigfixed to an emptySWIFT_ACTIVE_COMPILATION_CONDITIONS; archive signed by the holder with the App Group on app andMinutaWidgets.appex, no Tray, no update pill;-exportArchivewithExportOptions-ios.plist(Xcode Apple ID login) ->Upload succeeded, build 1.0.3 (5). - Mac: with
MinutaWidgetsback inMinutaArchive, a Catalyst archive still compiles the widget target as iOS, soTestFlight-mac.xcconfigmust give it the iOS profile and entitlements (tools.minuta.app.widgets AppStore,TestFlight/MinutaWidgets.entitlements); the Mac widget profile errors with “platform macOS does not match iOS” and stays unused. Archive succeeded with sandbox + bookmarks + group entitlements and a holder-signedMinutaTray.bundle;-exportArchivefailed withNo certificate for team 'G86VRDBMDK' matching 'Mac Installer Distribution' founduntil the.p12arrived, and still failed with the generic selector after the import (security find-identity -p macappstoreflags the identity “Missing required extension”);installerSigningCertificateset to the full identity name (the value ofinstallerSigningCertificateinMinuta/ExportOptions-mac.plist) uploads fine. 1.0.4 (6) for macOS uploaded 2026-10-02.
Risks and notes
Minuta/buildDerivedData is shared between personal-team and Developer ID builds. Xcode re-signs on setting changes; ifcodesign -dvvever shows the wrong authority,rm -rf Minuta/build.- Hardened runtime +
Bundle(url:).load()ofMinutaTray.bundleinTrayIconManager: a same-team signature satisfies library validation, nodisable-library-validationexception. - Three untracked xcconfigs, one rule:
TestFlight.xcconfig(iOS),TestFlight-mac.xcconfig(Mac App Store),DeveloperID.xcconfig(Homebrew).project.ymlkeeps the personal-team defaults; never move their values there. - Gatekeeper shows the Developer ID holder’s name as the verified developer; the same team publishes on TestFlight, so one publisher across channels.
- Release order for Homebrew is unchanged: artifacts, then tap bump (with the
postflightremoval), then site deploy.
Verification
- Now, without materials:
./scripts/release-brew.shprints the warning and produces the same artifacts as before (MINUTA_NOTARIZE=0forces that path); with aDeveloperID.xcconfigbut no notary profile it exits 1 with a clear message.cd Shared && swift test;xcodebuild test -scheme Minuta -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:MinutaTests(the sandbox guard test is green);./scripts/run-mac.shstill works (the guard is inert withoutAPP_SANDBOX_CONTAINER_ID);-showBuildSettingswithTestFlight-mac.xcconfiggivesMinutathe MacAppStore profile and macOS entitlements andMinutaTrayboth empty;git statusshows no xcconfig. - Homebrew, after materials: the script ends with
spctl ... accepted source=Notarized Developer IDandstapler validateOK;codesign -dvvon the app and onMinutaTray.bundleshowAuthority=Developer ID Application: <holder> (G86VRDBMDK); entitlements have noget-task-allow; the CLI binary with a simulated quarantine attribute (xattr -w com.apple.quarantine "0083;...;Safari;" minuta) is accepted byspctl -a -t exec. Fresh user:brew reinstall --cask minuta, thenspctl -a -t exec -vv /Applications/Minuta.appshowsNotarized Developer ID; the direct zip opens without the dialog. - TestFlight Mac, after materials:
ARCHIVE SUCCEEDED, entitlements show sandbox + group and the holder’sTeamIdentifier,Upload succeeded, the macOS build shows in TestFlight, installs on a Mac, launches with no “access data from other apps” prompt, and the storage folder chosen in Settings persists across relaunch. - Widgets: the iOS archive with widgets uploads; the Homebrew Catalyst build still succeeds with the widget target in the scheme.