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:
parent
2e7dd44ae6
commit
33eb24a3dc
17 changed files with 335 additions and 49 deletions
|
|
@ -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
12
cli/apple/submit.ts
Normal 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'); }
|
||||
Loading…
Add table
Add a link
Reference in a new issue