play: changesNotSentForReview goes only on edits.commit (validate answers 400 "Unknown name changesNotSentForReview: Cannot bind query parameter", found by a game on 220e9bb's real run): a refused commit is committed again with the parameter, a dry run's refused validate only says how the real run commits; the fake Play refuses the parameter on validate and a test asserts validate never gets it
This commit is contained in:
parent
dcb553fd16
commit
8a84ab4089
6 changed files with 22 additions and 13 deletions
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
- **Play edits Play will not send for review are committed anyway.** After a rejection, with other pending changes or
|
||||
managed publishing, Play answers validate / commit with 400 "Changes cannot be sent for review automatically. Please
|
||||
set the query parameter changesNotSentForReview to true". storebound now validates and commits the same edit again
|
||||
set the query parameter changesNotSentForReview to true". storebound now commits the same edit again (edits.commit only: validate refuses the parameter with 400 "Unknown name")
|
||||
with that parameter (a dry run validates that way and says the real run will commit so), then prints "committed;
|
||||
Play did not send it for review: press Send changes for review in Publishing overview: <link>" (the owner's button).
|
||||
`--not-sent-for-review` on `play release`, `upload`, `rollout`, `listing` and `images` (MCP / desktop
|
||||
|
|
|
|||
|
|
@ -86,15 +86,16 @@ export async function playClient(root: string, packageName: string) {
|
|||
let notSent = !!o.notSentForReview;
|
||||
const refused = (): void => {
|
||||
notSent = true;
|
||||
log('play', 'Play refuses to send these changes for review automatically (after a rejection, other pending changes or managed publishing): validating again with changesNotSentForReview=true');
|
||||
log('play', 'Play refuses to send these changes for review automatically (after a rejection, other pending changes or managed publishing): committing with changesNotSentForReview=true (only edits.commit takes it; validate answers 400 "Unknown name")');
|
||||
};
|
||||
try {
|
||||
const out = await fn(id, { unchanged: () => { same = true; } });
|
||||
if (o.dry) {
|
||||
try { await call('POST', `${API}/edits/${id}:validate${withNotSent(undefined, notSent)}`); } catch (err) {
|
||||
if (notSent || !isReviewRefused(err)) throw err;
|
||||
// validate never takes changesNotSentForReview (live: 400 "Unknown name changesNotSentForReview: Cannot bind
|
||||
// query parameter"); its refusal only tells the dry run how the real run will commit
|
||||
try { await call('POST', `${API}/edits/${id}:validate`); } catch (err) {
|
||||
if (!isReviewRefused(err)) throw err;
|
||||
refused();
|
||||
await call('POST', `${API}/edits/${id}:validate${withNotSent(undefined, true)}`);
|
||||
}
|
||||
log('play', 'dry run: edit validated, discarding');
|
||||
if (notSent) log('play', `dry run: the real run commits with changesNotSentForReview=true; Play then does not send it for review: Send changes for review in Publishing overview is the owner's button: ${publishingUrl()}`);
|
||||
|
|
@ -116,7 +117,6 @@ export async function playClient(root: string, packageName: string) {
|
|||
try { await commit(); } catch (err) {
|
||||
if (notSent || !isReviewRefused(err)) throw err;
|
||||
refused();
|
||||
await call('POST', `${API}/edits/${id}:validate${withNotSent(undefined, true)}`);
|
||||
await commit();
|
||||
}
|
||||
openEdit = null;
|
||||
|
|
@ -162,7 +162,7 @@ export async function playClient(root: string, packageName: string) {
|
|||
* invalidates an open edit ("A change was made to the application outside of this Edit"); then the whole edit is
|
||||
* redone, up to `retries` times. Keep edits small (e.g. one per language for images) so a retry is cheap.
|
||||
* Play's 400 "Changes cannot be sent for review automatically" (pitfall 72) is answered on the same edit: validate
|
||||
* and commit again with changesNotSentForReview=true (a dry run validates that way and says so); the owner then
|
||||
* and commit again with changesNotSentForReview=true (a dry run says so; validate never takes the parameter); the owner then
|
||||
* presses Send changes for review in Publishing overview, recorded in .storebound/ for `play status`.
|
||||
*/
|
||||
async edit<T>(fn: (id: string, ctl: EditCtl) => Promise<T>, o: EditOpts & { retries?: number } = {}): Promise<T> {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ fails without the fix.
|
|||
|
||||
| # | date | lesson | guard |
|
||||
|---|---|---|---|
|
||||
| 72 | 2026-10 | A game: after Play production 1.0.0 (3) was rejected (Broken Functionality), the fix 1.0.1 (5) went through upload and track updates, then edits.validate answered 400 "Changes cannot be sent for review automatically. Please set the query parameter changesNotSentForReview to true". Play refuses the automatic send after a rejection, with other pending changes or managed publishing. | `cli/play/client.ts` (edit flow) with `cli/play/review-pending.ts`: on exactly that 400 the same edit is validated and committed again with changesNotSentForReview=true (a dry run validates that way and says so), the Publishing overview deep link is printed (the owner's button), the commit is recorded in .storebound/ and `play status` shows it; `--not-sent-for-review` on play release / upload / rollout / listing / images (MCP `not_sent_for_review`). `tests/play-not-sent-for-review.test.ts`. |
|
||||
| 72 | 2026-10 | A game: after Play production 1.0.0 (3) was rejected (Broken Functionality), the fix 1.0.1 (5) went through upload and track updates, then edits.validate answered 400 "Changes cannot be sent for review automatically. Please set the query parameter changesNotSentForReview to true". Play refuses the automatic send after a rejection, with other pending changes or managed publishing. | `cli/play/client.ts` (edit flow) with `cli/play/review-pending.ts`: on exactly that 400 the same edit is validated and committed again with changesNotSentForReview=true (a dry run validates that way and says so), the Publishing overview deep link is printed (the owner's button), the commit is recorded in .storebound/ and `play status` shows it; `--not-sent-for-review` on play release / upload / rollout / listing / images (MCP `not_sent_for_review`). `tests/play-not-sent-for-review.test.ts`. Only edits.commit takes changesNotSentForReview: validate answers 400 "Unknown name ... Cannot bind query parameter" (the fake store does too). |
|
||||
| 71 | 2026-10 | Messages sent the owner to console pages as plain text paths ("App Store Connect > App Information > Age Rating", "Play Console > App content") without a link; the owner had to find the app and the page by hand. | `cli/lib/console-links.ts`: every message carries the absolute deep link (app list / developer chooser when ids are unknown), `url` in finding JSON, an Open button in the desktop; `tests/console-links.test.ts` fails on a console path in cli/ or templates/ without an https URL; agent rule 11 in AGENTS.md. |
|
||||
| 70 | 2026-10 | A game: the app's first in-app purchase (a starter pack) was not in the 1.0.0 review submission (its items held only the appStoreVersion); it stayed READY_TO_SUBMIT and was noticed after two days in WAITING_FOR_REVIEW. Apple reviews the first product of a type only with an app version; approved without it, it is not sellable. | `firstProductGaps` (`cli/lib/review-submit.ts`, read by `cli/apple/first-products.ts`): WARN in `apple status` (text and --json), `release status` and the desktop checks; blocker in `validate` / `release plan` and the `apple submit` plan; `iap-review` says "ready, not submitted yet". `tests/first-products.test.ts`. |
|
||||
| 69 | 2026-10 | A web app: `bun add -d storebound` plus the scaffolded `storebound.config.ts` broke `next build`: the package exported only its TypeScript source and shipped no declarations; Bun imports .ts, so every storebound test passed, but tsc never takes a package's .ts source as its types. | The package ships declarations for ES module (`dist/types`) and CommonJS projects (`dist/types-cjs`) and a Node entry (`dist/config.js`); Bun keeps the source (`buildEntry`, `pruneDeclarations` in `tools/build-node.ts`, run by prepack). `tests/package-types.test.ts` type-checks the template against the packed entry under bundler / node16 / nodenext / node10 in both project types with no @types/node and skipLibCheck off, and loads the package with `import()` and `require()` under Node. `check` fails on the template's own placeholders (Example Studio, bare example.com). |
|
||||
|
|
|
|||
|
|
@ -842,7 +842,7 @@ that fails without the fix (see CONTRIBUTING.md).
|
|||
- **Cause:** after a rejection (here production 1.0.0 (3), Broken Functionality), with other changes pending in the
|
||||
Console or with managed publishing, Play no longer sends an API commit for review by itself. The edit is fine; only
|
||||
the automatic send is refused. storebound <= 0.8.8 stopped there.
|
||||
- **Do:** nothing by hand: storebound validates and commits the same edit again with changesNotSentForReview=true
|
||||
- **Do:** nothing by hand: storebound commits the same edit again with changesNotSentForReview=true (only edits.commit binds it; validate answers 400 "Unknown name ... Cannot bind query parameter")
|
||||
(`--not-sent-for-review` sets it from the start; MCP `not_sent_for_review`). A dry run says the real run will commit
|
||||
that way. Then the owner presses **Send changes for review** in Publishing overview (storebound prints the deep link):
|
||||
that button is the owner's, never press or automate it. The API has no read of "waiting for Send changes for
|
||||
|
|
|
|||
|
|
@ -359,6 +359,8 @@ export class FakePlay {
|
|||
/** How many validate / commit calls without changesNotSentForReview=true Play refuses with its 400 "Changes cannot
|
||||
* be sent for review automatically" (after a rejection, pending changes, managed publishing: pitfall 72). */
|
||||
refuseAutoReview = 0;
|
||||
/** validate calls that carried changesNotSentForReview (live Play answers them 400 "Unknown name") */
|
||||
validateWithNotSent = 0;
|
||||
|
||||
handle(method: string, url: string, body: unknown): Response {
|
||||
const u = new URL(url);
|
||||
|
|
@ -371,6 +373,11 @@ export class FakePlay {
|
|||
const e = this.edits.get(id!);
|
||||
if (!e) return err(404, 'NOT_FOUND', 'Edit has been deleted');
|
||||
if (method === 'DELETE' && rest === '') { this.edits.delete(id!); return new Response(null, { status: 204 }); }
|
||||
// live: only edits.commit binds changesNotSentForReview; validate refuses the parameter
|
||||
if (method === 'POST' && rest === ':validate' && u.searchParams.has('changesNotSentForReview')) {
|
||||
this.validateWithNotSent++;
|
||||
return json({ error: { code: 400, status: 'INVALID_ARGUMENT', message: 'Invalid JSON payload received. Unknown name "changesNotSentForReview": Cannot bind query parameter. Field \'changesNotSentForReview\' could not be found in request message.' } }, 400);
|
||||
}
|
||||
if (method === 'POST' && (rest === ':validate' || rest === ':commit') && this.refuseAutoReview > 0 && u.searchParams.get('changesNotSentForReview') !== 'true') {
|
||||
this.refuseAutoReview--;
|
||||
return json({ error: { code: 400, status: 'INVALID_ARGUMENT', message: 'Changes cannot be sent for review automatically. Please set the query parameter changesNotSentForReview to true. Once committed, the changes in this edit can be sent for review from the Google Play Console UI.', errors: [{ message: 'Changes cannot be sent for review automatically. Please set the query parameter changesNotSentForReview to true.', domain: 'global', reason: 'badRequest' }] } }, 400);
|
||||
|
|
|
|||
|
|
@ -35,13 +35,14 @@ describe('Play refuses the automatic send for review', () => {
|
|||
expect(isReviewRefused(new Error('POST /edits/e1:commit -> 400 {"error":{"message":"Version code 5 has already been used."}}'))).toBe(false);
|
||||
});
|
||||
|
||||
test('real run: the commit is refused once, then the same edit is validated and committed with changesNotSentForReview=true', async () => {
|
||||
test('real run: the commit is refused once, then the same edit is committed with changesNotSentForReview=true (validate never takes it)', async () => {
|
||||
const p = project(BASE, { 'store/listing.md': LISTING });
|
||||
s.play.refuseAutoReview = 1;
|
||||
const r = await release(p);
|
||||
expect(r.exit ?? 0).toBe(0);
|
||||
expect(edits('commit')).toEqual(['edit-1:commit', 'edit-1:commit?changesNotSentForReview=true']);
|
||||
expect(edits('validate')).toEqual(['edit-1:validate?changesNotSentForReview=true']);
|
||||
expect(edits('validate').filter(v => v.includes('changesNotSentForReview'))).toEqual([]);
|
||||
expect(s.play.validateWithNotSent).toBe(0);
|
||||
expect(s.calls.filter(c => c.method === 'POST' && /\/edits$/.test(c.path))).toHaveLength(1); // one edit, never reopened
|
||||
expect(s.play.committed).toEqual(['edit-1']);
|
||||
expect(s.play.live.tracks.production).toBeDefined();
|
||||
|
|
@ -53,12 +54,13 @@ describe('Play refuses the automatic send for review', () => {
|
|||
expect(rec!.summary).toContain('production completed');
|
||||
});
|
||||
|
||||
test('dry run: validate is refused once, validated again with the parameter, no commit, says how the real run commits', async () => {
|
||||
test('dry run: validate is refused once, never sent the parameter, no commit, says how the real run commits', async () => {
|
||||
const p = project(BASE, { 'store/listing.md': LISTING });
|
||||
s.play.refuseAutoReview = 1;
|
||||
const r = await release(p, ['--dry']);
|
||||
expect(r.exit ?? 0).toBe(0);
|
||||
expect(edits('validate')).toEqual(['edit-1:validate', 'edit-1:validate?changesNotSentForReview=true']);
|
||||
expect(edits('validate')).toEqual(['edit-1:validate']);
|
||||
expect(s.play.validateWithNotSent).toBe(0);
|
||||
expect(edits('commit')).toEqual([]);
|
||||
expect(s.play.committed).toEqual([]);
|
||||
expect(s.play.edits.size).toBe(0);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue