mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
Require explicit iOS release versions
This commit is contained in:
parent
c1eee1a41a
commit
080481fec9
33 changed files with 523 additions and 577 deletions
|
|
@ -5,6 +5,7 @@ Root rules still apply. This file adds the iOS release guardrails.
|
|||
## App Store Releases
|
||||
|
||||
- Agent-driven App Store uploads must use only `pnpm ios:release:upload`.
|
||||
- App Store uploads must include explicit release intent: `pnpm ios:release:upload -- --version <YYYY.M.D>` and `--build-number <n>` when a specific build has been chosen.
|
||||
- If `pnpm ios:release:upload` exits non-zero, stop immediately and report the failing step.
|
||||
- After a failed `pnpm ios:release:upload`, do not continue with `pnpm ios:release:archive`, `asc builds upload`, `asc release stage`, `asc publish appstore`, `asc review submit`, direct Fastlane lanes, or any manual App Store Connect mutation command.
|
||||
- Do not submit an iOS App Store version for App Review. App Review submission stays manual unless the user explicitly asks to submit a specific already-prepared version after the failed state has been reported.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
# OpenClaw iOS Changelog
|
||||
|
||||
## 2026.6.11 - 2026-07-01
|
||||
|
||||
Maintenance update for the current OpenClaw beta release.
|
||||
|
||||
- Refreshed iOS 26 visual styling, Talk controls, Gateway recovery, localization, and App Store screenshots.
|
||||
|
||||
## 2026.6.10 - 2026-06-21
|
||||
|
||||
Maintenance update for the current OpenClaw beta release.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// Shared iOS version defaults.
|
||||
// Source of truth: apps/ios/version.json
|
||||
// Source of truth: package.json or explicit release --version.
|
||||
// Generated by scripts/ios-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_IOS_VERSION = 2026.6.10
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.10
|
||||
OPENCLAW_IOS_VERSION = 2026.6.11
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.11
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
|
|
|||
|
|
@ -74,13 +74,13 @@ Release behavior:
|
|||
- The release archive is validated before upload by inspecting the exported IPA's signed entitlements, embedded App Store profile, and push mode. The upload fails if the IPA is not an App Store production relay build.
|
||||
- App Review submission is manual in App Store Connect. The release lane uploads a build, public metadata, and the App Review PDF attachment, but it does not submit for review or upload the App Store Connect `Notes` field.
|
||||
- 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.
|
||||
- Release uploads require an explicit CalVer version passed with `--version`.
|
||||
- `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:
|
||||
- The release version must use CalVer like `2026.4.10`.
|
||||
- That release value becomes:
|
||||
- `CFBundleShortVersionString = 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.
|
||||
- Local defaults derive from root `package.json`; App Store uploads use the explicit `--version` value.
|
||||
- See `apps/ios/VERSIONING.md` for the full workflow.
|
||||
|
||||
Relay behavior for App Store builds:
|
||||
|
|
@ -110,13 +110,13 @@ Release-owner secrets:
|
|||
Prepare the generated release xcconfig/project without archiving:
|
||||
|
||||
```bash
|
||||
pnpm ios:release:prepare -- --build-number 7
|
||||
pnpm ios:release:prepare -- --version 2026.6.11 --build-number 7
|
||||
```
|
||||
|
||||
Archive without upload:
|
||||
|
||||
```bash
|
||||
pnpm ios:release:archive
|
||||
pnpm ios:release:archive -- --version 2026.6.11
|
||||
```
|
||||
|
||||
This command is for local archive validation only. It is not a fallback upload
|
||||
|
|
@ -125,13 +125,13 @@ path after `pnpm ios:release:upload` fails.
|
|||
Archive and upload to App Store Connect:
|
||||
|
||||
```bash
|
||||
pnpm ios:release:upload
|
||||
pnpm ios:release:upload -- --version 2026.6.11
|
||||
```
|
||||
|
||||
If you need to force a specific build number:
|
||||
|
||||
```bash
|
||||
pnpm ios:release:upload -- --build-number 7
|
||||
pnpm ios:release:upload -- --version 2026.6.11 --build-number 7
|
||||
```
|
||||
|
||||
### Maintainer Quick Release Checklist
|
||||
|
|
@ -166,16 +166,16 @@ 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, pin iOS to the current gateway version first:
|
||||
4. If you are starting a brand-new production release train, add or update the matching iOS changelog section and sync generated metadata:
|
||||
|
||||
```bash
|
||||
pnpm ios:version:pin -- --from-gateway
|
||||
pnpm ios:version:sync -- --version 2026.6.11
|
||||
```
|
||||
|
||||
5. Upload the build:
|
||||
5. Upload the build with explicit release intent:
|
||||
|
||||
```bash
|
||||
pnpm ios:release:upload
|
||||
pnpm ios:release:upload -- --version 2026.6.11 --build-number 3
|
||||
```
|
||||
|
||||
6. If `pnpm ios:release:upload` fails, stop at that failure. Do not archive
|
||||
|
|
@ -183,8 +183,8 @@ pnpm ios:release:upload
|
|||
step, then rerun `pnpm ios:release:upload`.
|
||||
|
||||
7. Expected behavior:
|
||||
- Fastlane reads `apps/ios/version.json`
|
||||
- verifies synced iOS versioning artifacts
|
||||
- Fastlane reads the explicit `--version` value
|
||||
- verifies synced iOS versioning artifacts 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
|
||||
|
|
@ -203,7 +203,8 @@ pnpm ios:release:upload
|
|||
|
||||
## iOS Versioning Workflow
|
||||
|
||||
- Pinned iOS release version: `apps/ios/version.json`
|
||||
- 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`
|
||||
|
|
@ -213,33 +214,27 @@ pnpm ios:release:upload
|
|||
```bash
|
||||
pnpm ios:version
|
||||
pnpm ios:version:check
|
||||
pnpm ios:version:sync
|
||||
pnpm ios:version:pin -- --from-gateway
|
||||
pnpm ios:version:pin -- --version 2026.4.10
|
||||
pnpm ios:version -- --version 2026.6.11
|
||||
pnpm ios:version:sync -- --version 2026.6.11
|
||||
```
|
||||
|
||||
Recommended flow:
|
||||
|
||||
### TestFlight iteration on an existing train
|
||||
|
||||
1. Keep `apps/ios/version.json` pinned to the current train version.
|
||||
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` after changelog changes.
|
||||
4. Upload more TestFlight builds with `pnpm ios:release:upload`.
|
||||
3. Run `pnpm ios:version:sync -- --version 2026.6.11` after changelog changes.
|
||||
4. Upload more TestFlight builds with `pnpm ios:release:upload -- --version 2026.6.11`.
|
||||
5. Let Fastlane bump only the numeric build number.
|
||||
|
||||
### Starting the next production release train
|
||||
|
||||
1. Pin iOS to the current gateway version:
|
||||
|
||||
```bash
|
||||
pnpm ios:version:pin -- --from-gateway
|
||||
```
|
||||
|
||||
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`.
|
||||
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.
|
||||
3. Run `pnpm ios:version:sync -- --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.
|
||||
|
||||
See `apps/ios/VERSIONING.md` for the detailed spec.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,57 +1,62 @@
|
|||
# OpenClaw iOS Versioning
|
||||
|
||||
OpenClaw iOS uses a **pinned CalVer release version** instead of reading the current gateway version automatically on every build.
|
||||
OpenClaw iOS release uploads use an explicit CalVer release version. The
|
||||
committed repo no longer has an iOS-only version manifest; release commands must
|
||||
name the App Store train they are uploading to.
|
||||
|
||||
## Goals
|
||||
|
||||
- keep TestFlight submissions on one stable app version while iterating
|
||||
- change only `CFBundleVersion` during normal TestFlight iteration
|
||||
- promote the iOS release version to the current gateway version only when a maintainer chooses to do that
|
||||
- make App Store release intent explicit at upload time
|
||||
- avoid stale committed iOS pins
|
||||
- keep Apple bundle fields valid for App Store Connect
|
||||
- keep normal local builds aligned with the current gateway release version
|
||||
- generate App Store release notes from an iOS-owned changelog
|
||||
|
||||
## Version model
|
||||
|
||||
The pinned iOS release version lives in `apps/ios/version.json`.
|
||||
Release uploads require a version argument:
|
||||
|
||||
Supported pinned format:
|
||||
```bash
|
||||
pnpm ios:release:upload -- --version 2026.6.11
|
||||
```
|
||||
|
||||
- `YYYY.M.D`
|
||||
Use `--build-number` when the build number is known or has been verified from
|
||||
App Store Connect:
|
||||
|
||||
Examples:
|
||||
```bash
|
||||
pnpm ios:release:upload -- --version 2026.6.11 --build-number 3
|
||||
```
|
||||
|
||||
- `2026.4.6`
|
||||
- `2026.4.10`
|
||||
The release version must use `YYYY.M.D` CalVer, for example `2026.4.6` or
|
||||
`2026.6.11`.
|
||||
|
||||
The root gateway version in `package.json` may still be one of:
|
||||
When no explicit release version is supplied to the version helper, iOS derives
|
||||
its default version from root `package.json.version` after stripping supported
|
||||
release suffixes:
|
||||
|
||||
- `YYYY.M.D`
|
||||
- `YYYY.M.D-beta.N`
|
||||
- `YYYY.M.D-N`
|
||||
|
||||
When you pin iOS from the gateway version, the iOS tooling strips the gateway suffix and keeps only the base CalVer.
|
||||
|
||||
Examples:
|
||||
|
||||
- gateway `2026.4.10` -> iOS `2026.4.10`
|
||||
- gateway `2026.4.10-beta.3` -> iOS `2026.4.10`
|
||||
- gateway `2026.4.10-2` -> iOS `2026.4.10`
|
||||
- gateway `2026.4.10` -> iOS default `2026.4.10`
|
||||
- gateway `2026.4.10-beta.3` -> iOS default `2026.4.10`
|
||||
- gateway `2026.4.10-2` -> iOS default `2026.4.10`
|
||||
|
||||
## Apple bundle mapping
|
||||
|
||||
Pinned iOS version `2026.4.10` maps to:
|
||||
Release version `2026.6.11` maps to:
|
||||
|
||||
- `CFBundleShortVersionString = 2026.4.10`
|
||||
- `CFBundleShortVersionString = 2026.6.11`
|
||||
- `CFBundleVersion = numeric build number only`
|
||||
|
||||
`CFBundleShortVersionString` stays fixed for a TestFlight train until you intentionally pin a newer iOS release version.
|
||||
Fastlane can resolve the next build number by querying App Store Connect for the
|
||||
explicit short version. Maintainers may still pass `--build-number` to make the
|
||||
upload fully deterministic.
|
||||
|
||||
## Source of truth and generated files
|
||||
|
||||
### Source files
|
||||
|
||||
- `apps/ios/version.json`
|
||||
- pinned iOS release version
|
||||
- `package.json`
|
||||
- default iOS version source for local builds and checked-in defaults
|
||||
- explicit `--version`
|
||||
- release upload source of truth
|
||||
- `apps/ios/CHANGELOG.md`
|
||||
- iOS-only changelog and release-note source
|
||||
- `apps/ios/VERSIONING.md`
|
||||
|
|
@ -60,7 +65,7 @@ Pinned iOS version `2026.4.10` maps to:
|
|||
### Generated or derived files
|
||||
|
||||
- `apps/ios/Config/Version.xcconfig`
|
||||
- checked-in defaults derived from `apps/ios/version.json`
|
||||
- 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`
|
||||
|
|
@ -68,35 +73,25 @@ Pinned iOS version `2026.4.10` maps to:
|
|||
|
||||
## Tooling surfaces
|
||||
|
||||
### Version parsing and sync tooling
|
||||
|
||||
- `scripts/lib/ios-version.ts`
|
||||
- validates pinned iOS CalVer
|
||||
- normalizes gateway version -> pinned iOS CalVer
|
||||
- validates iOS CalVer
|
||||
- normalizes gateway version -> iOS CalVer
|
||||
- renders checked-in xcconfig and release notes
|
||||
- `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 pinned iOS version
|
||||
- `scripts/ios-pin-version.ts`
|
||||
- explicitly pins iOS to a chosen release version or the current gateway version
|
||||
|
||||
### Build and App Store release flow
|
||||
|
||||
- syncs checked-in derived files from the default or explicit iOS version
|
||||
- `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-release-prepare.sh`
|
||||
- prepares App Store distribution signing and bundle settings against the pinned iOS version
|
||||
- `scripts/ios-release-signing.mjs`
|
||||
- validates the checked-in App Store signing manifest
|
||||
- renders the temporary release xcconfig profile pins
|
||||
- requires `--version` and prepares App Store distribution signing and bundle settings
|
||||
- `apps/ios/fastlane/Fastfile`
|
||||
- resolves version metadata from the pinned iOS helper
|
||||
- resolves version metadata from the explicit release version
|
||||
- creates or verifies Developer Portal bundle IDs/services through Fastlane `produce`
|
||||
- syncs encrypted App Store signing assets with Fastlane `match`
|
||||
- increments App Store Connect build numbers for the pinned short version
|
||||
- uploads screenshots, release notes, and the rendered App Review PDF attachment before archiving a release build
|
||||
- resolves App Store Connect build numbers for the explicit short version when needed
|
||||
- uploads screenshots, release notes, and the rendered App Review PDF attachment before archiving
|
||||
|
||||
Agent-driven App Store uploads must use `pnpm ios:release:upload` as the only
|
||||
release path. If that command fails, stop at the failing screenshot, metadata,
|
||||
|
|
@ -107,34 +102,39 @@ 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 `apps/ios/fastlane/metadata/en-US/release_notes.txt`, the
|
||||
tooling reads the first available changelog section in this order:
|
||||
|
||||
1. exact pinned version, for example `## 2026.4.10`
|
||||
1. exact release version, for example `## 2026.6.11`
|
||||
2. `## Unreleased`
|
||||
|
||||
Recommended workflow:
|
||||
Before production upload, prefer a final `## <release version>` section and run
|
||||
sync with the same version:
|
||||
|
||||
- while iterating on a TestFlight train, keep pending notes under `## Unreleased`
|
||||
- before the production release, move or copy the final notes under `## <pinned version>` and run sync again
|
||||
```bash
|
||||
pnpm ios:version:sync -- --version 2026.6.11
|
||||
```
|
||||
|
||||
## Common commands
|
||||
|
||||
```bash
|
||||
pnpm ios:version
|
||||
pnpm ios:version -- --version 2026.6.11
|
||||
pnpm ios:version:check
|
||||
pnpm ios:version:sync
|
||||
pnpm ios:version:pin -- --from-gateway
|
||||
pnpm ios:version:pin -- --version 2026.4.10
|
||||
pnpm ios:version:sync -- --version 2026.6.11
|
||||
pnpm ios:release:upload -- --version 2026.6.11 --build-number 3
|
||||
```
|
||||
|
||||
## Normal TestFlight iteration workflow
|
||||
|
||||
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 App Store Connect builds with `pnpm ios:release:upload`
|
||||
4. let Fastlane increment only `CFBundleVersion`
|
||||
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>`
|
||||
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>`
|
||||
|
||||
This keeps the TestFlight version stable while review is in flight.
|
||||
This keeps the version decision at the release command instead of in a committed
|
||||
state file.
|
||||
|
||||
## Release SHA tracking
|
||||
|
||||
|
|
@ -148,7 +148,7 @@ refs/openclaw/mobile-releases/ios/<CFBundleShortVersionString>-<CFBundleVersion>
|
|||
Example:
|
||||
|
||||
```text
|
||||
refs/openclaw/mobile-releases/ios/2026.6.10-8
|
||||
refs/openclaw/mobile-releases/ios/2026.6.11-3
|
||||
```
|
||||
|
||||
These refs are intentionally outside `refs/tags/*` and `refs/heads/*`. They do
|
||||
|
|
@ -166,38 +166,47 @@ evidence, not a repair mechanism for a failed `pnpm ios:release:upload` run.
|
|||
Useful direct commands:
|
||||
|
||||
```bash
|
||||
pnpm mobile:release:preflight -- --platform ios --version 2026.6.10 --build 8
|
||||
pnpm mobile:release:resolve -- --platform ios --version 2026.6.10 --build 8
|
||||
pnpm mobile:release:preflight -- --platform ios --version 2026.6.11 --build 3
|
||||
pnpm mobile:release:resolve -- --platform ios --version 2026.6.11 --build 3
|
||||
```
|
||||
|
||||
## New release promotion workflow
|
||||
## New release workflow
|
||||
|
||||
When you want the next production iOS release to align with the current gateway release:
|
||||
When you want the next production iOS release to align with the current gateway
|
||||
release:
|
||||
|
||||
1. pin iOS from the root gateway version:
|
||||
1. confirm the root gateway version:
|
||||
|
||||
```bash
|
||||
pnpm ios:version:pin -- --from-gateway
|
||||
node -e "console.log(require('./package.json').version)"
|
||||
```
|
||||
|
||||
2. review the generated changes in:
|
||||
- `apps/ios/version.json`
|
||||
- `apps/ios/Config/Version.xcconfig`
|
||||
- `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. upload 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. manually submit the reviewed build for App Review in App Store Connect
|
||||
8. release the approved build to production
|
||||
2. update `apps/ios/CHANGELOG.md` for that release
|
||||
3. sync iOS generated files:
|
||||
|
||||
```bash
|
||||
pnpm ios:version:sync -- --version 2026.6.11
|
||||
```
|
||||
|
||||
4. verify live App Store Connect state and choose the next build number
|
||||
5. upload with explicit release intent:
|
||||
|
||||
```bash
|
||||
pnpm ios:release:upload -- --version 2026.6.11 --build-number 3
|
||||
```
|
||||
|
||||
6. manually submit the reviewed build for App Review in App Store Connect
|
||||
7. release the approved build to production
|
||||
|
||||
## Important invariant
|
||||
|
||||
Fastlane and Xcode should consume only the pinned iOS version from `apps/ios/version.json`.
|
||||
App Store uploads must carry explicit version intent. Do not infer a release
|
||||
train from checked-in generated files.
|
||||
|
||||
Changing `package.json.version` alone must not change the iOS app version until a maintainer explicitly runs the pin step.
|
||||
|
||||
App Review submission must remain manual. Automation may create/update the editable App Store version, upload screenshots, upload release notes, upload the App Review PDF attachment, and upload builds, but it should not upload the App Store Connect `Notes` field or submit a build for review.
|
||||
App Review submission remains manual. Automation may create/update the editable
|
||||
App Store version, upload screenshots, upload release notes, upload the App
|
||||
Review PDF attachment, and upload builds, but it should not upload the App
|
||||
Store Connect `Notes` field or submit a build for review.
|
||||
|
||||
For agent-driven releases, a failed `pnpm ios:release:upload` is terminal for
|
||||
that attempt. Agents must report the failed step and wait for maintainer
|
||||
|
|
|
|||
|
|
@ -521,6 +521,18 @@ def preserve_local_signing
|
|||
end
|
||||
end
|
||||
|
||||
def without_xcode_xcconfig_file
|
||||
existing = ENV["XCODE_XCCONFIG_FILE"]
|
||||
ENV.delete("XCODE_XCCONFIG_FILE")
|
||||
yield
|
||||
ensure
|
||||
if env_present?(existing)
|
||||
ENV["XCODE_XCCONFIG_FILE"] = existing
|
||||
else
|
||||
ENV.delete("XCODE_XCCONFIG_FILE")
|
||||
end
|
||||
end
|
||||
|
||||
def app_store_signing_manifest
|
||||
JSON.parse(File.read(File.join(ios_root, "Config", "AppStoreSigning.json")))
|
||||
end
|
||||
|
|
@ -728,7 +740,7 @@ end
|
|||
|
||||
def release_notes_metadata_path
|
||||
source = release_notes_path
|
||||
UI.user_error!("Missing release notes at #{source}. Run `pnpm ios:version:sync`.") unless File.exist?(source)
|
||||
UI.user_error!("Missing release notes at #{source}. Run `pnpm ios:version:sync -- --version <release-version>`.") unless File.exist?(source)
|
||||
|
||||
temp_root = Dir.mktmpdir("openclaw-release-notes")
|
||||
target_dir = File.join(temp_root, "en-US")
|
||||
|
|
@ -1034,14 +1046,18 @@ def upload_app_store_screenshots_deterministically!(app_identifier:, app_id:, sh
|
|||
UI.success("Uploaded and verified #{screenshots.length} App Store screenshots for #{short_version}.")
|
||||
end
|
||||
|
||||
def read_ios_version_metadata
|
||||
def read_ios_version_metadata(release_version: nil)
|
||||
script_path = File.join(repo_root, "scripts", "ios-version.ts")
|
||||
stdout, stderr, status = Open3.capture3(
|
||||
args = [
|
||||
"node",
|
||||
"--import",
|
||||
"tsx",
|
||||
script_path,
|
||||
"--json",
|
||||
]
|
||||
args.push("--version", release_version) if env_present?(release_version)
|
||||
stdout, stderr, status = Open3.capture3(
|
||||
*args,
|
||||
chdir: repo_root
|
||||
)
|
||||
|
||||
|
|
@ -1066,21 +1082,26 @@ rescue JSON::ParserError => e
|
|||
UI.user_error!("Invalid JSON from iOS version helper: #{e.message}")
|
||||
end
|
||||
|
||||
def sync_ios_versioning!
|
||||
def sync_ios_versioning!(release_version: nil)
|
||||
script_path = File.join(repo_root, "scripts", "ios-sync-versioning.ts")
|
||||
stdout, stderr, status = Open3.capture3(
|
||||
args = [
|
||||
"node",
|
||||
"--import",
|
||||
"tsx",
|
||||
script_path,
|
||||
"--check",
|
||||
]
|
||||
args.push("--version", release_version) if env_present?(release_version)
|
||||
stdout, stderr, status = Open3.capture3(
|
||||
*args,
|
||||
chdir: repo_root
|
||||
)
|
||||
return if status.success?
|
||||
|
||||
detail = stderr.to_s.strip
|
||||
detail = stdout.to_s.strip if detail.empty?
|
||||
UI.user_error!("iOS versioning artifacts are stale. Run `pnpm ios:version:sync`.\n#{detail}")
|
||||
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}")
|
||||
end
|
||||
|
||||
def shell_join(parts)
|
||||
|
|
@ -1092,8 +1113,8 @@ def xcodebuild_shell_join(parts)
|
|||
shell_join(["env", "PATH=#{xcode_path}", *parts])
|
||||
end
|
||||
|
||||
def resolve_release_build_number(api_key:, short_version:)
|
||||
explicit = ENV["IOS_RELEASE_BUILD_NUMBER"]
|
||||
def resolve_release_build_number(api_key:, short_version:, explicit_build_number: nil)
|
||||
explicit = explicit_build_number.to_s.strip
|
||||
if env_present?(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}.")
|
||||
|
|
@ -1111,15 +1132,15 @@ def resolve_release_build_number(api_key:, short_version:)
|
|||
next_build.to_s
|
||||
end
|
||||
|
||||
def release_build_number_needs_app_store_connect_auth?
|
||||
explicit = ENV["IOS_RELEASE_BUILD_NUMBER"]
|
||||
def release_build_number_needs_app_store_connect_auth?(explicit_build_number: nil)
|
||||
explicit = explicit_build_number.to_s.strip
|
||||
!env_present?(explicit)
|
||||
end
|
||||
|
||||
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]))
|
||||
sh(shell_join(["bash", script_path, "--version", version, "--build-number", build_number]))
|
||||
|
||||
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)
|
||||
|
|
@ -1296,13 +1317,22 @@ end
|
|||
platform :ios do
|
||||
private_lane :prepare_app_store_context do |options|
|
||||
require_api_key = options[:require_api_key] == true
|
||||
needs_api_key = require_api_key || release_build_number_needs_app_store_connect_auth?
|
||||
release_version = options[:release_version].to_s.strip
|
||||
explicit_build_number = options[:build_number].to_s.strip
|
||||
needs_api_key = require_api_key || release_build_number_needs_app_store_connect_auth?(explicit_build_number: explicit_build_number)
|
||||
api_key = needs_api_key ? app_store_connect_api_key_config : nil
|
||||
sync_ios_versioning!
|
||||
version_metadata = read_ios_version_metadata
|
||||
if release_version.empty?
|
||||
UI.user_error!("Missing iOS release version. Use `pnpm ios:release:upload -- --version YYYY.M.D` or pass `release_version:YYYY.M.D` to the Fastlane lane.")
|
||||
end
|
||||
sync_ios_versioning!(release_version: release_version)
|
||||
version_metadata = read_ios_version_metadata(release_version: release_version)
|
||||
version = version_metadata[:version]
|
||||
short_version = version_metadata[:short_version]
|
||||
build_number = resolve_release_build_number(api_key: api_key, short_version: short_version)
|
||||
build_number = resolve_release_build_number(
|
||||
api_key: api_key,
|
||||
short_version: short_version,
|
||||
explicit_build_number: explicit_build_number
|
||||
)
|
||||
release_xcconfig = prepare_app_store_release!(version: version, build_number: build_number)
|
||||
|
||||
{
|
||||
|
|
@ -1346,8 +1376,12 @@ platform :ios do
|
|||
end
|
||||
|
||||
desc "Build an App Store distribution archive locally without uploading"
|
||||
lane :app_store_archive do
|
||||
context = prepare_app_store_context(require_api_key: false)
|
||||
lane :app_store_archive do |options|
|
||||
context = prepare_app_store_context(
|
||||
require_api_key: false,
|
||||
release_version: options[:release_version],
|
||||
build_number: options[:build_number]
|
||||
)
|
||||
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
|
||||
|
|
@ -1356,26 +1390,32 @@ platform :ios do
|
|||
end
|
||||
|
||||
desc "Generate screenshots, update App Store metadata and review attachment, then upload an App Store build"
|
||||
lane :release_upload do
|
||||
lane :release_upload do |options|
|
||||
unless ENV["OPENCLAW_IOS_RELEASE_WRAPPER"] == "1"
|
||||
UI.user_error!("Use `pnpm ios:release:upload`; direct Fastlane TestFlight upload is disabled.")
|
||||
end
|
||||
|
||||
release_sha = release_git_sha
|
||||
release_signing_check!
|
||||
preserve_local_signing do
|
||||
screenshots
|
||||
end
|
||||
context = prepare_app_store_context(require_api_key: true)
|
||||
context = prepare_app_store_context(
|
||||
require_api_key: true,
|
||||
release_version: options[:release_version],
|
||||
build_number: options[:build_number]
|
||||
)
|
||||
ensure_mobile_release_ref_available!(
|
||||
platform: "ios",
|
||||
version: context[:short_version],
|
||||
build: context[:build_number],
|
||||
sha: release_sha
|
||||
)
|
||||
without_xcode_xcconfig_file do
|
||||
preserve_local_signing do
|
||||
screenshots(release_version: context[:version], build_number: context[:build_number])
|
||||
end
|
||||
end
|
||||
ENV["DELIVER_SCREENSHOTS"] = "1"
|
||||
ENV["DELIVER_RELEASE_NOTES"] = "1"
|
||||
metadata
|
||||
metadata(release_version: context[:short_version])
|
||||
|
||||
build = build_app_store_release(context)
|
||||
|
||||
|
|
@ -1399,10 +1439,14 @@ platform :ios do
|
|||
end
|
||||
|
||||
desc "Upload App Store metadata, App Review PDF attachment, and optionally screenshots"
|
||||
lane :metadata do
|
||||
lane :metadata do |options|
|
||||
install_ready_for_review_edit_state_lookup!
|
||||
sync_ios_versioning!
|
||||
version_metadata = read_ios_version_metadata
|
||||
release_version = options[:release_version].to_s.strip
|
||||
if release_version.empty?
|
||||
UI.user_error!("Missing iOS release version. Use `pnpm ios:release:upload -- --version YYYY.M.D` or `fastlane ios metadata release_version:YYYY.M.D`.")
|
||||
end
|
||||
sync_ios_versioning!(release_version: release_version)
|
||||
version_metadata = read_ios_version_metadata(release_version: release_version)
|
||||
api_key = app_store_connect_api_key_config
|
||||
clear_empty_env_var("APP_STORE_CONNECT_API_KEY_PATH")
|
||||
app_identifier = ENV["APP_STORE_CONNECT_APP_IDENTIFIER"]
|
||||
|
|
@ -1464,9 +1508,15 @@ platform :ios do
|
|||
end
|
||||
|
||||
desc "Generate deterministic iOS screenshots for App Store metadata"
|
||||
lane :screenshots do
|
||||
lane :screenshots do |options|
|
||||
version_args = []
|
||||
release_version = options[:release_version].to_s.strip
|
||||
build_number = options[:build_number].to_s.strip
|
||||
version_args += ["--version", release_version] unless release_version.empty?
|
||||
version_args += ["--build-number", build_number] unless build_number.empty?
|
||||
|
||||
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")]))
|
||||
sh(shell_join(["bash", File.join(repo_root, "scripts", "ios-write-version-xcconfig.sh"), *version_args]))
|
||||
sh(shell_join(["xcodegen", "generate", "--spec", File.join(ios_root, "project.yml"), "--project", ios_root]))
|
||||
|
||||
capture_ios_screenshots(
|
||||
|
|
@ -1486,13 +1536,19 @@ platform :ios do
|
|||
xcargs: "-allowProvisioningUpdates"
|
||||
)
|
||||
|
||||
watch_screenshot
|
||||
watch_screenshot(release_version: release_version, build_number: build_number)
|
||||
end
|
||||
|
||||
desc "Generate deterministic Apple Watch screenshot for App Store metadata"
|
||||
lane :watch_screenshot do
|
||||
lane :watch_screenshot do |options|
|
||||
version_args = []
|
||||
release_version = options[:release_version].to_s.strip
|
||||
build_number = options[:build_number].to_s.strip
|
||||
version_args += ["--version", release_version] unless release_version.empty?
|
||||
version_args += ["--build-number", build_number] unless build_number.empty?
|
||||
|
||||
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")]))
|
||||
sh(shell_join(["bash", File.join(repo_root, "scripts", "ios-write-version-xcconfig.sh"), *version_args]))
|
||||
sh(shell_join(["xcodegen", "generate", "--spec", File.join(ios_root, "project.yml"), "--project", ios_root]))
|
||||
capture_watch_screenshot
|
||||
end
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ If you pass `--build-number` to `pnpm ios:release:archive`, the local archive pa
|
|||
Archive locally without upload:
|
||||
|
||||
```bash
|
||||
pnpm ios:release:archive
|
||||
pnpm ios:release:archive -- --version 2026.6.11 --build-number 3
|
||||
```
|
||||
|
||||
Generate deterministic App Store screenshots:
|
||||
|
|
@ -109,7 +109,7 @@ The screenshot lane runs the app with `--openclaw-screenshot-mode`, which enters
|
|||
Upload to App Store Connect:
|
||||
|
||||
```bash
|
||||
pnpm ios:release:upload
|
||||
pnpm ios:release:upload -- --version 2026.6.11
|
||||
```
|
||||
|
||||
Direct Fastlane TestFlight upload is disabled. Use the package script so the
|
||||
|
|
@ -135,16 +135,16 @@ cd apps/ios
|
|||
fastlane ios auth_check
|
||||
```
|
||||
|
||||
4. If you are starting a brand-new production release train, pin iOS to the current gateway version:
|
||||
4. If you are starting a brand-new production release train, sync iOS generated metadata for the release version:
|
||||
|
||||
```bash
|
||||
pnpm ios:version:pin -- --from-gateway
|
||||
pnpm ios:version:sync -- --version 2026.6.11
|
||||
```
|
||||
|
||||
5. Upload:
|
||||
|
||||
```bash
|
||||
pnpm ios:release:upload
|
||||
pnpm ios:release:upload -- --version 2026.6.11 --build-number 3
|
||||
```
|
||||
|
||||
Quick verification after upload:
|
||||
|
|
@ -156,14 +156,14 @@ Quick verification after upload:
|
|||
|
||||
Versioning rules:
|
||||
|
||||
- `apps/ios/version.json` is the pinned iOS release version source
|
||||
- App Store release uploads require an explicit `--version`
|
||||
- local defaults derive from root `package.json`
|
||||
- `apps/ios/CHANGELOG.md` is the iOS-only changelog and release-note source
|
||||
- Supported pinned iOS versions use CalVer: `YYYY.M.D`
|
||||
- `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`
|
||||
- Supported iOS release versions use CalVer: `YYYY.M.D`
|
||||
- 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` after changing `apps/ios/version.json` or `apps/ios/CHANGELOG.md`
|
||||
- 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
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ This directory is used by `fastlane deliver` for App Store Connect text metadata
|
|||
```bash
|
||||
cd apps/ios
|
||||
APP_STORE_CONNECT_APP_ID=YOUR_APP_STORE_CONNECT_APP_ID \
|
||||
DELIVER_METADATA=1 fastlane ios metadata
|
||||
DELIVER_METADATA=1 fastlane ios metadata release_version:2026.6.11
|
||||
```
|
||||
|
||||
## Release notes and App Review attachment
|
||||
|
|
@ -16,14 +16,14 @@ DELIVER_METADATA=1 fastlane ios metadata
|
|||
|
||||
```bash
|
||||
cd apps/ios
|
||||
DELIVER_RELEASE_NOTES=1 fastlane ios metadata
|
||||
DELIVER_RELEASE_NOTES=1 fastlane ios metadata release_version:2026.6.11
|
||||
```
|
||||
|
||||
## Optional: include screenshots
|
||||
|
||||
```bash
|
||||
cd apps/ios
|
||||
DELIVER_METADATA=1 DELIVER_SCREENSHOTS=1 fastlane ios metadata
|
||||
DELIVER_METADATA=1 DELIVER_SCREENSHOTS=1 fastlane ios metadata release_version:2026.6.11
|
||||
```
|
||||
|
||||
## Auth
|
||||
|
|
@ -45,10 +45,10 @@ 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`.
|
||||
- `release_notes.txt` is generated from `apps/ios/CHANGELOG.md`; after changelog updates, run `pnpm ios:version:sync -- --version <release-version>`.
|
||||
- `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 `## <pinned iOS version>` first, then fall back to `## Unreleased` while a TestFlight train is still in progress.
|
||||
- When starting a new production release train, pin the iOS version first with `pnpm ios:version:pin -- --from-gateway`.
|
||||
- Release notes resolve from `## <release version>` first, then fall back to `## Unreleased` while a TestFlight train is still in progress.
|
||||
- When starting a new production release train, sync metadata with `pnpm ios:version:sync -- --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:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
Maintenance update for the current OpenClaw beta release.
|
||||
|
||||
- Improved notification cleanup, Watch app compatibility, and native file input handling.
|
||||
- Refreshed iOS 26 visual styling, Talk controls, Gateway recovery, localization, and App Store screenshots.
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"version": "2026.6.10"
|
||||
}
|
||||
|
|
@ -1638,7 +1638,6 @@
|
|||
"ios:screenshots": "bash scripts/ios-screenshots.sh",
|
||||
"ios:version": "node --import tsx scripts/ios-version.ts --json",
|
||||
"ios:version:check": "node --import tsx scripts/ios-sync-versioning.ts --check",
|
||||
"ios:version:pin": "node --import tsx scripts/ios-pin-version.ts",
|
||||
"ios:version:sync": "node --import tsx scripts/ios-sync-versioning.ts --write",
|
||||
"leak:embedded-run": "node --import tsx --expose-gc scripts/embedded-run-abort-leak.ts",
|
||||
"lint": "node scripts/run-oxlint-shards.mjs",
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ export const RELEASE_METADATA_PATHS = new Set([
|
|||
"apps/ios/CHANGELOG.md",
|
||||
"apps/ios/Config/Version.xcconfig",
|
||||
"apps/ios/fastlane/metadata/en-US/release_notes.txt",
|
||||
"apps/ios/version.json",
|
||||
"apps/macos/Sources/OpenClaw/Resources/Info.plist",
|
||||
"docs/.generated/config-baseline.sha256",
|
||||
"docs/install/updating.md",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ const VERSION_ONLY_TEXT_PATHS = new Set([
|
|||
"apps/android/Config/Version.properties",
|
||||
"apps/android/version.json",
|
||||
"apps/ios/Config/Version.xcconfig",
|
||||
"apps/ios/version.json",
|
||||
"apps/macos/Sources/OpenClaw/Resources/Info.plist",
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,154 +0,0 @@
|
|||
// Ios Pin Version script supports OpenClaw repository automation.
|
||||
import path from "node:path";
|
||||
import {
|
||||
normalizePinnedIosVersion,
|
||||
resolveGatewayVersionForIosRelease,
|
||||
resolveIosVersion,
|
||||
syncIosVersioning,
|
||||
writeIosVersionManifest,
|
||||
} from "./lib/ios-version.ts";
|
||||
|
||||
type CliOptions = {
|
||||
explicitVersion: string | null;
|
||||
fromGateway: boolean;
|
||||
rootDir: string;
|
||||
sync: boolean;
|
||||
};
|
||||
|
||||
export type PinIosVersionResult = {
|
||||
previousVersion: string | null;
|
||||
nextVersion: string;
|
||||
packageVersion: string | null;
|
||||
versionFilePath: string;
|
||||
syncedPaths: string[];
|
||||
};
|
||||
|
||||
function usage(): string {
|
||||
return [
|
||||
"Usage: node --import tsx scripts/ios-pin-version.ts (--from-gateway | --version <YYYY.M.PATCH>) [--no-sync] [--root dir]",
|
||||
"",
|
||||
"Examples:",
|
||||
" node --import tsx scripts/ios-pin-version.ts --from-gateway",
|
||||
" node --import tsx scripts/ios-pin-version.ts --version 2026.4.10",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function parseArgs(argv: string[]): CliOptions {
|
||||
let explicitVersion: string | null = null;
|
||||
let fromGateway = false;
|
||||
let rootDir = path.resolve(".");
|
||||
let sync = true;
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
switch (arg) {
|
||||
case "--from-gateway": {
|
||||
fromGateway = true;
|
||||
break;
|
||||
}
|
||||
case "--version": {
|
||||
explicitVersion = readOptionValue(argv, index, "--version");
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "--no-sync": {
|
||||
sync = false;
|
||||
break;
|
||||
}
|
||||
case "--root": {
|
||||
const value = readOptionValue(argv, index, "--root");
|
||||
rootDir = path.resolve(value);
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "-h":
|
||||
case "--help": {
|
||||
console.log(`${usage()}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fromGateway === (explicitVersion !== null)) {
|
||||
throw new Error("Choose exactly one of --from-gateway or --version <YYYY.M.PATCH>.");
|
||||
}
|
||||
|
||||
if (explicitVersion !== null && !explicitVersion.trim()) {
|
||||
throw new Error("Missing value for --version.");
|
||||
}
|
||||
|
||||
return { explicitVersion, fromGateway, rootDir, sync };
|
||||
}
|
||||
|
||||
function readOptionValue(argv: string[], index: number, flag: string): string {
|
||||
const value = argv[index + 1];
|
||||
if (value === undefined || value === "" || value.startsWith("-")) {
|
||||
throw new Error(`Missing value for ${flag}.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function pinIosVersion(params: CliOptions): PinIosVersionResult {
|
||||
const rootDir = path.resolve(params.rootDir);
|
||||
let previousVersion: string | null;
|
||||
try {
|
||||
previousVersion = resolveIosVersion(rootDir).canonicalVersion;
|
||||
} catch {
|
||||
previousVersion = null;
|
||||
}
|
||||
|
||||
const gatewayVersion = params.fromGateway ? resolveGatewayVersionForIosRelease(rootDir) : null;
|
||||
const packageVersion = gatewayVersion?.packageVersion ?? null;
|
||||
const nextVersion =
|
||||
gatewayVersion?.pinnedIosVersion ?? normalizePinnedIosVersion(params.explicitVersion ?? "");
|
||||
const versionFilePath = writeIosVersionManifest(nextVersion, rootDir);
|
||||
const syncedPaths = params.sync ? syncIosVersioning({ mode: "write", rootDir }).updatedPaths : [];
|
||||
|
||||
return {
|
||||
previousVersion,
|
||||
nextVersion,
|
||||
packageVersion,
|
||||
versionFilePath,
|
||||
syncedPaths,
|
||||
};
|
||||
}
|
||||
|
||||
export async function main(argv: string[]): Promise<number> {
|
||||
try {
|
||||
const options = parseArgs(argv);
|
||||
const result = pinIosVersion(options);
|
||||
const sourceText = result.packageVersion
|
||||
? ` from gateway version ${result.packageVersion}`
|
||||
: "";
|
||||
process.stdout.write(`Pinned iOS version to ${result.nextVersion}${sourceText}.\n`);
|
||||
if (result.previousVersion && result.previousVersion !== result.nextVersion) {
|
||||
process.stdout.write(`Previous pinned iOS version: ${result.previousVersion}.\n`);
|
||||
}
|
||||
process.stdout.write(
|
||||
`Updated version manifest: ${path.relative(process.cwd(), result.versionFilePath)}\n`,
|
||||
);
|
||||
if (options.sync) {
|
||||
if (result.syncedPaths.length === 0) {
|
||||
process.stdout.write("iOS versioning artifacts already up to date.\n");
|
||||
} else {
|
||||
process.stdout.write(
|
||||
`Updated iOS versioning artifacts:\n- ${result.syncedPaths.map((filePath) => path.relative(process.cwd(), filePath)).join("\n- ")}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
} catch (error) {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const exitCode = await main(process.argv.slice(2));
|
||||
if (exitCode !== 0) {
|
||||
process.exit(exitCode);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,14 @@ set -euo pipefail
|
|||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/ios-release-archive.sh [--build-number 7]
|
||||
scripts/ios-release-archive.sh --version 2026.6.11 [--build-number 7]
|
||||
|
||||
Archives and exports an App Store distribution IPA locally without uploading.
|
||||
EOF
|
||||
}
|
||||
|
||||
BUILD_NUMBER="${IOS_RELEASE_BUILD_NUMBER:-}"
|
||||
BUILD_NUMBER=""
|
||||
RELEASE_VERSION=""
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
source "${ROOT_DIR}/scripts/lib/ios-fastlane.sh"
|
||||
|
||||
|
|
@ -35,6 +36,11 @@ while [[ $# -gt 0 ]]; do
|
|||
BUILD_NUMBER="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
require_option_value "$1" "${2-}"
|
||||
RELEASE_VERSION="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
|
|
@ -47,7 +53,18 @@ while [[ $# -gt 0 ]]; do
|
|||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${RELEASE_VERSION}" ]]; then
|
||||
echo "Missing required --version." >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FASTLANE_ARGS=(ios app_store_archive "release_version:${RELEASE_VERSION}")
|
||||
if [[ -n "${BUILD_NUMBER}" ]]; then
|
||||
FASTLANE_ARGS+=("build_number:${BUILD_NUMBER}")
|
||||
fi
|
||||
|
||||
(
|
||||
cd "${ROOT_DIR}/apps/ios"
|
||||
IOS_RELEASE_BUILD_NUMBER="${BUILD_NUMBER}" run_ios_fastlane ios app_store_archive
|
||||
run_ios_fastlane "${FASTLANE_ARGS[@]}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ set -euo pipefail
|
|||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/ios-release-prepare.sh --build-number 7 [--team-id TEAMID]
|
||||
scripts/ios-release-prepare.sh --version 2026.6.11 --build-number 7 [--team-id TEAMID]
|
||||
|
||||
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/Version.xcconfig for the explicit release version
|
||||
- writes apps/ios/build/AppStoreRelease.xcconfig with canonical bundle IDs
|
||||
- configures the release build for relay-backed APNs registration
|
||||
- configures manual App Store distribution signing with pinned provisioning profiles
|
||||
|
|
@ -27,6 +27,7 @@ RELEASE_SIGNING_HELPER="${ROOT_DIR}/scripts/ios-release-signing.mjs"
|
|||
CANONICAL_TEAM_ID="FWJYW4S8P8"
|
||||
|
||||
BUILD_NUMBER=""
|
||||
RELEASE_VERSION=""
|
||||
TEAM_ID="${IOS_DEVELOPMENT_TEAM:-}"
|
||||
IOS_VERSION=""
|
||||
RELEASE_SIGNING_XCCONFIG=""
|
||||
|
|
@ -75,6 +76,11 @@ while [[ $# -gt 0 ]]; do
|
|||
BUILD_NUMBER="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
require_option_value "$1" "${2-}"
|
||||
RELEASE_VERSION="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--team-id)
|
||||
require_option_value "$1" "${2-}"
|
||||
TEAM_ID="${2:-}"
|
||||
|
|
@ -94,7 +100,13 @@ done
|
|||
|
||||
if [[ -z "${BUILD_NUMBER}" ]]; then
|
||||
echo "Missing required --build-number." >&2
|
||||
usage
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${RELEASE_VERSION}" ]]; then
|
||||
echo "Missing required --version." >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -120,12 +132,12 @@ fi
|
|||
prepare_build_dir
|
||||
|
||||
(
|
||||
cd "${ROOT_DIR}" && node --import tsx "${VERSION_SYNC_HELPER}" --check
|
||||
cd "${ROOT_DIR}" && node --import tsx "${VERSION_SYNC_HELPER}" --check --version "${RELEASE_VERSION}"
|
||||
)
|
||||
|
||||
IOS_VERSION="$(cd "${ROOT_DIR}" && node --import tsx "${IOS_VERSION_HELPER}" --field canonicalVersion)"
|
||||
IOS_VERSION="$(cd "${ROOT_DIR}" && node --import tsx "${IOS_VERSION_HELPER}" --version "${RELEASE_VERSION}" --field canonicalVersion)"
|
||||
if [[ -z "${IOS_VERSION}" ]]; then
|
||||
echo "Unable to resolve iOS version from ${ROOT_DIR}/apps/ios/version.json." >&2
|
||||
echo "Unable to resolve iOS release version '${RELEASE_VERSION}'." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -136,7 +148,7 @@ if [[ -z "${RELEASE_SIGNING_XCCONFIG}" ]]; then
|
|||
fi
|
||||
|
||||
(
|
||||
bash "${VERSION_HELPER}" --build-number "${BUILD_NUMBER}"
|
||||
bash "${VERSION_HELPER}" --version "${IOS_VERSION}" --build-number "${BUILD_NUMBER}"
|
||||
)
|
||||
|
||||
write_generated_file "${RELEASE_XCCONFIG}" <<EOF
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ set -euo pipefail
|
|||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/ios-release-upload.sh [--build-number 7]
|
||||
scripts/ios-release-upload.sh --version 2026.6.11 [--build-number 7]
|
||||
|
||||
Generates App Store screenshots, updates release metadata, archives, and uploads
|
||||
an App Store distribution build to App Store Connect. This does not submit the
|
||||
|
|
@ -12,7 +12,8 @@ build for App Review.
|
|||
EOF
|
||||
}
|
||||
|
||||
BUILD_NUMBER="${IOS_RELEASE_BUILD_NUMBER:-}"
|
||||
BUILD_NUMBER=""
|
||||
RELEASE_VERSION=""
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
source "${ROOT_DIR}/scripts/lib/ios-fastlane.sh"
|
||||
|
||||
|
|
@ -37,6 +38,11 @@ while [[ $# -gt 0 ]]; do
|
|||
BUILD_NUMBER="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
require_option_value "$1" "${2-}"
|
||||
RELEASE_VERSION="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
|
|
@ -49,8 +55,19 @@ while [[ $# -gt 0 ]]; do
|
|||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${RELEASE_VERSION}" ]]; then
|
||||
echo "Missing required --version." >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FASTLANE_ARGS=(ios release_upload "release_version:${RELEASE_VERSION}")
|
||||
if [[ -n "${BUILD_NUMBER}" ]]; then
|
||||
FASTLANE_ARGS+=("build_number:${BUILD_NUMBER}")
|
||||
fi
|
||||
|
||||
(
|
||||
cd "${ROOT_DIR}/apps/ios"
|
||||
# App Store Connect screenshot reservations can fail with 500s under parallel deliver uploads.
|
||||
DELIVER_NUMBER_OF_THREADS=1 FL_MAX_NUMBER_OF_THREADS=1 OPENCLAW_IOS_RELEASE_WRAPPER=1 IOS_RELEASE_BUILD_NUMBER="${BUILD_NUMBER}" run_ios_fastlane ios release_upload
|
||||
DELIVER_NUMBER_OF_THREADS=1 FL_MAX_NUMBER_OF_THREADS=1 OPENCLAW_IOS_RELEASE_WRAPPER=1 run_ios_fastlane "${FASTLANE_ARGS[@]}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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] [--root dir]\n",
|
||||
"Usage: node --import tsx scripts/ios-sync-versioning.ts [--write|--check] [--version YYYY.M.D] [--root dir]\n",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -18,7 +18,11 @@ function main(argv = process.argv.slice(2)): number {
|
|||
return 0;
|
||||
}
|
||||
|
||||
const result = syncIosVersioning({ mode: options.mode, rootDir: options.rootDir });
|
||||
const result = syncIosVersioning({
|
||||
mode: options.mode,
|
||||
releaseVersion: options.releaseVersion,
|
||||
rootDir: options.rootDir,
|
||||
});
|
||||
|
||||
if (options.mode === "check") {
|
||||
process.stdout.write("iOS versioning artifacts are up to date.\n");
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { parseVersionQueryArgs } from "./lib/version-script-args.ts";
|
|||
|
||||
function printUsage(): void {
|
||||
process.stdout.write(
|
||||
"Usage: node --import tsx scripts/ios-version.ts [--json|--shell] [--field name] [--root dir]\n\n",
|
||||
"Usage: node --import tsx scripts/ios-version.ts [--json|--shell] [--field name] [--version YYYY.M.D] [--root dir]\n\n",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ function main(argv = process.argv.slice(2)): number {
|
|||
return 0;
|
||||
}
|
||||
|
||||
const version = resolveIosVersion(options.rootDir);
|
||||
const version = resolveIosVersion(options.rootDir, { releaseVersion: options.releaseVersion });
|
||||
|
||||
if (options.field) {
|
||||
const value = version[options.field as keyof typeof version];
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ set -euo pipefail
|
|||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/ios-write-version-xcconfig.sh [--build-number 7]
|
||||
scripts/ios-write-version-xcconfig.sh [--version 2026.6.11] [--build-number 7]
|
||||
|
||||
Writes apps/ios/build/Version.xcconfig from apps/ios/version.json:
|
||||
Writes apps/ios/build/Version.xcconfig from package.json or explicit --version:
|
||||
- OPENCLAW_IOS_VERSION = exact canonical iOS version
|
||||
- OPENCLAW_MARKETING_VERSION = short iOS/App Store version
|
||||
- OPENCLAW_BUILD_VERSION = explicit build number or local numeric fallback
|
||||
|
|
@ -21,6 +21,18 @@ VERSION_HELPER="${ROOT_DIR}/scripts/ios-version.ts"
|
|||
IOS_VERSION=""
|
||||
MARKETING_VERSION=""
|
||||
BUILD_NUMBER=""
|
||||
RELEASE_VERSION=""
|
||||
|
||||
require_option_value() {
|
||||
local option="$1"
|
||||
local value="${2-}"
|
||||
|
||||
if [[ -z "${value}" || "${value}" == --* ]]; then
|
||||
echo "Missing value for ${option}." >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_build_dir() {
|
||||
if [[ -L "${BUILD_DIR}" ]]; then
|
||||
|
|
@ -51,9 +63,15 @@ while [[ $# -gt 0 ]]; do
|
|||
shift
|
||||
;;
|
||||
--build-number)
|
||||
require_option_value "$1" "${2-}"
|
||||
BUILD_NUMBER="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
require_option_value "$1" "${2-}"
|
||||
RELEASE_VERSION="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
|
|
@ -66,6 +84,11 @@ while [[ $# -gt 0 ]]; do
|
|||
esac
|
||||
done
|
||||
|
||||
VERSION_HELPER_ARGS=(--shell)
|
||||
if [[ -n "${RELEASE_VERSION}" ]]; then
|
||||
VERSION_HELPER_ARGS+=(--version "${RELEASE_VERSION}")
|
||||
fi
|
||||
|
||||
while IFS='=' read -r key value; do
|
||||
case "${key}" in
|
||||
OPENCLAW_IOS_VERSION)
|
||||
|
|
@ -75,10 +98,10 @@ while IFS='=' read -r key value; do
|
|||
MARKETING_VERSION="${value}"
|
||||
;;
|
||||
esac
|
||||
done < <(cd "${ROOT_DIR}" && node --import tsx "${VERSION_HELPER}" --shell)
|
||||
done < <(cd "${ROOT_DIR}" && node --import tsx "${VERSION_HELPER}" "${VERSION_HELPER_ARGS[@]}")
|
||||
|
||||
if [[ -z "${IOS_VERSION}" || -z "${MARKETING_VERSION}" ]]; then
|
||||
echo "Unable to resolve iOS version metadata from ${ROOT_DIR}/apps/ios/version.json." >&2
|
||||
echo "Unable to resolve iOS version metadata." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -3,23 +3,19 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|||
import path from "node:path";
|
||||
import { parseReleaseVersion } from "./npm-publish-plan.mjs";
|
||||
|
||||
const IOS_VERSION_FILE = "apps/ios/version.json";
|
||||
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 IosVersionManifest = {
|
||||
version: string;
|
||||
};
|
||||
|
||||
type ResolvedIosVersion = {
|
||||
canonicalVersion: string;
|
||||
marketingVersion: string;
|
||||
buildVersion: string;
|
||||
versionFilePath: string;
|
||||
changelogPath: string;
|
||||
versionXcconfigPath: string;
|
||||
releaseNotesPath: string;
|
||||
versionSource: "explicit" | "package";
|
||||
versionSourcePath: string | null;
|
||||
};
|
||||
|
||||
type SyncIosVersioningMode = "check" | "write";
|
||||
|
|
@ -39,14 +35,12 @@ function parsePinnedReleaseVersion(rawVersion: string): string | null {
|
|||
export function normalizePinnedIosVersion(rawVersion: string): string {
|
||||
const trimmed = rawVersion.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error(`Missing iOS version in ${IOS_VERSION_FILE}.`);
|
||||
throw new Error("Missing iOS release version.");
|
||||
}
|
||||
|
||||
const pinnedVersion = parsePinnedReleaseVersion(trimmed);
|
||||
if (!pinnedVersion) {
|
||||
throw new Error(
|
||||
`Invalid iOS version '${rawVersion}'. Expected pinned release version like 2026.6.5.`,
|
||||
);
|
||||
throw new Error(`Invalid iOS version '${rawVersion}'. Expected release version like 2026.6.5.`);
|
||||
}
|
||||
|
||||
return pinnedVersion;
|
||||
|
|
@ -68,8 +62,12 @@ export function normalizeGatewayVersionToPinnedIosVersion(rawVersion: string): s
|
|||
return parsed.baseVersion;
|
||||
}
|
||||
|
||||
function rootPackageJsonPath(rootDir = path.resolve(".")): string {
|
||||
return path.join(rootDir, "package.json");
|
||||
}
|
||||
|
||||
function readRootPackageVersion(rootDir = path.resolve(".")): string {
|
||||
const packageJsonPath = path.join(rootDir, "package.json");
|
||||
const packageJsonPath = rootPackageJsonPath(rootDir);
|
||||
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { version?: unknown };
|
||||
const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
|
||||
if (!version) {
|
||||
|
|
@ -89,40 +87,32 @@ export function resolveGatewayVersionForIosRelease(rootDir = path.resolve(".")):
|
|||
};
|
||||
}
|
||||
|
||||
function readIosVersionManifest(rootDir = path.resolve(".")): IosVersionManifest {
|
||||
const versionFilePath = path.join(rootDir, IOS_VERSION_FILE);
|
||||
return JSON.parse(readFileSync(versionFilePath, "utf8")) as IosVersionManifest;
|
||||
}
|
||||
|
||||
export function writeIosVersionManifest(version: string, rootDir = path.resolve(".")): string {
|
||||
const versionFilePath = path.join(rootDir, IOS_VERSION_FILE);
|
||||
const normalizedVersion = normalizePinnedIosVersion(version);
|
||||
const nextContent = `${JSON.stringify({ version: normalizedVersion }, null, 2)}\n`;
|
||||
writeFileSync(versionFilePath, nextContent, "utf8");
|
||||
return versionFilePath;
|
||||
}
|
||||
|
||||
export function resolveIosVersion(rootDir = path.resolve(".")): ResolvedIosVersion {
|
||||
const versionFilePath = path.join(rootDir, IOS_VERSION_FILE);
|
||||
export function resolveIosVersion(
|
||||
rootDir = path.resolve("."),
|
||||
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 manifest = readIosVersionManifest(rootDir);
|
||||
const canonicalVersion = normalizePinnedIosVersion(manifest.version ?? "");
|
||||
const explicitReleaseVersion = options?.releaseVersion?.trim() ?? "";
|
||||
const canonicalVersion = explicitReleaseVersion
|
||||
? normalizePinnedIosVersion(explicitReleaseVersion)
|
||||
: resolveGatewayVersionForIosRelease(rootDir).pinnedIosVersion;
|
||||
|
||||
return {
|
||||
canonicalVersion,
|
||||
marketingVersion: canonicalVersion,
|
||||
buildVersion: "1",
|
||||
versionFilePath,
|
||||
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: apps/ios/version.json\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`;
|
||||
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 {
|
||||
|
|
@ -190,12 +180,17 @@ function syncFile(params: {
|
|||
return true;
|
||||
}
|
||||
|
||||
export function syncIosVersioning(params?: { mode?: SyncIosVersioningMode; rootDir?: string }): {
|
||||
export function syncIosVersioning(params?: {
|
||||
mode?: SyncIosVersioningMode;
|
||||
releaseVersion?: string | null;
|
||||
rootDir?: string;
|
||||
}): {
|
||||
updatedPaths: string[];
|
||||
} {
|
||||
const mode = params?.mode ?? "write";
|
||||
const rootDir = path.resolve(params?.rootDir ?? ".");
|
||||
const version = resolveIosVersion(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);
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ export type VersionQueryCliOptions = {
|
|||
field: string | null;
|
||||
format: VersionScriptFormat;
|
||||
help: boolean;
|
||||
releaseVersion: string | null;
|
||||
rootDir: string;
|
||||
};
|
||||
export type VersionSyncMode = "check" | "write";
|
||||
export type VersionSyncCliOptions = {
|
||||
help: boolean;
|
||||
mode: VersionSyncMode;
|
||||
releaseVersion: string | null;
|
||||
rootDir: string;
|
||||
};
|
||||
|
||||
|
|
@ -18,11 +20,15 @@ export function parseVersionQueryArgs(argv: string[]): VersionQueryCliOptions {
|
|||
let field: string | null = null;
|
||||
let format: VersionScriptFormat = "json";
|
||||
let help = false;
|
||||
let releaseVersion: string | null = null;
|
||||
let rootDir = path.resolve(".");
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
switch (arg) {
|
||||
case "--": {
|
||||
break;
|
||||
}
|
||||
case "--field": {
|
||||
field = readOptionValue(argv, index, "--field");
|
||||
index += 1;
|
||||
|
|
@ -42,38 +48,8 @@ export function parseVersionQueryArgs(argv: string[]): VersionQueryCliOptions {
|
|||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "-h":
|
||||
case "--help": {
|
||||
help = true;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { field, format, help, rootDir };
|
||||
}
|
||||
|
||||
export function parseVersionSyncArgs(argv: string[]): VersionSyncCliOptions {
|
||||
let help = false;
|
||||
let mode: VersionSyncMode = "write";
|
||||
let rootDir = path.resolve(".");
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
switch (arg) {
|
||||
case "--check": {
|
||||
mode = "check";
|
||||
break;
|
||||
}
|
||||
case "--write": {
|
||||
mode = "write";
|
||||
break;
|
||||
}
|
||||
case "--root": {
|
||||
rootDir = path.resolve(readOptionValue(argv, index, "--root"));
|
||||
case "--version": {
|
||||
releaseVersion = readOptionValue(argv, index, "--version");
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
|
|
@ -88,7 +64,51 @@ export function parseVersionSyncArgs(argv: string[]): VersionSyncCliOptions {
|
|||
}
|
||||
}
|
||||
|
||||
return { help, mode, rootDir };
|
||||
return { field, format, help, releaseVersion, rootDir };
|
||||
}
|
||||
|
||||
export function parseVersionSyncArgs(argv: string[]): VersionSyncCliOptions {
|
||||
let help = false;
|
||||
let mode: VersionSyncMode = "write";
|
||||
let releaseVersion: string | null = null;
|
||||
let rootDir = path.resolve(".");
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
switch (arg) {
|
||||
case "--": {
|
||||
break;
|
||||
}
|
||||
case "--check": {
|
||||
mode = "check";
|
||||
break;
|
||||
}
|
||||
case "--write": {
|
||||
mode = "write";
|
||||
break;
|
||||
}
|
||||
case "--root": {
|
||||
rootDir = path.resolve(readOptionValue(argv, index, "--root"));
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "--version": {
|
||||
releaseVersion = readOptionValue(argv, index, "--version");
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "-h":
|
||||
case "--help": {
|
||||
help = true;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { help, mode, releaseVersion, rootDir };
|
||||
}
|
||||
|
||||
function readOptionValue(argv: string[], index: number, flag: string): string {
|
||||
|
|
|
|||
|
|
@ -1169,10 +1169,7 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
|
|||
],
|
||||
],
|
||||
["scripts/lib/format-generated-module.mjs", ["test/scripts/format-generated-module.test.ts"]],
|
||||
[
|
||||
"scripts/lib/ios-version.ts",
|
||||
["test/scripts/ios-version.test.ts", "test/scripts/ios-pin-version.test.ts"],
|
||||
],
|
||||
["scripts/lib/ios-version.ts", ["test/scripts/ios-version.test.ts"]],
|
||||
["scripts/lib/live-docker-stage.sh", ["test/scripts/live-docker-stage.test.ts"]],
|
||||
["scripts/lib/local-heavy-check-runtime.mjs", ["test/scripts/local-heavy-check-runtime.test.ts"]],
|
||||
["scripts/lib/kova-report-gate.mjs", ["test/scripts/kova-report-gate.test.ts"]],
|
||||
|
|
|
|||
|
|
@ -907,7 +907,6 @@ describe("test-projects args", () => {
|
|||
"test/scripts/fixture-plugin-commands.test.ts",
|
||||
"test/scripts/incremental-line-reader.test.ts",
|
||||
"test/scripts/ios-configure-signing.test.ts",
|
||||
"test/scripts/ios-pin-version.test.ts",
|
||||
"test/scripts/ios-team-id.test.ts",
|
||||
"test/scripts/ios-version.test.ts",
|
||||
"test/scripts/kitchen-sink-rpc-walk.test.ts",
|
||||
|
|
|
|||
|
|
@ -1330,7 +1330,6 @@ describe("scripts/changed-lanes", () => {
|
|||
"apps/ios/CHANGELOG.md",
|
||||
"apps/ios/Config/Version.xcconfig",
|
||||
"apps/ios/fastlane/metadata/en-US/release_notes.txt",
|
||||
"apps/ios/version.json",
|
||||
"apps/macos/Sources/OpenClaw/Resources/Info.plist",
|
||||
"docs/.generated/config-baseline.sha256",
|
||||
"package.json",
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ describe("check-release-metadata-only", () => {
|
|||
"--head",
|
||||
"HEAD",
|
||||
"./package.json",
|
||||
"apps\\ios\\version.json",
|
||||
"apps\\ios\\Config\\Version.xcconfig",
|
||||
]),
|
||||
).toEqual({
|
||||
staged: false,
|
||||
base: "origin/release",
|
||||
head: "HEAD",
|
||||
paths: ["package.json", "apps/ios/version.json"],
|
||||
paths: ["package.json", "apps/ios/Config/Version.xcconfig"],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,125 +0,0 @@
|
|||
// Ios Pin Version tests cover ios pin version script behavior.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { pinIosVersion, parseArgs } from "../../scripts/ios-pin-version.ts";
|
||||
import { resolveIosVersion } from "../../scripts/lib/ios-version.ts";
|
||||
import { installIosFixtureCleanup, writeIosFixture } from "./ios-version.test-support.ts";
|
||||
|
||||
installIosFixtureCleanup();
|
||||
|
||||
describe("parseArgs", () => {
|
||||
it("requires exactly one pin source", () => {
|
||||
expect(() => parseArgs([])).toThrow(
|
||||
"Choose exactly one of --from-gateway or --version <YYYY.M.PATCH>",
|
||||
);
|
||||
expect(() => parseArgs(["--from-gateway", "--version", "2026.4.7"])).toThrow(
|
||||
"Choose exactly one of --from-gateway or --version <YYYY.M.PATCH>",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects flags where option values are required", () => {
|
||||
for (const { args, message } of [
|
||||
{ args: ["--version", "--no-sync"], message: "Missing value for --version." },
|
||||
{
|
||||
args: ["--version", "2026.4.7", "--root", "--no-sync"],
|
||||
message: "Missing value for --root.",
|
||||
},
|
||||
]) {
|
||||
expect(() => parseArgs(args)).toThrow(message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("pinIosVersion", () => {
|
||||
it("pins an explicit iOS release version and syncs generated artifacts", () => {
|
||||
const rootDir = writeIosFixture({
|
||||
version: "2026.4.6",
|
||||
changelog: `# OpenClaw iOS Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Draft release notes.
|
||||
`,
|
||||
prefix: "openclaw-ios-pin-",
|
||||
});
|
||||
|
||||
const result = pinIosVersion({
|
||||
explicitVersion: "2026.4.7",
|
||||
fromGateway: false,
|
||||
rootDir,
|
||||
sync: true,
|
||||
});
|
||||
|
||||
expect(result.previousVersion).toBe("2026.4.6");
|
||||
expect(result.nextVersion).toBe("2026.4.7");
|
||||
expect(result.packageVersion).toBeNull();
|
||||
expect(resolveIosVersion(rootDir).canonicalVersion).toBe("2026.4.7");
|
||||
expect(fs.readFileSync(path.join(rootDir, "apps", "ios", "version.json"), "utf8")).toContain(
|
||||
'"version": "2026.4.7"',
|
||||
);
|
||||
expect(
|
||||
fs.readFileSync(path.join(rootDir, "apps", "ios", "Config", "Version.xcconfig"), "utf8"),
|
||||
).toContain("OPENCLAW_MARKETING_VERSION = 2026.4.7");
|
||||
expect(
|
||||
fs.readFileSync(
|
||||
path.join(rootDir, "apps", "ios", "fastlane", "metadata", "en-US", "release_notes.txt"),
|
||||
"utf8",
|
||||
),
|
||||
).toContain("- Draft release notes.");
|
||||
expect(result.syncedPaths).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("pins from the current gateway version without carrying prerelease suffixes", () => {
|
||||
const rootDir = writeIosFixture({
|
||||
version: "2026.4.6",
|
||||
packageVersion: "2026.4.10-beta.3",
|
||||
changelog: `# OpenClaw iOS Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Candidate release notes.
|
||||
`,
|
||||
prefix: "openclaw-ios-pin-",
|
||||
});
|
||||
|
||||
const result = pinIosVersion({
|
||||
explicitVersion: null,
|
||||
fromGateway: true,
|
||||
rootDir,
|
||||
sync: true,
|
||||
});
|
||||
|
||||
expect(result.previousVersion).toBe("2026.4.6");
|
||||
expect(result.nextVersion).toBe("2026.4.10");
|
||||
expect(result.packageVersion).toBe("2026.4.10-beta.3");
|
||||
expect(resolveIosVersion(rootDir).marketingVersion).toBe("2026.4.10");
|
||||
});
|
||||
|
||||
it("can skip syncing checked-in artifacts when requested", () => {
|
||||
const rootDir = writeIosFixture({
|
||||
version: "2026.4.6",
|
||||
changelog: `# OpenClaw iOS Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Candidate release notes.
|
||||
`,
|
||||
versionXcconfig: "stale\n",
|
||||
releaseNotes: "stale\n",
|
||||
prefix: "openclaw-ios-pin-",
|
||||
});
|
||||
|
||||
const result = pinIosVersion({
|
||||
explicitVersion: "2026.4.8",
|
||||
fromGateway: false,
|
||||
rootDir,
|
||||
sync: false,
|
||||
});
|
||||
|
||||
expect(result.syncedPaths).toHaveLength(0);
|
||||
expect(
|
||||
fs.readFileSync(path.join(rootDir, "apps", "ios", "Config", "Version.xcconfig"), "utf8"),
|
||||
).toBe("stale\n");
|
||||
});
|
||||
});
|
||||
|
|
@ -39,9 +39,12 @@ describe("iOS Fastlane release upload gates", () => {
|
|||
const script = readFileSync(uploadScriptPath, "utf8");
|
||||
|
||||
expect(script).toContain("OPENCLAW_IOS_RELEASE_WRAPPER=1");
|
||||
expect(script).toContain("Missing required --version.");
|
||||
expect(script).toContain('"release_version:${RELEASE_VERSION}"');
|
||||
expect(script).toContain('"build_number:${BUILD_NUMBER}"');
|
||||
expect(script).toContain("DELIVER_NUMBER_OF_THREADS=1");
|
||||
expect(script).toContain("FL_MAX_NUMBER_OF_THREADS=1");
|
||||
expect(script).toContain("run_ios_fastlane ios release_upload");
|
||||
expect(script).toContain('run_ios_fastlane "${FASTLANE_ARGS[@]}"');
|
||||
});
|
||||
|
||||
it("keeps release_upload as the only Fastlane TestFlight upload implementation", () => {
|
||||
|
|
@ -57,9 +60,16 @@ describe("iOS Fastlane release upload gates", () => {
|
|||
it("rejects direct Fastlane upload before release work", () => {
|
||||
const fastfile = readFastfile();
|
||||
const releaseUpload = laneBody(fastfile, "release_upload");
|
||||
const prepareContext = laneBody(fastfile, "prepare_app_store_context");
|
||||
|
||||
expect(releaseUpload).toContain('ENV["OPENCLAW_IOS_RELEASE_WRAPPER"] == "1"');
|
||||
expect(releaseUpload).toContain("Use `pnpm ios:release:upload`");
|
||||
expect(prepareContext).toContain("options[:release_version]");
|
||||
expect(prepareContext).toContain("options[:build_number]");
|
||||
expect(prepareContext).toContain("Missing iOS release version");
|
||||
expect(releaseUpload).toContain("metadata(release_version: context[:short_version])");
|
||||
expect(laneBody(fastfile, "metadata")).toContain("options[:release_version]");
|
||||
expect(laneBody(fastfile, "metadata")).toContain("Missing iOS release version");
|
||||
expect(releaseUpload.indexOf("UI.user_error!")).toBeLessThan(
|
||||
releaseUpload.indexOf("prepare_app_store_context"),
|
||||
);
|
||||
|
|
@ -86,9 +96,20 @@ describe("iOS Fastlane release upload gates", () => {
|
|||
expect(releaseUpload).toContain("release_sha = release_git_sha");
|
||||
expect(releaseUpload).toContain("ensure_mobile_release_ref_available!");
|
||||
expect(releaseUpload).toContain("record_mobile_release_ref!");
|
||||
expect(releaseUpload).toContain(
|
||||
"screenshots(release_version: context[:version], build_number: context[:build_number])",
|
||||
);
|
||||
expect(fastfile).toContain("def without_xcode_xcconfig_file");
|
||||
expect(releaseUpload).toContain("without_xcode_xcconfig_file do");
|
||||
expect(releaseUpload.match(/sha: release_sha/g)).toHaveLength(2);
|
||||
expect(releaseUpload.indexOf("prepare_app_store_context")).toBeLessThan(
|
||||
releaseUpload.indexOf("screenshots(release_version: context[:version]"),
|
||||
);
|
||||
expect(releaseUpload.indexOf("ensure_mobile_release_ref_available!")).toBeLessThan(
|
||||
releaseUpload.indexOf("\n metadata\n"),
|
||||
releaseUpload.indexOf("screenshots(release_version: context[:version]"),
|
||||
);
|
||||
expect(releaseUpload.indexOf("ensure_mobile_release_ref_available!")).toBeLessThan(
|
||||
releaseUpload.indexOf("\n metadata(release_version: context[:short_version])\n"),
|
||||
);
|
||||
expect(releaseUpload.indexOf("record_mobile_release_ref!")).toBeGreaterThan(
|
||||
releaseUpload.indexOf("upload_to_testflight("),
|
||||
|
|
@ -98,6 +119,12 @@ describe("iOS Fastlane release upload gates", () => {
|
|||
it("normalizes Watch screenshots as opaque RGB PNGs for App Store upload", () => {
|
||||
const fastfile = readFastfile();
|
||||
|
||||
expect(laneBody(fastfile, "screenshots")).toContain(
|
||||
'File.join(repo_root, "scripts", "ios-write-version-xcconfig.sh"), *version_args',
|
||||
);
|
||||
expect(laneBody(fastfile, "watch_screenshot")).toContain(
|
||||
'File.join(repo_root, "scripts", "ios-write-version-xcconfig.sh"), *version_args',
|
||||
);
|
||||
expect(fastfile).toContain("def normalize_watch_screenshot_status_bar(path)");
|
||||
expect(fastfile).toContain("CGImageAlphaInfo.noneSkipLast.rawValue");
|
||||
expect(fastfile).toContain("CGImageDestinationCreateWithURL");
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ function runPrepare(extraArgs: string[]): { ok: boolean; stdout: string; stderr:
|
|||
|
||||
describe("scripts/ios-release-prepare.sh", () => {
|
||||
it("rejects non-canonical signing teams before generating release inputs", () => {
|
||||
const result = runPrepare(["--build-number", "7"]);
|
||||
const result = runPrepare(["--version", "2026.6.11", "--build-number", "7"]);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.stderr).toContain(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// iOS release wrapper tests keep release args fail-closed before Fastlane work.
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
|
|
@ -33,11 +34,18 @@ function runScript(
|
|||
describe("iOS release shell wrapper arguments", () => {
|
||||
const missingValueCases: readonly WrapperCase[] = [
|
||||
["scripts/ios-release-upload.sh", ["--build-number", "--bogus"], "--build-number"],
|
||||
["scripts/ios-release-upload.sh", ["--version", "--bogus"], "--version"],
|
||||
["scripts/ios-release-archive.sh", ["--build-number", "--bogus"], "--build-number"],
|
||||
["scripts/ios-release-archive.sh", ["--version", "--bogus"], "--version"],
|
||||
["scripts/ios-release-prepare.sh", ["--build-number", "--team-id"], "--build-number"],
|
||||
[
|
||||
"scripts/ios-release-prepare.sh",
|
||||
["--build-number", "7", "--team-id", "--bogus"],
|
||||
["--build-number", "7", "--version", "--bogus"],
|
||||
"--version",
|
||||
],
|
||||
[
|
||||
"scripts/ios-release-prepare.sh",
|
||||
["--version", "2026.6.11", "--build-number", "7", "--team-id", "--bogus"],
|
||||
"--team-id",
|
||||
],
|
||||
];
|
||||
|
|
@ -55,10 +63,37 @@ describe("iOS release shell wrapper arguments", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
"scripts/ios-release-upload.sh",
|
||||
"scripts/ios-release-archive.sh",
|
||||
"scripts/ios-release-prepare.sh",
|
||||
])("requires an explicit release version before release work in %s", (scriptPath) => {
|
||||
const args = scriptPath.endsWith("prepare.sh") ? ["--build-number", "7"] : [];
|
||||
const result = runScript(path.join(process.cwd(), scriptPath), args, {
|
||||
IOS_RELEASE_VERSION: "2026.6.10",
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.stderr).toContain("Missing required --version.");
|
||||
expect(result.stderr).not.toContain("No such file or directory");
|
||||
expect(result.stderr).not.toContain("fastlane");
|
||||
expect(result.stdout).toBe("");
|
||||
});
|
||||
|
||||
it.each(["scripts/ios-release-upload.sh", "scripts/ios-release-archive.sh"])(
|
||||
"does not accept ambient release build numbers in %s",
|
||||
(scriptPath) => {
|
||||
const script = readFileSync(path.join(process.cwd(), scriptPath), "utf8");
|
||||
|
||||
expect(script).toContain('BUILD_NUMBER=""');
|
||||
expect(script).not.toContain('BUILD_NUMBER="${IOS_RELEASE_BUILD_NUMBER:-}"');
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects App Store release relay URL overrides before release work", () => {
|
||||
const result = runScript(
|
||||
path.join(process.cwd(), "scripts/ios-release-prepare.sh"),
|
||||
["--build-number", "7"],
|
||||
["--version", "2026.6.11", "--build-number", "7"],
|
||||
{
|
||||
IOS_DEVELOPMENT_TEAM: "FWJYW4S8P8",
|
||||
OPENCLAW_PUSH_RELAY_BASE_URL: "https://relay.example.com",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function installIosFixtureCleanup(): void {
|
|||
}
|
||||
|
||||
export function writeIosFixture(params: {
|
||||
version: string;
|
||||
version?: string;
|
||||
changelog: string;
|
||||
packageVersion?: string;
|
||||
releaseNotes?: string;
|
||||
|
|
@ -27,12 +27,7 @@ export function writeIosFixture(params: {
|
|||
});
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "package.json"),
|
||||
`${JSON.stringify({ version: params.packageVersion ?? "2026.4.6" }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "apps", "ios", "version.json"),
|
||||
`${JSON.stringify({ version: params.version }, null, 2)}\n`,
|
||||
`${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");
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ describe("resolveIosVersion", () => {
|
|||
|
||||
it("prints selected fields from the CLI", () => {
|
||||
const rootDir = writeIosFixture({
|
||||
version: "2026.4.6",
|
||||
packageVersion: "2026.4.6",
|
||||
changelog: "# OpenClaw iOS Changelog\n\n## 2026.4.6\n\nStable notes.\n",
|
||||
});
|
||||
const result = spawnSync(
|
||||
|
|
@ -70,6 +70,35 @@ describe("resolveIosVersion", () => {
|
|||
expect(result.stderr).toBe("");
|
||||
});
|
||||
|
||||
it("prints explicit release version fields from the CLI", () => {
|
||||
const rootDir = writeIosFixture({
|
||||
packageVersion: "2026.4.6",
|
||||
changelog: "# OpenClaw iOS Changelog\n\n## 2026.4.7\n\nStable notes.\n",
|
||||
});
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
"tsx",
|
||||
"scripts/ios-version.ts",
|
||||
"--root",
|
||||
rootDir,
|
||||
"--version",
|
||||
"2026.4.7",
|
||||
"--field",
|
||||
"canonicalVersion",
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toBe("2026.4.7\n");
|
||||
expect(result.stderr).toBe("");
|
||||
});
|
||||
|
||||
it("rejects missing iOS sync CLI root values before reading version files", () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
|
|
@ -96,9 +125,9 @@ describe("resolveIosVersion", () => {
|
|||
expect(shortFlagResult.stderr).toBe("Missing value for --root.\n");
|
||||
});
|
||||
|
||||
it("parses pinned release versions and derives Apple marketing fields", () => {
|
||||
it("derives Apple marketing fields from the root package release version", () => {
|
||||
const rootDir = writeIosFixture({
|
||||
version: "2026.4.6",
|
||||
packageVersion: "2026.4.6",
|
||||
changelog: "# OpenClaw iOS Changelog\n\n## 2026.4.6\n\nStable notes.\n",
|
||||
});
|
||||
|
||||
|
|
@ -108,39 +137,38 @@ describe("resolveIosVersion", () => {
|
|||
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"),
|
||||
versionFilePath: path.join(rootDir, "apps/ios/version.json"),
|
||||
versionSource: "package",
|
||||
versionSourcePath: path.join(rootDir, "package.json"),
|
||||
versionXcconfigPath: path.join(rootDir, "apps/ios/Config/Version.xcconfig"),
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects semver-only versions", () => {
|
||||
it("rejects semver-only package versions", () => {
|
||||
const rootDir = writeIosFixture({
|
||||
version: "1.2.3",
|
||||
packageVersion: "1.2.3",
|
||||
changelog: "# OpenClaw iOS Changelog\n\n## Unreleased\n\nNotes.\n",
|
||||
});
|
||||
|
||||
expect(() => resolveIosVersion(rootDir)).toThrow(
|
||||
"Expected pinned release version like 2026.6.5",
|
||||
);
|
||||
expect(() => resolveIosVersion(rootDir)).toThrow("Expected YYYY.M.PATCH");
|
||||
});
|
||||
|
||||
it("rejects prerelease suffixes in the pinned iOS version file", () => {
|
||||
it("rejects prerelease suffixes in explicit release versions", () => {
|
||||
const rootDir = writeIosFixture({
|
||||
version: "2026.4.6-beta.1",
|
||||
packageVersion: "2026.4.6",
|
||||
changelog: "# OpenClaw iOS Changelog\n\n## Unreleased\n\nNotes.\n",
|
||||
});
|
||||
|
||||
expect(() => resolveIosVersion(rootDir)).toThrow(
|
||||
"Expected pinned release version like 2026.6.5",
|
||||
expect(() => resolveIosVersion(rootDir, { releaseVersion: "2026.4.6-beta.1" })).toThrow(
|
||||
"Expected release version like 2026.6.5",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects impossible pinned release versions", () => {
|
||||
expect(() => normalizePinnedIosVersion("2026.13.6")).toThrow(
|
||||
"Expected pinned release version like 2026.6.5",
|
||||
"Expected release version like 2026.6.5",
|
||||
);
|
||||
expect(() => normalizePinnedIosVersion("2026.4.9007199254740993")).toThrow(
|
||||
"Expected pinned release version like 2026.6.5",
|
||||
"Expected release version like 2026.6.5",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -173,7 +201,6 @@ describe("gateway version normalization", () => {
|
|||
|
||||
it("reads and normalizes the root package version for iOS releases", () => {
|
||||
const rootDir = writeIosFixture({
|
||||
version: "2026.4.6",
|
||||
packageVersion: "2026.4.7-beta.5",
|
||||
changelog: "# OpenClaw iOS Changelog\n\n## Unreleased\n\nNotes.\n",
|
||||
});
|
||||
|
|
@ -186,9 +213,9 @@ describe("gateway version normalization", () => {
|
|||
});
|
||||
|
||||
describe("renderIosVersionXcconfig", () => {
|
||||
it("renders checked-in defaults from the pinned iOS version", () => {
|
||||
it("renders checked-in defaults from the package-derived iOS version", () => {
|
||||
const rootDir = writeIosFixture({
|
||||
version: "2026.4.8",
|
||||
packageVersion: "2026.4.8",
|
||||
changelog: "# OpenClaw iOS Changelog\n\n## 2026.4.8\n\nNotes.\n",
|
||||
});
|
||||
const version = resolveIosVersion(rootDir);
|
||||
|
|
@ -202,7 +229,7 @@ describe("renderIosVersionXcconfig", () => {
|
|||
describe("release note extraction", () => {
|
||||
it("extracts exact pinned version sections first", () => {
|
||||
const rootDir = writeIosFixture({
|
||||
version: "2026.4.6",
|
||||
packageVersion: "2026.4.6",
|
||||
changelog: `# OpenClaw iOS Changelog
|
||||
|
||||
## Unreleased
|
||||
|
|
@ -222,7 +249,7 @@ Draft notes.
|
|||
|
||||
it("falls back to Unreleased when the release section does not exist yet", () => {
|
||||
const rootDir = writeIosFixture({
|
||||
version: "2026.4.6",
|
||||
packageVersion: "2026.4.6",
|
||||
changelog: `# OpenClaw iOS Changelog
|
||||
|
||||
## Unreleased
|
||||
|
|
|
|||
|
|
@ -1772,10 +1772,7 @@ describe("scripts/test-projects changed-target routing", () => {
|
|||
"scripts/lib/android-version.ts",
|
||||
["test/scripts/android-version.test.ts", "test/scripts/android-pin-version.test.ts"],
|
||||
],
|
||||
[
|
||||
"scripts/lib/ios-version.ts",
|
||||
["test/scripts/ios-version.test.ts", "test/scripts/ios-pin-version.test.ts"],
|
||||
],
|
||||
["scripts/lib/ios-version.ts", ["test/scripts/ios-version.test.ts"]],
|
||||
[
|
||||
".github/images/live-media-runner/Dockerfile",
|
||||
["test/scripts/package-acceptance-workflow.test.ts"],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue