Skip to content

fix(ci): pin iOS Release to manual signing, stop minting dev certs#16

Merged
pseudobun merged 1 commit intomainfrom
fix/ci-manual-signing
Apr 12, 2026
Merged

fix(ci): pin iOS Release to manual signing, stop minting dev certs#16
pseudobun merged 1 commit intomainfrom
fix/ci-manual-signing

Conversation

@pseudobun
Copy link
Copy Markdown
Owner

Summary

iOS archive in `release.yml` ran with `-allowProvisioningUpdates` against an Automatic-signing project. Each CI run minted a fresh Apple Development cert via the ASC API on the ephemeral runner keychain (which only has the imported Apple Distribution cert) and the team's cert quota filled after a handful of runs:

```
error: Choose a certificate to revoke. Your account has reached the maximum number of certificates.
error: No profiles for 'lutra-labs.toss' were found: Xcode couldn't find any
iOS App Development provisioning profiles matching 'lutra-labs.toss'.
```

Both errors are downstream of the same loop. Switching iOS Release to manual signing kills it.

Changes

`toss.xcodeproj/project.pbxproj` — only the Release configurations of `toss` and `tossShare` are touched. Debug stays Automatic so local development on iOS sim/device is unaffected. Settings are sdk-scoped on the multiplatform `toss` target so the macOS build path (which uses `CODE_SIGNING_ALLOWED=NO`) is untouched.

  • `tossShare` Release: `CODE_SIGN_STYLE = Manual`, `CODE_SIGN_IDENTITY = "Apple Distribution"`, `PROVISIONING_PROFILE_SPECIFIER = "TossShare AppStore"`, `DEVELOPMENT_TEAM = RFY9T5P84M`.
  • `toss` Release: same idea but with sdk-scoped variants — `"CODE_SIGN_IDENTITY[sdk=iphoneos*]"`, `"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]"`. Profile names match what's already in `export-options/ios-export-options.plist:14-20`.

`.github/workflows/release.yml` — drop `-allowProvisioningUpdates` and the `-authenticationKey*` flags from both `xcodebuild archive` and `xcodebuild -exportArchive`. With manual signing the cert is in the keychain and the profiles are on disk, so xcodebuild resolves everything locally and never calls Apple. The ASC API key file is still prepared for the `altool --upload-app` step that follows.

Manual step required before this can ship

The currently-orphaned Apple Development certs need to be revoked in the Apple Developer portal so the team has room to sign — this PR only stops the bleeding, it doesn't free the slots already burned. Path: portal → Certificates → revoke any `Apple Development` cert that isn't on a Mac you actively use. Don't touch `Apple Distribution` (that's the one CI imports).

Test plan

  • Revoke orphaned dev certs in the Apple Developer portal
  • Re-run the Release workflow (workflow_dispatch with `version=1.3.0`) after merge — archive should succeed without minting any new certs
  • Confirm the IPA exports cleanly with the AppStore profiles
  • Confirm TestFlight upload still works (altool still has the API key)
  • Verify local Xcode iOS development still works (Debug config is unchanged)

Note

This is independent of the upcoming v1.3.0 release that ships the force-update gate. Either order is fine, but if you ship 1.3.0 first you'll hit this same cert wall, so this should land first.

🤖 Generated with Claude Code

…gUpdates

The iOS archive step ran with -allowProvisioningUpdates against an
Automatic-signing project, so each CI run on the ephemeral runner
keychain (which only has the imported Apple Distribution cert) caused
Xcode to mint a fresh Apple Development cert via the ASC API. After a
handful of runs the team's cert quota filled and archives started
failing with "Choose a certificate to revoke" + "No iOS App Development
provisioning profiles matching 'lutra-labs.toss'" — both downstream of
the same loop.

Pin the iOS Release configurations of `toss` and `tossShare` to manual
signing with the Apple Distribution identity and the AppStore profile
names that the workflow already installs (matches the names in
export-options/ios-export-options.plist). Debug stays Automatic so
local development is unaffected. Settings are sdk-scoped on the
multiplatform `toss` target so the macOS build path
(CODE_SIGNING_ALLOWED=NO) is untouched.

Drop -allowProvisioningUpdates and the -authenticationKey* flags from
both archive and exportArchive — manual signing means xcodebuild
resolves everything from the local keychain + installed profiles and
never calls Apple. The ASC API key is still prepared for the altool
upload step.

The currently-orphaned Apple Development certs in the team's quota need
to be revoked manually in the Apple Developer portal before the next CI
run can succeed; this commit only stops the bleeding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@pseudobun pseudobun merged commit cd4df60 into main Apr 12, 2026
3 checks passed
@pseudobun pseudobun deleted the fix/ci-manual-signing branch April 12, 2026 09:09
pseudobun added a commit that referenced this pull request Apr 12, 2026
)

## Summary

Two related production bugs surfacing as one user-visible problem: the
CLI is unusable across machines.

### Bug 1: Cross-machine data divergence

The Mac mini and MacBook show completely different sets of tosses
despite both being signed in to the same iCloud account, both running
the same release, and both pointing at the same
\`iCloud.lutra-labs.toss\` container.

\`release.yml:173-177\` strips both \`aps-environment\` and
\`com.apple.developer.aps-environment\` from the macOS app's signed
entitlements before codesigning, with a comment claiming *"we don't use
APNs on macOS."* That's wrong on a load-bearing point:
**\`NSPersistentCloudKitContainer\` (the engine SwiftData uses for
\`cloudKitDatabase: .private(...)\` mode) needs \`aps-environment\` to
receive CloudKit silent push notifications.** Without it the OS never
wakes the app for "remote changes happened" pushes, so the local
SwiftData store never *pulls* from CloudKit. Each Mac becomes a
write-only island whose store reflects only what that machine itself
wrote.

I verified the parent app's embedded provisioning profile
(\`/Applications/Tossinger.app/Contents/embedded.provisionprofile\`)
authorizes \`aps-environment: production\`, so the entitlement *can* be
present — the workflow just deletes it. The fix substitutes the value
(development → production) instead of stripping the keys.

### Bug 2: CLI SIGABRTs at process exit

\`toss ls\` consistently crashes after printing results with:

\`\`\`
NSInternalInconsistencyException 'Invalid parameter not satisfying:
bundleIdentifier...'
-[PKUserNotificationsRemoteNotificationServiceConnection
initWithBundleIdentifier:]
  -[PKPushRegistry _registerForPushType:]
  -[NSCloudKitMirroringDelegate dealloc]
  -[NSPersistentStore dealloc]
  TossKit Container.deinit
  ListCommand.run
\`\`\`

Read bottom-up: the command finishes, prints results, exits → SwiftData
tears down → \`NSPersistentCloudKitContainer\` deallocs → CloudKit
cleanup tries to register for push types via \`PKPushRegistry\` →
PushKit looks up the bundle identifier through some path that doesn't
work for a sub-bundled \`.app\` helper invoked via
\`/opt/homebrew/bin/toss →
/Applications/Tossinger.app/Contents/Helpers/toss.app/Contents/MacOS/toss\`
→ \`bundleIdentifier\` resolves to nil → assertion fails → SIGABRT.

The CLI doesn't actually need CloudKit. It opens the same SQLite store
as the main app via the app group, performs a single read or write, and
exits. CloudKit sync is the main app's job.

## Changes

**\`.github/workflows/release.yml\`** (\`Prepare sanitized
entitlements\` step) — replace the two \`Delete :*aps-environment*\`
PlistBuddy lines with \`Set ... production\`. Updated the misleading
comment to explain why APNs is actually load-bearing for CloudKit silent
push.


**\`Packages/TossKit/Sources/TossKit/Persistence/PersistenceStack.swift\`**
— added a public \`CloudKitMode\` enum with two cases:
- \`.automatic\` — wires the store to the private CloudKit database
(current behavior)
- \`.disabled\` — uses \`ModelConfiguration.CloudKitDatabase.none\`,
preventing \`NSPersistentCloudKitContainer\` from being instantiated at
all

\`makeContainer\` now takes \`cloudKit: CloudKitMode = .automatic\`, so
the app and share extension are bytewise-equivalent at runtime through
the default.

**\`tossinger/Support/TossEnvironment.swift\`** — passes \`cloudKit:
.disabled\`. Doc comment explains the rationale (CLI doesn't await async
push delivery anyway, and instantiating the CloudKit container in a
sub-bundled helper SIGABRTs).

Same store file, same data; only the transport layer for the CLI
changes.

## Test plan

- [ ] Build the CLI helper in Xcode after the TossKit + TossEnvironment
edits — should compile cleanly
- [ ] Run \`toss ls\` against the existing populated store — results
print, process exits cleanly with no SIGABRT and no PushKit assertion in
the logs
- [ ] Run \`toss add "https://example.com"\` and \`toss delete <id>\` —
same expectation, no crashes
- [ ] Run \`toss ls\` 10× in a row on the macOS box where the crash was
reproducible — expect zero crashes
- [ ] Build and run the main macOS app — behaves identically to before
(default \`.automatic\` mode → same \`.private(...)\` configuration)
- [ ] Cut a release with this branch and inspect the workflow log for
the \`Prepare sanitized entitlements\` step — the dumped plist (\`cat
\"\${SANITIZED}\"\` already in the step) should now show
\`aps-environment: production\` and
\`com.apple.developer.aps-environment: production\`
- [ ] Confirm \`codesign --verify --strict --verbose=2\` (already in the
workflow at line 205) still passes — entitlements remain a subset of
what the Developer ID profile authorizes
- [ ] Install the new release on both Mac mini and MacBook, add a toss
on one, wait ~30s, verify it appears on the other without manual refresh
- [ ] Run \`toss ls\` on both machines after sync settles — they should
converge

## Notes

- The two open PRs (this one + #16 cert spam) touch different regions of
\`release.yml\` (this one edits ~line 176, #16 edits ~lines 437/450) so
they shouldn't conflict whichever order they merge in.
- The share extension stays on the default \`.automatic\` mode — iOS
extensions don't have the CLI's PushKit instantiation problem since they
run in the parent app's bundle context. Could be revisited if it ever
proves problematic.
- Existing \`~/Library/Group
Containers/group.lutra-labs.toss/default.store\` files (which carry
CloudKit sync metadata from prior \`.private(...)\` use) open cleanly
with \`.none\` — opening a CloudKit-mirrored store from a non-CloudKit
context is a supported pattern and a no-op on the metadata side. The
next time the app runs with full CloudKit it picks up CLI-written rows
via NSPersistentHistoryTracker and pushes them.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pseudobun added a commit that referenced this pull request Apr 12, 2026
🤖 I have created a release *beep* *boop*
---


##
[1.3.1](v1.3.0...v1.3.1)
(2026-04-12)


### Bug Fixes

* **ci:** pin iOS Release to manual signing, stop minting dev certs
([#16](#16))
([cd4df60](cd4df60))
* **macos:** restore CloudKit pull sync and stop CLI SIGABRT on exit
([#17](#17))
([2e447ef](2e447ef))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
pseudobun added a commit that referenced this pull request Apr 12, 2026
…19)

## Summary

v1.3.1 is dead on arrival: every launch SIGKILLs with \`Termination
Reason: Namespace CODESIGNING, Code 1, Taskgated Invalid Signature\`.
Diagnosed via side-by-side of the shipped binary's signed entitlements
vs. its embedded provisioning profile:

| Signed in v1.3.1 binary | Authorized by profile |
|---|---|
| \`aps-environment: production\` | **NOT PRESENT** |
| \`com.apple.developer.aps-environment: production\` | ✅ present |

The bare \`aps-environment\` key is the iOS convention. macOS uses only
the \`com.apple.developer.*\` namespace for developer entitlements, and
the Developer ID provisioning profile authorizes
\`com.apple.developer.aps-environment\` but not the bare iOS-shaped key.
amfi enforces "every restricted entitlement must be profile-backed" at
load time, so the bare key — which #17 started adding alongside the
correct one — causes amfi to SIGKILL the binary on exec.

\`codesign --verify --strict\` in CI didn't catch this because it only
validates structural integrity, not profile subset. That's amfi's job
and it only runs at load time on the end-user's machine.

## Changes

**\`.github/workflows/release.yml\`** — in the \`Prepare sanitized
entitlements\` step, **delete** the bare \`aps-environment\` key on
macOS releases instead of substituting it. Keep the \`Set
:com.apple.developer.aps-environment production\` line — that's the
macOS runtime key for CloudKit silent push delivery, and it IS in the
profile's authorized list.

## Test plan

- [ ] Merge this PR and let release-please open the v1.3.2 PR
- [ ] Merge v1.3.2 release PR
- [ ] Re-verify the \`Prepare sanitized entitlements\` log in the v1.3.2
release workflow — the dumped plist should show
\`com.apple.developer.aps-environment: production\` and NO bare
\`aps-environment\` key
- [ ] After release ships: \`brew upgrade --cask tossinger\` on both Mac
mini and MacBook
- [ ] Launch the app — should no longer crash with SIGKILL
- [ ] Add a toss on one machine, wait ~30s, verify it appears on the
other (cross-machine CloudKit push sync should work since
\`com.apple.developer.aps-environment\` is present)
- [ ] Run \`toss ls\` — should exit cleanly (CLI opt-out from #17 still
applies)

## Notes

- This PR reverses **only the iOS-shaped half** of the APNs entitlement
work from #17. The CLI CloudKit opt-out and the iOS cert-spam fix from
#16 are untouched.
- #17's \`toss/toss.entitlements\` source file still declares both keys
with value \`development\` — that's fine for local Debug Xcode builds
(Xcode automatic signing handles the iOS/macOS key split per-platform).
The release workflow is the only place we need to strip the iOS shape
for macOS distribution.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant