apple submit --yes submits a draft prepared with --no-submit that already holds the version and items (was 'nothing to submit'); iap-submit the same for a draft holding the asked-for items; test, pitfall 90, CHANGELOG

This commit is contained in:
CRTkafa 2026-10-21 11:19:50 +07:00
parent 0d1926f7d7
commit 4d70be3404
6 changed files with 52 additions and 6 deletions

View file

@ -121,7 +121,7 @@ export async function submitPlanned(ctx: Ctx, c: Asc, o: SubmitOpts): Promise<vo
const { state, tag } = o;
const plan = planIapSubmission(state, o.ids, { withVersion: o.withVersion, appVersionId: o.appVersionId, platform: o.platform });
const prepare = plan.prepare.map(r => bindIds(r, { app: o.appId }));
const submitting = willSubmit(state, plan);
const submitting = willSubmit(state, plan, o.appVersionId);
const busy = state.choice?.action === 'busy';
if (a.has('json') && !o.quiet) {
printJson({ dryRun: dry, items: plan.items, skipped: plan.skipped, firstOfType: plan.firstOfType, hasApprovedVersion: plan.hasApprovedVersion,
@ -149,7 +149,7 @@ export async function submitPlanned(ctx: Ctx, c: Asc, o: SubmitOpts): Promise<vo
const target = state.draft ? `${resubmit ? 'review submission' : 'draft'} ${state.draft.id}` : 'a new draft submission';
if (dry) {
await execute(c.asc, planRequests(prepare, plan, submitting), { dry, tag });
done(tag, plan.items.length ? `dry run: ${plan.items.length} item(s) would go into ${target}, then it is submitted` : `dry run: ${target} would be resubmitted as it is`);
done(tag, plan.items.length ? `dry run: ${plan.items.length} item(s) would go into ${target}, then it is submitted` : `dry run: ${target} would be ${resubmit ? 'resubmitted' : 'submitted'} as it is`);
return;
}
const what = `${plan.items.length} item(s)${plan.appVersion ? `, including app version ${plan.appVersion.versionString}` : ''}`;

View file

@ -55,6 +55,9 @@ export interface PSub { id: string; productId: string; groupId: string; state: s
export interface PGroup { id: string; name: string; versions: PVersion[] }
export interface PAppVersion { id: string; versionString: string; state: string }
/** Why an asked-for item is not added: the open draft holds it already. */
export const ALREADY_IN_DRAFT = 'already in the draft submission';
export interface SubmitState {
appVersions: PAppVersion[];
iaps: PIap[];
@ -162,7 +165,7 @@ export function planIapSubmission(s: SubmitState, want: string[], o: { withVersi
pickOne(id, productState, versions, kind, type);
// a never-approved first product left out while the app version goes to review: not sellable after approval (pitfall 70)
const firstOfType = kind === 'subscriptionVersion' ? !s.subs.some(x => x.groupId === s.subs.find(y => y.productId === id)?.groupId && APPROVED_PRODUCT.has(x.state)) : !approvedTypes.has(type);
if (plan.skipped.length > before && firstOfType && NEVER_SUBMITTED.has(productState) && plan.skipped.at(-1)?.why !== 'already in the draft submission') leftOut.push(`${id} (${plan.skipped.at(-1)?.why})`);
if (plan.skipped.length > before && firstOfType && NEVER_SUBMITTED.has(productState) && plan.skipped.at(-1)?.why !== ALREADY_IN_DRAFT) leftOut.push(`${id} (${plan.skipped.at(-1)?.why})`);
};
const leftOut: string[] = [];
const pickOne = (id: string, productState: string, versions: PVersion[], kind: ItemKind, type: string): void => {
@ -172,7 +175,7 @@ export function planIapSubmission(s: SubmitState, want: string[], o: { withVersi
if (!v) { plan.skipped.push({ id, why: 'no version on App Store Connect yet (create the product first: storebound apple iap / apple subscriptions)' }); return; }
if (IN_REVIEW.has(v.state)) { plan.skipped.push({ id, why: `already ${v.state}` }); return; }
if (!SUBMITTABLE.has(v.state)) { plan.skipped.push({ id, why: `latest version is ${v.state}: ${NEW_VERSION_FROM.has(v.state) ? 'add --new-version to send a new version' : 'change it in App Store Connect to send a new version'}` }); return; }
if (already.has(v.id)) { plan.skipped.push({ id, why: 'already in the draft submission' }); return; }
if (already.has(v.id)) { plan.skipped.push({ id, why: ALREADY_IN_DRAFT }); return; }
plan.items.push({ kind, versionId: v.id, label: `${id} (${typeLabel(type)}, version ${v.version ?? '?'})`, ...(v.state === 'DEVELOPER_REJECTED' ? { devRejected: true } : {}) });
if (!approvedTypes.has(type)) firstTypes.add(type);
};
@ -222,8 +225,15 @@ export function planIapSubmission(s: SubmitState, want: string[], o: { withVersi
/** The plan's word for an item it adds. Pure. */
export const addAction = (it: PlanItem): string => (it.devRejected ? `add (${DEV_REJECTED_NOTE})` : 'add');
/** Whether the run ends with a submit: new items, or a submission back from App Review that only needs resubmitting. Pure. */
export const willSubmit = (s: SubmitState, plan: SubmitPlan): boolean => plan.items.length > 0 || s.draft?.state === 'UNRESOLVED_ISSUES';
/** Whether the run ends with a submit: new items, a submission back from App Review that only needs resubmitting, or a
* draft prepared earlier (`--no-submit`) that already holds what was asked for: the app version when one is named,
* else at least one of the asked-for items. Pure. */
export function willSubmit(s: SubmitState, plan: SubmitPlan, appVersionId?: string): boolean {
if (plan.items.length > 0 || s.draft?.state === 'UNRESOLVED_ISSUES') return true;
const held = s.draft?.itemVersionIds ?? [];
if (!held.length) return false;
return appVersionId ? held.includes(appVersionId) : plan.skipped.some(sk => sk.why === ALREADY_IN_DRAFT);
}
/** Fills the placeholders of a planned request (app id, the id of the draft created a moment ago). Pure. */
export function bindIds(r: Req, ids: { app?: string; submission?: string }): Req {