Releasing to TestFlight
.github/workflows/testflight.yml archives both apps, signs them for the App Store, and uploads them to TestFlight.
Running a release
Section titled “Running a release”Either:
- go to Actions → TestFlight → Run workflow and pick a platform, or run
gh workflow run testflight.yml -f platform=both(orios,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).
Runner and Xcode
Section titled “Runner and Xcode”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
Section titled “Signing”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.
One App Store record for both platforms
Section titled “One App Store record for both platforms”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.
Required secrets
Section titled “Required secrets”| 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.
Notes for testers and App Review
Section titled “Notes for testers and App Review”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 opensstraight to the chat list. The demo runs a simulated gateway entirely on the device, with sampleagents, chats, approvals and settings. Nothing leaves the device.Privacy manifests and release checks
Section titled “Privacy manifests and release checks”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.