creative-assets: the dry run plans one upload per file, not per slot (a shared file used by 13 locales read as 26 uploads)
This commit is contained in:
parent
7e69883dfc
commit
98d3bf4410
3 changed files with 24 additions and 4 deletions
|
|
@ -12,7 +12,8 @@
|
||||||
twice, is not uploaded again; an archived copy of the same file is unarchived instead of uploaded. A live app without
|
twice, is not uploaded again; an archived copy of the same file is unarchived instead of uploaded. A live app without
|
||||||
an editable version uses the live version: approved assets are selected there, new ones are uploaded and wait for a
|
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.*`
|
review of their own. Art without words needs no copy per locale: `<renders>/creative/header.png` / `search.*`
|
||||||
fill every language without its own file. Pitfall 76 rewritten.
|
fill every language without its own file, uploaded once and placed per locale (the dry run counts files, not slots).
|
||||||
|
Pitfall 76 rewritten.
|
||||||
|
|
||||||
## 0.8.13 · 2026-10-18
|
## 0.8.13 · 2026-10-18
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ const reason = (a: J): string => {
|
||||||
/** Bounded wait for processing (every wait has a timeout and a heartbeat); tests set waitMs 0 / timeoutMs 0. */
|
/** Bounded wait for processing (every wait has a timeout and a heartbeat); tests set waitMs 0 / timeoutMs 0. */
|
||||||
export const processingTiming = { pollMs: 3000, timeoutMs: 120_000, heartbeatMs: 20_000, sleep: (ms: number): Promise<void> => (ms > 0 ? rt.sleep(ms) : Promise.resolve()) };
|
export const processingTiming = { pollMs: 3000, timeoutMs: 120_000, heartbeatMs: 20_000, sleep: (ms: number): Promise<void> => (ms > 0 ? rt.sleep(ms) : Promise.resolve()) };
|
||||||
|
|
||||||
export interface Row { locale: string; placement: Placement; file: string; action: 'keep' | 'upload' | 'wait' | 'select' | 'replace' | 'approve' | 'refuse'; state?: string; asset?: J }
|
export interface Row { locale: string; placement: Placement; file: string; action: 'keep' | 'upload' | 'reuse' | 'wait' | 'select' | 'replace' | 'approve' | 'refuse'; state?: string; asset?: J }
|
||||||
|
|
||||||
/** Local files per App Store locale, checked; throws on any error before a network call. */
|
/** Local files per App Store locale, checked; throws on any error before a network call. */
|
||||||
export function localCreative(ctx: Ctx): { src: string; local: Map<string, CreativeFile[]> } {
|
export function localCreative(ctx: Ctx): { src: string; local: Map<string, CreativeFile[]> } {
|
||||||
|
|
@ -158,6 +158,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
||||||
if (verify) return verifySlots();
|
if (verify) return verifySlots();
|
||||||
|
|
||||||
const rows: Row[] = [];
|
const rows: Row[] = [];
|
||||||
|
const planned = new Set<string>(); // dry run: MD5s already planned for upload
|
||||||
async function upload(f: CreativeFile, sum: string): Promise<J> {
|
async function upload(f: CreativeFile, sum: string): Promise<J> {
|
||||||
const buf = readFileSync(f.file);
|
const buf = readFileSync(f.file);
|
||||||
const type = f.kind === 'image' ? 'appAssetLibraryImages' : 'appAssetLibraryVideos';
|
const type = f.kind === 'image' ? 'appAssetLibraryImages' : 'appAssetLibraryVideos';
|
||||||
|
|
@ -202,7 +203,12 @@ export async function run(ctx: Ctx): Promise<void> {
|
||||||
if (pick.refuse) { warn(TAG, `${tag}: ${basename(f.file)} ${pick.refuse}`); row('refuse'); continue; }
|
if (pick.refuse) { warn(TAG, `${tag}: ${basename(f.file)} ${pick.refuse}`); row('refuse'); continue; }
|
||||||
let asset = pick.asset;
|
let asset = pick.asset;
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
if (dry) { log(TAG, `${tag}: would upload ${basename(f.file)} to the Asset Library and select it ${onLive ? 'on the live version once approved' : 'once processed'}`); row('upload'); continue; }
|
if (dry) {
|
||||||
|
// one upload per file: a later locale with the same file reuses it (the real run finds it by MD5)
|
||||||
|
const first = !planned.has(sum); planned.add(sum);
|
||||||
|
log(TAG, `${tag}: would ${first ? 'upload' : 'reuse'} ${basename(f.file)}${first ? ' to the Asset Library' : ' (uploaded once for an earlier locale)'} and select it ${onLive ? 'on the live version once approved' : 'once processed'}`);
|
||||||
|
row(first ? 'upload' : 'reuse'); continue;
|
||||||
|
}
|
||||||
log(TAG, `${tag}: uploading ${basename(f.file)} to the Asset Library`);
|
log(TAG, `${tag}: uploading ${basename(f.file)} to the Asset Library`);
|
||||||
asset = await upload(f, sum);
|
asset = await upload(f, sum);
|
||||||
assets = await library();
|
assets = await library();
|
||||||
|
|
@ -261,7 +267,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
||||||
log(TAG, `${added} creative asset(s) added to draft review submission ${sid}; nothing was submitted`);
|
log(TAG, `${added} creative asset(s) added to draft review submission ${sid}; nothing was submitted`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const summary = `creative assets: ${n(['keep'])} up to date, ${n(['select', 'replace'])} ${dry ? 'to select' : 'selected'}, ${n(['upload'])} to upload, ${n(['wait'])} still processing, ${approve ? `${approve} waiting for App Review (live version), ` : ''}${n(['refuse'])} refused${off ? `, ${off} did not read back` : ''}`;
|
const summary = `creative assets: ${n(['keep'])} up to date, ${n(['select', 'replace'])} ${dry ? 'to select' : 'selected'}, ${n(['upload'])} file(s) to upload${n(['reuse']) ? ` (used by ${n(['upload', 'reuse'])} slots)` : ''}, ${n(['wait'])} still processing, ${approve ? `${approve} waiting for App Review (live version), ` : ''}${n(['refuse'])} refused${off ? `, ${off} did not read back` : ''}`;
|
||||||
if (unreviewed.size) {
|
if (unreviewed.size) {
|
||||||
log(TAG, forReview
|
log(TAG, forReview
|
||||||
? `owner step: Submit for Review with the version (or alone once the app has an approved version): ${consoleLink('asc.app-review')}`
|
? `owner step: Submit for Review with the version (or alone once the app has an approved version): ${consoleLink('asc.app-review')}`
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,19 @@ describe('apple creative-assets against the fake App Store Connect', () => {
|
||||||
expect((await go(p, ['--verify'])).out).toContain('every slot shows the local file');
|
expect((await go(p, ['--verify'])).out).toContain('every slot shows the local file');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 } });
|
||||||
|
s.asc.put({ type: 'appStoreVersions', id: 'ver-1', attributes: { versionString: '1.0.0', appVersionState: 'PREPARE_FOR_SUBMISSION', platform: 'IOS' }, rels: { app: APP } });
|
||||||
|
for (const l of ['en-US', 'tr']) s.asc.put({ type: 'appStoreVersionLocalizations', id: `loc-${l}`, attributes: { locale: l }, rels: { appStoreVersion: 'ver-1' } });
|
||||||
|
const p = project(two, { 'store/renders/creative/header.png': png(3840, 1646, false, 4) });
|
||||||
|
const dry = await go(p, ['--dry']);
|
||||||
|
expect(dry.out).toContain('1 file(s) to upload (used by 2 slots)');
|
||||||
|
const r = await go(p);
|
||||||
|
expect(r.w.filter(x => x === 'POST /v1/appAssetLibraryImages')).toHaveLength(1);
|
||||||
|
expect(r.w.filter(x => x === 'POST /v1/appAssetLibraryPlacements')).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
test('a bad file stops the run before any call', async () => {
|
test('a bad file stops the run before any call', async () => {
|
||||||
const p = setup({ 'store/renders/en/creative/header.png': png(3840, 1600, true) });
|
const p = setup({ 'store/renders/en/creative/header.png': png(3840, 1600, true) });
|
||||||
const a = s.calls.length;
|
const a = s.calls.length;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue