vydanne 0.9.0 → 0.10.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.
package/README.md CHANGED
@@ -218,6 +218,11 @@ Two details worth knowing:
218
218
  > Upgrading from ≤ 0.5? The Apple half used to write immediately — `vydanne fill` now needs `--apply`.
219
219
  > `VYDANNE_COMMIT=1` still works as an alias so existing Play scripts keep running, but prefer the flag.
220
220
 
221
+ `fill` takes four more overrides, all off by default: `VYDANNE_REPLACE=1` replaces a store screenshot
222
+ set instead of leaving the live one alone (what you want after re-rendering them), and
223
+ `VYDANNE_SKIP_METADATA=1` / `VYDANNE_SKIP_SCREENSHOTS=1` do one half of the job when the other is
224
+ already right. `VYDANNE_FLATTEN=1` reads a flat screenshot folder rather than per-locale ones.
225
+
221
226
  <br>
222
227
 
223
228
  ## What each command does
@@ -379,7 +384,15 @@ Notes are truncated to Play's 500-char cap with a warning. The versionCode comes
379
384
  manifest — vydanne reads it out of the `.aab` locally and reports which changelog file each locale
380
385
  resolves to *before* the upload, so a wrong file costs a re-run, not a re-release. Re-uploading a used
381
386
  code fails loudly instead of silently replacing a binary. Overrides: `VYDANNE_AAB`, `VYDANNE_TRACK`,
382
- `VYDANNE_RELEASE_NAME`.
387
+ `VYDANNE_RELEASE_NAME`, `VYDANNE_STATUS`; and `VYDANNE_IPA` for the App Store half.
388
+
389
+ **An app that has never been published needs `releaseStatus: "draft"`.** Play calls it a *draft app*
390
+ and refuses a `completed` release on every track except `internal`, answering *"Only releases with
391
+ status draft may be created on draft app."* — which names neither the track nor the fix, and is why
392
+ the same bundle uploads to `internal` and fails on `alpha`. Set it in `google.releaseStatus` (or
393
+ `VYDANNE_STATUS=draft` for one run); the build then waits in Play Console for a person to start the
394
+ rollout, which is where an unpublished app's first one belongs. Remove it once the app is live.
395
+ vydanne explains this rejection rather than printing Play's version of it.
383
396
 
384
397
  **Play is dry by default on purpose.** Nothing goes live until you add `--apply`, so a
385
398
  half-finished folder can never overwrite a good listing. Play also uses its **own** language codes
package/SKILL.md CHANGED
@@ -286,6 +286,7 @@ With `--store google` it uploads an `.aab` to a **closed testing track** with re
286
286
  edit transaction. `production` is REFUSED — not flag-gated — so no argument combination ships to the
287
287
  public; promoting the tested build stays a human's job, mirroring the Apple side never submitting. Track
288
288
  comes from `google.track` / `VYDANNE_TRACK`, default `internal`; the bundle from `google.aab` /
289
+ - `VYDANNE_STATUS` — release status for `prerelease --store google`: `draft`, `inProgress`, `halted`, `completed` (default). Use `draft` for an app that has never been published: Play refuses a completed release on any track but `internal` until it is live, and says so in a message that names neither the track nor the fix. Also settable as `google.releaseStatus`.
289
290
  `VYDANNE_AAB` (a directory takes its newest `.aab`). **For a PAID app use `internal`** — it's the only
290
291
  track where testers install without buying. Notes follow supply's layout, per locale, first match wins:
291
292
  `<google.metadataDir>/<play-locale>/changelogs/<versionCode>.txt` → `next.txt` → `default.txt`, capped
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vydanne",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "App Store Connect + Google Play submission prep — the companion to zdymak (media). Native Node (no fastlane/Ruby/Python): localized listings, screenshot/preview/icon upload, ratings, review contact, accessibility & privacy labels, IAP, export docs, build upload to TestFlight / a Play closed track, a diff of local-vs-store, and a preflight verifier that encodes the store gotchas. Never submits for review. iOS/macOS via the ASC REST API; Android via the Play Developer Edits API (--store google).",
5
5
  "keywords": [
6
6
  "app-store-connect",
package/src/config.mjs CHANGED
@@ -98,6 +98,12 @@ export async function loadConfig(p) {
98
98
  defaultLocale: raw.google.defaultLocale || raw.primaryLocale,
99
99
  aab: raw.google.aab || null,
100
100
  track: raw.google.track || "internal", // testing only — `prerelease` refuses production
101
+ // What state a new release is created in: draft | inProgress | halted | completed.
102
+ // Left null so `prerelease` keeps its own default ("completed"). The one case that needs
103
+ // it is an app that has never been published — Play calls that a "draft app" and refuses
104
+ // a completed release on any track but internal, so the first closed or open rollout has
105
+ // to be created as "draft" and started by a person in Console.
106
+ releaseStatus: raw.google.releaseStatus || null,
101
107
  // Play image type -> local source path. Merged over the defaults so an app overrides only the
102
108
  // slots whose layout differs; the default for `icon` points at znachok's output, which is a
103
109
  // sensible default for this portfolio and a mystery to anyone else, so it is overridable.
@@ -92,13 +92,26 @@ export async function run(config, client) {
92
92
  if (!notes) notes = readNotes(g.metadataDir, versionCode, g.defaultLocale);
93
93
 
94
94
  // One complete release object: Play replaces the track's releases wholesale.
95
- const release = { status: "completed", versionCodes: [String(versionCode)] };
95
+ //
96
+ // The status is settable because a DRAFT APP — one that has never been published — refuses a
97
+ // "completed" release on any track but internal, with
98
+ // "Only releases with status draft may be created on draft app."
99
+ // which names neither the track nor the fix. Set VYDANNE_STATUS=draft for the first closed
100
+ // or open rollout of an app that is not live yet; the release then waits in Play Console for
101
+ // a human to start it, which is where an unpublished app's first rollout belongs anyway.
102
+ const status = process.env.VYDANNE_STATUS || g.releaseStatus || "completed";
103
+ if (!RELEASE_STATUSES.includes(status)) {
104
+ throw new Error(
105
+ `release status "${status}" is not one Play accepts — use one of ${RELEASE_STATUSES.join(", ")}`,
106
+ );
107
+ }
108
+ const release = { status, versionCodes: [String(versionCode)] };
96
109
  if (notes.entries.length) release.releaseNotes = notes.entries;
97
110
  const name = process.env.VYDANNE_RELEASE_NAME;
98
111
  if (name) release.name = name;
99
112
 
100
113
  const put = await client.putTrack(editId, track, [release]);
101
- if (put.status >= 300) throw new Error(`tracks.update ${put.status}: ${JSON.stringify(put.json).slice(0, 300)}`);
114
+ if (put.status >= 300) throw new Error(explainPlayError("tracks.update", put, track, status));
102
115
 
103
116
  if (client.dryRun) {
104
117
  await client.deleteEdit(editId);
@@ -106,8 +119,17 @@ export async function run(config, client) {
106
119
  return true;
107
120
  }
108
121
  const res = await client.commit(editId);
109
- if (res.status >= 300) throw new Error(`edits.commit ${res.status}: ${JSON.stringify(res.json).slice(0, 300)}`);
110
- console.log(green(`\n committed versionCode ${versionCode} is live on "${track}".`));
122
+ if (res.status >= 300) throw new Error(explainPlayError("edits.commit", res, track, status));
123
+ // "live" is only true of a release that has actually started. A draft one is uploaded and
124
+ // waiting, and telling somebody it is live is how a build sits unnoticed for a week.
125
+ console.log(
126
+ status === "draft"
127
+ ? green(`\n committed — versionCode ${versionCode} is on "${track}" as a DRAFT release.`)
128
+ : green(`\n committed — versionCode ${versionCode} is live on "${track}".`),
129
+ );
130
+ if (status === "draft") {
131
+ console.log(` Nobody has it yet: open Play Console and start the rollout when you are ready.`);
132
+ }
111
133
  archiveNextNotes(notes, versionCode);
112
134
  console.log(" Production stays manual: promote it in Play Console when you're ready.");
113
135
  return true;
@@ -209,3 +231,39 @@ function archiveNextNotes(notes, versionCode) {
209
231
  }
210
232
  }
211
233
  }
234
+
235
+ /** The release statuses Play's Publishing API accepts. A typo here costs a round trip otherwise. */
236
+ const RELEASE_STATUSES = ["draft", "inProgress", "halted", "completed"];
237
+
238
+ /**
239
+ * Play's rejection, plus what to do about it.
240
+ *
241
+ * One rejection is worth translating rather than printing. An app that has never been published is
242
+ * a "draft app", and Play will not accept a `completed` release on any track except internal:
243
+ *
244
+ * Only releases with status draft may be created on draft app.
245
+ *
246
+ * That sentence names neither the track it is talking about nor the setting that fixes it, and it
247
+ * arrives identically from the track update and from the commit — so the same upload succeeds on
248
+ * `internal` and fails on `alpha` with a message that suggests nothing about tracks at all. The
249
+ * fix is one setting, and it belongs in the error rather than in somebody's memory.
250
+ */
251
+ function explainPlayError(where, res, track, status) {
252
+ const body = JSON.stringify(res.json).slice(0, 300);
253
+ const message = res.json?.error?.message || "";
254
+ if (/draft app/i.test(message)) {
255
+ return [
256
+ `${where} ${res.status}: ${message}`,
257
+ "",
258
+ ` This app has never been published, so Play calls it a draft app — and a draft app only`,
259
+ ` accepts releases whose status is "draft". You asked for "${status}" on track "${track}".`,
260
+ "",
261
+ ` Set it once in your config: google: { releaseStatus: "draft" }`,
262
+ ` Or for this run only: VYDANNE_STATUS=draft`,
263
+ "",
264
+ ` The build then waits in Play Console for a person to start the rollout, which is where an`,
265
+ ` unpublished app's first one belongs. Remove the setting once the app is live.`,
266
+ ].join("\n");
267
+ }
268
+ return `${where} ${res.status}: ${body}`;
269
+ }
package/types/index.d.ts CHANGED
@@ -139,6 +139,15 @@ export interface GoogleConfig {
139
139
  * track you created in Play Console. Only 'production' is refused — that release is a human's.
140
140
  */
141
141
  track?: string;
142
+ /**
143
+ * What state a new release is created in. Defaults to 'completed' — the rollout starts on upload.
144
+ *
145
+ * Set 'draft' for an app that has NEVER been published: Play calls that a "draft app" and refuses
146
+ * a completed release on any track but internal, with a message that names neither the track nor
147
+ * the fix. A draft release waits in Play Console for a person to start it. Remove once live.
148
+ * Override: VYDANNE_STATUS.
149
+ */
150
+ releaseStatus?: "draft" | "inProgress" | "halted" | "completed";
142
151
  /**
143
152
  * Play image type -> local source path. Merged over the defaults, so declare only what differs.
144
153
  * A type whose source does not exist is skipped; a missing local set never deletes the live one.
@@ -100,6 +100,11 @@ export default {
100
100
  // 'internal' (default), 'alpha', 'beta', or the name of any closed track you made in Play Console.
101
101
  // Only 'production' is refused.
102
102
  // track: "internal",
103
+ // What state a new release is created in: "draft" | "inProgress" | "halted" | "completed".
104
+ // Defaults to "completed" — the rollout starts on upload. An app that has NEVER been published
105
+ // is a "draft app" to Play, which refuses a completed release on any track but internal; set
106
+ // "draft" until it is live and start the rollout yourself in Console. Override: VYDANNE_STATUS.
107
+ // releaseStatus: "draft",
103
108
  // Play image type -> local source. Merged over the defaults, so override only what differs.
104
109
  // images: { icon: "brand/icons/play/icon-512.png", phoneScreenshots: "marketing/out/play-phone-plain" },
105
110
  // Play holds graphics PER LANGUAGE. Default is one set at `defaultLocale`; list locales (or "*")