vydanne 0.8.1 → 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 +15 -2
- package/SKILL.md +1 -0
- package/package.json +1 -1
- package/src/commands/preflight.mjs +38 -0
- package/src/config.mjs +6 -0
- package/src/play/commands/prerelease.mjs +62 -4
- package/types/index.d.ts +9 -0
- package/vydanne.config.example.mjs +5 -0
package/README.md
CHANGED
|
@@ -218,13 +218,18 @@ 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
|
|
224
229
|
|
|
225
230
|
| Command | In plain English |
|
|
226
231
|
|---|---|
|
|
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. |
|
|
232
|
+
| `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. It also asserts the fields no local file drives and that no diff can therefore see: copyright, the App Review contact, the age rating, the primary category and the content-rights declaration. Those are each set by their own command, so a listing that never ran one had them EMPTY while this printed green. Green means submittable. |
|
|
228
233
|
| `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
234
|
| `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. |
|
|
230
235
|
| `diff` | Shows exactly what's different between your files and what's live. Nothing is changed — a safe preview. |
|
|
@@ -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.
|
|
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",
|
|
@@ -16,6 +16,27 @@ export async function run(config, client) {
|
|
|
16
16
|
// the README listed them among the limits "checked before upload". Read-only, so `allowLive` is
|
|
17
17
|
// right: on an app with nothing in preparation the live record is the only one to measure.
|
|
18
18
|
const info = await client.appInfo({ allowLive: true });
|
|
19
|
+
|
|
20
|
+
// ── The fields no LOCAL file drives ─────────────────────────────────────────────────────────────
|
|
21
|
+
//
|
|
22
|
+
// Everything else here compares local metadata against the store, which is the right question for
|
|
23
|
+
// anything `fill` uploads and the wrong one for the half-dozen fields set by their own standalone
|
|
24
|
+
// command. Those are invisible to a local-vs-remote diff: there is nothing local to differ from, so
|
|
25
|
+
// a field that was NEVER SET reads exactly like one that matches.
|
|
26
|
+
//
|
|
27
|
+
// Reported from the field: a listing reached PREPARE_FOR_SUBMISSION with an empty copyright and no
|
|
28
|
+
// App Review contact at all, while this command printed "no blockers". `prepare` sets the first and
|
|
29
|
+
// `review-contact` the second; neither is part of `fill`, and neither had ever been run. Preflight
|
|
30
|
+
// was not wrong so much as asked the wrong question — so it now also asks whether the listing is
|
|
31
|
+
// SUBMITTABLE, which only the store can answer.
|
|
32
|
+
const rights = client.app?.attributes?.contentRightsDeclaration;
|
|
33
|
+
if (!rights) problems.push("content rights not declared — `vydanne appinfo --apply`");
|
|
34
|
+
if (info) {
|
|
35
|
+
if (!info.attributes.appStoreAgeRating) problems.push("age rating not declared — `vydanne age-rating --apply`");
|
|
36
|
+
const primary = (await client.get(`/v1/appInfos/${info.id}/primaryCategory`)).json?.data?.id;
|
|
37
|
+
if (!primary) problems.push("primary category not set — `vydanne appinfo --apply`");
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
if (info) {
|
|
20
41
|
const infoLocs = (await client.get(`/v1/appInfos/${info.id}/appInfoLocalizations?limit=200`)).json.data || [];
|
|
21
42
|
for (const il of infoLocs) {
|
|
@@ -37,6 +58,23 @@ export async function run(config, client) {
|
|
|
37
58
|
continue;
|
|
38
59
|
}
|
|
39
60
|
console.log(` ${platform}: version ${v.attributes.versionString} (${v.attributes.appStoreState})`);
|
|
61
|
+
|
|
62
|
+
// Required, and set by `prepare` from <metadataDir>/copyright.txt — not by `fill`.
|
|
63
|
+
if (!v.attributes.copyright) {
|
|
64
|
+
problems.push(`${platform}: copyright EMPTY — set ${config.metadataDir}/copyright.txt, then \`vydanne prepare --apply\``);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The App Review contact. Apple will not take a submission without one, and it is per-VERSION, so
|
|
68
|
+
// a contact set on a previous version does not carry forward on its own.
|
|
69
|
+
const detail = (await client.get(`/v1/appStoreVersions/${v.id}/appStoreReviewDetail`)).json?.data?.attributes;
|
|
70
|
+
const CONTACT = ["contactFirstName", "contactLastName", "contactPhone", "contactEmail"];
|
|
71
|
+
const missing = detail ? CONTACT.filter((f) => !detail[f]) : CONTACT;
|
|
72
|
+
if (missing.length) {
|
|
73
|
+
problems.push(`${platform}: App Review contact ${detail ? `missing ${missing.join(", ")}` : "not set"} — \`vydanne review-contact --apply\``);
|
|
74
|
+
} else if (detail.demoAccountRequired && !detail.demoAccountName) {
|
|
75
|
+
// The rejection `review-contact` exists to avoid, arriving by the door of nobody running it.
|
|
76
|
+
problems.push(`${platform}: demoAccountRequired is true but no demo account is set — Apple rejects this`);
|
|
77
|
+
}
|
|
40
78
|
const locs = await client.versionLocalizations(v.id);
|
|
41
79
|
const ascLocales = [...new Set([config.primaryLocale, ...Object.values(res.supported)])];
|
|
42
80
|
|
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
|
-
|
|
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(
|
|
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(
|
|
110
|
-
|
|
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 "*")
|