apple build/upload: accept --allow-version-mismatch (flag list regenerated); 1.0 and 1.0.0 are one version train

This commit is contained in:
CRTkafa 2026-10-23 09:57:13 +08:00
parent ebd64e0f76
commit c7bae75ed0
4 changed files with 20 additions and 2 deletions

View file

@ -3,6 +3,8 @@
## Unreleased
- `apple build` / `apple upload`: `--allow-version-mismatch` is accepted (it was missing from the flag list); 1.0 and 1.0.0 count as the same version train, as on App Store Connect.
- Desktop: custom context menus everywhere (apps, releases, art, reviews, checks, fields, products, accounts, folders) with keyboard navigation, Shift+F10 and submenus; the WebView2 browser menu and browser shortcuts are off.
- Desktop: each app shows once again (a folder project no longer appears a second time as a store-only app); a store app with a folder's bundle id or package is never listed separately.
- Desktop UX pass on every screen: app tabs fit the window, slim app header, secondary actions in "…" menus, Ctrl+K lists app actions, Alt+Up/Down switches apps, accounts and folders decluttered.

View file

@ -68,6 +68,7 @@
"apple build": [
"allow-ssh",
"allow-stale-web",
"allow-version-mismatch",
"build-number",
"configuration",
"keychain",
@ -304,6 +305,7 @@
],
"apple upload": [
"allow-existing",
"allow-version-mismatch",
"build",
"csv",
"group",
@ -648,11 +650,17 @@
"allow-stale-web",
"dir",
"flavor",
"include-drafts",
"module",
"no-prebuild",
"not-sent-for-review",
"out",
"prune",
"src",
"variant",
"version-code",
"version-name"
"version-name",
"write"
],
"play data-safety": [
"csv",
@ -1018,6 +1026,7 @@
"allow-price-changes",
"allow-ssh",
"allow-stale-web",
"allow-version-mismatch",
"alpha",
"apk",
"app",
@ -1265,6 +1274,7 @@
"storebound accounts [list] [--json] every profile: Apple key, Play account (masked), which",
"storebound accounts import [--dir <project>] [--name <profile>] [--dry] [--yes]",
"storebound accounts use <profile> [--dir <project>] this project uses that profile (\"account\" in its",
"storebound accounts play-developer <profile> <id> | --clear",
"storebound accounts default <profile> | --clear the machine's default profile (a pointer file in the"
],
"ads": [

View file

@ -177,7 +177,9 @@ export function ipaInfo(ipa: string | Buffer): IpaInfo {
* that ignores the version storebound or the release script intends exports MARKETING_VERSION 1.0), else null.
* `wanted` is the --version of apple build or the editable App Store version; `source` names where it comes from. Pure. */
export function ipaVersionProblem(ipaVersion: string, wanted: string | null | undefined, source: string): string | null {
if (!wanted || ipaVersion === wanted) return null;
// Apple compares version strings numerically with missing parts as 0: 1.0 and 1.0.0 are the same train
const norm = (v: string) => v.trim().split('.').map(n => String(Number(n))).join('.').replace(/(\.0)+$/, '');
if (!wanted || norm(ipaVersion) === norm(wanted)) return null;
return `the IPA has version ${ipaVersion} (CFBundleShortVersionString), but ${source} is ${wanted}: the Xcode project sets MARKETING_VERSION itself, so the build lands in the wrong version train. Make the project read the intended version (xcodebuild MARKETING_VERSION=${wanted}, or set it in the project / Info.plist), build again, or pass --allow-version-mismatch to go on anyway.`;
}

View file

@ -559,6 +559,10 @@ describe('signed AAB, masked signing, IPA version train, release notes', () => {
test('the IPA version must be the version train being prepared', () => {
expect(ipaVersionProblem('1.0', '1.4.0', 'the editable App Store version')).toContain('--allow-version-mismatch');
expect(ipaVersionProblem('1.0', '1.4.0', 'x')).toContain('MARKETING_VERSION=1.4.0');
// Apple compares numerically: 1.0 and 1.0.0 are one train
expect(ipaVersionProblem('1.0.0', '1.0', 'x')).toBeNull();
expect(ipaVersionProblem('2.1', '2.1.0', 'x')).toBeNull();
expect(ipaVersionProblem('2.1.1', '2.1', 'x')).not.toBeNull();
expect(ipaVersionProblem('1.4.0', '1.4.0', 'x')).toBeNull();
expect(ipaVersionProblem('1.0', undefined, 'x')).toBeNull();
});