feat: configure ios app store release signing

This commit is contained in:
joshavant
2026-06-15 17:24:08 +02:00
parent 7650397a22
commit a23de348b2
16 changed files with 166 additions and 134 deletions
+5 -5
View File
@@ -58,11 +58,11 @@ Maintenance update for the current OpenClaw release.
## 2026.5.12 - 2026-05-12
Maintenance update for the current OpenClaw beta release.
Maintenance update for the current OpenClaw release.
## 2026.5.10 - 2026-05-10
Maintenance update for the current OpenClaw beta release.
Maintenance update for the current OpenClaw release.
- Gateway connections now recover after a trusted Gateway certificate changes by refreshing the stored certificate pin during reconnect.
@@ -128,7 +128,7 @@ Maintenance update for the current OpenClaw release.
## 2026.4.19 - 2026-04-19
Maintenance update for the current OpenClaw beta release.
Maintenance update for the current OpenClaw release.
## 2026.4.18 - 2026-04-18
@@ -136,11 +136,11 @@ Maintenance update for the current OpenClaw release.
## 2026.4.15 - 2026-04-15
Maintenance update for the current OpenClaw beta release.
Maintenance update for the current OpenClaw release.
## 2026.4.14 - 2026-04-14
Maintenance update for the current OpenClaw beta release.
Maintenance update for the current OpenClaw release.
## 2026.4.12 - 2026-04-12
+2 -1
View File
@@ -5,6 +5,7 @@ OPENCLAW_IOS_DEFAULT_TEAM = FWJYW4S8P8
OPENCLAW_IOS_SELECTED_TEAM = $(OPENCLAW_IOS_DEFAULT_TEAM)
OPENCLAW_DEVELOPMENT_TEAM = $(OPENCLAW_IOS_SELECTED_TEAM)
OPENCLAW_CODE_SIGN_STYLE = Automatic
OPENCLAW_CODE_SIGN_IDENTITY = Apple Development
OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp.extension
@@ -18,7 +19,7 @@ OPENCLAW_WATCH_EXTENSION_PROFILE =
#include? "../LocalSigning.xcconfig"
CODE_SIGN_STYLE = $(OPENCLAW_CODE_SIGN_STYLE)
CODE_SIGN_IDENTITY = Apple Development
CODE_SIGN_IDENTITY = $(OPENCLAW_CODE_SIGN_IDENTITY)
DEVELOPMENT_TEAM = $(OPENCLAW_DEVELOPMENT_TEAM)
// Let Xcode manage provisioning for the selected local team unless a local override pins one.
+1
View File
@@ -2,6 +2,7 @@
// This file is only an example and should stay committed.
OPENCLAW_CODE_SIGN_STYLE = Automatic
OPENCLAW_CODE_SIGN_IDENTITY = Apple Development
OPENCLAW_DEVELOPMENT_TEAM = YOUR_TEAM_ID
OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app
+48 -32
View File
@@ -4,8 +4,8 @@ This iOS app is super-alpha and internal-use only. The first public App Store re
## Distribution Status
- Public distribution: not available.
- Internal beta distribution: local archive + TestFlight upload via Fastlane.
- Public distribution: App Store Connect app created; production signing is configured through the App Store release Fastlane path.
- Internal TestFlight distribution: uses the same App Store distribution archive uploaded to App Store Connect.
- Local/manual deploy from source via Xcode remains the default development path.
## Super-Alpha Disclaimer
@@ -47,7 +47,7 @@ Shortcut command (same flow + open project):
pnpm ios:open
```
## Local Beta Release Flow
## App Store Release Flow
Prereqs:
@@ -55,52 +55,61 @@ Prereqs:
- `pnpm`
- `xcodegen`
- `fastlane`
- Apple account signed into Xcode for automatic signing/provisioning
- App Store Connect API key set up in Keychain via `scripts/ios-asc-keychain-setup.sh` when auto-resolving a beta build number or uploading to TestFlight
- Apple account signed into Xcode for the canonical OpenClaw team (`FWJYW4S8P8`)
- Xcode account permissions to create Apple Distribution certificates, App IDs, and App Store provisioning profiles
- App Store Connect app already created for `ai.openclawfoundation.app`
- App Store Connect API key set up in Keychain via `scripts/ios-asc-keychain-setup.sh` when auto-resolving a build number or uploading to App Store Connect
Release behavior:
- Local development uses the canonical `ai.openclawfoundation.app*` bundle IDs when the OpenClaw team is available, and unique `ai.openclawfoundation.app.test.*` bundle IDs only for non-canonical fallback teams.
- Beta release uses canonical `ai.openclawfoundation.app*` bundle IDs through a temporary generated xcconfig in `apps/ios/build/BetaRelease.xcconfig`.
- Beta release also switches the app to `OpenClawPushTransport=relay`, `OpenClawPushDistribution=official`, and `OpenClawPushAPNsEnvironment=production`.
- App Store release uses canonical `ai.openclawfoundation.app*` bundle IDs through a temporary generated xcconfig in `apps/ios/build/AppStoreRelease.xcconfig`.
- App Store release uses automatic signing with `Apple Distribution` and `-allowProvisioningUpdates`, so Xcode can create missing distribution certificates/profiles for the canonical team when the signed-in account has permission.
- App Store release also switches the app to `OpenClawPushTransport=relay`, `OpenClawPushDistribution=official`, `OpenClawPushAPNsEnvironment=production`, and a production `aps-environment` entitlement.
- App Store screenshots use `pnpm ios:screenshots`, which drives Fastlane Snapshot through a deterministic connected screenshot fixture instead of a live gateway.
- The beta flow does not modify `apps/ios/.local-signing.xcconfig` or `apps/ios/LocalSigning.xcconfig`.
- The release flow does not modify `apps/ios/.local-signing.xcconfig` or `apps/ios/LocalSigning.xcconfig`.
- `apps/ios/version.json` is the pinned iOS release version source.
- `apps/ios/CHANGELOG.md` is the iOS-only changelog and release-note source.
- The pinned iOS version must use CalVer like `2026.4.10`.
- That pinned value becomes:
- `CFBundleShortVersionString = 2026.4.10`
- `CFBundleVersion = next TestFlight build number for 2026.4.10`
- `CFBundleVersion = next App Store Connect build number for 2026.4.10`
- Changing the root gateway version does not change the iOS app version until you explicitly pin from the gateway.
- See `apps/ios/VERSIONING.md` for the full workflow.
Relay behavior for beta builds:
Relay behavior for App Store builds:
- Beta builds default to `https://ios-push-relay.openclaw.ai`.
- Optional custom relay override: `OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com`
This must be a plain `https://host[:port][/path]` base URL without whitespace, query params, fragments, or xcconfig metacharacters.
Prepare the generated release xcconfig/project without archiving:
```bash
pnpm ios:release:prepare -- --build-number 7
```
Archive without upload:
```bash
pnpm ios:beta:archive
pnpm ios:release:archive
```
Archive and upload to TestFlight:
Archive and upload to App Store Connect:
```bash
pnpm ios:beta
pnpm ios:release
```
If you need to force a specific build number:
```bash
pnpm ios:beta -- --build-number 7
pnpm ios:release -- --build-number 7
```
### Maintainer Quick Release Checklist
Use this when a clone is missing local iOS release setup and you want the shortest path to a TestFlight upload.
Use this when a clone is missing local iOS release setup and you want the shortest path to an App Store Connect upload.
1. Confirm Fastlane auth is set up:
@@ -120,38 +129,45 @@ scripts/ios-asc-keychain-setup.sh \
This should create `apps/ios/fastlane/.env` with the non-secret ASC variables while the private key stays in Keychain.
3. Optional: set a custom official/TestFlight relay URL for the build. If unset, the beta flow uses `https://ios-push-relay.openclaw.ai`.
3. Confirm the App Store Connect app and Apple Developer identifiers/capabilities exist for:
- `ai.openclawfoundation.app`
- `ai.openclawfoundation.app.share`
- `ai.openclawfoundation.app.activitywidget`
- `ai.openclawfoundation.app.watchkitapp`
- `ai.openclawfoundation.app.watchkitapp.extension`
4. Optional: set a custom official relay URL for the build. If unset, the release flow uses `https://ios-push-relay.openclaw.ai`.
```bash
export OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com
```
4. If you are starting a brand-new production release train, pin iOS to the current gateway version first:
5. If you are starting a brand-new production release train, pin iOS to the current gateway version first:
```bash
pnpm ios:version:pin -- --from-gateway
```
5. Upload the beta:
6. Upload the build:
```bash
pnpm ios:beta
pnpm ios:release
```
6. Expected behavior:
7. Expected behavior:
- Fastlane reads `apps/ios/version.json`
- verifies synced iOS versioning artifacts
- resolves the next TestFlight build number for that short version
- generates `apps/ios/build/BetaRelease.xcconfig`
- resolves the next App Store Connect build number for that short version
- generates `apps/ios/build/AppStoreRelease.xcconfig`
- archives `OpenClaw`
- uploads the IPA to TestFlight
- uploads the IPA to App Store Connect for TestFlight/App Review use
7. Expected outputs after a successful run:
- `apps/ios/build/beta/OpenClaw-<version>.ipa`
- `apps/ios/build/beta/OpenClaw-<version>.app.dSYM.zip`
- Fastlane log line like `Uploaded iOS beta: version=<version> short=<short> build=<build>`
8. Expected outputs after a successful run:
- `apps/ios/build/app-store/OpenClaw-<version>.ipa`
- `apps/ios/build/app-store/OpenClaw-<version>.app.dSYM.zip`
- Fastlane log line like `Uploaded iOS App Store build: version=<version> short=<short> build=<build>`
8. If this is a fresh clone on a maintainer machine that already works elsewhere, it is OK to copy the non-secret `apps/ios/fastlane/.env` from another trusted local clone on the same Mac. The Keychain-backed private key remains machine-local and is not stored in the repo.
9. If this is a fresh clone on a maintainer machine that already works elsewhere, it is OK to copy the non-secret `apps/ios/fastlane/.env` from another trusted local clone on the same Mac. The Keychain-backed private key remains machine-local and is not stored in the repo.
## iOS Versioning Workflow
@@ -177,7 +193,7 @@ Recommended flow:
1. Keep `apps/ios/version.json` pinned to the current train version.
2. Update `apps/ios/CHANGELOG.md`, usually under `## Unreleased` while iterating.
3. Run `pnpm ios:version:sync` after changelog changes.
4. Upload more TestFlight builds with `pnpm ios:beta`.
4. Upload more TestFlight builds with `pnpm ios:release`.
5. Let Fastlane bump only the numeric build number.
### Starting the next production release train
@@ -190,7 +206,7 @@ pnpm ios:version:pin -- --from-gateway
2. Update `apps/ios/CHANGELOG.md` for the new release as needed.
3. Run `pnpm ios:version:sync`.
4. Submit the first TestFlight build for that newly pinned version.
4. Submit the first App Store Connect build for that newly pinned version.
5. Keep iterating on that same version until the release candidate is ready.
See `apps/ios/VERSIONING.md` for the detailed spec.
@@ -198,9 +214,9 @@ See `apps/ios/VERSIONING.md` for the detailed spec.
## APNs Expectations For Local/Manual Builds
- The app calls `registerForRemoteNotifications()` at launch.
- `apps/ios/Sources/OpenClaw.entitlements` sets `aps-environment` to `development`.
- `apps/ios/Sources/OpenClaw.entitlements` derives `aps-environment` from the active build configuration/signing override.
- APNs token registration to gateway happens only after gateway connection (`push.apns.register`).
- Local/manual builds default to `OpenClawPushTransport=direct` and `OpenClawPushDistribution=local`.
- Local/manual builds default to `OpenClawPushTransport=direct`, `OpenClawPushDistribution=local`, and a development `aps-environment` entitlement.
- Your selected team/profile must support Push Notifications for the app bundle ID you are signing.
- If push capability or provisioning is wrong, APNs registration fails at runtime (check Xcode logs for `APNs registration failed`).
- The gateway host also needs direct APNs auth configured separately with `OPENCLAW_APNS_TEAM_ID`, `OPENCLAW_APNS_KEY_ID`, and either `OPENCLAW_APNS_PRIVATE_KEY_P8` or `OPENCLAW_APNS_PRIVATE_KEY_PATH`.
+2
View File
@@ -5,6 +5,7 @@
#include "Config/Version.xcconfig"
OPENCLAW_CODE_SIGN_STYLE = Manual
OPENCLAW_CODE_SIGN_IDENTITY = Apple Development
OPENCLAW_DEVELOPMENT_TEAM = FWJYW4S8P8
OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app
@@ -12,6 +13,7 @@ OPENCLAW_SHARE_BUNDLE_ID = ai.openclawfoundation.app.share
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp.extension
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclawfoundation.app.activitywidget
OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT = development
OPENCLAW_APP_PROFILE = ai.openclawfoundation.app Development
OPENCLAW_SHARE_PROFILE = ai.openclawfoundation.app.share Development
+1 -2
View File
@@ -3,7 +3,6 @@
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<string>$(OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT)</string>
</dict>
</plist>
+7 -7
View File
@@ -64,7 +64,7 @@ Pinned iOS version `2026.4.10` maps to:
- `apps/ios/fastlane/metadata/en-US/release_notes.txt`
- generated from `apps/ios/CHANGELOG.md`
- `apps/ios/build/Version.xcconfig`
- local gitignored build override generated per build or beta prep
- local gitignored build override generated per build or release prep
## Tooling surfaces
@@ -81,16 +81,16 @@ Pinned iOS version `2026.4.10` maps to:
- `scripts/ios-pin-version.ts`
- explicitly pins iOS to a chosen release version or the current gateway version
### Build and beta flow
### Build and App Store release flow
- `scripts/ios-write-version-xcconfig.sh`
- reads the pinned iOS version
- writes the local numeric build override file in `apps/ios/build/Version.xcconfig`
- `scripts/ios-beta-prepare.sh`
- prepares beta signing and bundle settings against the pinned iOS version
- `scripts/ios-release-prepare.sh`
- prepares App Store distribution signing and bundle settings against the pinned iOS version
- `apps/ios/fastlane/Fastfile`
- resolves version metadata from the pinned iOS helper
- increments TestFlight build numbers for the pinned short version
- increments App Store Connect build numbers for the pinned short version
## Release-note resolution order
@@ -118,7 +118,7 @@ pnpm ios:version:pin -- --version 2026.4.10
1. keep `apps/ios/version.json` pinned to the current TestFlight train version
2. update `apps/ios/CHANGELOG.md` under `## Unreleased` while iterating
3. upload more betas with the usual flow
3. upload more App Store Connect builds with the usual flow
4. let Fastlane increment only `CFBundleVersion`
This keeps the TestFlight version stable while review is in flight.
@@ -139,7 +139,7 @@ pnpm ios:version:pin -- --from-gateway
- `apps/ios/fastlane/metadata/en-US/release_notes.txt`
3. update `apps/ios/CHANGELOG.md` for the new release if needed
4. run `pnpm ios:version:sync` again if the changelog changed
5. submit the first TestFlight build for that newly pinned version
5. submit the first App Store Connect build for that newly pinned version
6. keep iterating only by build number until the release candidate is ready
7. release that reviewed TestFlight build to production
+33 -33
View File
@@ -4,7 +4,7 @@ require "json"
default_platform(:ios)
BETA_APP_IDENTIFIER = "ai.openclawfoundation.app"
APP_STORE_APP_IDENTIFIER = "ai.openclawfoundation.app"
DEFAULT_SNAPSHOT_DEVICES = ["iPhone 16 Pro Max", "iPad Pro 13-inch (M4)"].freeze
SNAPSHOT_STATUS_BAR_ARGUMENTS = "--time 09:41 --dataNetwork wifi --wifiMode active --wifiBars 3 --cellularMode active --cellularBars 4 --batteryState charged --batteryLevel 100".freeze
REQUIRED_SCREENSHOT_FAMILIES = {
@@ -178,45 +178,45 @@ def shell_join(parts)
Shellwords.join(parts.compact)
end
def resolve_beta_build_number(api_key:, short_version:)
explicit = ENV["IOS_BETA_BUILD_NUMBER"]
def resolve_release_build_number(api_key:, short_version:)
explicit = ENV["IOS_RELEASE_BUILD_NUMBER"]
if env_present?(explicit)
UI.user_error!("Invalid IOS_BETA_BUILD_NUMBER '#{explicit}'. Expected digits only.") unless explicit.match?(/\A\d+\z/)
UI.message("Using explicit iOS beta build number #{explicit}.")
UI.user_error!("Invalid iOS release build number '#{explicit}'. Expected digits only.") unless explicit.match?(/\A\d+\z/)
UI.message("Using explicit iOS release build number #{explicit}.")
return explicit
end
latest_build = latest_testflight_build_number(
api_key: api_key,
app_identifier: BETA_APP_IDENTIFIER,
app_identifier: APP_STORE_APP_IDENTIFIER,
version: short_version,
initial_build_number: 0
)
next_build = latest_build.to_i + 1
UI.message("Resolved iOS beta build number #{next_build} for #{short_version} (latest TestFlight build: #{latest_build}).")
UI.message("Resolved iOS release build number #{next_build} for #{short_version} (latest App Store Connect build: #{latest_build}).")
next_build.to_s
end
def beta_build_number_needs_asc_auth?
explicit = ENV["IOS_BETA_BUILD_NUMBER"]
def release_build_number_needs_asc_auth?
explicit = ENV["IOS_RELEASE_BUILD_NUMBER"]
!env_present?(explicit)
end
def prepare_beta_release!(version:, build_number:)
script_path = File.join(repo_root, "scripts", "ios-beta-prepare.sh")
UI.message("Preparing iOS beta release #{version} (build #{build_number}).")
def prepare_app_store_release!(version:, build_number:)
script_path = File.join(repo_root, "scripts", "ios-release-prepare.sh")
UI.message("Preparing iOS App Store release #{version} (build #{build_number}).")
sh(shell_join(["bash", script_path, "--build-number", build_number]))
beta_xcconfig = File.join(ios_root, "build", "BetaRelease.xcconfig")
UI.user_error!("Missing beta xcconfig at #{beta_xcconfig}.") unless File.exist?(beta_xcconfig)
release_xcconfig = File.join(ios_root, "build", "AppStoreRelease.xcconfig")
UI.user_error!("Missing App Store release xcconfig at #{release_xcconfig}.") unless File.exist?(release_xcconfig)
ENV["XCODE_XCCONFIG_FILE"] = beta_xcconfig
beta_xcconfig
ENV["XCODE_XCCONFIG_FILE"] = release_xcconfig
release_xcconfig
end
def build_beta_release(context)
def build_app_store_release(context)
version = context[:version]
output_directory = File.join("build", "beta")
output_directory = File.join("build", "app-store")
archive_path = File.join(output_directory, "OpenClaw-#{version}.xcarchive")
build_app(
@@ -294,40 +294,40 @@ platform :ios do
api_key
end
private_lane :prepare_beta_context do |options|
private_lane :prepare_app_store_context do |options|
require_api_key = options[:require_api_key] == true
needs_api_key = require_api_key || beta_build_number_needs_asc_auth?
needs_api_key = require_api_key || release_build_number_needs_asc_auth?
api_key = needs_api_key ? asc_api_key : nil
sync_ios_versioning!
version_metadata = read_ios_version_metadata
version = version_metadata[:version]
short_version = version_metadata[:short_version]
build_number = resolve_beta_build_number(api_key: api_key, short_version: short_version)
beta_xcconfig = prepare_beta_release!(version: version, build_number: build_number)
build_number = resolve_release_build_number(api_key: api_key, short_version: short_version)
release_xcconfig = prepare_app_store_release!(version: version, build_number: build_number)
{
api_key: api_key,
beta_xcconfig: beta_xcconfig,
build_number: build_number,
release_xcconfig: release_xcconfig,
short_version: short_version,
version: version
}
end
desc "Build a beta archive locally without uploading"
lane :beta_archive do
context = prepare_beta_context(require_api_key: false)
build = build_beta_release(context)
UI.success("Built iOS beta archive: version=#{build[:version]} short=#{build[:short_version]} build=#{build[:build_number]}")
desc "Build an App Store distribution archive locally without uploading"
lane :app_store_archive do
context = prepare_app_store_context(require_api_key: false)
build = build_app_store_release(context)
UI.success("Built iOS App Store archive: version=#{build[:version]} short=#{build[:short_version]} build=#{build[:build_number]}")
build
ensure
ENV.delete("XCODE_XCCONFIG_FILE")
end
desc "Build + upload a beta to TestFlight"
lane :beta do
context = prepare_beta_context(require_api_key: true)
build = build_beta_release(context)
desc "Build + upload an App Store distribution build to App Store Connect"
lane :app_store do
context = prepare_app_store_context(require_api_key: true)
build = build_app_store_release(context)
upload_to_testflight(
api_key: context[:api_key],
@@ -336,7 +336,7 @@ platform :ios do
uses_non_exempt_encryption: false
)
UI.success("Uploaded iOS beta: version=#{build[:version]} short=#{build[:short_version]} build=#{build[:build_number]}")
UI.success("Uploaded iOS App Store build: version=#{build[:version]} short=#{build[:short_version]} build=#{build[:build_number]}")
ensure
ENV.delete("XCODE_XCCONFIG_FILE")
end
+16 -16
View File
@@ -29,7 +29,7 @@ ASC_KEYCHAIN_SERVICE=openclaw-asc-key
ASC_KEYCHAIN_ACCOUNT=YOUR_MAC_USERNAME
```
Important: `apps/ios/fastlane/.env` is only for Fastlane/App Store Connect auth and optional beta-archive settings. It does **not** configure gateway-side direct APNs push delivery for local iOS builds.
Important: `apps/ios/fastlane/.env` is only for Fastlane/App Store Connect auth and optional release-archive settings. It does **not** configure gateway-side direct APNs push delivery for local iOS builds.
Optional app targeting variables (helpful if Fastlane cannot auto-resolve app by bundle):
@@ -53,7 +53,7 @@ Code signing variable (optional in `.env`):
IOS_DEVELOPMENT_TEAM=YOUR_TEAM_ID
```
Tip: run `scripts/ios-team-id.sh --require-canonical` from repo root to verify the canonical OpenClaw iOS team (`FWJYW4S8P8`) is available locally. Fastlane uses the same canonical-only path when `IOS_DEVELOPMENT_TEAM` is missing, and rejects non-canonical teams for beta archives.
Tip: run `scripts/ios-team-id.sh --require-canonical` from repo root to verify the canonical OpenClaw iOS team (`FWJYW4S8P8`) is available locally. Fastlane uses the same canonical-only path when `IOS_DEVELOPMENT_TEAM` is missing, and rejects non-canonical teams for release archives.
For local/manual iOS builds that stay on direct APNs, configure the gateway host separately with `OPENCLAW_APNS_TEAM_ID`, `OPENCLAW_APNS_KEY_ID`, and either `OPENCLAW_APNS_PRIVATE_KEY_P8` or `OPENCLAW_APNS_PRIVATE_KEY_PATH`. Those gateway runtime env vars are separate from Fastlane's `.env`.
@@ -66,15 +66,15 @@ fastlane ios auth_check
ASC auth is only required when:
- uploading to TestFlight
- uploading to App Store Connect
- auto-resolving the next build number from App Store Connect
If you pass `--build-number` to `pnpm ios:beta:archive`, the local archive path does not need ASC auth.
If you pass `--build-number` to `pnpm ios:release:archive`, the local archive path does not need ASC auth.
Archive locally without upload:
```bash
pnpm ios:beta:archive
pnpm ios:release:archive
```
Generate deterministic App Store screenshots:
@@ -85,17 +85,17 @@ pnpm ios:screenshots
The screenshot lane runs the app with `--openclaw-screenshot-mode`, which enters the built-in connected screenshot fixture instead of pairing with a live gateway. By default it captures the tab set on `iPhone 16 Pro Max` and `iPad Pro 13-inch (M4)`; override devices with a comma-separated `OPENCLAW_SNAPSHOT_DEVICES` value when the requested simulators exist locally.
Upload to TestFlight:
Upload to App Store Connect:
```bash
pnpm ios:beta
pnpm ios:release
```
Direct Fastlane entry point:
```bash
cd apps/ios
fastlane ios beta
fastlane ios app_store
```
Maintainer recovery path for a fresh clone on the same Mac:
@@ -123,7 +123,7 @@ fastlane ios auth_check
pnpm ios:version:pin -- --from-gateway
```
5. Set the official/TestFlight relay URL before release:
5. Set the official relay URL before release:
```bash
export OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com
@@ -132,14 +132,14 @@ export OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com
6. Upload:
```bash
pnpm ios:beta
pnpm ios:release
```
Quick verification after upload:
- confirm `apps/ios/build/beta/OpenClaw-<version>.ipa` exists
- confirm Fastlane prints `Uploaded iOS beta: version=<version> short=<short> build=<build>`
- remember that TestFlight processing can take a few minutes after the upload succeeds
- confirm `apps/ios/build/app-store/OpenClaw-<version>.ipa` exists
- confirm Fastlane prints `Uploaded iOS App Store build: version=<version> short=<short> build=<build>`
- remember that App Store Connect/TestFlight processing can take a few minutes after the upload succeeds
Versioning rules:
@@ -149,9 +149,9 @@ Versioning rules:
- `pnpm ios:version:pin -- --from-gateway` promotes the current root gateway version into the pinned iOS release version
- Fastlane uses the pinned iOS version only; changing `package.json.version` alone does not change the iOS app version
- Fastlane sets `CFBundleShortVersionString` to the pinned iOS version, for example `2026.4.10`
- Fastlane resolves `CFBundleVersion` as the next integer TestFlight build number for that short version
- Fastlane resolves `CFBundleVersion` as the next integer App Store Connect build number for that short version
- Run `pnpm ios:version:sync` after changing `apps/ios/version.json` or `apps/ios/CHANGELOG.md`
- `pnpm ios:version:check` validates that checked-in iOS version artifacts are in sync
- The beta flow regenerates `apps/ios/OpenClaw.xcodeproj` from `apps/ios/project.yml` before archiving
- Local beta signing uses a temporary generated xcconfig and leaves local development signing overrides untouched
- The release flow regenerates `apps/ios/OpenClaw.xcodeproj` from `apps/ios/project.yml` before archiving
- Local App Store signing uses a temporary generated xcconfig and leaves local development signing overrides untouched
- See `apps/ios/VERSIONING.md` for the detailed workflow
+10 -8
View File
@@ -99,7 +99,7 @@ targets:
swiftlint lint --config "$SRCROOT/.swiftlint.yml" --use-script-input-file-lists
settings:
base:
CODE_SIGN_IDENTITY: "Apple Development"
CODE_SIGN_IDENTITY: "$(OPENCLAW_CODE_SIGN_IDENTITY)"
CODE_SIGN_ENTITLEMENTS: Sources/OpenClaw.entitlements
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
@@ -113,11 +113,13 @@ targets:
ENABLE_APP_INTENTS_METADATA_GENERATION: NO
configs:
Debug:
OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT: development
OPENCLAW_PUSH_TRANSPORT: direct
OPENCLAW_PUSH_DISTRIBUTION: local
OPENCLAW_PUSH_RELAY_BASE_URL: ""
OPENCLAW_PUSH_APNS_ENVIRONMENT: sandbox
Release:
OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT: production
OPENCLAW_PUSH_TRANSPORT: direct
OPENCLAW_PUSH_DISTRIBUTION: local
OPENCLAW_PUSH_RELAY_BASE_URL: ""
@@ -184,7 +186,7 @@ targets:
- sdk: AppIntents.framework
settings:
base:
CODE_SIGN_IDENTITY: "Apple Development"
CODE_SIGN_IDENTITY: "$(OPENCLAW_CODE_SIGN_IDENTITY)"
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
ENABLE_APPINTENTS_METADATA: NO
@@ -224,7 +226,7 @@ targets:
- sdk: ActivityKit.framework
settings:
base:
CODE_SIGN_IDENTITY: "Apple Development"
CODE_SIGN_IDENTITY: "$(OPENCLAW_CODE_SIGN_IDENTITY)"
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID)"
@@ -259,7 +261,7 @@ targets:
settings:
base:
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
CODE_SIGN_IDENTITY: "Apple Development"
CODE_SIGN_IDENTITY: "$(OPENCLAW_CODE_SIGN_IDENTITY)"
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
ENABLE_APPINTENTS_METADATA: NO
@@ -293,7 +295,7 @@ targets:
ProvisioningStyle: "$(OPENCLAW_CODE_SIGN_STYLE)"
settings:
base:
CODE_SIGN_IDENTITY: "Apple Development"
CODE_SIGN_IDENTITY: "$(OPENCLAW_CODE_SIGN_IDENTITY)"
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_WATCH_EXTENSION_BUNDLE_ID)"
@@ -326,7 +328,7 @@ targets:
- sdk: AppIntents.framework
settings:
base:
CODE_SIGN_IDENTITY: "Apple Development"
CODE_SIGN_IDENTITY: "$(OPENCLAW_CODE_SIGN_IDENTITY)"
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_APP_BUNDLE_ID).tests"
@@ -354,7 +356,7 @@ targets:
- package: OpenClawKit
settings:
base:
CODE_SIGN_IDENTITY: "Apple Development"
CODE_SIGN_IDENTITY: "$(OPENCLAW_CODE_SIGN_IDENTITY)"
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_APP_BUNDLE_ID).logic-tests"
@@ -381,7 +383,7 @@ targets:
- target: OpenClaw
settings:
base:
CODE_SIGN_IDENTITY: "Apple Development"
CODE_SIGN_IDENTITY: "$(OPENCLAW_CODE_SIGN_IDENTITY)"
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_APP_BUNDLE_ID).ui-tests"
+3 -3
View File
@@ -1550,12 +1550,12 @@
"gateway:watch:raw": "node scripts/watch-node.mjs gateway --force",
"gen:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --write",
"ghsa:patch": "node scripts/ghsa-patch.mjs",
"ios:beta": "bash scripts/ios-beta-release.sh",
"ios:beta:archive": "bash scripts/ios-beta-archive.sh",
"ios:beta:prepare": "bash scripts/ios-beta-prepare.sh",
"ios:build": "bash -lc './scripts/ios-configure-signing.sh && ./scripts/ios-write-version-xcconfig.sh && cd apps/ios && xcodegen generate && xcodebuild -project OpenClaw.xcodeproj -scheme OpenClaw -destination \"${IOS_DEST:-platform=iOS Simulator,name=iPhone 17}\" -configuration Debug build'",
"ios:gen": "bash -lc './scripts/ios-configure-signing.sh && ./scripts/ios-write-version-xcconfig.sh && cd apps/ios && xcodegen generate'",
"ios:open": "bash -lc './scripts/ios-configure-signing.sh && ./scripts/ios-write-version-xcconfig.sh && cd apps/ios && xcodegen generate && open OpenClaw.xcodeproj'",
"ios:release": "bash scripts/ios-release.sh",
"ios:release:archive": "bash scripts/ios-release-archive.sh",
"ios:release:prepare": "bash scripts/ios-release-prepare.sh",
"ios:run": "bash scripts/ios-run.sh",
"ios:screenshots": "bash scripts/ios-screenshots.sh",
"ios:version": "node --import tsx scripts/ios-version.ts --json",
+8 -2
View File
@@ -73,6 +73,8 @@ watch_app_bundle_id="${OPENCLAW_IOS_WATCH_APP_BUNDLE_ID:-${bundle_base}.watchkit
watch_extension_bundle_id="${OPENCLAW_IOS_WATCH_EXTENSION_BUNDLE_ID:-${watch_app_bundle_id}.extension}"
code_sign_style="${OPENCLAW_IOS_CODE_SIGN_STYLE:-Automatic}"
code_sign_identity="${OPENCLAW_IOS_CODE_SIGN_IDENTITY:-Apple Development}"
apns_entitlement_environment="${OPENCLAW_IOS_APNS_ENTITLEMENT_ENVIRONMENT:-development}"
app_profile="${OPENCLAW_IOS_APP_PROFILE:-}"
share_profile="${OPENCLAW_IOS_SHARE_PROFILE:-}"
watch_app_profile="${OPENCLAW_IOS_WATCH_APP_PROFILE:-}"
@@ -87,9 +89,12 @@ cat >"${tmp_file}" <<EOF
// OPENCLAW_IOS_APP_BUNDLE_ID / OPENCLAW_IOS_BUNDLE_ID_BASE
// OPENCLAW_IOS_SHARE_BUNDLE_ID / OPENCLAW_IOS_ACTIVITY_WIDGET_BUNDLE_ID
// OPENCLAW_IOS_WATCH_APP_BUNDLE_ID / OPENCLAW_IOS_WATCH_EXTENSION_BUNDLE_ID
// OPENCLAW_IOS_CODE_SIGN_STYLE / OPENCLAW_IOS_APP_PROFILE / OPENCLAW_IOS_SHARE_PROFILE
// OPENCLAW_IOS_WATCH_APP_PROFILE / OPENCLAW_IOS_WATCH_EXTENSION_PROFILE
// OPENCLAW_IOS_CODE_SIGN_STYLE / OPENCLAW_IOS_CODE_SIGN_IDENTITY
// OPENCLAW_IOS_APNS_ENTITLEMENT_ENVIRONMENT / OPENCLAW_IOS_APP_PROFILE
// OPENCLAW_IOS_SHARE_PROFILE / OPENCLAW_IOS_WATCH_APP_PROFILE
// OPENCLAW_IOS_WATCH_EXTENSION_PROFILE
OPENCLAW_CODE_SIGN_STYLE = ${code_sign_style}
OPENCLAW_CODE_SIGN_IDENTITY = ${code_sign_identity}
OPENCLAW_DEVELOPMENT_TEAM = ${team_id}
// Keep legacy key for compatibility with older signing config paths.
OPENCLAW_IOS_SELECTED_TEAM = ${team_id}
@@ -98,6 +103,7 @@ OPENCLAW_SHARE_BUNDLE_ID = ${share_bundle_id}
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ${activity_widget_bundle_id}
OPENCLAW_WATCH_APP_BUNDLE_ID = ${watch_app_bundle_id}
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ${watch_extension_bundle_id}
OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT = ${apns_entitlement_environment}
OPENCLAW_APP_PROFILE = ${app_profile}
OPENCLAW_SHARE_PROFILE = ${share_profile}
OPENCLAW_WATCH_APP_PROFILE = ${watch_app_profile}
@@ -4,13 +4,13 @@ set -euo pipefail
usage() {
cat <<'EOF'
Usage:
scripts/ios-beta-release.sh [--build-number 7]
scripts/ios-release-archive.sh [--build-number 7]
Archives and uploads a beta-release IPA to TestFlight locally.
Archives and exports an App Store distribution IPA locally without uploading.
EOF
}
BUILD_NUMBER="${IOS_BETA_BUILD_NUMBER:-}"
BUILD_NUMBER="${IOS_RELEASE_BUILD_NUMBER:-}"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
while [[ $# -gt 0 ]]; do
@@ -36,5 +36,5 @@ done
(
cd "${ROOT_DIR}/apps/ios"
IOS_BETA_BUILD_NUMBER="${BUILD_NUMBER}" fastlane ios beta
IOS_RELEASE_BUILD_NUMBER="${BUILD_NUMBER}" fastlane ios app_store_archive
)
@@ -4,16 +4,17 @@ set -euo pipefail
usage() {
cat <<'EOF'
Usage:
scripts/ios-beta-prepare.sh --build-number 7 [--team-id TEAMID]
scripts/ios-release-prepare.sh --build-number 7 [--team-id TEAMID]
Optional custom relay:
OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com \
scripts/ios-beta-prepare.sh --build-number 7 [--team-id TEAMID]
scripts/ios-release-prepare.sh --build-number 7 [--team-id TEAMID]
Prepares local beta-release inputs without touching local signing overrides:
Prepares local App Store release inputs without touching local signing overrides:
- reads apps/ios/version.json and writes apps/ios/build/Version.xcconfig
- writes apps/ios/build/BetaRelease.xcconfig with canonical bundle IDs
- configures the beta build for relay-backed APNs registration
- writes apps/ios/build/AppStoreRelease.xcconfig with canonical bundle IDs
- configures the release build for relay-backed APNs registration
- configures automatic App Store distribution signing for the canonical team
- regenerates apps/ios/OpenClaw.xcodeproj via xcodegen
EOF
}
@@ -21,7 +22,7 @@ EOF
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
IOS_DIR="${ROOT_DIR}/apps/ios"
BUILD_DIR="${IOS_DIR}/build"
BETA_XCCONFIG="${IOS_DIR}/build/BetaRelease.xcconfig"
RELEASE_XCCONFIG="${IOS_DIR}/build/AppStoreRelease.xcconfig"
TEAM_HELPER="${ROOT_DIR}/scripts/ios-team-id.sh"
VERSION_HELPER="${ROOT_DIR}/scripts/ios-write-version-xcconfig.sh"
IOS_VERSION_HELPER="${ROOT_DIR}/scripts/ios-version.ts"
@@ -124,7 +125,7 @@ if [[ -z "${TEAM_ID}" ]]; then
fi
if [[ "${TEAM_ID}" != "${CANONICAL_TEAM_ID}" ]]; then
echo "iOS beta release must use canonical OpenClaw Team ID ${CANONICAL_TEAM_ID}; got ${TEAM_ID}." >&2
echo "iOS App Store release must use canonical OpenClaw Team ID ${CANONICAL_TEAM_ID}; got ${TEAM_ID}." >&2
exit 1
fi
@@ -153,10 +154,11 @@ fi
bash "${VERSION_HELPER}" --build-number "${BUILD_NUMBER}"
)
write_generated_file "${BETA_XCCONFIG}" <<EOF
// Auto-generated by scripts/ios-beta-prepare.sh.
// Local beta-release override; do not commit.
write_generated_file "${RELEASE_XCCONFIG}" <<EOF
// Auto-generated by scripts/ios-release-prepare.sh.
// Local App Store release override; do not commit.
OPENCLAW_CODE_SIGN_STYLE = Automatic
OPENCLAW_CODE_SIGN_IDENTITY = Apple Distribution
OPENCLAW_DEVELOPMENT_TEAM = ${TEAM_ID}
OPENCLAW_IOS_SELECTED_TEAM = ${TEAM_ID}
OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app
@@ -164,8 +166,11 @@ OPENCLAW_SHARE_BUNDLE_ID = ai.openclawfoundation.app.share
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclawfoundation.app.activitywidget
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp.extension
OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT = production
OPENCLAW_APP_PROFILE =
OPENCLAW_SHARE_PROFILE =
OPENCLAW_WATCH_APP_PROFILE =
OPENCLAW_WATCH_EXTENSION_PROFILE =
OPENCLAW_PUSH_TRANSPORT = relay
OPENCLAW_PUSH_DISTRIBUTION = official
OPENCLAW_URL_SLASH = /
@@ -178,5 +183,5 @@ EOF
xcodegen generate
)
echo "Prepared iOS beta release: version=${IOS_VERSION} build=${BUILD_NUMBER} team=${TEAM_ID}"
echo "XCODE_XCCONFIG_FILE=${BETA_XCCONFIG}"
echo "Prepared iOS App Store release: version=${IOS_VERSION} build=${BUILD_NUMBER} team=${TEAM_ID}"
echo "XCODE_XCCONFIG_FILE=${RELEASE_XCCONFIG}"
@@ -4,13 +4,13 @@ set -euo pipefail
usage() {
cat <<'EOF'
Usage:
scripts/ios-beta-archive.sh [--build-number 7]
scripts/ios-release.sh [--build-number 7]
Archives and exports a beta-release IPA locally without uploading.
Archives and uploads an App Store distribution build to App Store Connect.
EOF
}
BUILD_NUMBER="${IOS_BETA_BUILD_NUMBER:-}"
BUILD_NUMBER="${IOS_RELEASE_BUILD_NUMBER:-}"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
while [[ $# -gt 0 ]]; do
@@ -36,5 +36,5 @@ done
(
cd "${ROOT_DIR}/apps/ios"
IOS_BETA_BUILD_NUMBER="${BUILD_NUMBER}" fastlane ios beta_archive
IOS_RELEASE_BUILD_NUMBER="${BUILD_NUMBER}" fastlane ios app_store
)
@@ -1,9 +1,9 @@
// iOS beta prepare tests cover release-signing guardrails.
// iOS release prepare tests cover release-signing guardrails.
import { execFileSync } from "node:child_process";
import path from "node:path";
import { describe, expect, it } from "vitest";
const SCRIPT = path.join(process.cwd(), "scripts", "ios-beta-prepare.sh");
const SCRIPT = path.join(process.cwd(), "scripts", "ios-release-prepare.sh");
const BASH_BIN = process.platform === "win32" ? "bash" : "/bin/bash";
const BASH_ARGS = process.platform === "win32" ? [SCRIPT] : ["--noprofile", "--norc", SCRIPT];
@@ -36,13 +36,13 @@ function runPrepare(extraArgs: string[]): { ok: boolean; stdout: string; stderr:
}
}
describe("scripts/ios-beta-prepare.sh", () => {
it("rejects non-canonical beta signing teams before generating release inputs", () => {
describe("scripts/ios-release-prepare.sh", () => {
it("rejects non-canonical signing teams before generating release inputs", () => {
const result = runPrepare(["--build-number", "7"]);
expect(result.ok).toBe(false);
expect(result.stderr).toContain(
"iOS beta release must use canonical OpenClaw Team ID FWJYW4S8P8",
"iOS App Store release must use canonical OpenClaw Team ID FWJYW4S8P8",
);
expect(result.stderr).toContain("got Y3YUZP442G");
});