vydanne 0.3.0 → 0.4.1
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 +32 -1
- package/SKILL.md +11 -1
- package/bin/vydanne.mjs +1 -0
- package/package.json +1 -1
- package/src/config.mjs +3 -0
- package/src/play/client.mjs +25 -0
- package/src/play/commands/prerelease.mjs +137 -0
- package/src/registry.mjs +2 -1
- package/types/index.d.ts +6 -0
package/README.md
CHANGED
|
@@ -180,7 +180,7 @@ Run vydanne **from your project folder** — it finds everything relative to whe
|
|
|
180
180
|
| `compliance` | Generates the US encryption self-classification PDF that Apple asks for. |
|
|
181
181
|
| `version` | Prints the version of vydanne. |
|
|
182
182
|
|
|
183
|
-
For **Google Play**, add `--store google` to `inspect`, `diff`, `preflight`, or `
|
|
183
|
+
For **Google Play**, add `--store google` to `inspect`, `diff`, `preflight`, `fill`, or `prerelease`.
|
|
184
184
|
|
|
185
185
|
<br>
|
|
186
186
|
|
|
@@ -196,6 +196,37 @@ VYDANNE_COMMIT=1 npx vydanne fill --store google # actually do it
|
|
|
196
196
|
# Windows PowerShell: $env:VYDANNE_COMMIT = "1"; npx vydanne fill --store google
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
+
### `prerelease` — the build, to a testing track
|
|
200
|
+
|
|
201
|
+
`fill` writes the *listing*; `prerelease` uploads the **binary** to a **closed testing track** with
|
|
202
|
+
release notes, all inside one edit transaction:
|
|
203
|
+
|
|
204
|
+
```sh
|
|
205
|
+
npx vydanne prerelease --store google # dry run
|
|
206
|
+
VYDANNE_COMMIT=1 npx vydanne prerelease --store google # publish to the track
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
google: {
|
|
211
|
+
packageName: "com.x.app",
|
|
212
|
+
aab: "./dist", // a file, or a directory whose NEWEST .aab is taken
|
|
213
|
+
track: "internal", // 'internal' (default) | 'alpha' | 'beta'
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**It refuses `production`** — that isn't a flag you can pass, it's a refusal. A staged rollout can be
|
|
218
|
+
halted but never un-shipped, so promoting a tested build stays a human decision in Play Console. This is
|
|
219
|
+
the same line the Apple side draws by never submitting.
|
|
220
|
+
|
|
221
|
+
**Paid app? Use `internal`.** It is the only track where testers install without buying; closed and open
|
|
222
|
+
testers pay like everyone else.
|
|
223
|
+
|
|
224
|
+
Release notes follow supply's layout, so an existing repo needs no migration —
|
|
225
|
+
`<metadataDir>/<play-locale>/changelogs/<versionCode>.txt`, falling back to `default.txt`, truncated to
|
|
226
|
+
Play's 500-char cap with a warning. The versionCode comes from the bundle's own manifest, so build
|
|
227
|
+
numbering stays with the build and re-uploading a used code fails loudly instead of silently replacing a
|
|
228
|
+
binary. Overrides: `VYDANNE_AAB`, `VYDANNE_TRACK`, `VYDANNE_RELEASE_NAME`.
|
|
229
|
+
|
|
199
230
|
**Play is dry by default on purpose.** Nothing goes live until you add `VYDANNE_COMMIT=1`, so a
|
|
200
231
|
half-finished folder can never overwrite a good listing. Play also uses its **own** language codes
|
|
201
232
|
(`zh-CN`, `iw-IL`) which are *not* Apple's — `vydanne locales` and the
|
package/SKILL.md
CHANGED
|
@@ -164,7 +164,17 @@ live) · `privacy` (prints answers for the UI — the API can't reach Apple's ir
|
|
|
164
164
|
RGB flatten) · `compliance` (US self-classification PDF) · `diff` (what differs vs live) · `preflight`
|
|
165
165
|
(completeness gate) · `inspect` · `auth` (what credentials resolved, and from where) · `locales` · `version`.
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
`prerelease` (**Play only**) uploads an `.aab` to a **closed testing track** with release notes, inside one
|
|
168
|
+
edit transaction. `production` is REFUSED — not flag-gated — so no argument combination ships to the
|
|
169
|
+
public; promoting the tested build stays a human's job, mirroring the Apple side never submitting. Track
|
|
170
|
+
comes from `google.track` / `VYDANNE_TRACK`, default `internal`; the bundle from `google.aab` /
|
|
171
|
+
`VYDANNE_AAB` (a directory takes its newest `.aab`). **For a PAID app use `internal`** — it's the only
|
|
172
|
+
track where testers install without buying. Notes follow supply's layout:
|
|
173
|
+
`<google.metadataDir>/<play-locale>/changelogs/<versionCode>.txt`, falling back to `default.txt`, capped
|
|
174
|
+
at Play's 500 chars. DRY by default like `fill --store google`; `VYDANNE_COMMIT=1` publishes. The
|
|
175
|
+
versionCode comes from the bundle itself, so re-uploading one fails loudly instead of silently replacing.
|
|
176
|
+
|
|
177
|
+
`--store google` routes `inspect` · `diff` · `preflight` · `fill` · `prerelease` to the Play Developer **Edits** API
|
|
168
178
|
(OAuth2 service account; **scoped to the config's `packageName`** — a shared key can't touch another app).
|
|
169
179
|
`fill --store google` is **DRY by default**; `VYDANNE_COMMIT=1` commits. The AAB binary and the
|
|
170
180
|
(YouTube-URL) promo video stay outside vydanne.
|
package/bin/vydanne.mjs
CHANGED
|
@@ -87,6 +87,7 @@ usage: vydanne <command> [--config vydanne.config.mjs]
|
|
|
87
87
|
inspect read-only ASC state
|
|
88
88
|
diff show what differs between local (metadata/screenshots/previews) and ASC
|
|
89
89
|
preflight verify submission-completeness (the gotcha checker)
|
|
90
|
+
prerelease --store google: upload the .aab to a closed testing track (refuses production)
|
|
90
91
|
locales UI -> ASC locale mapping + unsupported
|
|
91
92
|
auth which credentials resolved, and from where (masked) — run this on a 401
|
|
92
93
|
credentials: env > .env cascade (.env, .env.<mode>, .env.local, .env.<mode>.local) > user config
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vydanne",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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, a diff of local-vs-store, and a preflight verifier that encodes the store gotchas. 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
|
@@ -45,6 +45,9 @@ export async function loadConfig(p) {
|
|
|
45
45
|
serviceAccountKey: creds.playJsonKeyFile,
|
|
46
46
|
metadataDir: raw.google.metadataDir || "fastlane/metadata/android",
|
|
47
47
|
defaultLocale: raw.google.defaultLocale || raw.primaryLocale,
|
|
48
|
+
aab: raw.google.aab || null,
|
|
49
|
+
track: raw.google.track || "internal", // testing only — `prerelease` refuses production
|
|
50
|
+
|
|
48
51
|
}
|
|
49
52
|
: null,
|
|
50
53
|
};
|
package/src/play/client.mjs
CHANGED
|
@@ -65,4 +65,29 @@ export class PlayClient {
|
|
|
65
65
|
if (res.status >= 300) throw new Error(`image upload ${res.status}: ${JSON.stringify(j).slice(0, 200)}`);
|
|
66
66
|
return j;
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
// ── Binaries & tracks ─────────────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/** Upload an .aab. The returned versionCode comes from the BUNDLE's manifest — Play assigns it, not us. */
|
|
72
|
+
async uploadBundle(editId, filePath) {
|
|
73
|
+
const bytes = fs.readFileSync(filePath);
|
|
74
|
+
const res = await fetch(`${UPLOAD}/applications/${this.pkg}/edits/${editId}/bundles?uploadType=media`, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { Authorization: `Bearer ${this.token}`, "Content-Type": "application/octet-stream" },
|
|
77
|
+
body: bytes,
|
|
78
|
+
});
|
|
79
|
+
const j = await res.json().catch(() => ({}));
|
|
80
|
+
if (res.status >= 300) throw new Error(`bundle upload ${res.status}: ${JSON.stringify(j).slice(0, 300)}`);
|
|
81
|
+
return j;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getTrack(editId, track) { return this.req("GET", `/edits/${editId}/tracks/${track}`); }
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Point a track at version codes. `releases` is the FULL desired state of that track — Play replaces
|
|
88
|
+
* it wholesale, so send one complete release object rather than appending to what is already there.
|
|
89
|
+
*/
|
|
90
|
+
putTrack(editId, track, releases) {
|
|
91
|
+
return this.req("PUT", `/edits/${editId}/tracks/${track}`, { body: { track, releases } });
|
|
92
|
+
}
|
|
68
93
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { green, yellow, red } from "../../util.mjs";
|
|
4
|
+
|
|
5
|
+
/** Tracks this command will write. `production` is deliberately absent — see below. */
|
|
6
|
+
const TESTING_TRACKS = new Set(["internal", "alpha", "beta"]);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Upload an .aab to a CLOSED TESTING track, with release notes.
|
|
10
|
+
*
|
|
11
|
+
* Why testing-only: shipping to everyone is a judgement call with no undo — a staged rollout can be
|
|
12
|
+
* halted but not un-shipped, and the reviewer-facing consequences are the operator's to own. So this
|
|
13
|
+
* mirrors the Apple side, which never submits: `production` is REFUSED, not gated behind a flag, so
|
|
14
|
+
* there is no arrangement of arguments that ships to the public by accident.
|
|
15
|
+
*
|
|
16
|
+
* For a PAID app, `internal` is usually the only track you want: it is the one where testers install
|
|
17
|
+
* without buying. Closed/open testers must purchase it like anyone else.
|
|
18
|
+
*
|
|
19
|
+
* Play assigns the versionCode from the bundle's own manifest, so build numbering stays with the build,
|
|
20
|
+
* and re-uploading the same code fails loudly rather than silently replacing a binary.
|
|
21
|
+
*
|
|
22
|
+
* Everything happens inside one edit transaction: nothing is live until commit, and any throw leaves the
|
|
23
|
+
* edit uncommitted — i.e. the track untouched.
|
|
24
|
+
*/
|
|
25
|
+
export async function run(config, client) {
|
|
26
|
+
const g = config.google;
|
|
27
|
+
const track = process.env.VYDANNE_TRACK || g.track || "internal";
|
|
28
|
+
|
|
29
|
+
if (track === "production") {
|
|
30
|
+
console.error(red("prerelease: refusing to write the production track — that release is a human's to make."));
|
|
31
|
+
console.error(" Promote the tested build in Play Console when you're ready.");
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (!TESTING_TRACKS.has(track)) {
|
|
35
|
+
console.error(red(`prerelease: unknown track "${track}" (expected: ${[...TESTING_TRACKS].join(", ")})`));
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const aab = resolveAab(g.aab);
|
|
40
|
+
if (!aab) {
|
|
41
|
+
console.error(red("prerelease: no .aab found — set `google.aab` in the config, or pass VYDANNE_AAB=<path>."));
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
console.log(green(`prerelease → track "${track}"`));
|
|
45
|
+
console.log(` bundle: ${path.relative(process.cwd(), aab) || aab} (${(fs.statSync(aab).size / 1e6).toFixed(1)} MB)`);
|
|
46
|
+
|
|
47
|
+
const editId = await client.newEdit();
|
|
48
|
+
try {
|
|
49
|
+
// Upload, or REUSE. Play rejects a versionCode it already holds, which is the right answer for an
|
|
50
|
+
// accidental re-upload but wrong for the common case of promoting a build you already pushed to one
|
|
51
|
+
// testing track onto another. The API names the code in its refusal, so take it and carry on — the
|
|
52
|
+
// bytes are already up there, and a bundle is immutable, so reusing it can't diverge from the file.
|
|
53
|
+
let versionCode;
|
|
54
|
+
try {
|
|
55
|
+
const bundle = await client.uploadBundle(editId, aab);
|
|
56
|
+
versionCode = bundle.versionCode;
|
|
57
|
+
if (!versionCode) throw new Error(`upload returned no versionCode: ${JSON.stringify(bundle).slice(0, 200)}`);
|
|
58
|
+
console.log(green(` uploaded versionCode ${versionCode}`));
|
|
59
|
+
} catch (e) {
|
|
60
|
+
const used = /Version code (\d+) has already been used/.exec(e.message);
|
|
61
|
+
if (!used) throw e;
|
|
62
|
+
versionCode = Number(used[1]);
|
|
63
|
+
console.log(yellow(` versionCode ${versionCode} already uploaded — reusing that bundle`));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const releaseNotes = readNotes(g.metadataDir, versionCode, g.defaultLocale);
|
|
67
|
+
if (releaseNotes.length) {
|
|
68
|
+
console.log(` release notes: ${releaseNotes.length} locale(s) — ${releaseNotes.map((n) => n.language).join(", ")}`);
|
|
69
|
+
} else {
|
|
70
|
+
console.log(yellow(` no release notes found under ${g.metadataDir}/<locale>/changelogs/{${versionCode},default}.txt`));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// One complete release object: Play replaces the track's releases wholesale.
|
|
74
|
+
const release = { status: "completed", versionCodes: [String(versionCode)] };
|
|
75
|
+
if (releaseNotes.length) release.releaseNotes = releaseNotes;
|
|
76
|
+
const name = process.env.VYDANNE_RELEASE_NAME;
|
|
77
|
+
if (name) release.name = name;
|
|
78
|
+
|
|
79
|
+
const put = await client.putTrack(editId, track, [release]);
|
|
80
|
+
if (put.status >= 300) throw new Error(`tracks.update ${put.status}: ${JSON.stringify(put.json).slice(0, 300)}`);
|
|
81
|
+
|
|
82
|
+
if (process.env.VYDANNE_COMMIT !== "1") {
|
|
83
|
+
await client.deleteEdit(editId);
|
|
84
|
+
console.log(yellow(`\n DRY RUN — edit discarded, nothing changed. Re-run with VYDANNE_COMMIT=1 to publish to "${track}".`));
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
const res = await client.commit(editId);
|
|
88
|
+
if (res.status >= 300) throw new Error(`edits.commit ${res.status}: ${JSON.stringify(res.json).slice(0, 300)}`);
|
|
89
|
+
console.log(green(`\n committed — versionCode ${versionCode} is live on "${track}".`));
|
|
90
|
+
console.log(" Production stays manual: promote it in Play Console when you're ready.");
|
|
91
|
+
return true;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// Abandon the edit so a failed run leaves the track exactly as it was.
|
|
94
|
+
await client.deleteEdit(editId).catch(() => {});
|
|
95
|
+
console.error(red(`prerelease: ${e.message}`));
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** `google.aab` may be a file or a directory; a directory takes its newest .aab. */
|
|
101
|
+
function resolveAab(configured) {
|
|
102
|
+
const p = process.env.VYDANNE_AAB || configured;
|
|
103
|
+
if (!p) return null;
|
|
104
|
+
const abs = path.resolve(p);
|
|
105
|
+
if (!fs.existsSync(abs)) return null;
|
|
106
|
+
if (!fs.statSync(abs).isDirectory()) return abs;
|
|
107
|
+
const files = fs.readdirSync(abs).filter((f) => f.endsWith(".aab"))
|
|
108
|
+
.map((f) => path.join(abs, f))
|
|
109
|
+
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
110
|
+
return files[0] || null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Release notes per locale, following fastlane supply's layout so an existing repo needs no migration:
|
|
115
|
+
* `<metadataDir>/<play-locale>/changelogs/<versionCode>.txt`, falling back to `default.txt`.
|
|
116
|
+
*/
|
|
117
|
+
function readNotes(metadataDir, versionCode, defaultLocale) {
|
|
118
|
+
const out = [];
|
|
119
|
+
if (!metadataDir || !fs.existsSync(metadataDir)) return out;
|
|
120
|
+
for (const language of fs.readdirSync(metadataDir)) {
|
|
121
|
+
const dir = path.join(metadataDir, language, "changelogs");
|
|
122
|
+
if (!fs.existsSync(dir)) continue;
|
|
123
|
+
const file = [path.join(dir, `${versionCode}.txt`), path.join(dir, "default.txt")].find((f) => fs.existsSync(f));
|
|
124
|
+
if (!file) continue;
|
|
125
|
+
const text = fs.readFileSync(file, "utf8").trim();
|
|
126
|
+
if (!text) continue;
|
|
127
|
+
// Play caps release notes at 500 chars and rejects the whole edit if any locale is over.
|
|
128
|
+
if (text.length > 500) {
|
|
129
|
+
console.log(yellow(` ${language}: release notes ${text.length}/500 chars — truncated`));
|
|
130
|
+
out.push({ language, text: text.slice(0, 500) });
|
|
131
|
+
} else {
|
|
132
|
+
out.push({ language, text });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Keep the default locale first purely so the log reads sensibly.
|
|
136
|
+
return out.sort((a, b) => (a.language === defaultLocale ? -1 : b.language === defaultLocale ? 1 : 0));
|
|
137
|
+
}
|
package/src/registry.mjs
CHANGED
|
@@ -23,7 +23,8 @@ export const PLAY_COMMANDS = {
|
|
|
23
23
|
preflight: { mod: "preflight" },
|
|
24
24
|
diff: { mod: "diff" },
|
|
25
25
|
fill: { mod: "fill" },
|
|
26
|
+
prerelease: { mod: "prerelease" },
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
// Full public command surface (the module-dispatched ones above + the three handled inline in bin/).
|
|
29
|
-
export const COMMAND_NAMES = [...Object.keys(COMMANDS), "auth", "locales", "version"];
|
|
30
|
+
export const COMMAND_NAMES = [...Object.keys(COMMANDS), "prerelease", "auth", "locales", "version"];
|
package/types/index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type CommandName =
|
|
|
16
16
|
| 'inspect'
|
|
17
17
|
| 'diff'
|
|
18
18
|
| 'preflight'
|
|
19
|
+
/** Google Play only: upload an .aab to a closed testing track. Refuses `production`. */
|
|
20
|
+
| 'prerelease'
|
|
19
21
|
| 'auth'
|
|
20
22
|
| 'locales'
|
|
21
23
|
| 'version';
|
|
@@ -58,6 +60,10 @@ export interface GoogleConfig {
|
|
|
58
60
|
/** Listing-text folders, supply convention. Default 'fastlane/metadata/android'. */
|
|
59
61
|
metadataDir?: string;
|
|
60
62
|
defaultLocale?: string;
|
|
63
|
+
/** `.aab` for `prerelease` — a file, or a directory whose NEWEST .aab is taken. Override: VYDANNE_AAB. */
|
|
64
|
+
aab?: string;
|
|
65
|
+
/** Testing track for `prerelease`: 'internal' (default) | 'alpha' | 'beta'. 'production' is refused. */
|
|
66
|
+
track?: "internal" | "alpha" | "beta";
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
export interface ExportConfig {
|