Skip to main content

continuum-release

Use when building, running, screenshotting, or releasing the Continuum iOS app — launching it in the simulator, driving its UI to verify a visual change, or shipping a version to TestFlight/App Store. Contains the verified account facts, the toolchain floor (iOS 26 SDK), the signing setup, and the archive→upload→submit runbook.

Source facts

Repository
ryanfrigo/continuum
Last source activity
September 30, 2026 at 17:26
Detected SKILL.md language
English
Stars
1
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
continuum-release
description
Use when building, running, screenshotting, or releasing the Continuum iOS app — launching it in the simulator, driving its UI to verify a visual change, or shipping a version to TestFlight/App Store. Contains the verified account facts, the toolchain floor (iOS 26 SDK), the signing setup, and the archive→upload→submit runbook.
# Continuum release & verification runbook SwiftUI + SwiftData habit tracker with a widget extension. Bundle `orion-labs.continuum`, widget `continuumWidget`, app group `group.com.orionlabs.continuum`, CloudKit container `iCloud.com.orionlabs.continuum`. Release *gates* (what to test before shipping) live in `RELEASE_CHECKLIST.md`. This file is the *mechanics*: commands, toolchain floors, signing, and how to verify a change visually. ## Verified account facts | Thing | Value | |---|---| | Team ID | `NVN2NY8GZC` | | ASC API key ID | `6M245PSNS9` | | ASC key path | `~/.appstoreconnect/private_keys/AuthKey_6M245PSNS9.p8` | | ASC issuer UUID | `9603f3d8-7877-4300-93fb-f740c62cd44e` (also a GitHub secret). One key = one team; this orion-labs key covers Continuum, WakeEarly and Siftbird. | | Apple ID | `rf@stoaked.co` | ## Toolchain floor — check FIRST, it invalidates everything else Since **April 28, 2026** App Store Connect rejects any iOS upload not built with the **iOS 26 SDK**, i.e. **Xcode 26+**. Verify before any release work: ```bash xcodebuild -version # need 26.x xcodebuild -showsdks | grep -i "iOS " # need iphoneos26.x sw_vers -productVersion # Xcode 26.0–26.3 need macOS >= 15.6 ``` - Xcode **26.0–26.3** run on macOS Sequoia **15.6+**. - Xcode **26.4+** requires macOS Tahoe **26.2+**. - Pick the newest Xcode matching the installed macOS; don't upgrade macOS just to get a newer Xcode unless something needs it. Install a specific version without the Mac App Store: ```bash brew install xcodes xcodes list | grep -E "^26\.[0-9.]+ \(" | grep -viE "beta|candidate" xcodes install 26.3 # prompts for Apple ID + 2FA — needs a human xcodes select 26.3 ``` `xcodes install` needs interactive Apple ID auth. An agent cannot complete the 2FA prompt alone; hand this step to the user. ### You probably don't need local Xcode 26 at all — build on CI **Preferred path.** `.github/workflows/release.yml` builds on GitHub's `macos-26` runner, which ships **Xcode 26.6** (and 26.0–26.3). That sidesteps the local toolchain, the macOS upgrade, and the ~10 GB download completely: ```bash gh workflow run release.yml # build what's on main gh workflow run release.yml -f build_number=5 # ASC rejects duplicate build numbers gh run watch $(gh run list -w release.yml -L1 --json databaseId -q '.[0].databaseId') ``` All three repo secrets are set (`gh secret list` to confirm): `ASC_KEY_ID`, `ASC_KEY_P8_BASE64`, `ASC_ISSUER_ID`. **This path shipped 3.4 to the App Store on 2026-08-11**, so it is proven, not theoretical. Check the live version before assuming what's shipped: ```bash curl -s "https://itunes.apple.com/lookup?bundleId=orion-labs.continuum" | python3 -m json.tool | grep -E '"version"|ReleaseDate' ``` Signing needs no p12: `-allowProvisioningUpdates` plus the ASC API key mints the Apple Distribution cert on the clean runner. ### Local Xcode 26 — only if you truly need to build on this Mac Every route ends at an Apple authentication boundary, so prefer CI above: | Route | Blocker | |---|---| | `xcodes install 26.x` | Apple ID + 2FA prompt | | Mac App Store / `mas` | signed-in App Store account | | developer.apple.com .xip | authenticated web session | | macOS update (needed to *run* Xcode 26) | `sudo` password, then a restart | Browser automation does not rescue the local path either. Both stacks were unavailable on 2026-08-11: `claude-in-chrome` reported "extension is not connected", and `chrome-devtools` MCP refused with "browser is already running for `~/.cache/chrome-devtools-mcp/chrome-profile`" (its Chrome uses `--remote-debugging-pipe`, so there's no TCP port to attach to, and `--isolated` isn't settable from the tool call). Even when it works, that MCP uses its own scratch profile with **no Apple session**, so a human sign-in is still required. **Conclusion: use CI.** The only thing a human must supply is the issuer UUID, because it lives solely behind an authenticated App Store Connect session. Record it in the table above so it's a one-time cost. Never attempt to ship with an older SDK — App Store Connect rejects it outright, so there is no partial-credit path, and never fake `DTSDKName` to look newer. ### Disk space Xcode 26 needs roughly **60–80 GB free** (≈10 GB .xip + expansion + install). Safe, fully regenerable things to clear, largest first: ```bash rm -rf "$HOME/Library/Developer/Xcode/iOS DeviceSupport" # tens of GB; rebuilt per device rm -rf "$HOME/Library/Developer/Xcode/DerivedData" # rebuilt on next build xcrun simctl delete unavailable # keeps available sims + their data brew cleanup --prune=all ``` Never clear `~/Documents`, `~/dev`, `~/models`, or `~/Library/Developer/Xcode/Archives` (real release artifacts). If a GateGuard hook blocks `rm -rf`, don't disable the hook — hand the user the command prefixed with `!` so they run it themselves. ## Signing The App Store needs an **Apple Distribution** identity. Check: ```bash security find-identity -v -p codesigning | grep -i distribution defaults read com.apple.dt.Xcode DVTDeveloperAccountManagerAppleIDLists # empty = no Xcode account ``` An `Apple Development` cert alone is **not** enough. Archiving still succeeds (development-signed, and `-allowProvisioningUpdates` quietly falls back to it), but export/upload fails — so check the archive's `SigningIdentity`, don't trust `ARCHIVE SUCCEEDED`. Fix by signing into Xcode (Settings → Accounts) as Team Agent/Admin, or let `-allowProvisioningUpdates` mint one once the ASC API key is supplied. ## Issuer ID **Recorded above.** It was in `.claude/skills/siftbird-appstore-ship` the whole time — same key, same team — while this file said it was unavailable and cost several sessions' worth of dead ends. If a value looks missing, grep the other skills for the key id before concluding it can't be had. Historical note on obtaining one: **App Store Connect → Users and Access → Integrations → App Store Connect API**, shown as "Issuer ID" at the top. It cannot be derived from the `.p8`. Once you have it, **write it into the table above** so future releases are unattended. Don't re-test these — all verified dead ends on 2026-08-11: - Key `6M245PSNS9` is **team-scoped**, not an individual key. The individual-key JWT form (omit `iss`, use `sub: "user"`) returns `401 NOT_AUTHORIZED`, so the issuer really is mandatory. - Not cached anywhere on this Mac: shell history, `~/.appstoreconnect`, `~/.fastlane`, `com.apple.dt.Xcode.plist`, the login keychain, `~/.config`. - No ASC endpoint returns it, and a wrong value just fails auth — never guess. - Browser rescue re-tested 2026-09-17: `claude-in-chrome` still reports "extension is not connected", and `chrome-devtools` MCP now runs but on a blank profile — appstoreconnect.apple.com bounces to `authResult=FAILED`. Don't retry; run it through CI instead. ## Release runbook **Use the script** — it encodes every preflight below and fails fast: ```bash ./scripts/release.sh <ASC_ISSUER_UUID> ``` It verifies the ASC key, the iOS 26+ SDK, the distribution identity, and version consistency across all 8 configs; runs the tests; archives; asserts the archive is *actually* distribution-signed; then uploads. It stops before submission on purpose. The manual equivalent, step by step: ```bash # 1. Preflight xcodebuild test -scheme continuum -testPlan continuum \ -destination 'platform=iOS Simulator,name=iPhone 16 Plus' # 93 tests as of 3.8 (incl. 2 screenshot seeds) # 2. Bump version — 8 occurrences each, app + widget + test targets. # Widget Info.plist already tracks $(MARKETING_VERSION)/$(CURRENT_PROJECT_VERSION). sed -i '' 's/MARKETING_VERSION = 3\.4;/MARKETING_VERSION = 3.5;/g; \ s/CURRENT_PROJECT_VERSION = 4;/CURRENT_PROJECT_VERSION = 5;/g' \ continuum.xcodeproj/project.pbxproj grep -o "MARKETING_VERSION = [0-9.]*;" continuum.xcodeproj/project.pbxproj | sort | uniq -c # 3. Archive xcodebuild archive -scheme continuum -configuration Release \ -destination 'generic/platform=iOS' \ -archivePath build/Continuum.xcarchive -allowProvisioningUpdates # verify what you actually built /usr/libexec/PlistBuddy -c 'Print :ApplicationProperties:CFBundleShortVersionString' \ -c 'Print :ApplicationProperties:CFBundleVersion' \ -c 'Print :ApplicationProperties:SigningIdentity' build/Continuum.xcarchive/Info.plist plutil -extract DTSDKName raw build/Continuum.xcarchive/Products/Applications/continuum.app/Info.plist # 4. Upload cat > build/exportOptions.plist <<'PLIST' <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"><dict> <key>method</key><string>app-store-connect</string> <key>teamID</key><string>NVN2NY8GZC</string> <key>signingStyle</key><string>automatic</string> <key>destination</key><string>upload</string> </dict></plist> PLIST xcodebuild -exportArchive \ -archivePath build/Continuum.xcarchive \ -exportOptionsPlist build/exportOptions.plist \ -allowProvisioningUpdates \ -authenticationKeyPath ~/.appstoreconnect/private_keys/AuthKey_6M245PSNS9.p8 \ -authenticationKeyID 6M245PSNS9 \ -authenticationKeyIssuerID <ISSUER-UUID> ``` ### Submitting for review — scriptable after all `scripts/asc-submit.mjs` does what the upload stops short of: create the version, set What's New from `scripts/whats-new.txt`, attach the build, turn on phased release, submit. ```bash node scripts/asc-submit.mjs --version 3.5 --build 6 # prepare; prints what it staged node scripts/asc-submit.mjs --version 3.5 --build 6 --submit # sends it to Apple ``` Prepare is idempotent — run it first and read the output. The build must report `VALID` (processing finished) or the attach leaves the version buildless and review bounces it. Used for 3.5 on 2026-09-21 → `WAITING_FOR_REVIEW`. Sign-off is a *people* gate, not a tooling one: run the script only once Ryan has said to ship. The gates in `RELEASE_CHECKLIST.md` are deliberate: upgrade-path test on real data, TestFlight soak, phased release ON (this app touches the data layer + CloudKit). ## CloudKit schema **A new `@Model` type does not sync until its record type exists in the PRODUCTION schema.** Nothing crashes when it's missing — the container falls back to local-only — so this fails completely silently. It bit hard: the schema was never deployed at all until 2026-09-21, meaning iCloud sync had never once worked for a real user since 3.3 shipped it as the headline feature in July. Check Production first, and don't trust the console UI alone: ```bash xcrun cktool save-token --type management # CloudKit console → Tokens & Keys xcrun cktool export-schema --team-id NVN2NY8GZC \ --container-id iCloud.com.orionlabs.continuum --environment production ``` To add a record type without a device or an iCloud sign-in: export the Development schema, hand-write the `RECORD TYPE` block mirroring an existing one for field types (SwiftData maps UUID → STRING, Int/Bool → INT64, Date → TIMESTAMP, `[Date]?` → BYTES, plus `CD_entityName STRING QUERYABLE SEARCHABLE SORTABLE` and the same GRANTs), then: ```bash xcrun cktool validate-schema ... --environment development --file new.ckdb xcrun cktool import-schema ... --environment development --file new.ckdb ``` `import-schema` **refuses Production** ("endpoint not applicable in the environment 'production'") and cktool has **no deploy/promote command** — the Development → Production step is console-only, by Apple's design: CloudKit Console → container → Development → *Deploy Schema Changes…*. A human has to click it. Verify afterwards with `export-schema --environment production` and diff it against development. Production schema changes are **permanent and additive**: you can add a record type or field, never remove one or change a field's type. ## Running it in the simulator ```bash SIM=$(xcrun simctl list devices available | grep -m1 "iPhone 16 Plus" | grep -oE "[0-9A-F-]{36}") open -a Simulator xcodebuild -scheme continuum -configuration Debug \ -destination "platform=iOS Simulator,id=$SIM" -derivedDataPath /tmp/dd build xcrun simctl install $SIM /tmp/dd/Build/Products/Debug-iphonesimulator/continuum.app xcrun simctl launch $SIM orion-labs.continuum xcrun simctl io $SIM screenshot shot.png ``` **Always look at the screenshot.** A green build proves nothing about layout, z-order, or fonts — three real bugs in the 3.4 celebration work were invisible to the compiler and obvious on screen. ### Driving the UI (taps) `simctl` has no tap command. Use `cliclick` against the Simulator window: ```bash osascript -e 'tell application "System Events" to tell process "Simulator" \ to get {position, size} of window 1' # e.g. 1174, 50, 484, 1030 ``` Device origin = window origin + **27pt bezel**, plus **44pt titlebar** on y. For a 430x932pt device in a 484x1030 window at (1174,50): `screen_x = 1201 + pt_x`, `screen_y = 121 + pt_y`, and `pt = screenshot_px / 3`. ```bash osascript -e 'tell application "Simulator" to activate' # REQUIRED before each batch sleep 1 cliclick c:1233,196 # tap cliclick dd:1311,559; sleep 2; cliclick du:1311,559 # hold-to-complete gesture ``` Gotchas: clicks are silently dropped unless the window was just activated, and `kp:esc` does **not** dismiss a SwiftUI context menu — tap empty space instead. ### Xcode 27: no Simulator.app, use DeviceHub (2026-09-29) Xcode 27 (installed locally, macOS 26.6) ships `DeviceHub.app` in `/Applications/Xcode.app/Contents/Applications/` instead of Simulator.app. Booted simulators run headless — `simctl io … screenshot` and `simctl launch` work without it — but to tap you open DeviceHub, select the device in its sidebar, and click the live screen it shows. `xcodebuild` wants the device by id (`-destination 'platform=iOS Simulator,id=…'`); the name alone was ambiguous across the iOS 18.6 and 27.0 runtimes. With the window at (314,175) 1100x800 and the 16 Pro Max at DeviceHub's default zoom: `screen_x = 838.4 + 0.681 * pt_x`, `screen_y = 253.8 + 0.682 * pt_y`. Re-measure from a `screencapture` of the window if it moves. Roughly one click in two is dropped, most often the first after a relaunch or an `activate`. A tap on empty space first helps, but check every step with a screenshot. Menus (the card's ⋯, the share sheet) often need the item tapped twice. The home-screen widget gallery never accepted the "Add Widget" tap; it's easier to check widget layout another way. **Prefer launch switches to DeviceHub taps.** The 2026-09-30 screenshots took no clicks. Temporary env-gated code, reverted after: `HARNESS_COMPLETE=<habit>` in ContentView's `onAppear` marks the habit done and calls `checkForMilestones` (the comeback tile, or graduation after `seedForGraduationScreenshot`); `HARNESS_HOLD=<habit>` in HabitCardView sets `isAnimatingCompletion`, `completionProgress = 0.5`, `cardScale = 0.97`; `TileCelebration.duration = 120` keeps a tile up. Launch with `SIMCTL_CHILD_HARNESS_…=… xcrun simctl launch`, capture with `simctl io`. Commit the real work first so `git checkout` strips the switches cleanly. The stats sheet works the same way: present `HabitStatsView` in a `.sheet` over `ContentView` from a harness in `continuumApp.swift`. **Widgets without the home screen:** copy `continuumWidget/continuumWidget.swift` into `continuum/` (no `@main` there, no name clashes) and render `SmallWidgetView` at 170x170 and `MediumWidgetView` at 364x170, each with `.padding(16)` for the content margins and `.fontDesign(.monospaced)`. That's how the medium widget's overflow turned up. Delete the copy after. **Never `screencapture -R` the DeviceHub rectangle** to check its state: other apps' windows share that rectangle, and one capture caught a private Messages thread. `simctl io … screenshot` sees only the simulator.
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub