apple submit --build <n> (first submission, the resubmit steps; plan table with --json under serve); desktop owner actions to send for review: owner.apple_submit, owner.apple_resubmit, owner.apple_iap_submit, owner.play_production, and the price gate owner actions (owner.prices_play_products / _play_subscriptions / _apple_subscriptions; owner.prices_import passes --allow-price-changes after the dialog); OWNER_ALLOWED_ARGS ties each forbidden flag to its one action, dry runs never carry owner flags, MCP still has no submit or production release; submit dialog text in the host; system.tools androidSdk, project.inspect playRelease, declarations dataDeletion

This commit is contained in:
CRTkafa 2026-10-12 12:07:55 +08:00
parent 2e7dd44ae6
commit 33eb24a3dc
17 changed files with 335 additions and 49 deletions

View file

@ -13,6 +13,9 @@
// the item and how to resolve it (retrying never passes); only when none is left is the submit retried with backoff
// (right after a build is attached), with heartbeat lines and a 10-minute limit.
// Submitting is the owner's step: without --yes this is a dry run with the plan table. Not offered through MCP.
// `storebound apple submit --build <n>` (cli/apple/submit.ts) runs the same steps for a first submission: the version
// is --version, else the one an open submission holds, else the editable one (Prepare for Submission or rejected).
// Under `serve` (the desktop's owner action) the plan table is printed next to the --json result.
import { sellsOn } from '../lib/config';
import type { Ctx } from '../lib/ctx';
import { need } from '../lib/ctx';
@ -23,13 +26,19 @@ import { ascClient, type J } from './asc';
import { readSubmitState, submitPlanned } from './iap-submit';
import { openSubmissions } from './submission';
const TAG = 'resubmit';
/** App Store version states a first submission may send: the editable version (Prepare for Submission, a rejected
* one, or one already Ready for Review in a draft). */
export const SUBMITTABLE_APP = new Set(['PREPARE_FOR_SUBMISSION', 'READY_FOR_REVIEW', ...REJECTED_APP]);
export async function run(ctx: Ctx): Promise<void> {
export async function run(ctx: Ctx): Promise<void> { await submitVersion(ctx, 'resubmit'); }
/** apple resubmit (after a rejection) and apple submit (a version to App Review), with a build picked by number. */
export async function submitVersion(ctx: Ctx, mode: 'submit' | 'resubmit'): Promise<void> {
const TAG = mode;
const { config, root } = ctx.loaded;
const a = ctx.args;
const number = a.get('build');
if (!number || !/^\d+(\.\d+)*$/.test(number)) throw new Error('usage: apple resubmit --build <n> [--version x.y.z] [--dry] [--yes]: the build number you just uploaded (never "the latest")');
if (!number || !/^\d+(\.\d+)*$/.test(number)) throw new Error(`usage: apple ${mode} --build <n> [--version x.y.z] [--dry] [--yes]: the build number you just uploaded (never "the latest")`);
const c = await ascClient(root, need(config.apple?.bundleId, 'apple.bundleId'), config.apple?.appId);
const appId = await c.app();
const ids = [...(config.products ?? []).filter(p => sellsOn(config, p, 'apple')).map(p => p.id), ...allSubscriptions(config.subscriptions).filter(x => sellsOn(config, x.sub, 'apple')).map(x => x.sub.id)];
@ -41,8 +50,11 @@ export async function run(ctx: Ctx): Promise<void> {
const held = new Set(open.filter(s => s.state === 'UNRESOLVED_ISSUES').flatMap(s => s.items.filter(i => i.kind === 'appStoreVersion').map(i => i.versionId)));
const want = a.get('version');
const v: J | undefined = want ? versions.find((x: J) => x.attributes.versionString === want)
: versions.find((x: J) => held.has(x.id)) ?? versions.find((x: J) => REJECTED_APP.has(stateOf(x)));
if (!v) throw new Error(want ? `no App Store version ${want} on iOS` : 'no rejected App Store version and no review submission with UNRESOLVED_ISSUES: nothing to resubmit (storebound apple status)');
: versions.find((x: J) => held.has(x.id)) ?? versions.find((x: J) => (mode === 'submit' ? SUBMITTABLE_APP : REJECTED_APP).has(stateOf(x)));
if (!v) throw new Error(want ? `no App Store version ${want} on iOS`
: mode === 'submit' ? 'no App Store version to submit (none in Prepare for Submission or rejected): storebound apple listing --version x.y.z first'
: 'no rejected App Store version and no review submission with UNRESOLVED_ISSUES: nothing to resubmit (storebound apple status)');
if (mode === 'submit' && !SUBMITTABLE_APP.has(stateOf(v)) && !held.has(v.id)) throw new Error(`App Store version ${String(v.attributes.versionString)} is ${stateOf(v)}: only a version in Prepare for Submission (or a rejected one) can be submitted`);
const versionString = String(v.attributes.versionString);
if (!a.has('json')) log(TAG, `app version ${versionString}: ${stateOf(v)}`);
@ -66,6 +78,8 @@ export async function run(ctx: Ctx): Promise<void> {
const dry = a.has('dry') || !a.has('yes');
if (a.has('json')) {
printJson({ dryRun: dry, version: { id: v.id, versionString, state: stateOf(v) }, build, submission: state.choice ?? null, resolve: marks, rows, skipped: plan.skipped, blockers });
// the desktop's owner dialog shows the output text: the plan table goes with the result there
if (process.env.STOREBOUND_MACHINE === '1') { console.log(formatRows(rows)); for (const x of blockers) warn(TAG, x); }
if (blockers.length) process.exitCode = 1;
if (dry || blockers.length || state.choice?.action === 'busy') return;
} else {
@ -74,9 +88,9 @@ export async function run(ctx: Ctx): Promise<void> {
for (const x of blockers) warn(TAG, x);
}
if (blockers.length) { process.exitCode = 1; done(TAG, 'nothing sent: fix the blockers above'); return; }
if (state.choice?.action === 'busy') { done(TAG, `nothing to resubmit: ${state.choice.why}`); return; }
if (state.choice?.action === 'busy') { done(TAG, `nothing to ${mode}: ${state.choice.why}`); return; }
if (dry) {
done(TAG, a.has('dry') ? 'dry run: nothing sent' : 'dry run (no --yes): nothing sent. Resubmitting is the owner\'s step: after the owner\'s yes, run it again with --yes');
done(TAG, a.has('dry') ? 'dry run: nothing sent' : `dry run (no --yes): nothing sent. ${mode === 'submit' ? 'Submitting' : 'Resubmitting'} is the owner's step: after the owner's yes, run it again with --yes`);
return;
}

12
cli/apple/submit.ts Normal file
View file

@ -0,0 +1,12 @@
// Send an App Store version to App Review with a build picked by its number (the owner's step).
// storebound apple submit --build <n> [--version 1.0.0] [--resolve-iaps] [--dry] [--yes] [--json]
// The version is --version, else the one an open review submission holds, else the editable one (Prepare for
// Submission, or rejected). Build n (VALID) is attached, the open review submission is reused (a draft or one back
// from App Review; a new draft only when none is open), the in-app purchases and subscriptions of the config that are
// ready go in with the version, then it is submitted (retried while Apple says "not ready yet"). The same steps as
// `apple resubmit` (cli/apple/resubmit.ts). Without --yes: a dry run with the plan table. Never in MCP or flows; the
// desktop offers it as an owner action (native dialog, host-minted token).
import type { Ctx } from '../lib/ctx';
import { submitVersion } from './resubmit';
export async function run(ctx: Ctx): Promise<void> { await submitVersion(ctx, 'submit'); }