play build: find the Android SDK like Android Studio (ANDROID_HOME, ANDROID_SDK_ROOT, sdk.dir, default folder), pass it in the env, stop before Gradle when missing; doctor shows it. apple upload: size, per-part lines, 20 s heartbeat (api, altool, processing wait), per-part and per-call timeouts, --timeout exits 7 with the resume hint. Pitfall 25

This commit is contained in:
CRTkafa 2026-10-12 08:48:02 +08:00
parent 366362d62b
commit fc524cd2d4
15 changed files with 460 additions and 34 deletions

View file

@ -14,6 +14,7 @@
// (once the group has a build), so `testers` asks first; `--remove` takes testers out of the managed groups.
// `distribute --submit-review` sends the build to Beta App Review (external groups) after a confirmation; neither
// testers nor external distribution nor the review submission are MCP tools. Pure planning: cli/lib/testflight.ts.
import { elapsed, heartbeat, type HeartbeatOptions } from '../lib/progress';
import { existsSync, readFileSync } from 'node:fs';
import type { Ctx } from '../lib/ctx';
import { confirm, need } from '../lib/ctx';
@ -51,10 +52,12 @@ export interface BuildWait { outcome: WaitOutcome; timedOut: boolean; seen: Buil
* Polls build `number` until processing ends (VALID, or FAILED / INVALID) or `timeoutMs` passes; logs each state change.
* `once` reads the state a single time. Shared by `testflight wait` and `apple upload` (which waits after the upload).
*/
export async function waitForBuild(c: Asc, appId: string, number: string, o: { marketing?: string; timeoutMs: number; once?: boolean; tag?: string }): Promise<BuildWait> {
export async function waitForBuild(c: Asc, appId: string, number: string, o: { marketing?: string; timeoutMs: number; once?: boolean; tag?: string } & HeartbeatOptions): Promise<BuildWait> {
const started = Date.now();
let delay: number | null = null, last = '';
for (;;) {
// polls are up to 2 minutes apart: a heartbeat in between says the wait is alive
const beat = o.once ? null : heartbeat(ms => log(o.tag ?? TAG, `still waiting for build ${number}: ${last || 'first check'}, ${elapsed(ms)} of ${elapsed(o.timeoutMs)}`), o);
try { for (;;) {
const seen = await seeBuild(c, appId, number, o.marketing);
const outcome = waitOutcome(seen);
const line = outcome === 'missing' ? 'not listed yet (App Store Connect lists it once processing starts)' : `${seen.processingState}${seen.internal ? `, internal ${seen.internal}` : ''}`;
@ -65,7 +68,7 @@ export async function waitForBuild(c: Asc, appId: string, number: string, o: { m
delay = waitDelay(delay);
if (Date.now() + delay - started > o.timeoutMs) return result(true);
await Bun.sleep(delay);
}
} } finally { beat?.stop(); }
}
/** The closing line for a finished wait (VALID with the next step, or FAILED / INVALID with what to do). */