chore(ios): generate release artifacts locally

This commit is contained in:
joshavant
2026-07-01 21:45:32 -05:00
parent 4a2a97777e
commit 8e95e56e2d
26 changed files with 248 additions and 331 deletions

View File

@@ -102,6 +102,7 @@ jobs:
set -euo pipefail
./scripts/ios-configure-signing.sh
./scripts/ios-write-version-xcconfig.sh
node scripts/ios-write-swift-filelist.mjs
cd apps/ios
xcodegen generate

1
.gitignore vendored
View File

@@ -81,6 +81,7 @@ apps/android/fastlane/README.md
apps/ios/fastlane/report.xml
apps/ios/fastlane/Preview.html
apps/ios/fastlane/screenshots/
apps/ios/fastlane/metadata/*/release_notes.txt
apps/ios/fastlane/test_output/
apps/ios/fastlane/logs/
apps/ios/fastlane/.env

View File

@@ -1,5 +1,5 @@
// Shared iOS signing defaults for local development + CI.
#include "Version.xcconfig"
#include "../build/Version.xcconfig"
OPENCLAW_IOS_DEFAULT_TEAM = FWJYW4S8P8
OPENCLAW_IOS_SELECTED_TEAM = $(OPENCLAW_IOS_DEFAULT_TEAM)

View File

@@ -1,9 +0,0 @@
// Shared iOS version defaults.
// Source of truth: package.json or explicit release --version.
// Generated by scripts/ios-sync-versioning.ts.
OPENCLAW_IOS_VERSION = 2026.6.11
OPENCLAW_MARKETING_VERSION = 2026.6.11
OPENCLAW_BUILD_VERSION = 1
#include? "../build/Version.xcconfig"

View File

@@ -25,10 +25,7 @@ OpenClaw iOS is the officially released iPhone app. It connects to an OpenClaw G
```bash
pnpm install
./scripts/ios-configure-signing.sh
cd apps/ios
xcodegen generate
open OpenClaw.xcodeproj
pnpm ios:open
```
3. In Xcode:
@@ -40,10 +37,10 @@ open OpenClaw.xcodeproj
- Use unique local bundle IDs via `apps/ios/LocalSigning.xcconfig`.
- Start from `apps/ios/LocalSigning.xcconfig.example`.
Shortcut command (same flow + open project):
Generate without opening Xcode:
```bash
pnpm ios:open
pnpm ios:gen
```
## App Store Release Flow
@@ -165,10 +162,10 @@ This should create `apps/ios/fastlane/.env` with non-secret App Store Connect va
Use `pnpm ios:release:signing:setup` for the initial portal setup, then `MATCH_PASSWORD=... pnpm ios:release:signing:sync:push` to publish encrypted Fastlane match assets to the shared private repo.
4. If you are starting a brand-new production release train, add or update the matching iOS changelog section and sync generated metadata:
4. If you are starting a brand-new production release train, add or update the matching iOS changelog section and validate the release notes:
```bash
pnpm ios:version:sync -- --version 2026.6.11
pnpm ios:version:check -- --version 2026.6.11
```
5. Upload the build with explicit release intent:
@@ -183,7 +180,7 @@ pnpm ios:release:upload -- --version 2026.6.11 --build-number 3
7. Expected behavior:
- Fastlane reads the explicit `--version` value
- verifies synced iOS versioning artifacts for that version
- validates iOS versioning inputs for that version
- resolves the next App Store Connect build number for that short version
- generates deterministic App Store screenshots
- uploads release notes, screenshots, and the App Review PDF attachment to the editable App Store version
@@ -205,16 +202,17 @@ pnpm ios:release:upload -- --version 2026.6.11 --build-number 3
- Release upload version: explicit `--version`
- Local default version: root `package.json`
- iOS-only changelog: `apps/ios/CHANGELOG.md`
- Generated checked-in artifacts:
- `apps/ios/Config/Version.xcconfig`
- `apps/ios/fastlane/metadata/en-US/release_notes.txt`
- Generated local artifacts:
- `apps/ios/build/Version.xcconfig`
- `apps/ios/SwiftSources.input.xcfilelist`
- temporary Fastlane metadata containing release notes rendered from `apps/ios/CHANGELOG.md`
- Useful commands:
```bash
pnpm ios:version
pnpm ios:version:check
pnpm ios:version -- --version 2026.6.11
pnpm ios:version:sync -- --version 2026.6.11
pnpm ios:filelist:gen
```
Recommended flow:
@@ -223,7 +221,7 @@ Recommended flow:
1. Choose the App Store train explicitly, for example `2026.6.11`.
2. Update `apps/ios/CHANGELOG.md`, usually under `## Unreleased` while iterating.
3. Run `pnpm ios:version:sync -- --version 2026.6.11` after changelog changes.
3. Run `pnpm ios:version:check -- --version 2026.6.11` after changelog changes.
4. Upload additional App Store Connect builds with `pnpm ios:release:upload -- --version 2026.6.11`.
5. Let Fastlane bump only the numeric build number.
@@ -231,7 +229,7 @@ Recommended flow:
1. Confirm the target gateway version in root `package.json`.
2. Update `apps/ios/CHANGELOG.md` for the new release as needed.
3. Run `pnpm ios:version:sync -- --version <release-version>`.
3. Run `pnpm ios:version:check -- --version <release-version>`.
4. Submit the first App Store Connect build with `pnpm ios:release:upload -- --version <release-version>`.
5. Keep iterating on that same explicit version until the release candidate is ready.

View File

@@ -2,7 +2,7 @@
// Auto-selected local team overrides live in .local-signing.xcconfig (git-ignored).
// Manual local overrides can go in LocalSigning.xcconfig (git-ignored).
#include "Config/Version.xcconfig"
#include "build/Version.xcconfig"
OPENCLAW_CODE_SIGN_STYLE = Manual
OPENCLAW_CODE_SIGN_IDENTITY = Apple Development

View File

@@ -1,153 +0,0 @@
Sources/Calendar/CalendarService.swift
Sources/Camera/CameraController.swift
Sources/Capabilities/NodeCapabilityRouter.swift
Sources/Chat/AppleReviewDemoChatTransport.swift
Sources/Chat/IOSGatewayChatTransport.swift
Sources/Contacts/ContactsService.swift
Sources/Device/DeviceInfoHelper.swift
Sources/Device/DeviceStatusService.swift
Sources/Device/NetworkStatusService.swift
Sources/Device/NodeDisplayName.swift
Sources/Design/OpenClawBrand.swift
Sources/Design/AgentProDreamingDestination.swift
Sources/Design/AgentProNodesDestination.swift
Sources/Design/AgentProTab.swift
Sources/Design/ChatProTab.swift
Sources/Design/CommandCenterTab.swift
Sources/Design/TalkProTab.swift
Sources/Design/OpenClawProComponents.swift
Sources/Design/SettingsProTab.swift
Sources/Design/SettingsProTabSupport.swift
Sources/Design/SettingsProTabSections.swift
Sources/Design/SettingsProTabActions.swift
Sources/Design/TalkRuntimeIssueBanner.swift
Sources/Design/CommandCenterSupport.swift
Sources/Design/AgentProTab+Overview.swift
Sources/Design/AgentProTab+Destinations.swift
Sources/Design/AgentProTab+Skills.swift
Sources/Design/AgentProTab+Cron.swift
Sources/Design/AgentProTab+Usage.swift
Sources/Design/AgentProTab+DetailComponents.swift
Sources/Design/AgentProTab+GatewayData.swift
Sources/Design/AgentProModels.swift
Sources/Design/IPadActivityScreen.swift
Sources/Design/IPadSidebarFeaturePreviews.swift
Sources/Design/IPadSidebarFeatureScreens.swift
Sources/Design/IPadSkillWorkshopScreen.swift
Sources/Design/IPadSidebarScreenChrome.swift
Sources/Design/IPadWorkboardScreen.swift
Sources/Design/OpenClawDocsScreen.swift
Sources/Design/RootTabsPhoneControlHub.swift
Sources/Design/SettingsChannelsDestination.swift
Sources/EventKit/EventKitAuthorization.swift
Sources/Gateway/DeepLinkAgentPromptAlert.swift
Sources/Gateway/ExecApprovalPromptDialog.swift
Sources/Gateway/GatewayConnectConfig.swift
Sources/Gateway/GatewayConnectionController.swift
Sources/Gateway/GatewayConnectionIssue.swift
Sources/Gateway/GatewayDiscoveryDebugLogView.swift
Sources/Gateway/GatewayDiscoveryModel.swift
Sources/Gateway/GatewayHealthMonitor.swift
Sources/Gateway/GatewayProblemPrimaryAction.swift
Sources/Gateway/GatewayProblemView.swift
Sources/Gateway/GatewayQuickSetupSheet.swift
Sources/Gateway/NotificationPermissionGuidanceDialog.swift
Sources/Gateway/GatewayServiceResolver.swift
Sources/Gateway/GatewaySettingsStore.swift
Sources/Gateway/GatewayTrustPromptAlert.swift
Sources/Gateway/KeychainStore.swift
Sources/Gateway/TCPProbe.swift
Sources/LiveActivity/LiveActivityManager.swift
Sources/LiveActivity/OpenClawActivityAttributes.swift
Sources/Location/LocationService.swift
Sources/Location/SignificantLocationMonitor.swift
Sources/Media/PhotoLibraryService.swift
Sources/Model/NodeAppModel+Canvas.swift
Sources/Model/NodeAppModel+WatchNotifyNormalization.swift
Sources/Model/NodeAppModel.swift
Sources/Model/WatchReplyCoordinator.swift
Sources/Motion/MotionService.swift
Sources/Onboarding/GatewayOnboardingReset.swift
Sources/Onboarding/OnboardingStateStore.swift
Sources/Onboarding/OnboardingWizardSteps.swift
Sources/Onboarding/OnboardingWizardView.swift
Sources/Onboarding/QRScannerView.swift
Sources/OpenClawApp.swift
Sources/Permissions/PermissionRequestBridge.swift
Sources/Push/ExecApprovalNotificationBridge.swift
Sources/Push/BackgroundAliveBeacon.swift
Sources/Push/PushBuildConfig.swift
Sources/Push/PushEnrollmentConsent.swift
Sources/Push/PushRegistrationManager.swift
Sources/Push/PushRelayClient.swift
Sources/Push/PushRelayKeychainStore.swift
Sources/Reminders/RemindersService.swift
Sources/RootTabs.swift
Sources/RootTabsNavigation.swift
Sources/Screen/ScreenController.swift
Sources/Screen/ScreenRecordService.swift
Sources/Screen/ScreenWebView.swift
Sources/Services/NodeServiceProtocols.swift
Sources/Services/NotificationService.swift
Sources/Services/WatchConnectivityTransport.swift
Sources/Services/WatchMessagingPayloadCodec.swift
Sources/Services/WatchMessagingService.swift
Sources/SessionKey.swift
Sources/Settings/PrivacyAccessSectionView.swift
Sources/Settings/VoiceWakeWordsSettingsView.swift
Sources/Status/GatewayStatusBuilder.swift
Sources/Status/VoiceWakeToast.swift
Sources/Voice/TalkGatewayPermissionState.swift
Sources/Voice/TalkDefaults.swift
Sources/Voice/RealtimeTalkRelaySession.swift
Sources/Voice/TalkGatewaySpeechClient.swift
Sources/Voice/TalkModeGatewayConfig.swift
Sources/Voice/TalkModeManager.swift
Sources/Voice/TalkModeManager+Permissions.swift
Sources/Voice/TalkPermissionPromptView.swift
Sources/Voice/TalkRealtimeClientSession.swift
Sources/Voice/TalkRealtimeWebRTCSession.swift
Sources/Voice/TalkSpeechLocale.swift
Sources/Voice/VoiceWakeManager.swift
Sources/Voice/VoiceWakePreferences.swift
ShareExtension/ShareViewController.swift
ActivityWidget/OpenClawActivityWidgetBundle.swift
ActivityWidget/OpenClawLiveActivity.swift
WatchApp/Sources/OpenClawWatchApp.swift
WatchApp/Sources/WatchConnectivityReceiver.swift
WatchApp/Sources/WatchInboxStore.swift
WatchApp/Sources/WatchInboxView.swift
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatPayloadDecoding.swift
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatSheets.swift
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatTheme.swift
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatTransport.swift
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Attachments.swift
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+SessionKeys.swift
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift
../shared/OpenClawKit/Sources/OpenClawKit/AnyCodable.swift
../shared/OpenClawKit/Sources/OpenClawKit/BonjourEscapes.swift
../shared/OpenClawKit/Sources/OpenClawKit/BonjourTypes.swift
../shared/OpenClawKit/Sources/OpenClawKit/BridgeFrames.swift
../shared/OpenClawKit/Sources/OpenClawKit/CameraCommands.swift
../shared/OpenClawKit/Sources/OpenClawKit/CanvasA2UIAction.swift
../shared/OpenClawKit/Sources/OpenClawKit/CanvasA2UICommands.swift
../shared/OpenClawKit/Sources/OpenClawKit/CanvasA2UIJSONL.swift
../shared/OpenClawKit/Sources/OpenClawKit/CanvasCommandParams.swift
../shared/OpenClawKit/Sources/OpenClawKit/CanvasCommands.swift
../shared/OpenClawKit/Sources/OpenClawKit/Capabilities.swift
../shared/OpenClawKit/Sources/OpenClawKit/OpenClawKitResources.swift
../shared/OpenClawKit/Sources/OpenClawKit/DeepLinks.swift
../shared/OpenClawKit/Sources/OpenClawKit/JPEGTranscoder.swift
../shared/OpenClawKit/Sources/OpenClawKit/NodeError.swift
../shared/OpenClawKit/Sources/OpenClawKit/ScreenCommands.swift
../shared/OpenClawKit/Sources/OpenClawKit/StoragePaths.swift
../shared/OpenClawKit/Sources/OpenClawKit/SystemCommands.swift
../shared/OpenClawKit/Sources/OpenClawKit/TalkDirective.swift
../swabble/Sources/SwabbleKit/WakeWordGate.swift

View File

@@ -54,7 +54,7 @@ upload fully deterministic.
### Source files
- `package.json`
- default iOS version source for local builds and checked-in defaults
- default iOS version source for local builds
- explicit `--version`
- release upload source of truth
- `apps/ios/CHANGELOG.md`
@@ -64,26 +64,28 @@ upload fully deterministic.
### Generated or derived files
- `apps/ios/Config/Version.xcconfig`
- checked-in defaults derived from `package.json` or `pnpm ios:version:sync -- --version ...`
- `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 release prep
- `apps/ios/SwiftSources.input.xcfilelist`
- local gitignored Swift lint input file generated before Xcode project generation
- temporary Fastlane metadata
- release notes generated from `apps/ios/CHANGELOG.md` during metadata upload
## Tooling surfaces
- `scripts/lib/ios-version.ts`
- validates iOS CalVer
- normalizes gateway version -> iOS CalVer
- renders checked-in xcconfig and release notes
- renders release notes from the iOS changelog
- `scripts/ios-version.ts`
- CLI for JSON, shell, or single-field version reads
- accepts `--version YYYY.M.D` for explicit release queries
- `scripts/ios-sync-versioning.ts`
- syncs checked-in derived files from the default or explicit iOS version
- validates that release notes can be rendered from the default or explicit iOS version
- `scripts/ios-write-version-xcconfig.sh`
- writes the local numeric build override file in `apps/ios/build/Version.xcconfig`
- `scripts/ios-write-swift-filelist.mjs`
- writes the local Swift file list consumed by Xcode pre-build lint phases
- `scripts/ios-release-prepare.sh`
- requires `--version` and prepares App Store distribution signing and bundle settings
- `apps/ios/fastlane/Fastfile`
@@ -102,17 +104,17 @@ Store Connect mutation commands.
## Release-note resolution order
When generating `apps/ios/fastlane/metadata/en-US/release_notes.txt`, the
tooling reads the first available changelog section in this order:
When generating the temporary Fastlane release notes metadata, the tooling reads
the first available changelog section in this order:
1. exact release version, for example `## 2026.6.11`
2. `## Unreleased`
Before production upload, prefer a final `## <release version>` section and run
sync with the same version:
Before production upload, prefer a final `## <release version>` section and
validate with the same version:
```bash
pnpm ios:version:sync -- --version 2026.6.11
pnpm ios:version:check -- --version 2026.6.11
```
## Common commands
@@ -121,7 +123,7 @@ pnpm ios:version:sync -- --version 2026.6.11
pnpm ios:version
pnpm ios:version -- --version 2026.6.11
pnpm ios:version:check
pnpm ios:version:sync -- --version 2026.6.11
pnpm ios:filelist:gen
pnpm ios:release:upload -- --version 2026.6.11 --build-number 3
```
@@ -129,7 +131,7 @@ pnpm ios:release:upload -- --version 2026.6.11 --build-number 3
1. choose the App Store release train explicitly, for example `2026.6.11`
2. update `apps/ios/CHANGELOG.md` under `## <release version>` or `## Unreleased`
3. run `pnpm ios:version:sync -- --version <release version>`
3. run `pnpm ios:version:check -- --version <release version>`
4. check App Store Connect for the latest build number when needed
5. upload another build with `pnpm ios:release:upload -- --version <release version> --build-number <next>`
@@ -182,10 +184,10 @@ node -e "console.log(require('./package.json').version)"
```
2. update `apps/ios/CHANGELOG.md` for that release
3. sync iOS generated files:
3. validate iOS release notes:
```bash
pnpm ios:version:sync -- --version 2026.6.11
pnpm ios:version:check -- --version 2026.6.11
```
4. verify live App Store Connect state and choose the next build number
@@ -201,7 +203,7 @@ pnpm ios:release:upload -- --version 2026.6.11 --build-number 3
## Important invariant
App Store uploads must carry explicit version intent. Do not infer a release
train from checked-in generated files.
train from generated local files.
App Review submission remains manual. Automation may create/update the editable
App Store version, upload screenshots, upload release notes, upload the App

View File

@@ -734,18 +734,33 @@ def release_signing_check!
sync_app_store_signing!(readonly: true)
end
def release_notes_path
File.join(__dir__, "metadata", "en-US", "release_notes.txt")
def render_ios_release_notes(release_version:)
script_path = File.join(repo_root, "scripts", "ios-version.ts")
args = [
"node",
"--import",
"tsx",
script_path,
"--field",
"releaseNotes"
]
args.push("--version", release_version) if env_present?(release_version)
stdout, stderr, status = Open3.capture3(
*args,
chdir: repo_root
)
return stdout if status.success?
detail = stderr.to_s.strip
detail = stdout.to_s.strip if detail.empty?
UI.user_error!("Failed to render iOS release notes: #{detail}")
end
def release_notes_metadata_path
source = release_notes_path
UI.user_error!("Missing release notes at #{source}. Run `pnpm ios:version:sync -- --version <release-version>`.") unless File.exist?(source)
def release_notes_metadata_path(release_version:)
temp_root = Dir.mktmpdir("openclaw-release-notes")
target_dir = File.join(temp_root, "en-US")
FileUtils.mkdir_p(target_dir)
FileUtils.cp(source, File.join(target_dir, "release_notes.txt"))
File.write(File.join(target_dir, "release_notes.txt"), render_ios_release_notes(release_version: release_version))
temp_root
end
@@ -780,7 +795,7 @@ def assert_no_app_review_notes_field_metadata!(metadata_path)
end
end
def public_metadata_path
def public_metadata_path(release_version: nil)
source = File.join(__dir__, "metadata")
temp_root = Dir.mktmpdir("openclaw-app-store-metadata")
Dir.children(source).each do |entry|
@@ -790,6 +805,12 @@ def public_metadata_path
FileUtils.cp_r(source_entry, File.join(temp_root, entry))
end
Dir[File.join(temp_root, "*", "release_notes.txt")].each { |path| FileUtils.rm_f(path) }
if release_notes_upload_requested?
target_dir = File.join(temp_root, "en-US")
FileUtils.mkdir_p(target_dir)
File.write(File.join(target_dir, "release_notes.txt"), render_ios_release_notes(release_version: release_version))
end
temp_root
end
@@ -1100,8 +1121,8 @@ def sync_ios_versioning!(release_version: nil)
detail = stderr.to_s.strip
detail = stdout.to_s.strip if detail.empty?
sync_command = env_present?(release_version) ? "pnpm ios:version:sync -- --version #{release_version}" : "pnpm ios:version:sync"
UI.user_error!("iOS versioning artifacts are stale. Run `#{sync_command}`.\n#{detail}")
check_command = env_present?(release_version) ? "pnpm ios:version:check -- --version #{release_version}" : "pnpm ios:version:check"
UI.user_error!("iOS versioning inputs are invalid. Run `#{check_command}`.\n#{detail}")
end
def shell_join(parts)
@@ -1463,10 +1484,10 @@ platform :ios do
end
assert_no_app_review_notes_field_metadata!(File.join(__dir__, "metadata"))
metadata_path = public_metadata_path
metadata_path = public_metadata_path(release_version: release_version)
skip_metadata = ENV["DELIVER_METADATA"] != "1"
if release_notes_upload_requested? && skip_metadata
metadata_path = release_notes_metadata_path
metadata_path = release_notes_metadata_path(release_version: release_version)
skip_metadata = false
end
assert_no_app_review_notes_field_metadata!(metadata_path) unless skip_metadata
@@ -1517,6 +1538,7 @@ platform :ios do
sh(shell_join(["bash", File.join(repo_root, "scripts", "ios-configure-signing.sh")]))
sh(shell_join(["bash", File.join(repo_root, "scripts", "ios-write-version-xcconfig.sh"), *version_args]))
sh(shell_join(["node", File.join(repo_root, "scripts", "ios-write-swift-filelist.mjs")]))
sh(shell_join(["xcodegen", "generate", "--spec", File.join(ios_root, "project.yml"), "--project", ios_root]))
capture_ios_screenshots(
@@ -1549,6 +1571,7 @@ platform :ios do
sh(shell_join(["bash", File.join(repo_root, "scripts", "ios-configure-signing.sh")]))
sh(shell_join(["bash", File.join(repo_root, "scripts", "ios-write-version-xcconfig.sh"), *version_args]))
sh(shell_join(["node", File.join(repo_root, "scripts", "ios-write-swift-filelist.mjs")]))
sh(shell_join(["xcodegen", "generate", "--spec", File.join(ios_root, "project.yml"), "--project", ios_root]))
capture_watch_screenshot
end

View File

@@ -135,10 +135,10 @@ cd apps/ios
fastlane ios auth_check
```
4. If you are starting a brand-new production release train, sync iOS generated metadata for the release version:
4. If you are starting a brand-new production release train, validate iOS release notes for the release version:
```bash
pnpm ios:version:sync -- --version 2026.6.11
pnpm ios:version:check -- --version 2026.6.11
```
5. Upload:
@@ -163,8 +163,8 @@ Versioning rules:
- Fastlane uses the explicit release version for App Store upload
- Fastlane sets `CFBundleShortVersionString` to the release version, for example `2026.4.10`
- Fastlane resolves `CFBundleVersion` as the next integer App Store Connect build number for that short version
- Run `pnpm ios:version:sync -- --version <release-version>` after changing `apps/ios/CHANGELOG.md`
- `pnpm ios:version:check` validates that checked-in iOS version artifacts are in sync
- Run `pnpm ios:version:check -- --version <release-version>` after changing `apps/ios/CHANGELOG.md`
- `pnpm ios:version:check` validates that release notes can be generated from the iOS changelog
- The release flow regenerates `apps/ios/OpenClaw.xcodeproj` from `apps/ios/project.yml` before archiving
- Local App Store signing uses a temporary generated xcconfig with profile names from `apps/ios/Config/AppStoreSigning.json` and leaves local development signing overrides untouched
- App Store release uses `OpenClawPushMode=appStore`, which derives the canonical production hosted relay, production APNs, production relay profile, and `appleStrict` proof. The release lane rejects custom production relay URL overrides.

View File

@@ -45,10 +45,11 @@ Or set `APP_STORE_CONNECT_API_KEY_PATH`.
## Notes
- Locale files live under `metadata/<locale>/`, for example `metadata/en-US/` and `metadata/sv-SE/`. Each locale directory should use the public metadata filenames consumed by the `ios metadata` lane.
- `release_notes.txt` is generated from `apps/ios/CHANGELOG.md`; after changelog updates, run `pnpm ios:version:sync -- --version <release-version>`.
- Release notes are generated from `apps/ios/CHANGELOG.md` into temporary Fastlane metadata during upload; after changelog updates, run `pnpm ios:version:check -- --version <release-version>`.
- Do not check in `release_notes.txt` under locale metadata directories; the lane strips copied release-note files and writes the current generated en-US release notes when requested.
- `apps/ios/APP-REVIEW-NOTES.md` is rendered to `apps/ios/build/app-review/APP-REVIEW-NOTES.pdf` and uploaded as the App Review attachment when metadata is uploaded.
- Release notes resolve from `## <release version>` first, then fall back to `## Unreleased` while an App Store Connect build train is still in progress.
- When starting a new production release train, sync metadata with `pnpm ios:version:sync -- --version <release-version>`.
- When starting a new production release train, validate metadata with `pnpm ios:version:check -- --version <release-version>`.
- The release upload flow uploads release notes, screenshots, and the App Review PDF attachment before the IPA, and never submits for App Review.
- `privacy_url.txt` is set to `https://openclaw.ai/privacy`.
- If app lookup fails in `deliver`, set one of:

View File

@@ -1,3 +0,0 @@
Maintenance update for the current OpenClaw release.
- Refreshed iOS 26 visual styling, Talk controls, Gateway recovery, localization, and App Store screenshots.

View File

@@ -1,3 +0,0 @@
OpenClaw finns nu för iPhone.
Anslut till din OpenClaw Gateway för att chatta med din assistent, använda Talk-läge i realtid, granska godkännanden, dela innehåll från iOS och använda enhetsfunktioner som kamera, plats, skärm och aviseringar i dina privata automatiseringar.

View File

@@ -1623,9 +1623,10 @@
"gen:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --write",
"ghsa:patch": "node scripts/ghsa-patch.mjs",
"ios:app-review-notes:pdf": "xcrun swift scripts/ios-app-review-notes-pdf.swift apps/ios/APP-REVIEW-NOTES.md apps/ios/build/app-review/APP-REVIEW-NOTES.pdf",
"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:build": "bash -lc './scripts/ios-configure-signing.sh && ./scripts/ios-write-version-xcconfig.sh && node scripts/ios-write-swift-filelist.mjs && cd apps/ios && xcodegen generate && xcodebuild -project OpenClaw.xcodeproj -scheme OpenClaw -destination \"${IOS_DEST:-platform=iOS Simulator,name=iPhone 17}\" -configuration Debug build'",
"ios:filelist:gen": "node scripts/ios-write-swift-filelist.mjs",
"ios:gen": "bash -lc './scripts/ios-configure-signing.sh && ./scripts/ios-write-version-xcconfig.sh && node scripts/ios-write-swift-filelist.mjs && cd apps/ios && xcodegen generate'",
"ios:open": "bash -lc './scripts/ios-configure-signing.sh && ./scripts/ios-write-version-xcconfig.sh && node scripts/ios-write-swift-filelist.mjs && cd apps/ios && xcodegen generate && open OpenClaw.xcodeproj'",
"ios:release:archive": "bash scripts/ios-release-archive.sh",
"ios:release:prepare": "bash scripts/ios-release-prepare.sh",
"ios:release:signing:check": "bash -lc 'source ./scripts/lib/ios-fastlane.sh && cd apps/ios && run_ios_fastlane ios signing_check'",

View File

@@ -35,8 +35,6 @@ export const RELEASE_METADATA_PATHS = new Set([
"apps/android/fastlane/metadata/android/en-US/release_notes.txt",
"apps/android/version.json",
"apps/ios/CHANGELOG.md",
"apps/ios/Config/Version.xcconfig",
"apps/ios/fastlane/metadata/en-US/release_notes.txt",
"apps/macos/Sources/OpenClaw/Resources/Info.plist",
"docs/.generated/config-baseline.sha256",
"docs/install/updating.md",

View File

@@ -8,7 +8,6 @@ import { RELEASE_METADATA_PATHS } from "./changed-lanes.mjs";
const VERSION_ONLY_TEXT_PATHS = new Set([
"apps/android/Config/Version.properties",
"apps/android/version.json",
"apps/ios/Config/Version.xcconfig",
"apps/macos/Sources/OpenClaw/Resources/Info.plist",
]);

View File

@@ -150,6 +150,7 @@ fi
(
bash "${VERSION_HELPER}" --version "${IOS_VERSION}" --build-number "${BUILD_NUMBER}"
)
node "${ROOT_DIR}/scripts/ios-write-swift-filelist.mjs"
write_generated_file "${RELEASE_XCCONFIG}" <<EOF
// Auto-generated by scripts/ios-release-prepare.sh.

View File

@@ -110,6 +110,7 @@ fi
"${ROOT_DIR}/scripts/ios-configure-signing.sh"
"${ROOT_DIR}/scripts/ios-write-version-xcconfig.sh"
node "${ROOT_DIR}/scripts/ios-write-swift-filelist.mjs"
cd "${IOS_DIR}"
"${XCODEGEN_BIN}" generate

View File

@@ -7,7 +7,7 @@ export { parseVersionSyncArgs as parseArgs } from "./lib/version-script-args.ts"
function printUsage(): void {
process.stdout.write(
"Usage: node --import tsx scripts/ios-sync-versioning.ts [--write|--check] [--version YYYY.M.D] [--root dir]\n",
"Usage: node --import tsx scripts/ios-sync-versioning.ts [--write|--check] [--version YYYY.M.D] [--root dir]\n\nValidates that iOS versioning inputs can produce generated local artifacts.\n",
);
}
@@ -25,9 +25,11 @@ function main(argv = process.argv.slice(2)): number {
});
if (options.mode === "check") {
process.stdout.write("iOS versioning artifacts are up to date.\n");
process.stdout.write("iOS versioning inputs are valid.\n");
} else if (result.updatedPaths.length === 0) {
process.stdout.write("iOS versioning artifacts already up to date.\n");
process.stdout.write(
"iOS versioning inputs are valid; local artifacts are generated by iOS prep commands.\n",
);
} else {
process.stdout.write(
`Updated iOS versioning artifacts:\n- ${result.updatedPaths.map((filePath) => path.relative(process.cwd(), filePath)).join("\n- ")}\n`,

View File

@@ -1,5 +1,5 @@
// Ios Version script supports OpenClaw repository automation.
import { resolveIosVersion } from "./lib/ios-version.ts";
import { renderIosReleaseNotesForVersion, resolveIosVersion } from "./lib/ios-version.ts";
import { parseVersionQueryArgs } from "./lib/version-script-args.ts";
function printUsage(): void {
@@ -18,6 +18,16 @@ function main(argv = process.argv.slice(2)): number {
const version = resolveIosVersion(options.rootDir, { releaseVersion: options.releaseVersion });
if (options.field) {
if (options.field === "releaseNotes") {
process.stdout.write(
renderIosReleaseNotesForVersion({
releaseVersion: options.releaseVersion,
rootDir: options.rootDir,
}),
);
return 0;
}
const value = version[options.field as keyof typeof version];
if (value === undefined) {
throw new Error(`Unknown iOS version field '${options.field}'.`);

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env node
import { existsSync, lstatSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
import path from "node:path";
const repoRoot = path.resolve(import.meta.dirname, "..");
const iosRoot = path.join(repoRoot, "apps", "ios");
const outputPath = path.join(iosRoot, "SwiftSources.input.xcfilelist");
const iosSourceRoots = [
"Sources",
"ShareExtension",
"ActivityWidget",
path.join("WatchApp", "Sources"),
];
const sharedSwiftFiles = [
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatPayloadDecoding.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatSheets.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatTheme.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatTransport.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Attachments.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+SessionKeys.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/AnyCodable.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/BonjourEscapes.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/BonjourTypes.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/BridgeFrames.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/CameraCommands.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/CanvasA2UIAction.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/CanvasA2UICommands.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/CanvasA2UIJSONL.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/CanvasCommandParams.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/CanvasCommands.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/Capabilities.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/DeepLinks.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/JPEGTranscoder.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/NodeError.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/OpenClawKitResources.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/ScreenCommands.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/StoragePaths.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/SystemCommands.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/TalkDirective.swift",
"../swabble/Sources/SwabbleKit/WakeWordGate.swift",
];
function normalizeFileListPath(filePath) {
return filePath.split(path.sep).join("/");
}
function collectSwiftFiles(rootRelativePath) {
const root = path.join(iosRoot, rootRelativePath);
if (!existsSync(root)) {
throw new Error(`Missing iOS Swift source root: ${rootRelativePath}`);
}
const entries = [];
const visit = (dir) => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
visit(fullPath);
} else if (entry.isFile() && entry.name.endsWith(".swift")) {
entries.push(normalizeFileListPath(path.relative(iosRoot, fullPath)));
}
}
};
visit(root);
return entries;
}
function assertSharedFilesExist(filePaths) {
for (const filePath of filePaths) {
const absolutePath = path.resolve(iosRoot, filePath);
if (!existsSync(absolutePath)) {
throw new Error(`Missing shared Swift file listed for iOS lint: ${filePath}`);
}
}
}
function writeGeneratedFile(filePath, contents) {
if (existsSync(filePath) && lstatSync(filePath).isSymbolicLink()) {
throw new Error(`Refusing to overwrite symlinked file: ${filePath}`);
}
mkdirSync(path.dirname(filePath), { recursive: true });
writeFileSync(filePath, contents, "utf8");
}
assertSharedFilesExist(sharedSwiftFiles);
const iosFiles = iosSourceRoots.flatMap(collectSwiftFiles);
const fileList = [...new Set([...iosFiles, ...sharedSwiftFiles])].toSorted((left, right) =>
left.localeCompare(right),
);
writeGeneratedFile(outputPath, `${fileList.join("\n")}\n`);
process.stdout.write(`Prepared iOS Swift file list: ${path.relative(repoRoot, outputPath)}\n`);

View File

@@ -1,29 +1,21 @@
// Ios Version script supports OpenClaw repository automation.
import { readFileSync, writeFileSync } from "node:fs";
import { readFileSync } from "node:fs";
import path from "node:path";
import { parseReleaseVersion } from "./npm-publish-plan.mjs";
const IOS_CHANGELOG_FILE = "apps/ios/CHANGELOG.md";
const IOS_VERSION_XCCONFIG_FILE = "apps/ios/Config/Version.xcconfig";
const IOS_RELEASE_NOTES_FILE = "apps/ios/fastlane/metadata/en-US/release_notes.txt";
type ResolvedIosVersion = {
canonicalVersion: string;
marketingVersion: string;
buildVersion: string;
changelogPath: string;
versionXcconfigPath: string;
releaseNotesPath: string;
versionSource: "explicit" | "package";
versionSourcePath: string | null;
};
type SyncIosVersioningMode = "check" | "write";
function normalizeTrailingNewline(value: string): string {
return value.endsWith("\n") ? value : `${value}\n`;
}
function parsePinnedReleaseVersion(rawVersion: string): string | null {
const parsed = parseReleaseVersion(rawVersion.trim());
if (!parsed || parsed.version !== parsed.baseVersion) {
@@ -92,8 +84,6 @@ export function resolveIosVersion(
options?: { releaseVersion?: string | null },
): ResolvedIosVersion {
const changelogPath = path.join(rootDir, IOS_CHANGELOG_FILE);
const versionXcconfigPath = path.join(rootDir, IOS_VERSION_XCCONFIG_FILE);
const releaseNotesPath = path.join(rootDir, IOS_RELEASE_NOTES_FILE);
const explicitReleaseVersion = options?.releaseVersion?.trim() ?? "";
const canonicalVersion = explicitReleaseVersion
? normalizePinnedIosVersion(explicitReleaseVersion)
@@ -104,17 +94,11 @@ export function resolveIosVersion(
marketingVersion: canonicalVersion,
buildVersion: "1",
changelogPath,
versionXcconfigPath,
releaseNotesPath,
versionSource: explicitReleaseVersion ? "explicit" : "package",
versionSourcePath: explicitReleaseVersion ? null : rootPackageJsonPath(rootDir),
};
}
export function renderIosVersionXcconfig(version: ResolvedIosVersion): string {
return `// Shared iOS version defaults.\n// Source of truth: package.json or explicit release --version.\n// Generated by scripts/ios-sync-versioning.ts.\n\nOPENCLAW_IOS_VERSION = ${version.canonicalVersion}\nOPENCLAW_MARKETING_VERSION = ${version.marketingVersion}\nOPENCLAW_BUILD_VERSION = ${version.buildVersion}\n\n#include? "../build/Version.xcconfig"\n`;
}
function matchChangelogHeading(line: string, heading: string): boolean {
const normalized = line.trim();
return normalized === `## ${heading}` || normalized.startsWith(`## ${heading} - `);
@@ -160,26 +144,6 @@ export function renderIosReleaseNotes(
);
}
function syncFile(params: {
mode: SyncIosVersioningMode;
path: string;
nextContent: string;
label: string;
}): boolean {
const nextContent = normalizeTrailingNewline(params.nextContent);
const currentContent = readFileSync(params.path, "utf8");
if (currentContent === nextContent) {
return false;
}
if (params.mode === "check") {
throw new Error(`${params.label} is stale: ${path.relative(process.cwd(), params.path)}`);
}
writeFileSync(params.path, nextContent, "utf8");
return true;
}
export function syncIosVersioning(params?: {
mode?: SyncIosVersioningMode;
releaseVersion?: string | null;
@@ -187,36 +151,21 @@ export function syncIosVersioning(params?: {
}): {
updatedPaths: string[];
} {
const mode = params?.mode ?? "write";
const rootDir = path.resolve(params?.rootDir ?? ".");
const releaseVersion = params?.releaseVersion;
const version = resolveIosVersion(rootDir, { releaseVersion });
const changelogContent = readFileSync(version.changelogPath, "utf8");
const nextVersionXcconfig = renderIosVersionXcconfig(version);
const nextReleaseNotes = renderIosReleaseNotes(version, changelogContent);
const updatedPaths: string[] = [];
renderIosReleaseNotes(version, changelogContent);
if (
syncFile({
mode,
path: version.versionXcconfigPath,
nextContent: nextVersionXcconfig,
label: "iOS version xcconfig",
})
) {
updatedPaths.push(version.versionXcconfigPath);
}
if (
syncFile({
mode,
path: version.releaseNotesPath,
nextContent: nextReleaseNotes,
label: "iOS release notes",
})
) {
updatedPaths.push(version.releaseNotesPath);
}
return { updatedPaths };
return { updatedPaths: [] };
}
export function renderIosReleaseNotesForVersion(params?: {
releaseVersion?: string | null;
rootDir?: string;
}): string {
const rootDir = path.resolve(params?.rootDir ?? ".");
const version = resolveIosVersion(rootDir, { releaseVersion: params?.releaseVersion });
const changelogContent = readFileSync(version.changelogPath, "utf8");
return renderIosReleaseNotes(version, changelogContent);
}

View File

@@ -1328,8 +1328,6 @@ describe("scripts/changed-lanes", () => {
"apps/android/fastlane/metadata/android/en-US/release_notes.txt",
"apps/android/version.json",
"apps/ios/CHANGELOG.md",
"apps/ios/Config/Version.xcconfig",
"apps/ios/fastlane/metadata/en-US/release_notes.txt",
"apps/macos/Sources/OpenClaw/Resources/Info.plist",
"docs/.generated/config-baseline.sha256",
"package.json",

View File

@@ -10,13 +10,13 @@ describe("check-release-metadata-only", () => {
"--head",
"HEAD",
"./package.json",
"apps\\ios\\Config\\Version.xcconfig",
"apps\\ios\\CHANGELOG.md",
]),
).toEqual({
staged: false,
base: "origin/release",
head: "HEAD",
paths: ["package.json", "apps/ios/Config/Version.xcconfig"],
paths: ["package.json", "apps/ios/CHANGELOG.md"],
});
});

View File

@@ -16,30 +16,15 @@ export function writeIosFixture(params: {
version?: string;
changelog: string;
packageVersion?: string;
releaseNotes?: string;
versionXcconfig?: string;
prefix?: string;
}): string {
const rootDir = makeTempDir(tempDirs, params.prefix ?? "openclaw-ios-version-");
fs.mkdirSync(path.join(rootDir, "apps", "ios", "Config"), { recursive: true });
fs.mkdirSync(path.join(rootDir, "apps", "ios", "fastlane", "metadata", "en-US"), {
recursive: true,
});
fs.mkdirSync(path.join(rootDir, "apps", "ios"), { recursive: true });
fs.writeFileSync(
path.join(rootDir, "package.json"),
`${JSON.stringify({ version: params.packageVersion ?? params.version ?? "2026.4.6" }, null, 2)}\n`,
"utf8",
);
fs.writeFileSync(path.join(rootDir, "apps", "ios", "CHANGELOG.md"), params.changelog, "utf8");
fs.writeFileSync(
path.join(rootDir, "apps", "ios", "Config", "Version.xcconfig"),
params.versionXcconfig ?? "",
"utf8",
);
fs.writeFileSync(
path.join(rootDir, "apps", "ios", "fastlane", "metadata", "en-US", "release_notes.txt"),
params.releaseNotes ?? "",
"utf8",
);
return rootDir;
}

View File

@@ -8,7 +8,6 @@ import {
normalizeGatewayVersionToPinnedIosVersion,
normalizePinnedIosVersion,
renderIosReleaseNotes,
renderIosVersionXcconfig,
resolveGatewayVersionForIosRelease,
resolveIosVersion,
} from "../../scripts/lib/ios-version.ts";
@@ -99,6 +98,35 @@ describe("resolveIosVersion", () => {
expect(result.stderr).toBe("");
});
it("prints derived release notes from the CLI", () => {
const rootDir = writeIosFixture({
packageVersion: "2026.4.6",
changelog: "# OpenClaw iOS Changelog\n\n## 2026.4.7\n\nGenerated notes.\n",
});
const result = spawnSync(
process.execPath,
[
"--import",
"tsx",
"scripts/ios-version.ts",
"--root",
rootDir,
"--version",
"2026.4.7",
"--field",
"releaseNotes",
],
{
cwd: process.cwd(),
encoding: "utf8",
},
);
expect(result.status).toBe(0);
expect(result.stdout).toBe("Generated notes.\n");
expect(result.stderr).toBe("");
});
it("rejects missing iOS sync CLI root values before reading version files", () => {
const result = spawnSync(
process.execPath,
@@ -136,10 +164,8 @@ describe("resolveIosVersion", () => {
canonicalVersion: "2026.4.6",
changelogPath: path.join(rootDir, "apps/ios/CHANGELOG.md"),
marketingVersion: "2026.4.6",
releaseNotesPath: path.join(rootDir, "apps/ios/fastlane/metadata/en-US/release_notes.txt"),
versionSource: "package",
versionSourcePath: path.join(rootDir, "package.json"),
versionXcconfigPath: path.join(rootDir, "apps/ios/Config/Version.xcconfig"),
});
});
@@ -212,20 +238,6 @@ describe("gateway version normalization", () => {
});
});
describe("renderIosVersionXcconfig", () => {
it("renders checked-in defaults from the package-derived iOS version", () => {
const rootDir = writeIosFixture({
packageVersion: "2026.4.8",
changelog: "# OpenClaw iOS Changelog\n\n## 2026.4.8\n\nNotes.\n",
});
const version = resolveIosVersion(rootDir);
expect(renderIosVersionXcconfig(version)).toContain("OPENCLAW_IOS_VERSION = 2026.4.8");
expect(renderIosVersionXcconfig(version)).toContain("OPENCLAW_MARKETING_VERSION = 2026.4.8");
expect(renderIosVersionXcconfig(version)).toContain("OPENCLAW_BUILD_VERSION = 1");
});
});
describe("release note extraction", () => {
it("extracts exact pinned version sections first", () => {
const rootDir = writeIosFixture({