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:
parent
0d1926f7d7
commit
4d70be3404
6 changed files with 52 additions and 6 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- **`apple submit --yes` submits a draft prepared with `--no-submit`.** The second step of the two-step flow
|
||||
ended with "nothing to submit" when the draft already held the version and every item; it now sends the submit
|
||||
(same "not ready yet" retry). `iap-submit` the same for a draft that holds the asked-for items. Pitfall 90.
|
||||
|
||||
## 0.8.22 · 2026-10-20
|
||||
|
||||
- **`apple submit` / `resubmit` switch Game Center on first.** A build with the Game Center entitlement and an app
|
||||
|
|
|
|||
|
|
@ -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}` : ''}`;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ fails without the fix.
|
|||
|
||||
| # | date | lesson | guard |
|
||||
|---|---|---|---|
|
||||
| 90 | 2026-10 | `apple submit --build N --version X --yes` after `apple submit --no-submit --yes` (the documented two-step flow) printed the plan (every item "in it already", then "submit PATCH submitted: true"), logged "not added ...: already in the draft submission" and ended with "nothing to submit" without the PATCH: the submit only ran for new items or an UNRESOLVED_ISSUES submission. | `willSubmit` (cli/lib/review-submit.ts) also submits a READY_FOR_REVIEW draft that already holds the named app version (iap-submit: one of the asked-for items), with the same "not ready yet" retry. `tests/resubmit.test.ts` ("the second step submits the prepared draft"). |
|
||||
| 85 | 2026-10 | `shots record --source web` filmed at the page's CSS size: Chromium's screencast ignores the deviceScaleFactor (maxWidth only shrinks), so a 1080x1920 preview of a 430x932 @2x page was a 2.5x upscale. | `Page.captureScreenshot` with `clip.scale` = dpr (long side capped at 1920), 3 requests in flight, timestamped frames joined by ffmpeg; a warning when the first frame is smaller than asked or the rate is under 20 frames/s. `recordSize` / `captureShortfall` in `cli/shots/devices.ts`, `tests/video.test.ts`. |
|
||||
| 80 | 2026-10 | A game's hero logo and titles sat against the screenshot edges, a game with a macOS version had no Mac screenshots, and the contact sheet timed out at a flat 30 s with 9 sizes. | Templates keep 7 % / 5 % safe margins and `shots check` fails text or logo boxes under 6 % / 4 % (`marginViolations`, report.json `boxes`); `apple.mac` adds the `mac` size (APP_DESKTOP, macOS version); `sheetTimeoutMs` scales with images, sizes and load. `tests/mac-landscape.test.ts`, `tests/browser/landscape.browser.ts`. |
|
||||
| 78 | 2026-10 | `storebound api <write> --yes` with stdout piped or redirected printed a dry run and exited 0 (twice); the owner took it as sent. | `decide` (`cli/api/gate.ts`) refuses with `NOT_A_TTY`: exit 1, "refused: writes through `storebound api` need an interactive terminal ... Nothing was sent." on stderr, the dedicated command named; without `--yes` still a dry run. `tests/api.test.ts`. |
|
||||
|
|
|
|||
|
|
@ -1100,3 +1100,16 @@ that fails without the fix (see CONTRIBUTING.md).
|
|||
- **Guard:** `readGcSwitch` / `gcSwitchPlan` / `ensureGcSwitch` / `gameCenterInUse` in `cli/apple/game-center.ts`,
|
||||
`gameServicesFindings` (codes game-center.detail, game-center.version), the fake store's `gameCenterBuild` 409 and
|
||||
`tests/game-services.test.ts`.
|
||||
|
||||
## 90. The second step of `apple submit --no-submit` said "nothing to submit"
|
||||
- **Symptom:** `apple submit --build N --version X --yes` after `apple submit --no-submit --yes` prepared the draft
|
||||
printed the plan (every item "in it already", last row "submit PATCH submitted: true · owner step (--yes)"),
|
||||
logged "not added <iap>: already in the draft submission" for each item and ended with "nothing to submit"; the
|
||||
draft stayed READY_FOR_REVIEW and was never sent.
|
||||
- **Cause:** the submit ran only when the run added items or the submission was back from App Review
|
||||
(UNRESOLVED_ISSUES). A draft that already held everything counted as nothing to do.
|
||||
- **Do:** update storebound and run the same `--yes` again: the prepared draft is submitted as it is (with the
|
||||
"not ready yet" retry). A plan that ends with a submit row and a run that ends with "nothing to submit" is a bug:
|
||||
check `apple status` for the draft's state before re-running anything else.
|
||||
- **Guard:** `willSubmit` in `cli/lib/review-submit.ts` (a non-empty READY_FOR_REVIEW draft that holds the named app
|
||||
version, or for `iap-submit` an asked-for item, is submitted); `tests/resubmit.test.ts`.
|
||||
|
|
|
|||
|
|
@ -225,6 +225,24 @@ describe('apple submit --build <n> (a first submission, the desktop owner action
|
|||
expect(shown(b)).toEqual([]);
|
||||
});
|
||||
|
||||
test('--no-submit --yes, then --yes: the second step submits the prepared draft that holds everything already', async () => {
|
||||
fresh();
|
||||
const p = project(ONE);
|
||||
await capture(() => appleSubmit(p.ctx(['--build', '8', '--no-submit', '--yes'])));
|
||||
const sid = s.asc.all('reviewSubmissions')[0]!.id;
|
||||
expect(s.asc.all('reviewSubmissions').map(x => x.attributes.state)).toEqual(['READY_FOR_REVIEW']);
|
||||
expect(s.asc.children(sid, 'reviewSubmissionItems')).toHaveLength(2);
|
||||
s.faults.push({ ...NOT_READY, match: new RegExp(`^/v1/reviewSubmissions/${sid}$`), times: 1 });
|
||||
const a = s.calls.length;
|
||||
const r = await capture(() => appleSubmit(p.ctx(['--build', '8', '--yes'])));
|
||||
expect([r.exit ?? 0, r.err]).toEqual([0, '']);
|
||||
expect(r.out).toContain('in it already');
|
||||
expect(r.out).not.toContain('nothing to submit');
|
||||
expect(shown(a)).toEqual([`PATCH /v1/reviewSubmissions/${sid}`, `PATCH /v1/reviewSubmissions/${sid}`]);
|
||||
expect(s.asc.all('reviewSubmissions').map(x => x.attributes.state)).toEqual(['WAITING_FOR_REVIEW']);
|
||||
expect(r.out).toContain(`review submission ${sid} submitted`);
|
||||
});
|
||||
|
||||
test('a live version is not submitted; no editable version is a clear stop', async () => {
|
||||
fresh();
|
||||
s.asc.res.get('ver-1')!.attributes.appVersionState = 'READY_FOR_SALE';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue