fix(ios): stop xcodegen from archiving the previous App Store version

Cloud #50 exported 1.74.0 because project.yml hardcoded MARKETING_VERSION
and ci_post_clone regenerated the Xcode project from that YAML. Keep the
version in Version.xcconfig and stamp it into the pbxproj before archive.
This commit is contained in:
tianma-if committed 2026-09-20 23:57:33 +08:00
1 parent 5880699347
commit fabfede7e0
9 files changed
+68 -20

No files matched your search

+11 -5
View File
@@ -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
+2 -2
View File
@@ -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;
+10 -3
View File
@@ -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.)
+3 -2
View File
@@ -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.
+11 -4
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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`。元数据、协议、审核信息或凭据不完整时
+2 -1
View File
@@ -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(
+27 -1
View File
@@ -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};`);
});
});