diff --git a/.github/workflows/store-delivery.yml b/.github/workflows/store-delivery.yml index 08e53468..5f07b0fd 100644 --- a/.github/workflows/store-delivery.yml +++ b/.github/workflows/store-delivery.yml @@ -311,7 +311,9 @@ jobs: log_file="$RUNNER_TEMP/xcode-cloud-control.log" # Manual Xcode Cloud workflows reject tags that are not in the start # condition. Start the Archive workflow on its configured default - # (main) and require the iOS runtime tree to match this Release. + # (main) and require that source's marketing version matches this + # Release. The Git tag itself may still contain a stale project.yml + # that xcodegen would archive as the previous live version. python3 .edgeever-ci/scripts/xcode-cloud-control.py start \ --wait \ --wait-valid | @@ -322,10 +324,14 @@ jobs: source_sha="$(grep -E '^source_sha=' "$log_file" | tail -1 | cut -d= -f2)" test -n "$source_sha" git fetch --no-tags origin "$source_sha" - changed="$(git diff --name-only "$source_sha" HEAD -- apps/ios packages/shared)" - if [[ -n "$changed" ]]; then - echo "Xcode Cloud source $source_sha differs from Release $(git rev-parse HEAD) in iOS runtime files:" >&2 - echo "$changed" >&2 + cloud_version="$( + git show "$source_sha:apps/ios/Config/Version.xcconfig" | + sed -nE 's/^MARKETING_VERSION[[:space:]]*=[[:space:]]*([^[:space:]]+).*/\1/p' | + head -1 + )" + release_version="${RELEASE_TAG#v}" + if [[ "$cloud_version" != "$release_version" ]]; then + echo "Xcode Cloud source $source_sha MARKETING_VERSION=$cloud_version does not match $RELEASE_TAG." >&2 exit 1 fi diff --git a/apps/ios/EdgeEver.xcodeproj/project.pbxproj b/apps/ios/EdgeEver.xcodeproj/project.pbxproj index 6e2d80cc..ea5c3918 100644 --- a/apps/ios/EdgeEver.xcodeproj/project.pbxproj +++ b/apps/ios/EdgeEver.xcodeproj/project.pbxproj @@ -795,7 +795,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.0; - MARKETING_VERSION = 1.74.0; + MARKETING_VERSION = 1.79.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -885,7 +885,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.0; - MARKETING_VERSION = 1.74.0; + MARKETING_VERSION = 1.79.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; diff --git a/apps/ios/ci_scripts/ci_pre_xcodebuild.sh b/apps/ios/ci_scripts/ci_pre_xcodebuild.sh index a1ec0db7..da089914 100755 --- a/apps/ios/ci_scripts/ci_pre_xcodebuild.sh +++ b/apps/ios/ci_scripts/ci_pre_xcodebuild.sh @@ -16,6 +16,12 @@ cd "$IOS_ROOT" # Cloud stamps CFBundleVersion from App Store Connect "next build number". # Mirror that into Version.xcconfig so the archive and any tooling agree. +# Also copy MARKETING_VERSION into the generated pbxproj so xcodegen cannot +# keep a stale CFBundleShortVersionString from project.yml. +MARKETING_VERSION="" +if [ -f Config/Version.xcconfig ]; then + MARKETING_VERSION="$(sed -nE 's/^MARKETING_VERSION[[:space:]]*=[[:space:]]*([^[:space:]]+).*/\1/p' Config/Version.xcconfig | head -1)" +fi if [ -n "${CI_BUILD_NUMBER:-}" ]; then echo "CI_BUILD_NUMBER=$CI_BUILD_NUMBER" if [ -f Config/Version.xcconfig ]; then @@ -24,16 +30,17 @@ if [ -n "${CI_BUILD_NUMBER:-}" ]; then echo "Stamped CURRENT_PROJECT_VERSION=${CI_BUILD_NUMBER} into Config/Version.xcconfig" grep -E '^(MARKETING_VERSION|CURRENT_PROJECT_VERSION)' Config/Version.xcconfig || true fi - # Also inject into the generated project settings if present if [ -f EdgeEver.xcodeproj/project.pbxproj ]; then - /usr/libexec/PlistBuddy -c "Print" EdgeEver.xcodeproj/project.pbxproj >/dev/null 2>&1 || true - # pbxproj is not a plist; use sed for CURRENT_PROJECT_VERSION assignments sed -i '' -E "s/(CURRENT_PROJECT_VERSION = )[0-9]+;/\\1${CI_BUILD_NUMBER};/g" EdgeEver.xcodeproj/project.pbxproj echo "Stamped CURRENT_PROJECT_VERSION in project.pbxproj" fi else echo "CI_BUILD_NUMBER unset (local dry-run); leaving Version.xcconfig unchanged" fi +if [ -n "$MARKETING_VERSION" ] && [ -f EdgeEver.xcodeproj/project.pbxproj ]; then + sed -i '' -E "s/(MARKETING_VERSION = )[^;]+;/\\1${MARKETING_VERSION};/g" EdgeEver.xcodeproj/project.pbxproj + echo "Stamped MARKETING_VERSION=${MARKETING_VERSION} in project.pbxproj" +fi # Defensive: ensure Automatic signing so Cloud can create Dev + Distribution profiles. # (project.yml already prefers Automatic; this repairs accidental Manual settings.) diff --git a/apps/ios/project.yml b/apps/ios/project.yml index 6ec04986..db00c183 100644 --- a/apps/ios/project.yml +++ b/apps/ios/project.yml @@ -20,8 +20,9 @@ settings: TARGETED_DEVICE_FAMILY: "1" SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: false IPHONEOS_DEPLOYMENT_TARGET: "17.0" - CURRENT_PROJECT_VERSION: 49 - MARKETING_VERSION: 1.74.0 + # MARKETING_VERSION and CURRENT_PROJECT_VERSION live only in + # Config/Version.xcconfig. Hardcoding them here makes xcodegen overwrite + # the Release tag during ci_post_clone. # Automatic signing: required for Xcode Cloud (managed certs/profiles for # App Store + Development exports). Local ExportOptions.plist still pins # App Store profiles for Scripts/archive-app-store.sh export. diff --git a/docs/ios-xcode-cloud.md b/docs/ios-xcode-cloud.md index b099e036..2a604bb5 100644 --- a/docs/ios-xcode-cloud.md +++ b/docs/ios-xcode-cloud.md @@ -63,7 +63,12 @@ Xcode Cloud **overwrites** `CFBundleVersion` with its managed counter: Rules: 1. Next Cloud number must be **greater than** every build already on ASC for this app. -2. Keep `apps/ios/Config/Version.xcconfig` → `CURRENT_PROJECT_VERSION` in the same ballpark (the repo stamps Cloud’s `CI_BUILD_NUMBER` into the archive via `ci_pre_xcodebuild.sh`). +2. Keep `apps/ios/Config/Version.xcconfig` as the only marketing-version source. + Do not hardcode `MARKETING_VERSION` in `project.yml`; Cloud runs `xcodegen` + in `ci_post_clone`, and a stale YAML value would archive the previous App + Store version. `ci_pre_xcodebuild.sh` copies the xcconfig version into the + generated pbxproj and stamps Cloud’s `CI_BUILD_NUMBER` into + `CURRENT_PROJECT_VERSION`. 3. Before a store run, from a machine with API credentials: ```sh @@ -92,10 +97,11 @@ Rules: Formal Releases do this automatically when `apps/ios` or the shared editor runtime changed: `bun run release` bumps `MARKETING_VERSION`, starts the Manual -Xcode Cloud Archive workflow, requires the Cloud source to match the Release -tag's iOS runtime tree, waits until the build is **Valid**, and submits App +Xcode Cloud Archive workflow, requires the Cloud source's marketing version +to match the Release tag, waits until the build is **Valid**, and submits App Review. The Cloud workflow stays Manual, so Apple rejects starting it on a -tag that is not in the start condition. +tag that is not in the start condition. `project.yml` must not hardcode +`MARKETING_VERSION`; Cloud regenerates the Xcode project during `ci_post_clone`. To retry or ship iOS without a new GitHub Release: @@ -132,6 +138,7 @@ On **macOS beta**, do **not** upload that IPA; use Xcode Cloud instead. `archive | Cloud fails in post-clone on bun/xcodegen | Network / Homebrew on Cloud | Re-run; inspect `ci_post_clone` log | | Editor blank in app | EditorBundle not built | Confirm `ci_post_clone` log shows bundle build | | Hours running out | Workflow not Manual | Disable push/PR start conditions | +| PrepareBuildForAppStoreConnect fails with the previous live version (for example 1.74.0) | `project.yml` hardcoded `MARKETING_VERSION` and `xcodegen` overwrote `Version.xcconfig` | Keep marketing version only in `Config/Version.xcconfig`; `ci_pre_xcodebuild.sh` must stamp it into the pbxproj | ## Related diff --git a/docs/store-delivery.md b/docs/store-delivery.md index fd19b327..0a288295 100644 --- a/docs/store-delivery.md +++ b/docs/store-delivery.md @@ -141,7 +141,7 @@ On macOS beta hosts, archives must go through **Xcode Cloud** so `BuildMachineOSBuild` is a release OS image — see [iOS Xcode Cloud](ios-xcode-cloud.md). The store-delivery workflow starts that Manual Archive workflow on its configured default branch, requires the Cloud -source to match the Release tag's iOS runtime tree, waits until App Store Connect marks the +source's `MARKETING_VERSION` to match the Release tag, waits until App Store Connect marks the build Valid, then Fastlane (`apps/ios` `submit_review`) submits App Review and configures automatic release after approval. Pass `--ios-build-number` only to reuse an already uploaded build. Missing metadata, agreements, review diff --git a/docs/store-delivery.zh-CN.md b/docs/store-delivery.zh-CN.md index c7a417ae..e2ea4ae0 100644 --- a/docs/store-delivery.zh-CN.md +++ b/docs/store-delivery.zh-CN.md @@ -125,7 +125,7 @@ Release。默认命令直接使用 Production;只有明确要求测试交付 在 macOS beta 本机上,Archive 必须通过 **Xcode Cloud**,保证 `BuildMachineOSBuild` 来自正式系统镜像——见 [iOS Xcode Cloud](ios-xcode-cloud.md)。商店交付工作流会按 Archive 工作流配置的 -默认分支启动这次手动构建,并要求 Cloud 源码与 Release tag 的 iOS 运行时一致,等到 App Store +默认分支启动这次手动构建,并要求 Cloud 源码的 MARKETING_VERSION 与 Release tag 一致,等到 App Store Connect 将构建标为 Valid,再用 Fastlane(`apps/ios` 的 `submit_review`)提交 App Review,并设置为审核通过后自动发布。只有复用已经上传的构建时才传入 `--ios-build-number`。元数据、协议、审核信息或凭据不完整时 diff --git a/scripts/store-delivery.test.mjs b/scripts/store-delivery.test.mjs index 5f93baea..de0cb72d 100644 --- a/scripts/store-delivery.test.mjs +++ b/scripts/store-delivery.test.mjs @@ -78,7 +78,8 @@ describe("store delivery command", () => { expect(workflow).toContain("--wait-valid"); expect(workflow).not.toContain("--require-sha"); expect(workflow).not.toContain("--git-ref"); - expect(workflow).toContain("git diff --name-only \"$source_sha\" HEAD -- apps/ios packages/shared"); + expect(workflow).toContain("git show \"$source_sha:apps/ios/Config/Version.xcconfig\""); + expect(workflow).toContain("cloud_version"); expect(workflow).toContain("grep -E '^(build_number|app_store_build_id|build_run_id|source_sha|processing_state)='"); expect(workflow).toContain('refs/tags/${RELEASE_TAG}'); expect(workflow).toContain( diff --git a/scripts/xcode-cloud-control.test.mjs b/scripts/xcode-cloud-control.test.mjs index 74a960e9..2eb93313 100644 --- a/scripts/xcode-cloud-control.test.mjs +++ b/scripts/xcode-cloud-control.test.mjs @@ -29,11 +29,37 @@ describe("Xcode Cloud control", () => { expect(workflow).toContain(".edgeever-ci/scripts/xcode-cloud-control.py"); expect(workflow).toContain("--wait-valid"); - expect(workflow).toContain("git diff --name-only"); + expect(workflow).toContain("git show \"$source_sha:apps/ios/Config/Version.xcconfig\""); expect(cloudWorkflow).toContain("scripts/xcode-cloud-control.py"); expect(cloudWorkflow).toContain("describe"); expect(cloudWorkflow).not.toContain("submit-review"); expect(cloudWorkflow).not.toContain('APP_STORE_BUILD_NUMBER: "49"'); }); + + test("keeps iOS marketing version in Version.xcconfig instead of project.yml", () => { + const projectYml = readFileSync( + new URL("../apps/ios/project.yml", import.meta.url), + "utf8", + ); + const preXcodebuild = readFileSync( + new URL("../apps/ios/ci_scripts/ci_pre_xcodebuild.sh", import.meta.url), + "utf8", + ); + const pbxproj = readFileSync( + new URL("../apps/ios/EdgeEver.xcodeproj/project.pbxproj", import.meta.url), + "utf8", + ); + const xcconfig = readFileSync( + new URL("../apps/ios/Config/Version.xcconfig", import.meta.url), + "utf8", + ); + const marketing = xcconfig.match(/^MARKETING_VERSION\s*=\s*(\S+)/m)?.[1]; + + expect(projectYml).not.toMatch(/MARKETING_VERSION\s*:/); + expect(preXcodebuild).toContain("Stamped MARKETING_VERSION="); + expect(marketing).toBeTruthy(); + expect(pbxproj).not.toContain("MARKETING_VERSION = 1.74.0;"); + expect(pbxproj).toContain(`MARKETING_VERSION = ${marketing};`); + }); });