Skip to content

Releasing to TestFlight

.github/workflows/testflight.yml archives both apps, signs them for the App Store, and uploads them to TestFlight.

Either:

  • go to Actions → TestFlight → Run workflow and pick a platform, or run gh workflow run testflight.yml -f platform=both (or ios, macos), or
  • push a v* tag, which uploads both.

Only the selected platforms start a runner.

Each build number is <run number>.<attempt>.

The upload workflow first requires a completed, successful Tests push run on main for the exact commit being uploaded. Pending, failed, cancelled, skipped, PR-only or older-commit checks do not authorize an upload. Wait for the main checks to finish and rerun TestFlight if this gate fails. For a known CI flake only, a manual run can bypass the gate with -f skip_ci_gate=true (tag pushes are always gated).

The upload jobs run on GitHub’s xcode-27 runner (macOS 27, arm64; its image Readme is images/macos/xcode-27-arm64-Readme.md in actions/runner-images). They build with a pinned Xcode 27.1 (the iOS 27.1 SDK, which iPhone Duo support needs), set by XCODE_VERSION at the top of the workflow. The image also ships beta Xcodes, and App Store Connect rejects builds made with a beta, so the workflow never picks “newest”. If /Applications/Xcode_<XCODE_VERSION>.app is missing, the job fails and lists the installed Xcodes. The log prints xcodebuild -version and xcodebuild -showsdks.

To bump Xcode, check the runner image Readme for a released (non-beta) version, change XCODE_VERSION, and change runs-on too if the new Xcode lives on a different runner label.

Signing uses manual App Store profiles through project.appstore.yml, which is only included when PINCER_APP_STORE_SIGNING=YES. scripts/testflight.sh ios|macos does the work and also runs locally. Use UPLOAD=0 to export without uploading.

The iOS app ships as the iOS platform of the Mac app’s App Store Connect record (universal purchase), so both use the bundle ID chat.pincer.mac, the Share extensions chat.pincer.mac.share, and the iOS notification service chat.pincer.mac.notifications. iOS builds before 1.1 used chat.pincer.ios. Provisioning profile names are unique per team, so the iOS profiles for the new IDs are the Pincer_iOS_Universal_* ones below.

The App Group (group.chat.pincer) and the shared Keychain group (<TeamID>.chat.pincer.shared) didn’t change, so the new install reads the gateways, secrets and device identity the old chat.pincer.ios build saved. Per-app preferences, drafts, the offline outbox and the push relay URL stay with the old app. Install Pincer tells testers what to expect.

Secret Contents
ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_P8 App Store Connect API key (the .p8 is base64)
DISTRIBUTION_P12, MAC_INSTALLER_P12, P12_PASSWORD Apple Distribution and Mac Installer Distribution certificates (base64 .p12)
IOS_PROFILE, MACOS_PROFILE Base64 Pincer_iOS_Universal_AppStore_CI / Pincer_macOS_AppStore_CI provisioning profiles. The chat.pincer.mac App ID needs the Push Notifications capability.
IOS_NOTIFICATIONS_PROFILE Base64 Pincer_iOS_Universal_Notifications_AppStore_CI profile for the chat.pincer.mac.notifications notification service extension
IOS_SHARE_PROFILE, MACOS_SHARE_PROFILE Base64 Pincer_iOS_Universal_Share_AppStore_CI / Pincer_macOS_Share_AppStore_CI profiles for the Share extensions (both chat.pincer.mac.share)

The iOS app and Share extension profiles need the App Groups capability with group.chat.pincer. macOS uses the team-prefixed group <TeamID>.chat.pincer, which needs no portal setup. The shared Keychain group <TeamID>.chat.pincer.shared is covered by the default keychain entitlement.

Testers and App Review don’t need a gateway. Paste this into TestFlight’s What to Test and App Store Connect’s App Review Information → Notes:

Pincer is a client for a self-hosted OpenClaw Gateway. No account or server is needed to review it:
open the app and tap "Try the Demo" on the first screen (right under "Get Started"). It opens
straight to the chat list. The demo runs a simulated gateway entirely on the device, with sample
agents, chats, approvals and settings. Nothing leaves the device.

PrivacyInfo.xcprivacy is copied into each app and extension. PincerKit and PincerUI also ship their own SwiftPM resource manifests. The declarations cover app preferences, shared App Group preferences, elapsed-time measurements, and metadata for files in app-owned containers. They do not authorize fingerprinting.

Before archiving, scripts/testflight.sh validates the generated project’s Copy Bundle Resources phases. After archiving, it inspects the actual app, Share extension, iOS notification extension and package resource bundles. Missing or changed declarations fail the release before export/upload. These checks do not replace Apple’s archive privacy report or App Store privacy questionnaires.

Run python3 scripts/test_release_privacy.py for the omission/malformed-manifest regressions, and python3 scripts/check_release_privacy.py --project Pincer.xcodeproj to check XcodeGen output. PincerChecks includes the same regression harness in its offline suite. Review API use when dependencies or persistence change.