From 722613440fdb5a5d0f6c719315975d97cf322674 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Sun, 9 Aug 2026 16:59:06 -0400 Subject: [PATCH] fix(release): strip semver prerelease suffix from framework CFBundleVersion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CFBundleVersion accepts at most three period-separated non-negative integers, so a prerelease version like `0.19.0-alpha.0` is rejected by altool with error 90058. Prerelease identifiers are alphanumeric by spec, so no substitution satisfies both grammars — the suffix has to be dropped. Stamp CFBundleVersion with the `major.minor.patch` core while leaving CFBundleShortVersionString on the full version, which Apple accepts and which keeps the exact release visible in the shipped framework. Reusing a core version across an alpha and its final release is safe: artifacts are identified by git SHA and by CDN URL plus SwiftPM checksum, never by this key. Also assert Apple's grammar in verify_framework_plist. The gate only checked that the plist matched package.json, so an illegal-but-matching value passed here and failed three hops downstream in a consumer's TestFlight upload — exactly what the gate exists to prevent. Co-Authored-By: Claude Opus 5 (1M context) --- build_xcframework.sh | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/build_xcframework.sh b/build_xcframework.sh index 33e9d4986..f877788ae 100755 --- a/build_xcframework.sh +++ b/build_xcframework.sh @@ -39,6 +39,19 @@ if [[ -z "${MARKETING_VERSION}" ]]; then echo "Error: could not read the version field from package.json (is 'jq' installed and is this the repo root?)" >&2 exit 1 fi + +# CFBundleVersion allows at most three period-separated non-negative integers, +# so a SemVer prerelease like `0.19.0-alpha.0` is rejected (altool 90058). No +# substitution helps — prerelease identifiers are alphanumeric by spec — so +# strip the prerelease (`-...`) and build-metadata (`+...`) suffixes and stamp +# the `major.minor.patch` core. CFBundleShortVersionString keeps the full +# version, which Apple accepts. +# +# Reusing a core version across an alpha and its final release is safe: +# artifacts are identified by git SHA and by CDN URL plus SwiftPM checksum, +# never by this key. +BUNDLE_VERSION="${MARKETING_VERSION%%[-+]*}" + BUILD_DIR="$(pwd)/build" DERIVED_DATA_PATH="${BUILD_DIR}/DerivedData" @@ -173,7 +186,7 @@ build_framework() { CFBundleShortVersionString ${MARKETING_VERSION} CFBundleVersion - ${MARKETING_VERSION} + ${BUNDLE_VERSION} MinimumOSVersion ${MINIMUM_IOS_VERSION} @@ -279,8 +292,14 @@ verify_framework_plist() { echo "Error: ${plist}: CFBundleShortVersionString='${short_version}' does not match package.json version '${MARKETING_VERSION}'" >&2 exit 1 fi - if [[ "${bundle_version}" != "${MARKETING_VERSION}" ]]; then - echo "Error: ${plist}: CFBundleVersion='${bundle_version}' does not match package.json version '${MARKETING_VERSION}'" >&2 + if [[ "${bundle_version}" != "${BUNDLE_VERSION}" ]]; then + echo "Error: ${plist}: CFBundleVersion='${bundle_version}' does not match expected '${BUNDLE_VERSION}' (derived from package.json version '${MARKETING_VERSION}')" >&2 + exit 1 + fi + # Assert Apple's grammar directly, not just agreement with package.json: a + # value can track the release perfectly and still be rejected on upload. + if [[ ! "${bundle_version}" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then + echo "Error: ${plist}: CFBundleVersion='${bundle_version}' must be at most three period-separated non-negative integers (altool 90058)" >&2 exit 1 fi if [[ "${min_os}" != "${MINIMUM_IOS_VERSION}" ]]; then