upload --pre-check/--evidence, shots copy translate, game-center --locale-map, play games API-disabled hint

This commit is contained in:
CRTkafa 2026-10-24 12:10:23 +08:00
parent 26f9559e1a
commit fb1609cba7
24 changed files with 599 additions and 27 deletions

View file

@ -1,5 +1,5 @@
// Upload an IPA to App Store Connect and wait until Apple has processed the build.
// storebound apple upload [<app.ipa>] [--build N] [--allow-existing] [--via api|altool] [--no-wait] [--timeout 60m] [--dry] [--yes] [--json]
// storebound apple upload [<app.ipa>] [--build N] [--allow-existing] [--via api|altool] [--no-wait] [--timeout 60m] [--pre-check "<cmd with {file}>"] [--evidence <path.json>] [--dry] [--yes] [--json]
// Default IPA: the newest in <apple.build.out or build/ios>/export (and its <version>-<build>/ folders), or with
// --build N the one with that build number. The bundle id, version and build number come from the IPA's Info.plist.
// An IPA whose build number is already on App Store Connect fails with exit 1 (a stale IPA left by a failed
@ -16,6 +16,7 @@
// with `apple testflight wait` (waitForBuild) until processingState is VALID (exit 0), INVALID / FAILED (exit 1) or the
// timeout (exit 7).
// An upload is a write: --dry shows the plan; a real run asks first (--yes in scripts).
import { runPreCheck } from '../lib/pre-check';
import { appleSdkProblem } from '../lib/store-minimums';
import { closeSync, existsSync, openSync, readdirSync, readSync, statSync } from 'node:fs';
import { basename, join, relative } from 'node:path';
@ -251,6 +252,7 @@ export async function run(ctx: Ctx): Promise<void> {
// upload when NSPrivacyTracking and NSPrivacyTrackingDomains disagree (ITMS-91064), so stop before any network call
const privacy = bundleTrackingIssues(ipaManifests(ipa));
if (privacy.length) throw new Error(`${relative(root, ipa)}: not uploaded, Apple would refuse it as Invalid Binary:\n${privacy.map(x => ` ${x}`).join('\n')}\nFix the manifest, rebuild (storebound apple build) and upload the new IPA.`);
const pre = await runPreCheck({ args: a, config, root, resolve, file: ipa, shown: relative(root, ipa), tag: TAG });
const size = statSync(ipa).size;
const dry = a.has('dry');
const json = a.has('json');
@ -294,7 +296,7 @@ export async function run(ctx: Ctx): Promise<void> {
? [describeReq(reqs.upload), describeReq(reqs.file('<upload id>')), `PUT the file parts to the upload URLs Apple returns`, describeReq(reqs.commit('<file id>', '<md5>')), 'GET /v1/buildUploads/<id> until COMPLETE']
: [showCmd(altoolArgs(ipa, { keyPath: '$ASC_P8', keyId: '$ASC_KEY_ID', issuerId: '$ASC_ISSUER_ID' }), null)];
if (wait) plan.push(`GET /v1/builds?filter[version]=${info.build} until processingState is VALID (timeout ${a.str('timeout', '60m')})`);
const summary = { ipa: relative(root, ipa), bundleId: info.bundleId, version: info.version, build: info.build, sizeMB: Math.round(size / 104857.6) / 10, via, plan };
const summary = { ipa: relative(root, ipa), bundleId: info.bundleId, version: info.version, build: info.build, sizeMB: Math.round(size / 104857.6) / 10, via, plan, ...(pre ? { preCheck: pre } : {}) };
if (dry) {
if (json) { printJson(summary); return; }
log(TAG, `${summary.ipa}: ${info.bundleId} ${info.version} (${info.build}), ${summary.sizeMB} MB, build number free`);