apple creative-assets: a placement App Store Connect refuses on the live version is reported with the live version page's deep link, the live page stays unchanged (new placement first, old one removed after) and the other live slots are not retried; pitfall 76
This commit is contained in:
parent
98d3bf4410
commit
1736300784
4 changed files with 42 additions and 5 deletions
|
|
@ -13,7 +13,9 @@
|
|||
an editable version uses the live version: approved assets are selected there, new ones are uploaded and wait for a
|
||||
review of their own. Art without words needs no copy per locale: `<renders>/creative/header.png` / `search.*`
|
||||
fill every language without its own file, uploaded once and placed per locale (the dry run counts files, not slots).
|
||||
Pitfall 76 rewritten.
|
||||
If App Store Connect refuses a placement on the live version, the run says so, leaves the live page unchanged (the
|
||||
new placement goes first, the old one is removed only after Apple accepted it), links the live version page and does
|
||||
not retry the other live slots. Pitfall 76 rewritten.
|
||||
|
||||
## 0.8.13 · 2026-10-18
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { ffprobe } from '../shots/report';
|
|||
import { imageInfo } from '../shots/spec';
|
||||
import { checkCreativeImage, checkCreativeVideo, type CreativeFile, creativeFiles, PLACEMENT_TYPE, type Placement } from '../shots/creative';
|
||||
import { ascClient, md5, type J } from './asc';
|
||||
import { AmbiguousWriteError } from '../lib/upsert';
|
||||
import { addItem, ensureDraft, OtherSubmissionError } from './submission';
|
||||
|
||||
const TAG = 'apple';
|
||||
|
|
@ -158,6 +159,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
if (verify) return verifySlots();
|
||||
|
||||
const rows: Row[] = [];
|
||||
let liveRefused = ''; // the first 4xx on a live-version placement: later live slots are not tried
|
||||
const planned = new Set<string>(); // dry run: MD5s already planned for upload
|
||||
async function upload(f: CreativeFile, sum: string): Promise<J> {
|
||||
const buf = readFileSync(f.file);
|
||||
|
|
@ -231,10 +233,26 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
}
|
||||
const action = placed.length ? 'replace' : 'select';
|
||||
if (dry) { log(TAG, `${tag}: would ${action === 'replace' ? `replace ${placed.length} selected asset(s) with` : 'select'} ${basename(f.file)} (${asset.attributes?.state})`); row(action, asset); continue; }
|
||||
for (const p of placed) await c.asc('DELETE', `/v1/appAssetLibraryPlacements/${p.id}`);
|
||||
if (onLive && liveRefused) { warn(TAG, `${tag}: not tried (App Store Connect refused a placement on the live version above; no blind retries)`); row('refuse', asset); continue; }
|
||||
const media = f.kind === 'image' ? 'image' : 'video';
|
||||
await c.asc('POST', '/v1/appAssetLibraryPlacements', { data: { type: 'appAssetLibraryPlacements', attributes: { placementType: ptype },
|
||||
const place = () => c.asc('POST', '/v1/appAssetLibraryPlacements', { data: { type: 'appAssetLibraryPlacements', attributes: { placementType: ptype },
|
||||
relationships: { [media]: { data: { type: asset.type, id: asset.id } }, appStoreVersionLocalization: { data: { type: 'appStoreVersionLocalizations', id: loc.id } } } } });
|
||||
if (onLive) {
|
||||
// Live version: place the new asset first, remove the old one only after Apple accepted it, so a refusal
|
||||
// leaves the live page as it was. Whether the API takes placements on a live version is not documented.
|
||||
try { await place(); }
|
||||
catch (e) {
|
||||
const status = /-> (4\d\d)/.exec(String((e as Error).message))?.[1];
|
||||
if (!status || e instanceof AmbiguousWriteError) throw e;
|
||||
liveRefused = String(e instanceof Error ? e.message : e).split('\n')[0]!;
|
||||
warn(TAG, `${tag}: App Store Connect refused to select ${basename(f.file)} on the live version ${version.attributes.versionString} (${status}): ${liveRefused}. The live page is unchanged. Select it in the console instead (Header and Search Results on the live version page): ${consoleLink('asc.version', '', { appleAppId: appId }).replace('/version/inflight', '/version/deliverable')}, or create a new version and re-run; the remaining live slots are not tried`);
|
||||
row('refuse', asset); continue;
|
||||
}
|
||||
for (const p of placed) await c.asc('DELETE', `/v1/appAssetLibraryPlacements/${p.id}`);
|
||||
} else {
|
||||
for (const p of placed) await c.asc('DELETE', `/v1/appAssetLibraryPlacements/${p.id}`);
|
||||
await place();
|
||||
}
|
||||
log(TAG, `${tag}: selected ${basename(f.file)} (${asset.attributes?.state})`);
|
||||
row(action, asset);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -930,8 +930,11 @@ that fails without the fix (see CONTRIBUTING.md).
|
|||
asset with the same MD5, select it once processed, never re-upload a rejected file or one that failed processing
|
||||
twice. Then `--add-for-review` puts the unreviewed assets into the draft submission (with the first version, or alone
|
||||
once the app has an approved version) and `--verify` reads every slot back. With no editable version the live version
|
||||
is used and only APPROVED assets go onto it; the rest wait for their own review. Uploading, selecting and adding to the
|
||||
is used and only APPROVED assets go onto it; the rest wait for their own review. Whether the API accepts a new
|
||||
placement on a live version is not documented: storebound places the new asset first and removes the old one only
|
||||
after Apple accepted it, and on a 4xx it says so, leaves the live page unchanged, gives the live version page's deep
|
||||
link (select it there, or create a version and re-run) and does not try the remaining live slots. Uploading, selecting and adding to the
|
||||
draft are normal steps, not owner buttons; Submit for Review stays the owner's.
|
||||
- **Guard:** `cli/apple/creative-assets.ts` (`classify`, `pickAsset`, `processingTiming`), `tests/creative-assets.test.ts`
|
||||
(fake App Store Connect: upload and select in one run, still processing, rejected / archived files, add-for-review
|
||||
without submitting, verify read-only); the fake commits library assets to PREPARE_FOR_SUBMISSION, as Apple does.
|
||||
without submitting, verify read-only, a refused live placement); the fake commits library assets to PREPARE_FOR_SUBMISSION, as Apple does.
|
||||
|
|
|
|||
|
|
@ -194,6 +194,20 @@ describe('apple creative-assets against the fake App Store Connect', () => {
|
|||
expect((await go(p, ['--verify'])).out).toContain('every slot shows the local file');
|
||||
});
|
||||
|
||||
test('live version refuses a placement: clear message with the console deep link, live page unchanged, no blind retry', async () => {
|
||||
const p = setup(files());
|
||||
s.asc.all('appStoreVersions')[0]!.attributes.appVersionState = 'READY_FOR_DISTRIBUTION';
|
||||
await go(p);
|
||||
for (const x of lib()) x.attributes.state = 'APPROVED';
|
||||
s.faults.push({ method: 'POST', match: /appAssetLibraryPlacements/, when: 'before', status: 409, code: 'STATE_ERROR', detail: 'version not editable', times: 5 });
|
||||
const r = await go(p, [], 1);
|
||||
expect(r.out).toContain('refused to select header.png on the live version 1.0.0 (409)');
|
||||
expect(r.out).toContain('https://appstoreconnect.apple.com/apps/');
|
||||
expect(r.out).toContain('not tried (App Store Connect refused a placement on the live version above');
|
||||
expect(r.w).toEqual(['POST /v1/appAssetLibraryPlacements']);
|
||||
expect(s.asc.all('appAssetLibraryPlacements')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('one shared file for many locales: the dry run plans one upload, the run uploads once and places it per locale', async () => {
|
||||
const two = { ...CFG, languages: [{ id: 'en', apple: 'en-US', play: 'en-US' }, { id: 'tr', apple: 'tr', play: 'tr-TR' }] };
|
||||
s.asc.put({ type: 'appAssetLibraries', id: 'lib-1', attributes: {}, rels: { app: APP } });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue