apple.macos listing / review notes per platform + iOS-only claim warnings; apple upload of a .pkg: bounded pkgutil, macOS processing wait, heartbeats; release 409 pattern

This commit is contained in:
CRTkafa 2026-11-03 23:30:59 +07:00
parent 53d57544bf
commit 1e47c2ee60
18 changed files with 417 additions and 63 deletions

View file

@ -37,8 +37,8 @@ const TAG = 'testflight';
const USAGE = 'usage: storebound apple testflight status | groups [sync] | testers | what-to-test --build N | info | distribute --build N --group <names> | wait --build N | expire --keep N | --older-than D';
/** The state of build `number` (of `marketing` version) right now; found: false while App Store Connect does not list it. */
async function seeBuild(c: Asc, appId: string, number: string, marketing?: string): Promise<BuildSeen & { version?: string }> {
const q = new URLSearchParams({ 'filter[app]': appId, 'filter[version]': number, 'filter[preReleaseVersion.platform]': 'IOS', include: 'preReleaseVersion,buildBetaDetail', limit: '50' });
async function seeBuild(c: Asc, appId: string, number: string, marketing?: string, platform = 'IOS'): Promise<BuildSeen & { version?: string }> {
const q = new URLSearchParams({ 'filter[app]': appId, 'filter[version]': number, 'filter[preReleaseVersion.platform]': platform, include: 'preReleaseVersion,buildBetaDetail', limit: '50' });
if (marketing) q.set('filter[preReleaseVersion.version]', marketing);
const resp = await c.asc('GET', `/v1/builds?${q}`);
const hits = buildsWithVersion(resp).filter(b => b.attributes.version === number && (!marketing || b.marketing === marketing));
@ -54,13 +54,13 @@ 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 } & HeartbeatOptions): Promise<BuildWait> {
export async function waitForBuild(c: Asc, appId: string, number: string, o: { marketing?: string; platform?: 'IOS' | 'MAC_OS'; timeoutMs: number; once?: boolean; tag?: string } & HeartbeatOptions): Promise<BuildWait> {
const started = Date.now();
let delay: number | null = null, last = '';
// 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 seen = await seeBuild(c, appId, number, o.marketing, o.platform);
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}` : ''}`;
if (line !== last) { log(o.tag ?? TAG, `build ${number}${seen.version ? ` (${seen.version})` : ''}: ${line} [${Math.round((Date.now() - started) / 60_000)} min]`); last = line; }