mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 20:40:43 +00:00
feat(ios): add local beta release flow
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
// Shared iOS signing defaults for local development + CI.
|
||||
OPENCLAW_IOS_DEFAULT_TEAM = Y5PE65HELJ
|
||||
OPENCLAW_IOS_SELECTED_TEAM = $(OPENCLAW_IOS_DEFAULT_TEAM)
|
||||
OPENCLAW_APP_BUNDLE_ID = ai.openclaw.ios
|
||||
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.ios.watchkitapp
|
||||
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.ios.watchkitapp.extension
|
||||
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclaw.ios.activitywidget
|
||||
OPENCLAW_APP_BUNDLE_ID = ai.openclaw.client
|
||||
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.client.watchkitapp
|
||||
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.client.watchkitapp.extension
|
||||
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclaw.client.activitywidget
|
||||
|
||||
// Local contributors can override this by running scripts/ios-configure-signing.sh.
|
||||
// Keep include after defaults: xcconfig is evaluated top-to-bottom.
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
# OpenClaw iOS (Super Alpha)
|
||||
|
||||
NO TEST FLIGHT AVAILABLE AT THIS POINT
|
||||
|
||||
This iPhone app is super-alpha and internal-use only. It connects to an OpenClaw Gateway as a `role: node`.
|
||||
|
||||
## Distribution Status
|
||||
|
||||
NO TEST FLIGHT AVAILABLE AT THIS POINT
|
||||
|
||||
- Current distribution: local/manual deploy from source via Xcode.
|
||||
- App Store flow is not part of the current internal development path.
|
||||
- Public distribution: not available.
|
||||
- Internal beta distribution: local archive + TestFlight upload via Fastlane.
|
||||
- Local/manual deploy from source via Xcode remains the default development path.
|
||||
|
||||
## Super-Alpha Disclaimer
|
||||
|
||||
@@ -50,6 +47,44 @@ Shortcut command (same flow + open project):
|
||||
pnpm ios:open
|
||||
```
|
||||
|
||||
## Local Beta Release Flow
|
||||
|
||||
Prereqs:
|
||||
|
||||
- Xcode 16+
|
||||
- `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`
|
||||
|
||||
Release behavior:
|
||||
|
||||
- Local development keeps using unique per-developer bundle IDs from `scripts/ios-configure-signing.sh`.
|
||||
- Beta release uses canonical `ai.openclaw.client*` bundle IDs through a temporary generated xcconfig in `apps/ios/build/BetaRelease.xcconfig`.
|
||||
- The beta flow does not modify `apps/ios/.local-signing.xcconfig` or `apps/ios/LocalSigning.xcconfig`.
|
||||
- Version input `2026.3.9-beta.1` becomes:
|
||||
- `CFBundleShortVersionString = 2026.3.9`
|
||||
- `CFBundleVersion = next TestFlight build number for 2026.3.9`
|
||||
|
||||
Archive without upload:
|
||||
|
||||
```bash
|
||||
pnpm ios:beta:archive -- --version 2026.3.9-beta.1
|
||||
```
|
||||
|
||||
Archive and upload to TestFlight:
|
||||
|
||||
```bash
|
||||
pnpm ios:beta -- --version 2026.3.9-beta.1
|
||||
```
|
||||
|
||||
If you need to force a specific build number:
|
||||
|
||||
```bash
|
||||
pnpm ios:beta -- --version 2026.3.9-beta.1 --build-number 7
|
||||
```
|
||||
|
||||
## APNs Expectations For Local/Manual Builds
|
||||
|
||||
- The app calls `registerForRemoteNotifications()` at launch.
|
||||
|
||||
@@ -2255,8 +2255,7 @@ extension NodeAppModel {
|
||||
from: payload)
|
||||
guard !decoded.actions.isEmpty else { return }
|
||||
self.pendingActionLogger.info(
|
||||
"Pending actions pulled trigger=\(trigger, privacy: .public) "
|
||||
+ "count=\(decoded.actions.count, privacy: .public)")
|
||||
"Pending actions pulled trigger=\(trigger, privacy: .public) count=\(decoded.actions.count, privacy: .public)")
|
||||
await self.applyPendingForegroundNodeActions(decoded.actions, trigger: trigger)
|
||||
} catch {
|
||||
// Best-effort only.
|
||||
@@ -2279,9 +2278,7 @@ extension NodeAppModel {
|
||||
paramsJSON: action.paramsJSON)
|
||||
let result = await self.handleInvoke(req)
|
||||
self.pendingActionLogger.info(
|
||||
"Pending action replay trigger=\(trigger, privacy: .public) "
|
||||
+ "id=\(action.id, privacy: .public) command=\(action.command, privacy: .public) "
|
||||
+ "ok=\(result.ok, privacy: .public)")
|
||||
"Pending action replay trigger=\(trigger, privacy: .public) id=\(action.id, privacy: .public) command=\(action.command, privacy: .public) ok=\(result.ok, privacy: .public)")
|
||||
guard result.ok else { return }
|
||||
let acked = await self.ackPendingForegroundNodeAction(
|
||||
id: action.id,
|
||||
@@ -2306,9 +2303,7 @@ extension NodeAppModel {
|
||||
return true
|
||||
} catch {
|
||||
self.pendingActionLogger.error(
|
||||
"Pending action ack failed trigger=\(trigger, privacy: .public) "
|
||||
+ "id=\(id, privacy: .public) command=\(command, privacy: .public) "
|
||||
+ "error=\(String(describing: error), privacy: .public)")
|
||||
"Pending action ack failed trigger=\(trigger, privacy: .public) id=\(id, privacy: .public) command=\(command, privacy: .public) error=\(String(describing: error), privacy: .public)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ require "open3"
|
||||
|
||||
default_platform(:ios)
|
||||
|
||||
BETA_APP_IDENTIFIER = "ai.openclaw.client"
|
||||
|
||||
def load_env_file(path)
|
||||
return unless File.exist?(path)
|
||||
|
||||
@@ -84,6 +86,96 @@ def read_asc_key_content_from_keychain
|
||||
end
|
||||
end
|
||||
|
||||
def repo_root
|
||||
File.expand_path("../../..", __dir__)
|
||||
end
|
||||
|
||||
def ios_root
|
||||
File.expand_path("..", __dir__)
|
||||
end
|
||||
|
||||
def normalize_beta_version(raw_value)
|
||||
version = raw_value.to_s.strip.sub(/\Av/, "")
|
||||
UI.user_error!("Missing IOS_BETA_VERSION. Example: IOS_BETA_VERSION=2026.3.9-beta.1 fastlane ios beta") unless env_present?(version)
|
||||
unless version.match?(/\A\d+\.\d+\.\d+(?:[.-]?beta[.-]\d+)?\z/i)
|
||||
UI.user_error!("Invalid IOS_BETA_VERSION '#{raw_value}'. Expected 2026.3.9 or 2026.3.9-beta.1.")
|
||||
end
|
||||
|
||||
version
|
||||
end
|
||||
|
||||
def short_beta_version(version)
|
||||
normalize_beta_version(version).sub(/([.-]?beta[.-]\d+)\z/i, "")
|
||||
end
|
||||
|
||||
def shell_join(parts)
|
||||
Shellwords.join(parts.compact)
|
||||
end
|
||||
|
||||
def resolve_beta_build_number(api_key:, version:)
|
||||
explicit = ENV["IOS_BETA_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}.")
|
||||
return explicit
|
||||
end
|
||||
|
||||
short_version = short_beta_version(version)
|
||||
latest_build = latest_testflight_build_number(
|
||||
api_key: api_key,
|
||||
app_identifier: BETA_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}).")
|
||||
next_build.to_s
|
||||
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}).")
|
||||
sh(shell_join(["bash", script_path, "--version", version, "--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)
|
||||
|
||||
ENV["XCODE_XCCONFIG_FILE"] = beta_xcconfig
|
||||
beta_xcconfig
|
||||
end
|
||||
|
||||
def build_beta_release(context)
|
||||
version = context[:version]
|
||||
output_directory = File.join("build", "beta")
|
||||
archive_path = File.join(output_directory, "OpenClaw-#{version}.xcarchive")
|
||||
|
||||
build_app(
|
||||
project: "OpenClaw.xcodeproj",
|
||||
scheme: "OpenClaw",
|
||||
configuration: "Release",
|
||||
export_method: "app-store",
|
||||
clean: true,
|
||||
skip_profile_detection: true,
|
||||
build_path: "build",
|
||||
archive_path: archive_path,
|
||||
output_directory: output_directory,
|
||||
output_name: "OpenClaw-#{version}.ipa",
|
||||
xcargs: "-allowProvisioningUpdates",
|
||||
export_xcargs: "-allowProvisioningUpdates",
|
||||
export_options: {
|
||||
signingStyle: "automatic"
|
||||
}
|
||||
)
|
||||
|
||||
{
|
||||
archive_path: archive_path,
|
||||
build_number: context[:build_number],
|
||||
ipa_path: lane_context[SharedValues::IPA_OUTPUT_PATH],
|
||||
short_version: context[:short_version],
|
||||
version: version
|
||||
}
|
||||
end
|
||||
|
||||
platform :ios do
|
||||
private_lane :asc_api_key do
|
||||
load_env_file(File.join(__dir__, ".env"))
|
||||
@@ -132,38 +224,46 @@ platform :ios do
|
||||
api_key
|
||||
end
|
||||
|
||||
desc "Build + upload to TestFlight"
|
||||
lane :beta do
|
||||
private_lane :prepare_beta_context do
|
||||
api_key = asc_api_key
|
||||
version = normalize_beta_version(ENV["IOS_BETA_VERSION"])
|
||||
build_number = resolve_beta_build_number(api_key: api_key, version: version)
|
||||
beta_xcconfig = prepare_beta_release!(version: version, build_number: build_number)
|
||||
|
||||
team_id = ENV["IOS_DEVELOPMENT_TEAM"]
|
||||
if team_id.nil? || team_id.strip.empty?
|
||||
helper_path = File.expand_path("../../../scripts/ios-team-id.sh", __dir__)
|
||||
if File.exist?(helper_path)
|
||||
# Keep CI/local compatibility where teams are present in keychain but not Xcode account metadata.
|
||||
team_id = sh("IOS_ALLOW_KEYCHAIN_TEAM_FALLBACK=1 bash #{helper_path.shellescape}").strip
|
||||
end
|
||||
end
|
||||
UI.user_error!("Missing IOS_DEVELOPMENT_TEAM (Apple Team ID). Add it to fastlane/.env or export it in your shell.") if team_id.nil? || team_id.strip.empty?
|
||||
{
|
||||
api_key: api_key,
|
||||
beta_xcconfig: beta_xcconfig,
|
||||
build_number: build_number,
|
||||
short_version: short_beta_version(version),
|
||||
version: version
|
||||
}
|
||||
end
|
||||
|
||||
build_app(
|
||||
project: "OpenClaw.xcodeproj",
|
||||
scheme: "OpenClaw",
|
||||
export_method: "app-store",
|
||||
clean: true,
|
||||
skip_profile_detection: true,
|
||||
xcargs: "DEVELOPMENT_TEAM=#{team_id} -allowProvisioningUpdates",
|
||||
export_xcargs: "-allowProvisioningUpdates",
|
||||
export_options: {
|
||||
signingStyle: "automatic"
|
||||
}
|
||||
)
|
||||
desc "Build a beta archive locally without uploading"
|
||||
lane :beta_archive do
|
||||
context = prepare_beta_context
|
||||
build = build_beta_release(context)
|
||||
UI.success("Built iOS beta 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
|
||||
build = build_beta_release(context)
|
||||
|
||||
upload_to_testflight(
|
||||
api_key: api_key,
|
||||
api_key: context[:api_key],
|
||||
ipa: build[:ipa_path],
|
||||
skip_waiting_for_build_processing: true,
|
||||
uses_non_exempt_encryption: false
|
||||
)
|
||||
|
||||
UI.success("Uploaded iOS beta: version=#{build[:version]} short=#{build[:short_version]} build=#{build[:build_number]}")
|
||||
ensure
|
||||
ENV.delete("XCODE_XCCONFIG_FILE")
|
||||
end
|
||||
|
||||
desc "Upload App Store metadata (and optionally screenshots)"
|
||||
|
||||
@@ -32,9 +32,9 @@ ASC_KEYCHAIN_ACCOUNT=YOUR_MAC_USERNAME
|
||||
Optional app targeting variables (helpful if Fastlane cannot auto-resolve app by bundle):
|
||||
|
||||
```bash
|
||||
ASC_APP_IDENTIFIER=ai.openclaw.ios
|
||||
ASC_APP_IDENTIFIER=ai.openclaw.client
|
||||
# or
|
||||
ASC_APP_ID=6760218713
|
||||
ASC_APP_ID=YOUR_APP_STORE_CONNECT_APP_ID
|
||||
```
|
||||
|
||||
File-based fallback (CI/non-macOS):
|
||||
@@ -60,9 +60,29 @@ cd apps/ios
|
||||
fastlane ios auth_check
|
||||
```
|
||||
|
||||
Run:
|
||||
Archive locally without upload:
|
||||
|
||||
```bash
|
||||
pnpm ios:beta:archive -- --version 2026.3.9-beta.1
|
||||
```
|
||||
|
||||
Upload to TestFlight:
|
||||
|
||||
```bash
|
||||
pnpm ios:beta -- --version 2026.3.9-beta.1
|
||||
```
|
||||
|
||||
Direct Fastlane entry point:
|
||||
|
||||
```bash
|
||||
cd apps/ios
|
||||
fastlane beta
|
||||
IOS_BETA_VERSION=2026.3.9-beta.1 fastlane ios beta
|
||||
```
|
||||
|
||||
Versioning rules:
|
||||
|
||||
- Input release version uses CalVer beta format: `YYYY.M.D-beta.N`
|
||||
- Fastlane stamps `CFBundleShortVersionString` to `YYYY.M.D`
|
||||
- Fastlane resolves `CFBundleVersion` as the next integer TestFlight build number for that short version
|
||||
- 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
|
||||
|
||||
@@ -6,7 +6,7 @@ This directory is used by `fastlane deliver` for App Store Connect text metadata
|
||||
|
||||
```bash
|
||||
cd apps/ios
|
||||
ASC_APP_ID=6760218713 \
|
||||
ASC_APP_ID=YOUR_APP_STORE_CONNECT_APP_ID \
|
||||
DELIVER_METADATA=1 fastlane ios metadata
|
||||
```
|
||||
|
||||
|
||||
@@ -224,6 +224,7 @@ targets:
|
||||
Release: Config/Signing.xcconfig
|
||||
settings:
|
||||
base:
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||
ENABLE_APPINTENTS_METADATA: NO
|
||||
ENABLE_APP_INTENTS_METADATA_GENERATION: NO
|
||||
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_WATCH_APP_BUNDLE_ID)"
|
||||
|
||||
@@ -262,6 +262,9 @@
|
||||
"gateway:watch": "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 && 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 && cd apps/ios && xcodegen generate'",
|
||||
"ios:open": "bash -lc './scripts/ios-configure-signing.sh && cd apps/ios && xcodegen generate && open OpenClaw.xcodeproj'",
|
||||
|
||||
53
scripts/ios-beta-archive.sh
Executable file
53
scripts/ios-beta-archive.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/ios-beta-archive.sh --version 2026.3.9-beta.1 [--build-number 7]
|
||||
|
||||
Archives and exports a beta-release IPA locally without uploading.
|
||||
EOF
|
||||
}
|
||||
|
||||
VERSION="${IOS_BETA_VERSION:-}"
|
||||
BUILD_NUMBER="${IOS_BETA_BUILD_NUMBER:-}"
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--)
|
||||
shift
|
||||
;;
|
||||
--version)
|
||||
VERSION="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--build-number)
|
||||
BUILD_NUMBER="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${VERSION}" ]]; then
|
||||
echo "Missing required --version (or IOS_BETA_VERSION)." >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(
|
||||
cd "${ROOT_DIR}/apps/ios"
|
||||
IOS_BETA_VERSION="${VERSION}" \
|
||||
IOS_BETA_BUILD_NUMBER="${BUILD_NUMBER}" \
|
||||
fastlane ios beta_archive
|
||||
)
|
||||
94
scripts/ios-beta-prepare.sh
Executable file
94
scripts/ios-beta-prepare.sh
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/ios-beta-prepare.sh --version 2026.3.9-beta.1 --build-number 7 [--team-id TEAMID]
|
||||
|
||||
Prepares local beta-release inputs without touching local signing overrides:
|
||||
- stamps apps/ios/project.yml with the short version + build number
|
||||
- writes apps/ios/build/BetaRelease.xcconfig with canonical bundle IDs
|
||||
- regenerates apps/ios/OpenClaw.xcodeproj via xcodegen
|
||||
EOF
|
||||
}
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
IOS_DIR="${ROOT_DIR}/apps/ios"
|
||||
BETA_XCCONFIG="${IOS_DIR}/build/BetaRelease.xcconfig"
|
||||
TEAM_HELPER="${ROOT_DIR}/scripts/ios-team-id.sh"
|
||||
|
||||
VERSION=""
|
||||
BUILD_NUMBER=""
|
||||
TEAM_ID="${IOS_DEVELOPMENT_TEAM:-}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--)
|
||||
shift
|
||||
;;
|
||||
--version)
|
||||
VERSION="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--build-number)
|
||||
BUILD_NUMBER="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--team-id)
|
||||
TEAM_ID="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${VERSION}" || -z "${BUILD_NUMBER}" ]]; then
|
||||
echo "Missing required --version or --build-number." >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${TEAM_ID}" ]]; then
|
||||
TEAM_ID="$(IOS_ALLOW_KEYCHAIN_TEAM_FALLBACK=1 bash "${TEAM_HELPER}")"
|
||||
fi
|
||||
|
||||
mkdir -p "${IOS_DIR}/build"
|
||||
|
||||
(
|
||||
cd "${ROOT_DIR}"
|
||||
node --import tsx scripts/ios-sync-version.ts \
|
||||
--version "${VERSION}" \
|
||||
--build-number "${BUILD_NUMBER}"
|
||||
)
|
||||
|
||||
cat >"${BETA_XCCONFIG}" <<EOF
|
||||
// Auto-generated by scripts/ios-beta-prepare.sh.
|
||||
// Local beta-release override; do not commit.
|
||||
OPENCLAW_CODE_SIGN_STYLE = Automatic
|
||||
OPENCLAW_DEVELOPMENT_TEAM = ${TEAM_ID}
|
||||
OPENCLAW_IOS_SELECTED_TEAM = ${TEAM_ID}
|
||||
OPENCLAW_APP_BUNDLE_ID = ai.openclaw.client
|
||||
OPENCLAW_SHARE_BUNDLE_ID = ai.openclaw.client.share
|
||||
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclaw.client.activitywidget
|
||||
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.client.watchkitapp
|
||||
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.client.watchkitapp.extension
|
||||
OPENCLAW_APP_PROFILE =
|
||||
OPENCLAW_SHARE_PROFILE =
|
||||
EOF
|
||||
|
||||
(
|
||||
cd "${IOS_DIR}"
|
||||
xcodegen generate
|
||||
)
|
||||
|
||||
echo "Prepared iOS beta release: version=${VERSION} build=${BUILD_NUMBER} team=${TEAM_ID}"
|
||||
echo "XCODE_XCCONFIG_FILE=${BETA_XCCONFIG}"
|
||||
53
scripts/ios-beta-release.sh
Executable file
53
scripts/ios-beta-release.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/ios-beta-release.sh --version 2026.3.9-beta.1 [--build-number 7]
|
||||
|
||||
Archives and uploads a beta-release IPA to TestFlight locally.
|
||||
EOF
|
||||
}
|
||||
|
||||
VERSION="${IOS_BETA_VERSION:-}"
|
||||
BUILD_NUMBER="${IOS_BETA_BUILD_NUMBER:-}"
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--)
|
||||
shift
|
||||
;;
|
||||
--version)
|
||||
VERSION="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--build-number)
|
||||
BUILD_NUMBER="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${VERSION}" ]]; then
|
||||
echo "Missing required --version (or IOS_BETA_VERSION)." >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(
|
||||
cd "${ROOT_DIR}/apps/ios"
|
||||
IOS_BETA_VERSION="${VERSION}" \
|
||||
IOS_BETA_BUILD_NUMBER="${BUILD_NUMBER}" \
|
||||
fastlane ios beta
|
||||
)
|
||||
97
scripts/ios-sync-version.ts
Executable file
97
scripts/ios-sync-version.ts
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env -S node --import tsx
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
type CliOptions = {
|
||||
buildNumber: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
function parseArgs(argv: string[]): CliOptions {
|
||||
const options = new Map<string, string>();
|
||||
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (!arg.startsWith("--")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = arg.slice(2);
|
||||
const value = argv[i + 1];
|
||||
if (!value || value.startsWith("--")) {
|
||||
throw new Error(`Missing value for --${key}`);
|
||||
}
|
||||
options.set(key, value);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
const version = options.get("version")?.trim();
|
||||
const buildNumber = options.get("build-number")?.trim();
|
||||
|
||||
if (!version) {
|
||||
throw new Error("Missing required --version");
|
||||
}
|
||||
if (!buildNumber) {
|
||||
throw new Error("Missing required --build-number");
|
||||
}
|
||||
if (!/^[0-9]+$/.test(buildNumber)) {
|
||||
throw new Error(`Invalid --build-number '${buildNumber}'; expected digits only.`);
|
||||
}
|
||||
|
||||
return { buildNumber, version };
|
||||
}
|
||||
|
||||
function toShortVersion(input: string): string {
|
||||
const trimmed = input.trim().replace(/^v/, "");
|
||||
const shortVersion = trimmed.replace(/([.-]?beta[.-]\d+)$/i, "");
|
||||
if (!/^\d+\.\d+\.\d+$/.test(shortVersion)) {
|
||||
throw new Error(
|
||||
`Invalid --version '${input}'; expected CalVer like 2026.3.9 or 2026.3.9-beta.1.`,
|
||||
);
|
||||
}
|
||||
return shortVersion;
|
||||
}
|
||||
|
||||
function replaceAllExact(params: { content: string; pattern: RegExp; replacement: string }) {
|
||||
const { content, pattern, replacement } = params;
|
||||
const matches = [...content.matchAll(pattern)];
|
||||
if (matches.length === 0) {
|
||||
throw new Error(`Pattern not found: ${pattern}`);
|
||||
}
|
||||
return content.replace(pattern, replacement);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const shortVersion = toShortVersion(options.version);
|
||||
const projectPath = resolve("apps/ios/project.yml");
|
||||
const original = readFileSync(projectPath, "utf8");
|
||||
|
||||
let updated = original;
|
||||
updated = replaceAllExact({
|
||||
content: updated,
|
||||
pattern: /(CFBundleShortVersionString:\s*")[^"]+(")/g,
|
||||
replacement: `$1${shortVersion}$2`,
|
||||
});
|
||||
updated = replaceAllExact({
|
||||
content: updated,
|
||||
pattern: /(CFBundleVersion:\s*")[^"]+(")/g,
|
||||
replacement: `$1${options.buildNumber}$2`,
|
||||
});
|
||||
|
||||
if (updated === original) {
|
||||
console.log(`iOS version already set: short=${shortVersion} build=${options.buildNumber}`);
|
||||
return;
|
||||
}
|
||||
|
||||
writeFileSync(projectPath, updated);
|
||||
console.log(`Updated iOS project version: short=${shortVersion} build=${options.buildNumber}`);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(`ios-sync-version: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user