Ship-step contract for pipelines: uniform exit codes (0/1/2/3/4/7) in cli/lib/ship-step.ts, one final --json line, --result-file (atomic, also on failure), --progress json, --env-file with secret redaction, --version-from; idempotent play upload / apple upload / play release / apple submit re-runs end with exit 7; play upload takes --track production and --version-code
This commit is contained in:
parent
4eac8e8cd8
commit
06a0b424f5
14 changed files with 928 additions and 24 deletions
|
|
@ -35,6 +35,14 @@ import { elapsed, heartbeat, type HeartbeatOptions, mb, share, withHeartbeat } f
|
|||
import { pump } from '../lib/runner';
|
||||
import * as rt from '../lib/rt';
|
||||
import { partHeaders } from './screenshot-health';
|
||||
import { SHIP_EXIT, ShipStepError, shipNoop, shipPhase, shipReport, shipRun } from '../lib/ship-step';
|
||||
import { consoleUrl } from '../lib/console-links';
|
||||
|
||||
/** Ends the run with a legacy exit code; under the ship-step contract a ShipStepError with the uniform code instead. */
|
||||
function stop(legacy: number, message: string): never {
|
||||
if (shipRun()) throw legacy === BUILD_WAIT_EXIT.timeout ? new ShipStepError(message, SHIP_EXIT.failure, 'TIMEOUT') : new ShipStepError(message, SHIP_EXIT.refused, 'BUILD_INVALID', consoleUrl('asc.testflight'));
|
||||
process.exit(legacy);
|
||||
}
|
||||
|
||||
const TAG = 'upload';
|
||||
|
||||
|
|
@ -124,6 +132,7 @@ export async function sendParts(o: SendPartsOptions): Promise<void> {
|
|||
await sleep(2000 * attempt);
|
||||
}
|
||||
sent += op.length;
|
||||
shipPhase('upload', `${i + 1}/${n} parts`, (sent / o.size) * 100);
|
||||
if (n < 20 || (i + 1) % 10 === 0 || i === n - 1) say(`${i + 1}/${n} parts, ${share(sent, o.size)}, ${elapsed(now() - started)}`);
|
||||
}
|
||||
} finally { beat.stop(); }
|
||||
|
|
@ -230,7 +239,9 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
if (!found[0]) throw new Error(`usage: storebound apple upload <app.ipa> (no ${want ? `build ${want} ` : ''}IPA in ${relative(root, dir)}; storebound apple build makes one)`);
|
||||
ipa = found[0];
|
||||
} else ipa = resolve(ipa);
|
||||
shipPhase('verify', `checking ${relative(root, ipa)}`);
|
||||
const info = ipaInfo(ipa);
|
||||
shipReport({ version: info.version, build: info.build, url: consoleUrl('asc.testflight') });
|
||||
if (want && info.build !== want) throw new Error(`${relative(root, ipa)} is build ${info.build}, not ${want} (--build): did the last apple build fail?`);
|
||||
if (info.bundleId !== bundleId) throw new Error(`${relative(root, ipa)} is ${info.bundleId}, the config says ${bundleId}`);
|
||||
// App Store Connect refuses IPAs from an Xcode / SDK older than its current minimum (cli/lib/store-minimums.ts)
|
||||
|
|
@ -260,6 +271,14 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const already = via === 'api' && !dry ? (await uploadsOf(c, appId, info).catch(() => [])).find((u: J) => u.attributes?.state?.state === 'COMPLETE') : undefined;
|
||||
const onAsc = builds.find(b => String(b.attributes.version) === info.build);
|
||||
const decision = existingBuild({ ipa: relative(root, ipa), build: info.build, onAsc: !!onAsc, state: onAsc?.attributes?.processingState, uploadComplete: !!already, allowExisting: a.has('allow-existing'), next });
|
||||
// the ship-step contract: a build number App Store Connect has already is "nothing to do" (exit 7), never a second upload
|
||||
if (shipRun() && (onAsc || already)) {
|
||||
const state = String(onAsc?.attributes?.processingState ?? 'UPLOADED');
|
||||
shipNoop({ state, url: consoleUrl('asc.testflight') });
|
||||
if (json) printJson({ ipa: relative(root, ipa), version: info.version, build: info.build, uploaded: true, alreadyUploaded: true, processingState: state });
|
||||
done(TAG, `build ${info.version} (${info.build}) is on App Store Connect already (${state}): nothing uploaded`);
|
||||
return;
|
||||
}
|
||||
if (decision instanceof Error) throw decision;
|
||||
if (decision === 'recheck') {
|
||||
log(TAG, `build ${info.version} (${info.build}) is on App Store Connect already${already ? ` (build upload ${already.id})` : ''}; --allow-existing: not sending it again`);
|
||||
|
|
@ -267,7 +286,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const r = await waitForBuild(c, appId, info.build, { marketing: info.version, timeoutMs: timeout, tag: TAG });
|
||||
if (json) printJson({ ipa: relative(root, ipa), version: info.version, build: info.build, uploaded: true, alreadyUploaded: true, processingState: r.seen.processingState ?? null });
|
||||
if (r.outcome === 'valid') { done(TAG, waitMessage(info.build, r)); return; }
|
||||
process.exit(r.outcome === 'failed' ? BUILD_WAIT_EXIT.failed : BUILD_WAIT_EXIT.timeout);
|
||||
stop(r.outcome === 'failed' ? BUILD_WAIT_EXIT.failed : BUILD_WAIT_EXIT.timeout, waitMessage(info.build, r));
|
||||
}
|
||||
if (numbers.length && compareBuild(info.build, next) < 0) warn(TAG, `build ${info.build} is lower than the highest uploaded one (next free: ${next}); Apple accepts it only if this version train has no higher build`);
|
||||
const reqs = buildUploadReqs(appId, info, basename(ipa), size);
|
||||
|
|
@ -285,19 +304,21 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
}
|
||||
if (!(await confirm(ctx, `Upload ${info.bundleId} ${info.version} (${info.build}) to App Store Connect? It shows up in TestFlight once processed.`))) return;
|
||||
const deadline = Date.now() + timeout;
|
||||
shipPhase('upload', `uploading ${mb(size)} via ${via}`, 0);
|
||||
log(TAG, `${relative(root, ipa)}: ${info.bundleId} ${info.version} (${info.build}), ${mb(size)}, via ${via}, timeout ${a.str('timeout', '60m')}`);
|
||||
if (via === 'api') {
|
||||
try { await apiUpload(c, appId, ipa, info, deadline); } catch (e) { if ((e as { exit?: number }).exit) { warn(TAG, (e as Error).message); process.exit((e as { exit: number }).exit); } throw e; }
|
||||
try { await apiUpload(c, appId, ipa, info, deadline); } catch (e) { if ((e as { exit?: number }).exit) { warn(TAG, (e as Error).message); stop((e as { exit: number }).exit, (e as Error).message); } throw e; }
|
||||
} else {
|
||||
try { await altoolUpload(root, ipa, deadline); } catch (e) { if ((e as { exit?: number }).exit) { warn(TAG, (e as Error).message); process.exit((e as { exit: number }).exit); } throw e; }
|
||||
try { await altoolUpload(root, ipa, deadline); } catch (e) { if ((e as { exit?: number }).exit) { warn(TAG, (e as Error).message); stop((e as { exit: number }).exit, (e as Error).message); } throw e; }
|
||||
}
|
||||
if (!wait) { if (json) printJson({ ...summary, uploaded: true }); done(TAG, `build ${info.build} uploaded; storebound apple status shows when it is VALID`); return; }
|
||||
// the same poll as `storebound apple testflight wait`, within what is left of --timeout
|
||||
shipPhase('processing', `waiting for App Store Connect to process build ${info.build}`);
|
||||
const r = await waitForBuild(c, appId, info.build, { marketing: info.version, timeoutMs: Math.max(0, deadline - Date.now()), tag: TAG });
|
||||
const state = r.timedOut || (r.outcome !== 'valid' && r.outcome !== 'failed') ? 'TIMEOUT' : r.seen.processingState ?? 'UNKNOWN';
|
||||
if (json) printJson({ ...summary, uploaded: true, processingState: state });
|
||||
if (r.outcome === 'valid') { done(TAG, `${waitMessage(info.build, r)}. Then: apple testflight distribute --build ${info.build} --group <name> --dry`); return; }
|
||||
if (state === 'TIMEOUT') { warn(TAG, `build ${info.build} not processed after ${a.str('timeout', '60m')}; keep waiting with storebound apple testflight wait --build ${info.build} --version ${info.version}`); process.exit(BUILD_WAIT_EXIT.timeout); }
|
||||
if (state === 'TIMEOUT') { warn(TAG, `build ${info.build} not processed after ${a.str('timeout', '60m')}; keep waiting with storebound apple testflight wait --build ${info.build} --version ${info.version}`); stop(BUILD_WAIT_EXIT.timeout, `build ${info.build} not processed after ${a.str('timeout', '60m')}`); }
|
||||
warn(TAG, `${waitMessage(info.build, r)} (Apple emails the ITMS-xxxxx reason to the team)`);
|
||||
process.exit(BUILD_WAIT_EXIT.failed);
|
||||
stop(BUILD_WAIT_EXIT.failed, `${waitMessage(info.build, r)} (Apple emails the ITMS-xxxxx reason to the team)`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue