vydanne 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,10 +20,10 @@ submit anything for review.
20
20
  | vydanne does | You (or your developer) still do |
21
21
  |---|---|
22
22
  | Write/push the store text in every language | **Create the app record** in App Store Connect / Play Console |
23
- | Upload screenshots, app previews, Play graphics | **Build and upload the binary** (`.ipa` / `.aab`) |
24
- | Set age rating, App Review contact, IAP text | Answer **App Privacy** in Apple's web UI (see [step 9](#9-finish-in-the-browser-the-parts-no-api-can-do)) |
25
- | Generate the US export-compliance PDF | Play **Data Safety** + content-rating questionnaires |
26
- | Check everything is complete (`preflight`) | **Press Submit for Review** |
23
+ | Upload screenshots, app previews, Play graphics | **Build and sign the binary** (`.ipa` / `.aab`) |
24
+ | Upload that binary to testers (`prerelease`: TestFlight internal / a Play testing track) | Answer **App Privacy** in Apple's web UI (see [step 9](#9-finish-in-the-browser-the-parts-no-api-can-do)) |
25
+ | Set age rating, App Review contact, IAP text | Play **Data Safety** + content-rating questionnaires |
26
+ | Generate the US export-compliance PDF · check everything is complete (`preflight`) | **Press Submit for Review** |
27
27
 
28
28
  > **It never submits.** That's deliberate — a human should always be the one who ships.
29
29
 
@@ -220,14 +220,20 @@ export default {
220
220
  | `primaryLocale` | Your main App Store language. Any language you *don't* translate falls back to this, so it must be complete. |
221
221
  | `platforms` | `["IOS"]`, or `["IOS", "MAC_OS"]` if you ship a Mac app too. **Mac is a separate listing** — its text is not shared with iOS. |
222
222
  | `uiLocales` | Short language codes you publish in. vydanne converts them to Apple's codes (`de` → `de-DE`) and warns about any language the App Store doesn't offer. |
223
+ | `localeMap` | Optional. Your code → Apple's code, for anything the built-in table doesn't cover (`{ "nb": "no" }`). Merged over the defaults, so list only your exceptions. |
223
224
  | `metadataDir` | Where your listing text lives. `fastlane/metadata` is the default. |
224
- | `rating` | Age rating, e.g. `"4+"`. |
225
+ | `screenshots` | Optional. Where your screenshots live, per platform: `{ IOS: "", MAC_OS: "…" }`. Defaults to `fastlane/screenshots` and `fastlane/screenshots-macos`. |
226
+ | `rating` | Age rating, e.g. `"4+"`. Anything other than `"4+"` also needs `ageRating` below. |
227
+ | `ageRating` | The content descriptors behind a rating above 4+, e.g. `{ violenceCartoonOrFantasy: "INFREQUENT_OR_MILD" }`. Merged over an all-NONE base — Apple computes the band from what you declare. |
228
+ | `reviewContact` | Optional `{ demoAccountRequired }`. By default this is inferred from whether `review_information/demo_user.txt` exists. |
225
229
  | `asc` | Optional `{ keyId, issuerId }` — only if you'd rather not use environment variables. |
226
230
  | `privacy` | What data actually leaves the device, e.g. `{ collected: ["CRASH_DATA"], tracking: false }`. |
227
231
  | `iaps` | Your in-app purchases (name ≤30 chars, description ≤45). |
228
232
  | `previews` | App Preview videos — see step 7. |
229
- | `export` | Export-compliance details for the PDF: `{ encryption: "standard", appName, version, teamId }`. |
230
- | `google` | The Play block — `{ packageName, metadataDir, defaultLocale }`. Omit it if you're iOS-only. |
233
+ | `export` | Export-compliance details for the PDF: `{ encryption, appName, version, teamId, algorithms, statement, filed }`. `algorithms` and `statement` are **required** when `encryption` is `"standard"` — the command will not invent your cryptography. |
234
+ | `google` | The Play block — `{ packageName, metadataDir, defaultLocale, track, images, imageLocales }`. Omit it if you're iOS-only. |
235
+ | `bridge` | Optional `{ out, apple, play }` — where zdymak wrote, and which of its output folders feed which store slot. Only needed when `dir:` in `zdymak.config.mjs` makes a folder name differ from its target name. |
236
+ | `push` | Optional `{ skip: [...] }` — pipeline steps this app never runs. |
231
237
 
232
238
  Check your languages resolved correctly:
233
239
 
@@ -288,11 +294,16 @@ image is for. Anything after it is yours — but files upload in alphabetical or
288
294
  | `watch_` | Apple Watch Ultra |
289
295
  | `macos_` | Mac (in `screenshots-macos/`) |
290
296
 
291
- A file whose prefix isn't in that table is **silently skipped** if a screenshot doesn't appear, check
292
- the name first.
297
+ A file whose prefix isn't in that table is **not uploaded**, and `fill` says so by name — it lists every
298
+ file it skipped and the prefixes it knows. Same for a folder that isn't an App Store locale code (`de`
299
+ instead of `de-DE`, the classic one): named, not dropped in silence.
293
300
 
294
- > **Screenshots must be RGB PNGs with no transparency.** Simulator captures often have an alpha channel
295
- > and Apple rejects those. `npx vydanne iap` with `VYDANNE_FLATTEN=path/to.png` converts one for you.
301
+ PNG and JPEG are both accepted.
302
+
303
+ > **Screenshots must have no transparency.** Simulator captures often carry an alpha channel and Apple
304
+ > rejects those. `npx vydanne bridge` refuses before copying anything if it finds one, and tells you
305
+ > which file; to fix a single image in place, `VYDANNE_FLATTEN=path/to.png npx vydanne iap` converts it
306
+ > to RGB. Note that `fill` itself does **not** convert — flatten at the source.
296
307
 
297
308
  **Google Play** uses its **own** language codes (`de-DE`, `zh-CN`, `iw-IL`, `ar` — *not* Apple's
298
309
  `zh-Hans`/`he`), and only three text files:
@@ -305,8 +316,8 @@ fastlane/metadata/android/
305
316
  └─ full_description.txt (≤4000)
306
317
  ```
307
318
 
308
- Play images are read from **fixed paths** (they're where [zdymak](https://www.npmjs.com/package/zdymak)
309
- writes them). Each is uploaded only if the file exists, so a missing set never wipes what's live:
319
+ Play images are read from **fixed paths**. Each is uploaded only if the file exists, so a missing set
320
+ never wipes what's live:
310
321
 
311
322
  | Play asset | Path vydanne reads |
312
323
  |---|---|
@@ -318,6 +329,12 @@ writes them). Each is uploaded only if the file exists, so a missing set never w
318
329
 
319
330
  *(These paths are not configurable yet — create the folders at those locations, or symlink them.)*
320
331
 
332
+ > **Capturing with [zdymak](https://www.npmjs.com/package/zdymak)?** Its output layout is different
333
+ > from all of the above (one `store-assets/` tree, short locale codes, numbered filenames). Run
334
+ > **`npx vydanne bridge`** after every capture — it renames and files everything into the Apple and
335
+ > Play layouts on this page, holds back screenshots for locales that have no listing text yet, and
336
+ > refuses images with an alpha channel before Apple can. `--dry-run` shows what it would do.
337
+
321
338
  ---
322
339
 
323
340
  ## 7. Optional extras
@@ -345,40 +362,46 @@ separate ANSSI declaration.
345
362
 
346
363
  ## 8. Push it to the store
347
364
 
348
- Always look before you leap:
365
+ **Every command that changes a store is a dry run until you add `--apply`** — it prints each write it
366
+ would make and sends nothing. So the safe rhythm is always: run it, read the plan, run it again with
367
+ `--apply`.
368
+
369
+ The whole Apple release is one command, which runs the seven steps in the only order that works
370
+ (`prepare` → `fill` → `previews` → `age-rating` → `review-contact` → `accessibility` → `preflight`) and
371
+ stops at the first problem:
349
372
 
350
373
  ```sh
351
- npx vydanne preflight # is anything missing or over a character limit?
352
374
  npx vydanne diff # exactly what would change vs what's live now
375
+ npx vydanne push # DRY RUN of the whole pipeline — read it
376
+ npx vydanne push --apply # do it; ends at a green preflight
377
+ npx vydanne privacy # prints the answers to type into Apple's web UI
353
378
  ```
354
379
 
355
- `preflight` must be **green**. Then:
380
+ Each step is also its own command (`npx vydanne fill --apply`, etc.) if you prefer to go one at a time.
381
+ Two things worth knowing:
356
382
 
357
- ```sh
358
- npx vydanne fill # text + screenshots (iOS and Mac both)
359
- npx vydanne previews # App Preview videos
360
- npx vydanne age-rating
361
- npx vydanne review-contact
362
- npx vydanne accessibility # saved as a draft; publishes only once your app is live
363
- npx vydanne privacy # prints the answers to type into Apple's web UI
364
- ```
383
+ - **Updating an app that's already live?** The draft version to write into doesn't exist until
384
+ `prepare` creates it `push` runs it first, or run `npx vydanne prepare --apply` yourself. Without
385
+ it, `fill` refuses rather than touching the listing your customers are reading.
386
+ - **Replacing screenshots or previews that are already on the store** needs `VYDANNE_REPLACE=1` — by
387
+ default a populated slot is skipped (and says so), never overwritten.
365
388
 
366
- **For Google Play**, `fill` is a **dry run by default** — it validates and throws the change away so a
367
- half-finished local folder can't overwrite your live listing:
389
+ **For Google Play**, the same `--apply` rule applies — a dry run validates against Google for real,
390
+ then throws the change away, so a half-finished local folder can't overwrite your live listing:
368
391
 
369
392
  ```sh
370
393
  # macOS / Linux
371
- npx vydanne fill --store google # dry run: shows what would happen
372
- VYDANNE_COMMIT=1 npx vydanne fill --store google # actually commit it
394
+ npx vydanne fill --store google # dry run: shows what would happen
395
+ npx vydanne fill --store google --apply # actually commit it
373
396
  ```
374
397
  ```powershell
375
- # Windows PowerShell — set the variable first; the `VAR=1 command` form does NOT work here
398
+ # Windows PowerShell — same flag, no environment variables needed
376
399
  npx vydanne fill --store google
377
- $env:VYDANNE_COMMIT = "1"; npx vydanne fill --store google
378
- Remove-Item Env:\VYDANNE_COMMIT # clear it so later runs stay dry
400
+ npx vydanne fill --store google --apply
379
401
  ```
380
402
 
381
- Finally, run `npx vydanne diff` once more and eyeball one screenshot per platform in the web UI.
403
+ Finally, run `npx vydanne diff` once more it compares text *and* image content against the store
404
+ and eyeball one screenshot per platform in the web UI.
382
405
 
383
406
  ---
384
407
 
@@ -386,7 +409,9 @@ Finally, run `npx vydanne diff` once more and eyeball one screenshot per platfor
386
409
 
387
410
  1. **App Privacy** (Apple) — Apple's privacy API isn't reachable with an API key, so `vydanne privacy`
388
411
  prints the exact answers; you paste them into App Store Connect by hand.
389
- 2. **Upload the binary** — Xcode, Transporter, or your CI. vydanne never touches your build.
412
+ 2. **Build the binary** — Xcode / Gradle, as usual. `npx vydanne prerelease --apply` can then upload it
413
+ for you (TestFlight internal groups, or a Play testing track). Shipping it to the *public* stays in
414
+ the consoles, always.
390
415
  3. **Play Data Safety + content rating** — questionnaires in the Play Console.
391
416
  4. **Submit for Review** — yours to press.
392
417
 
@@ -400,13 +425,13 @@ Finally, run `npx vydanne diff` once more and eyeball one screenshot per platfor
400
425
  | `ASC key not found at …` | The `.p8` isn't where vydanne looks. | The error prints the exact path it wants — move the file there. List it with `ls ~/.appstoreconnect/private_keys/` (macOS/Linux) or `dir "$env:USERPROFILE\.appstoreconnect\private_keys"` (Windows). |
401
426
  | `app '…' not found for this ASC key` | The bundle ID is wrong, or the key's team doesn't own the app. | Check `bundleId` matches App Store Connect exactly. |
402
427
  | `401` / `403` from Apple | Key lacks permission, or the IDs are swapped. | Key access must be **App Manager**+. Confirm `ASC_KEY_ID` vs `ASC_ISSUER_ID` aren't reversed. |
403
- | `no editable version` | There's no version in an editable state. | In App Store Connect, create the next version (e.g. "1.0 Prepare for Submission"). |
428
+ | `no editable version` | The only version is live (read-only) and no draft exists yet. | `npx vydanne prepare --apply` creates the next version that's its job. |
404
429
  | A locale was ignored | The folder name isn't an Apple code. | Run `npx vydanne locales` and rename the folder to the code shown. |
405
- | Screenshots didn't upload | Wrong filename prefix, or the slot already has images. | Use the prefix table above. vydanne never overwrites a set that already has screenshots. |
406
- | Apple rejects a screenshot | It has an alpha channel. | Flatten to RGB — macOS/Linux: `VYDANNE_FLATTEN=shot.png npx vydanne iap` · Windows: `$env:VYDANNE_FLATTEN="shot.png"; npx vydanne iap`. |
407
- | `VYDANNE_… =1` "does nothing" on Windows | PowerShell doesn't support the Unix `VAR=1 command` form. | Set it first: `$env:VYDANNE_COMMIT = "1"`, then run the command. |
430
+ | Screenshots didn't upload | Wrong filename prefix, or the slot already has images. | `fill` now names both cases in its output. Use the prefix table above; to replace an already-populated slot, re-run with `VYDANNE_REPLACE=1`. |
431
+ | Apple rejects a screenshot | It has an alpha channel. | Flatten to RGB — macOS/Linux: `VYDANNE_FLATTEN=shot.png npx vydanne iap` · Windows: `$env:VYDANNE_FLATTEN="shot.png"; npx vydanne iap`. (`bridge` checks this for you.) |
432
+ | `VYDANNE_… =1` "does nothing" on Windows | PowerShell doesn't support the Unix `VAR=1 command` form. | Set it first: `$env:VYDANNE_REPLACE = "1"`, then run the command. |
408
433
  | `no google block in config` | Play isn't configured. | Add the `google` block and set `PLAY_JSON_KEY_FILE`. |
409
- | Play changes didn't stick | `fill --store google` is dry by default. | Re-run with `VYDANNE_COMMIT=1`. |
434
+ | Play changes didn't stick | Store-changing commands are dry by default. | Re-run with `--apply`. |
410
435
  | `accessibility` returns 409 | Labels can't publish before the app is live. | Leave it as a draft; publish after launch with `VYDANNE_A11Y_PUBLISH=1`. |
411
436
 
412
437
  ---
@@ -419,10 +444,14 @@ Finally, run `npx vydanne diff` once more and eyeball one screenshot per platfor
419
444
  | `PLAY_JSON_KEY_FILE` | Path to the Play service-account JSON (**required for Play**). |
420
445
  | `VYDANNE_CONFIG` | Use a different config file (same as `--config`). |
421
446
  | `VYDANNE_SKIP_METADATA` / `VYDANNE_SKIP_SCREENSHOTS` | `fill`: push only one half. |
422
- | `VYDANNE_COMMIT=1` | `fill --store google`: actually commit (otherwise dry). |
423
- | `VYDANNE_REPLACE=1` | `previews`: delete the existing preview and upload a new one. |
447
+ | `VYDANNE_REPLACE=1` | `fill` / `previews`: delete what's in an already-populated slot and upload yours. |
448
+ | `VYDANNE_VERSION=<x>` | `prepare`: name the version when it's created before its build exists. |
449
+ | `VYDANNE_IPA=<path>` / `VYDANNE_AAB=<path>` | `prerelease`: override where the binary is found. |
450
+ | `VYDANNE_TRACK` / `VYDANNE_RELEASE_NAME` | `prerelease --store google`: testing track / release name. |
424
451
  | `VYDANNE_FLATTEN=<png>` | `iap`: convert an image to RGB (removes transparency). |
425
452
  | `VYDANNE_A11Y_PUBLISH=1` | `accessibility`: publish the labels (only once the app is live). |
453
+ | `VYDANNE_ALLOW_CROSS_STORE=1` | Skip the other-store-mention check for one run. |
454
+ | `VYDANNE_COMMIT=1` | Legacy alias for `--apply` — prefer the flag. |
426
455
 
427
456
  Set them with `export NAME=value` on macOS/Linux, or `$env:NAME = "value"` in Windows PowerShell.
428
457
 
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # vydanne
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/vydanne.svg)](https://www.npmjs.com/package/vydanne)
4
+ [![npm downloads](https://img.shields.io/npm/dm/vydanne.svg)](https://www.npmjs.com/package/vydanne)
5
+ [![node](https://img.shields.io/node/v/vydanne.svg)](https://nodejs.org)
6
+ [![license](https://img.shields.io/npm/l/vydanne.svg)](LICENSE)
7
+ [![companion: zdymak](https://img.shields.io/badge/companion-zdymak-6e40c9?logo=github)](https://github.com/Lonli-Lokli/zdymak)
8
+
3
9
  **Fill in your app's App Store and Google Play listing — in every language — from files on your computer.**
4
10
 
5
11
  Publishing an app means typing the same things into a web form over and over: a name, a subtitle, a
@@ -29,15 +35,19 @@ actual app. vydanne handles the *listing*, not the software.
29
35
 
30
36
  ## The two halves of a release
31
37
 
32
- vydanne has a sibling — **[zdymak](https://www.npmjs.com/package/zdymak)**. They split the job cleanly:
38
+ vydanne has a sibling — **[zdymak](https://github.com/Lonli-Lokli/zdymak)**. They split the job cleanly:
33
39
 
34
40
  | | [zdymak](https://github.com/Lonli-Lokli/zdymak) 📸 | **vydanne** 📝 |
35
41
  |---|---|---|
36
42
  | Makes | The **pictures** — screenshots, App Preview videos, the Play feature graphic | The **words and the paperwork** — listing text, ratings, contacts, privacy, compliance |
37
43
  | Ends with | Image and video files on disk | A listing filled in and verified, ready for a human to submit |
38
44
 
39
- Use them together: zdymak produces the assets, vydanne uploads them alongside your text. Use vydanne
40
- alone if you already have your screenshots.
45
+ Use them together: zdymak produces the assets, **`npx vydanne bridge`** moves them to where vydanne
46
+ reads (zdymak writes one `store-assets/` tree with short locale codes and numbered files; vydanne reads
47
+ `fastlane/screenshots/<Apple-locale>/` with device-prefixed names, and fixed `marketing/out/` paths for
48
+ Play), and `fill` uploads them alongside your text. Run `bridge` after every capture — skipping it means
49
+ `fill` quietly re-uploads whatever was bridged last time. Use vydanne alone if you already have your
50
+ screenshots in its layout.
41
51
 
42
52
  <br>
43
53
 
@@ -143,6 +153,19 @@ export default {
143
153
  };
144
154
  ```
145
155
 
156
+ Every field is optional except `bundleId` and `primaryLocale`. The ones you reach for next:
157
+
158
+ | Field | What it's for |
159
+ |---|---|
160
+ | `screenshots` | Where your screenshots live, per platform. Defaults to `fastlane/screenshots` + `fastlane/screenshots-macos`. |
161
+ | `localeMap` | Your language codes → Apple's, for anything the built-in table misses (`{ nb: "no" }`). |
162
+ | `ageRating` | The content descriptors behind any rating above `4+`. Apple computes the band from them. |
163
+ | `reviewContact` | `{ demoAccountRequired }` — otherwise inferred from whether `review_information/demo_user.txt` exists. |
164
+ | `export` | Export-compliance details. `algorithms` + `statement` are required for `encryption: "standard"`. |
165
+ | `bridge` | `{ out, apple, play }` — which zdymak output folder feeds which store slot, when `dir:` overrides make them differ. |
166
+ | `push` | `{ skip: [...] }` — pipeline steps this app never runs. |
167
+ | `google` | The Play block, including `track`, `images` and `imageLocales`. |
168
+
146
169
  **3. Write your listing** as plain text files, one folder per language:
147
170
 
148
171
  ```
@@ -177,8 +200,9 @@ DRY RUN — 'fill' will not change App Store Connect. Add --apply to write.
177
200
  DRY RUN — 40 store write(s) withheld. Re-run with --apply to perform them.
178
201
  ```
179
202
 
180
- The commands this applies to are marked `✎` in `vydanne help`: `fill`, `previews`, `age-rating`,
181
- `review-contact`, `accessibility`, `prerelease`. Everything else only reads, and ignores the flag.
203
+ The commands this applies to are marked `✎` in the usage text (`vydanne` with no arguments): `prepare`, `push`, `fill`, `previews`,
204
+ `age-rating`, `review-contact`, `accessibility`, `prerelease`. Everything else only reads, and ignores
205
+ the flag.
182
206
 
183
207
  Two details worth knowing:
184
208
 
@@ -200,24 +224,66 @@ Two details worth knowing:
200
224
 
201
225
  | Command | In plain English |
202
226
  |---|---|
203
- | `preflight` | **Run this first.** Checks the listing is complete, nothing is over a character limit, and no locale mentions the other app store. Green means submittable. |
227
+ | `preflight` | **Run this first.** Checks the listing is complete, nothing is over a character limit, no locale mentions the other app store — and that the screenshots on the store are your *current* ones, not a stale set. Green means submittable. |
228
+ | `prepare` | Starts the next release: creates the App Store version you're preparing and attaches your newest build to it. **Needed before `fill` on an app that already has a version on sale** — until a draft exists there is nothing for the listing text to go into. Reuses the draft if it's already there, so it's safe to re-run. It does *not* submit. |
229
+ | `push` | **The whole release, one command**: runs `prepare` → `fill` → `previews` → `age-rating` → `review-contact` → `accessibility` → `preflight`, in that order, stopping at the first failure. Dry run without `--apply`, like everything else. Ends at a green preflight — it never submits. |
204
230
  | `diff` | Shows exactly what's different between your files and what's live. Nothing is changed — a safe preview. |
205
231
  | `fill` | Uploads your listing text and screenshots. Handles iPhone, iPad and Mac. Refuses to upload text that names the other mobile platform. |
206
232
  | `previews` | Uploads App Preview videos. |
207
233
  | `inspect` | Shows the app's current state in the store. Read-only. |
208
234
  | `locales` | Lists your languages and Apple's code for each — and warns about any language the App Store doesn't offer. |
209
- | `age-rating` | Sets the age rating. |
235
+ | `age-rating` | Sets the age rating. `rating: "4+"` needs nothing else; any higher rating is described feature-by-feature in `ageRating` and Apple computes the band from it. |
210
236
  | `review-contact` | Fills in the App Review contact details (who Apple calls if there's a problem). |
211
237
  | `accessibility` | Saves Accessibility Nutrition Labels from the `accessibility` block in your config. Stays a draft until your app is live. Refuses to run if you have not declared one — see below. |
212
238
  | `privacy` | Prints the privacy answers to paste into Apple's website (Apple's privacy section has no API). |
213
239
  | `iap` | Checks your in-app purchase text fits, and can strip transparency from an image. |
214
240
  | `compliance` | Generates the US encryption self-classification PDF that Apple asks for. |
241
+ | `bridge` | Moves [zdymak](https://github.com/Lonli-Lokli/zdymak)'s captured screenshots into the folders `fill` reads — renaming locales to the stores' codes and files to the device-slot convention. Local files only; `--dry-run` previews. Run it after every capture. |
215
242
  | `version` | Prints the version of vydanne. |
216
243
 
217
244
  For **Google Play**, add `--store google` to `inspect`, `diff`, `preflight`, `fill`, or `prerelease`.
218
245
 
219
- `fill`, `previews`, `age-rating`, `review-contact`, `accessibility` and `prerelease` change the store, so
220
- they need [`--apply`](#--apply-or-nothing-happens); without it they report and exit.
246
+ `prepare`, `push`, `fill`, `previews`, `age-rating`, `review-contact`, `accessibility` and `prerelease`
247
+ change the store, so they need [`--apply`](#--apply-or-nothing-happens); without it they report and exit.
248
+
249
+ ## The release pipeline — the order matters
250
+
251
+ A release is the same seven steps in the same order, every time. `prepare` must come first (until the
252
+ draft version exists, nothing has anywhere to write) and `preflight` must come last (green has to be
253
+ measured *after* the writes it blesses, or it blesses nothing). That ordering is exactly the kind of
254
+ thing that lives in someone's head until the day it doesn't — so `push` runs it for you, stopping at the
255
+ first failure, and each step's own refusals still apply:
256
+
257
+ ```sh
258
+ npx vydanne prerelease --apply # whenever the build is ready — before or after push is fine
259
+ npx vydanne push # DRY RUN of the whole pipeline: read the plan
260
+ npx vydanne push --apply # do it: prepare → fill → previews → age-rating
261
+ # → review-contact → accessibility → preflight
262
+ # then, in App Store Connect: Add to Review -> Submit (always you, never vydanne)
263
+ ```
264
+
265
+ The two flows differ only at the first step, and `push` absorbs the difference:
266
+
267
+ - **First release.** Creating the app record in App Store Connect already gave you a version in *Prepare
268
+ for Submission*, so `prepare` finds it and reuses it — a no-op that simply reports what's there.
269
+ - **An update to a live app.** A version on sale is read-only and there is no draft until someone makes
270
+ one. Here `prepare` is the step that matters: it creates the next version (numbered from the newest
271
+ build's own `CFBundleShortVersionString`, so it can't drift from the binary) and attaches that build.
272
+
273
+ Skip `prepare` on a live app and `fill` has nowhere valid to aim — it refuses, rather than falling back
274
+ to the version **on sale** and rewriting the listing your customers are reading.
275
+
276
+ One wrinkle worth knowing: on a live app, a *dry run* of `push` stops at `fill`, because the draft the
277
+ later steps write into doesn't exist until `prepare` is applied. `push` says so up front. Creating the
278
+ draft is safe — it isn't a submission — so `vydanne prepare --apply` first, then a dry `push`, previews
279
+ the whole plan. Preparing a version before its build exists is the one case that needs telling:
280
+
281
+ ```sh
282
+ VYDANNE_VERSION=1.2 npx vydanne prepare --apply
283
+ ```
284
+
285
+ Prefer doing it step by step? Every step is its own command (the table above), and `push` is nothing
286
+ more than those commands in the right order.
221
287
 
222
288
  <br>
223
289
 
@@ -299,11 +365,20 @@ the same line the Apple side draws by never submitting.
299
365
  **Paid app? Use `internal`.** It is the only track where testers install without buying; closed and open
300
366
  testers pay like everyone else.
301
367
 
302
- Release notes follow supply's layout, so an existing repo needs no migration —
303
- `<metadataDir>/<play-locale>/changelogs/<versionCode>.txt`, falling back to `default.txt`, truncated to
304
- Play's 500-char cap with a warning. The versionCode comes from the bundle's own manifest, so build
305
- numbering stays with the build and re-uploading a used code fails loudly instead of silently replacing a
306
- binary. Overrides: `VYDANNE_AAB`, `VYDANNE_TRACK`, `VYDANNE_RELEASE_NAME`.
368
+ Release notes follow supply's layout, so an existing repo needs no migration — per locale, first match
369
+ wins:
370
+
371
+ | File in `<metadataDir>/<play-locale>/changelogs/` | When to use it |
372
+ |---|---|
373
+ | `<versionCode>.txt` | You know the exact code (supply's own convention). |
374
+ | `next.txt` | **The notes for the release you're about to cut** — for when the versionCode isn't knowable in advance (e.g. it's derived from the git commit count, so every commit moves it). After a real (`--apply`) publish, vydanne renames it to `<versionCode>.txt`, so the *next* release can't inherit this one's notes by accident. |
375
+ | `default.txt` | Evergreen fallback ("bug fixes and improvements"). Falling back to it is **warned**, because notes written for one release quietly serving every later one is how a listing shows last release's news. |
376
+
377
+ Notes are truncated to Play's 500-char cap with a warning. The versionCode comes from the bundle's own
378
+ manifest — vydanne reads it out of the `.aab` locally and reports which changelog file each locale
379
+ resolves to *before* the upload, so a wrong file costs a re-run, not a re-release. Re-uploading a used
380
+ code fails loudly instead of silently replacing a binary. Overrides: `VYDANNE_AAB`, `VYDANNE_TRACK`,
381
+ `VYDANNE_RELEASE_NAME`.
307
382
 
308
383
  **Play is dry by default on purpose.** Nothing goes live until you add `--apply`, so a
309
384
  half-finished folder can never overwrite a good listing. Play also uses its **own** language codes
@@ -336,9 +411,9 @@ don't have to learn them the hard way.
336
411
  | Your main language is left empty → most of the world sees a blank page | `preflight` refuses to pass |
337
412
  | macOS is a **separate** listing; its text is not shared with iOS | Fills each platform independently |
338
413
  | Apple's list endpoints return blank text, so tools "see" an empty listing | Reads each language individually |
339
- | Screenshots with transparency get rejected | Converts them to RGB |
414
+ | Screenshots with transparency get rejected | `bridge` refuses before copying and names the files; `VYDANNE_FLATTEN` converts one |
340
415
  | Once a version is *Ready for Review*, most tools can no longer edit it | Uses a method that still works |
341
- | Character limits (30 / 30 / 100 / 170; purchases 30 / 45) | Checked before upload, not after rejection |
416
+ | Character limits (name/subtitle 30, keywords 100, promo 170; purchases 30 / 45) | All checked by `preflight` before upload, not after rejection |
342
417
  | One translation says "also on Google Play" → rejected under guideline 2.3.10 | Every locale is scanned before upload; `preflight` and `fill` both refuse |
343
418
  | Apple's privacy section can't be reached by any API key | Prints the exact answers to paste in |
344
419
  | Accessibility labels can't publish before launch | Saved as a draft automatically |
package/SKILL.md CHANGED
@@ -11,8 +11,12 @@ fastlane/Ruby/Python. Two jobs: **(A) write the listing well (ASO)**, **(B) push
11
11
  **Companion tool — [zdymak](https://www.npmjs.com/package/zdymak)** makes the *media* (screenshots, App
12
12
  Preview videos, Play feature graphic); vydanne pushes that media plus all the *text and paperwork*. If the
13
13
  user needs screenshots or a preview video produced, that's zdymak's job, not vydanne's — vydanne only
14
- uploads files that already exist. zdymak's default output paths are exactly the paths vydanne reads for
15
- Play images (below), so the two line up with no glue.
14
+ uploads files that already exist. **The two do NOT line up on disk** (they did before zdymak 0.15):
15
+ zdymak writes one root shaped `store-assets/<locale>/<target>/NN-name.png`, while vydanne reads several
16
+ hardcoded roots, with Apple's locale codes and a device-prefix filename convention. **`vydanne bridge`
17
+ is the glue** — run it after `zdymak screenshots`/`zdymak build` and before `fill`, every time.
18
+ Skipping it is silent and dangerous: zdymak reports success, vydanne re-uploads whatever was bridged
19
+ LAST time, and the store quietly keeps stale art.
16
20
 
17
21
  **Never submits for review; never ships to the public.** It *does* upload builds — `prerelease` sends
18
22
  the `.ipa` to TestFlight (internal groups) or the `.aab` to a Play closed track, and points the version
@@ -51,8 +55,30 @@ which user file was used, and whether the `.p8` is on disk.
51
55
 
52
56
  **Config fields:** `bundleId` · `primaryLocale` (the fallback — must be populated) · `asc` (optional
53
57
  `{profile}` — selection only, never secrets) · `platforms` (iOS and macOS are SEPARATE) · `uiLocales`
54
- (auto-mapped to ASC codes) · `metadataDir` · `rating` · `privacy` · `iaps` · `previews` · `export` ·
55
- `google` (Google Play).
58
+ (auto-mapped to ASC codes) · `localeMap` · `metadataDir` · `screenshots` · `rating` · `ageRating` ·
59
+ `privacy` · `iaps` · `previews` · `export` · `accessibility` · `ios` · `google` (Google Play) ·
60
+ `bridge` · `push` · `reviewContact` · `allowCrossStoreTerms`.
61
+
62
+ **Paths are defaults, not laws.** `metadataDir` (default `fastlane/metadata`), `screenshots`
63
+ (`{IOS, MAC_OS}`, default `fastlane/screenshots` + `-macos`) and `google.images` (Play image type →
64
+ local path) all follow fastlane's supply convention out of the box and are all overridable. Point them
65
+ at the repo you have rather than reshaping the repo around the tool.
66
+
67
+ **Four blocks the tool will NOT fill in for you.** Each publishes a CLAIM rather than a fact, so silence
68
+ is refused instead of defaulted — that is deliberate, and re-adding a default is the bug, not the fix:
69
+
70
+ - **`accessibility`** — Accessibility Nutrition Labels. Declare every feature true/false from what was
71
+ actually verified. No block → the command errors.
72
+ - **`export.algorithms` + `export.statement`** — the cryptography inventory and statement in the US
73
+ export-compliance PDF (`compliance`). Required when `export.encryption` is `"standard"`. Never invent
74
+ these; ask what the app actually ships. `export.filed` stays **false** until the report has really
75
+ been emailed to BIS and the NSA, and while it is false the PDF does not claim it was submitted.
76
+ - **`ageRating`** — needed for any `rating` above `"4+"`. Describe the CONTENT
77
+ (`{ violenceCartoonOrFantasy: "INFREQUENT_OR_MILD" }`, merged over an all-NONE base); Apple computes
78
+ the band. `"4+"` alone needs nothing else.
79
+ - **`reviewContact` / demo account** — whether App Review needs a login is inferred from
80
+ `<metadataDir>/review_information/demo_user.txt` + `demo_password.txt` (both gitignored). An app with
81
+ a sign-in wall and no demo account is a guaranteed rejection.
56
82
 
57
83
  For a non-technical user asking how to set this up from scratch, walk them through
58
84
  **`GETTING_STARTED.md`** (accounts → API key → config → folders → push) rather than improvising.
@@ -94,17 +120,38 @@ before the **first underscore** selects the device slot; files upload in sorted
94
120
  | `watch_` | `APP_WATCH_ULTRA` |
95
121
  | `macos_` | `APP_DESKTOP` (in `screenshots-macos/`) |
96
122
 
97
- An unknown prefix is silently ignored. A set that **already has screenshots is skipped**, never
98
- duplicated to replace shots, delete them in ASC first. PNGs must be **RGB with no alpha**.
123
+ A file with an unknown prefix is not uploaded, and `fill` names it. A set that **already has
124
+ screenshots is skipped** (and says so), never duplicated `VYDANNE_REPLACE=1` deletes the store's set
125
+ and uploads yours, the same flag `previews` uses. PNGs must be **RGB with no alpha**.
99
126
 
100
127
  **Play listing text** — `<google.metadataDir>/<PLAY-locale>/{title,short_description,full_description}.txt`
101
128
  (30 / 80 / 4000). Play uses its **own** codes (`de-DE`, `zh-CN`, `iw-IL`, `ar`, `be`) — *not* Apple's
102
129
  `zh-Hans`/`he`/`ar-SA`.
103
130
 
104
- **Play images** — hardcoded source paths (zdymak's output), each pushed only when the file exists, so a
105
- missing local set never deletes the live one: `brand/icons/play/icon-512.png` (512²) ·
106
- `marketing/out/play-feature-graphic.png` (1024×500) · `marketing/out/play-phone-plain/` ·
107
- `marketing/out/play-tablet7-plain/` (7″) · `marketing/out/play-tablet-plain/` (10″).
131
+ **Play images** — DEFAULT source paths (override any of them with `google.images`), each pushed only
132
+ when the file exists, so a missing local set never deletes the live one:
133
+ `brand/icons/play/icon-512.png` (512², from znachok) · `marketing/out/play-feature-graphic.png`
134
+ (1024×500) · `marketing/out/play-phone-plain/` · `marketing/out/play-tablet7-plain/` (7″) ·
135
+ `marketing/out/play-tablet-plain/` (10″) · `marketing/out/play-wear/`. `wearScreenshots`, `tvScreenshots`
136
+ and `tvBanner` are also understood, so a Wear OS or Android TV release is a config line, not a code
137
+ change. An image dir that exists but is EMPTY is reported (by `fill` and `diff`) as a live set only
138
+ Play Console can remove.
139
+
140
+ Play holds graphics **per language**. The default uploads one untranslated set at
141
+ `google.defaultLocale`; `google.imageLocales` (a list, or `"*"` for every local listing folder) opts
142
+ into localized art, and a `<source>/<lang>/` subdirectory overrides the shared source for that language.
143
+ `diff --store google` compares exactly the locales `fill` would write, so the two never disagree.
144
+
145
+ **`bridge` populates both screenshot layouts from zdymak's output.** It maps
146
+ `store-assets/<locale>/<dir>/NN-name.png` onto the Apple and Play paths above: locale codes via the
147
+ same `toAsc` table `fill` uses (`de` → `de-DE`; a code with no App Store language is skipped and falls
148
+ back to the primary listing), the device-slot prefix prepended (`iphone69_01-fresh.png`), and the Play
149
+ sets into their configured destinations. A locale with screenshots but NO listing text is held back
150
+ (uploading pictures alone would create the localization and break its fallback to the primary
151
+ language), and every bridged image is checked for an alpha channel — which Apple rejects — on the
152
+ SOURCE, before anything is copied, so a refusal leaves the destinations untouched and `--dry-run`
153
+ catches it too. Local files only; `--dry-run` previews what would be written *and removed*. See the
154
+ directory-vs-target note under Commands for why the source folder name is the thing that matters.
108
155
 
109
156
  ## A. Writing the listing (the ASO craft — the durable value)
110
157
 
@@ -165,8 +212,20 @@ who it's for → honest close. Keep it scannable; lead each bullet with the payo
165
212
  `fill` (metadata + screenshots, native PATCH/chunked upload — works even at READY_FOR_REVIEW) ·
166
213
  `previews` (App Preview videos) · `age-rating` · `review-contact` · `accessibility` (draft; publish once
167
214
  live) · `privacy` (prints answers for the UI — the API can't reach Apple's iris host) · `iap` (validate +
168
- RGB flatten) · `compliance` (US self-classification PDF) · `diff` (what differs vs live) · `preflight`
169
- (completeness gate + cross-store lint) · `inspect` · `auth` (what credentials resolved, and from where) · `locales` · `version`.
215
+ RGB flatten) · `compliance` (US self-classification PDF) · `bridge` (zdymak's output the folders
216
+ `fill` reads) · `diff` (what differs vs live, text AND media by checksum) · `preflight`
217
+ (completeness gate + cross-store lint + stale-screenshot check) · `inspect` · `auth` (what credentials
218
+ resolved, and from where) · `locales` · `version`.
219
+
220
+ **`bridge` maps by DIRECTORY, not by target.** zdymak writes each shot to `<dir || target>`, so a
221
+ `dir:` override makes the folder name differ from the target name — and Play's 7" slot can *only* exist
222
+ that way (`{ target: 'play-tablet', dir: 'play-tablet7-plain' }`; there is no `play-tablet7` target).
223
+ Defaults prefer the `-plain` convention Google asks for on listings and fall back to the bare target
224
+ name; `bridge.apple` / `bridge.play` override per slot. It **owns its destinations per store**: any
225
+ Apple output means both Apple roots are rebuilt (so art dropped upstream stops being uploaded), while
226
+ an app that bridges only Play never has its hand-managed Apple screenshots touched. It plans before it
227
+ writes — a failure (alpha channel, empty source) leaves every destination untouched, and `--dry-run`
228
+ reports exactly what a real run would write *and remove*.
170
229
 
171
230
  **Cross-store lint.** `preflight` and `fill` refuse listing text that names the other mobile platform
172
231
  — App Review 2.3.10 for Apple, the Store Listing and Promotion policy for Google — scanning every
@@ -176,6 +235,43 @@ blocking; `allowCrossStoreTerms: [...]` in the config silences a specific one, a
176
235
  `VYDANNE_ALLOW_CROSS_STORE=1` overrides a single run. This is a rejection that surfaces days later in
177
236
  one locale out of twenty, so it is checked where it is free to fix.
178
237
 
238
+ **The pipeline has ONE working order** — `prepare` → `fill` → `previews` → `age-rating` →
239
+ `review-contact` → `accessibility` → `preflight` → **a human submits**. `prepare` must be first (until
240
+ the draft version exists, nothing has anywhere to write) and `preflight` last (green must be measured
241
+ after the writes it blesses). `push` runs exactly that sequence — each step the same `run` as the
242
+ standalone command, on the same client, stopping at the first failure — so prefer `vydanne push` over
243
+ re-deriving the order; the near-miss that motivated it was `fill` pointed at a live-only app out of
244
+ order. The two flows differ only at step one and `push` absorbs it: a FIRST release already has a
245
+ PREPARE_FOR_SUBMISSION version (creating the app record made it), so `prepare` is a find-and-reuse
246
+ no-op; an UPDATE has only the read-only live version until `prepare` creates the next one. On a live
247
+ app a DRY `push` stops at `fill` — the draft the later steps target doesn't exist until `prepare` is
248
+ applied — and says so up front; `prepare --apply` (a draft, not a submission) then a dry `push`
249
+ previews the whole plan. `prepare` creates a version for EVERY declared platform, so an iOS+macOS app
250
+ gets both drafts. `prerelease` is deliberately not a step (macOS-only, shells out to altool); run it
251
+ whenever the build is ready — `prepare` attaches the newest build either way.
252
+
253
+ **`push --skip <step>[,<step>]`** (or `push: { skip: [...] }`) drops a step that doesn't apply — the
254
+ usual case being an app with no audited `accessibility` block, which the command correctly refuses to
255
+ guess. `prepare` and `preflight` cannot be skipped. Every skip is reported on its own line AND again
256
+ after the final green, because the whole value of that last line is that green means green: never let
257
+ a skipped run read like a complete one.
258
+
259
+ `prepare` creates the version to write INTO, and is REQUIRED as the first push step on any app that
260
+ already has a version on sale. Every other Apple command finds its target through
261
+ `client.editVersion()`, which returns the first version Apple has not marked dead; when the only
262
+ version is live it has no editable record to return and falls back to the live one — so `fill` aims
263
+ its `description`/`whatsNew` PATCHes at the listing customers are reading, and `prerelease` declines
264
+ to attach the build it just uploaded. `prepare` POSTs `/v1/appStoreVersions` with `releaseType:
265
+ MANUAL` (so approval still doesn't release), sets `copyright` from `<metadataDir>/copyright.txt`
266
+ (nothing else in vydanne writes that version-level field), and attaches the newest build. The version
267
+ number is read off that build's `preReleaseVersion` — the archive's own
268
+ `CFBundleShortVersionString`, so it cannot drift from the binary — or `VYDANNE_VERSION=<x>` when the
269
+ version is being prepared before its build exists. It is find-or-create, so re-running is safe; a
270
+ version Apple has locked (`IN_REVIEW`, `READY_FOR_SALE`, …) is refused rather than edited, because
271
+ withdrawing a submission is the operator's call. It pre-checks the number against the version on sale,
272
+ turning Apple's 409 into a sentence. **Creating a draft is not submitting** — Add to Review and Submit
273
+ stay manual.
274
+
179
275
  `prerelease` uploads the BUILD. On Apple it validates and uploads the `.ipa` to **TestFlight** via
180
276
  `xcrun altool` — the one command that shells out, because the ASC REST API has never carried a binary,
181
277
  which also makes it macOS-only. `.ipa` comes from `ios.ipa` / `VYDANNE_IPA` (a directory takes its
@@ -191,10 +287,14 @@ edit transaction. `production` is REFUSED — not flag-gated — so no argument
191
287
  public; promoting the tested build stays a human's job, mirroring the Apple side never submitting. Track
192
288
  comes from `google.track` / `VYDANNE_TRACK`, default `internal`; the bundle from `google.aab` /
193
289
  `VYDANNE_AAB` (a directory takes its newest `.aab`). **For a PAID app use `internal`** — it's the only
194
- track where testers install without buying. Notes follow supply's layout:
195
- `<google.metadataDir>/<play-locale>/changelogs/<versionCode>.txt`, falling back to `default.txt`, capped
196
- at Play's 500 chars. DRY by default; `--apply` publishes. The versionCode comes from the bundle itself,
197
- so re-uploading one fails loudly instead of silently replacing.
290
+ track where testers install without buying. Notes follow supply's layout, per locale, first match wins:
291
+ `<google.metadataDir>/<play-locale>/changelogs/<versionCode>.txt` `next.txt` `default.txt`, capped
292
+ at Play's 500 chars. **Write the upcoming release's notes as `next.txt`** when the versionCode is not
293
+ knowable in advance (derived from the commit count, say): after a real publish vydanne renames it to
294
+ `<versionCode>.txt` so the next release can't inherit it, and a `default.txt` fallback is WARNED rather
295
+ than silent. The versionCode is read out of the `.aab` locally and the changelog resolution reported
296
+ BEFORE the upload; re-uploading a used code fails loudly instead of silently replacing. DRY by default;
297
+ `--apply` publishes.
198
298
 
199
299
  `--store google` routes `inspect` · `diff` · `preflight` · `fill` · `prerelease` to the Play Developer **Edits** API
200
300
  (OAuth2 service account; **scoped to the config's `packageName`** — a shared key can't touch another app).
@@ -202,9 +302,10 @@ The AAB binary and the (YouTube-URL) promo video stay outside vydanne.
202
302
 
203
303
  ## `--apply` — writes are opt-in
204
304
 
205
- **Every store-mutating command is a DRY RUN without `--apply`**: `fill` · `previews` · `age-rating` ·
206
- `review-contact` · `accessibility` · `prerelease` (marked `✎` in `vydanne help`). They read the store,
207
- print each write they would make, and send nothing. Read-only commands ignore the flag.
305
+ **Every store-mutating command is a DRY RUN without `--apply`**: `prepare` · `push` · `fill` ·
306
+ `previews` · `age-rating` · `review-contact` · `accessibility` · `prerelease` (marked `✎` in
307
+ the usage text, printed by `vydanne` with no arguments). They read the store, print each write they would make, and send nothing. Read-only
308
+ commands ignore the flag.
208
309
 
209
310
  Never reach for `--apply` to "check whether it works" — the dry run IS the check, and it walks the whole
210
311
  plan rather than stopping at the first locale. Its closing count is what you compare against `diff`.
@@ -217,14 +318,18 @@ through that client. **A new command that touches the store must be marked `writ
217
318
  `src/registry.mjs`** — that flag is the whole opt-in, not a label.
218
319
 
219
320
  **Env toggles:** `VYDANNE_CONFIG` · `VYDANNE_SKIP_METADATA` / `VYDANNE_SKIP_SCREENSHOTS` (fill) ·
220
- `VYDANNE_REPLACE` (previews) · `VYDANNE_FLATTEN=<png>` (iap) · `VYDANNE_A11Y_PUBLISH` (accessibility) ·
221
- `VYDANNE_COMMIT=1` (legacy alias for `--apply`; prefer the flag).
321
+ `VYDANNE_REPLACE` (fill screenshots + previews: replace populated slots) · `VYDANNE_VERSION` (prepare) ·
322
+ `VYDANNE_IPA` / `VYDANNE_AAB` / `VYDANNE_TRACK` / `VYDANNE_RELEASE_NAME` (prerelease) ·
323
+ `VYDANNE_FLATTEN=<png>` (iap) · `VYDANNE_A11Y_PUBLISH` (accessibility) · `VYDANNE_ALLOW_CROSS_STORE`
324
+ (one run past the cross-store lint) · `VYDANNE_COMMIT=1` (legacy alias for `--apply`; prefer the flag).
222
325
 
223
326
  ## Flow
224
327
 
225
- config → **write the English master listing (ASO, research-grounded)** → `preflight` (char limits) fan
226
- out one copywriter agent per locale → media from zdymak → `fill` + `previews` + declarations (read the
227
- dry run, then re-run with `--apply`) `diff` → `preflight` (must be green) **a human submits**.
328
+ config → **write the English master listing (ASO, research-grounded)** → fan out one copywriter agent
329
+ per locale → media from zdymak → **`bridge`** (zdymak's output into vydanne's folders — every time) →
330
+ build via `prerelease --apply` whenever it is ready → `push` (read the dry run, then re-run with
331
+ `--apply` — it is prepare → fill → previews → age-rating → review-contact → accessibility → preflight,
332
+ stopping at the first failure) → `diff` to confirm → **a human submits**.
228
333
 
229
334
  ## Gotchas it encodes (don't re-derive)
230
335