vydanne 0.12.0 → 0.13.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 +20 -0
- package/SKILL.md +11 -2
- package/package.json +1 -1
- package/src/buildCommit.mjs +28 -5
- package/src/commands/releases.mjs +5 -1
- package/src/config.mjs +23 -1
- package/src/play/commands/releases.mjs +5 -1
- package/types/index.d.ts +12 -0
package/README.md
CHANGED
|
@@ -491,6 +491,26 @@ allowCrossStoreTerms: ["Apple"],
|
|
|
491
491
|
|
|
492
492
|
`VYDANNE_ALLOW_CROSS_STORE=1` overrides the whole check for one run.
|
|
493
493
|
|
|
494
|
+
### When the build number is not the commit count
|
|
495
|
+
|
|
496
|
+
`releases` maps a store's build number back to a commit because the build number IS
|
|
497
|
+
`git rev-list --count`. A repo can be forced off that and be unable to get back: ship from a long
|
|
498
|
+
branch, squash it onto the release branch, and the count lands *below* build numbers already
|
|
499
|
+
uploaded. Google Play reserves every versionCode it has ever been given, so those numbers cannot
|
|
500
|
+
be freed and the only way past them is a constant added to the count.
|
|
501
|
+
|
|
502
|
+
```js
|
|
503
|
+
buildNumberOffset: 100, // build number = git rev-list --count <commit> + 100
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
`releases` then resolves commits again and prints the offset it applied, on both stores. Leave it
|
|
507
|
+
at 0 — the default — for every app that never had the accident.
|
|
508
|
+
|
|
509
|
+
It is the one input here vydanne **cannot** verify. The count check validates an index against its
|
|
510
|
+
own commit, which a wrong offset passes just as cleanly as a right one, so a bad value names a
|
|
511
|
+
wrong commit with full confidence. It is refused unless it is an integer, and reported wherever it
|
|
512
|
+
is used, but keeping it true is yours.
|
|
513
|
+
|
|
494
514
|
### Accessibility Nutrition Labels
|
|
495
515
|
|
|
496
516
|
Every other thing vydanne writes is a *fact* about your app. This one is a **claim about its
|
package/SKILL.md
CHANGED
|
@@ -57,7 +57,7 @@ which user file was used, and whether the `.p8` is on disk.
|
|
|
57
57
|
`{profile}` — selection only, never secrets) · `platforms` (iOS and macOS are SEPARATE) · `uiLocales`
|
|
58
58
|
(auto-mapped to ASC codes) · `localeMap` · `metadataDir` · `screenshots` · `rating` · `ageRating` ·
|
|
59
59
|
`privacy` · `iaps` · `previews` · `export` · `accessibility` · `ios` · `google` (Google Play) ·
|
|
60
|
-
`bridge` · `push` · `reviewContact` · `allowCrossStoreTerms`.
|
|
60
|
+
`bridge` · `push` · `reviewContact` · `allowCrossStoreTerms` · `buildNumberOffset`.
|
|
61
61
|
|
|
62
62
|
**Paths are defaults, not laws.** `metadataDir` (default `fastlane/metadata`), `screenshots`
|
|
63
63
|
(`{IOS, MAC_OS}`, default `fastlane/screenshots` + `-macos`) and `google.images` (Play image type →
|
|
@@ -316,7 +316,16 @@ names and whether it carries a **tag**. Both stores in this portfolio derive the
|
|
|
316
316
|
`git rev-list --count HEAD` (Android's `versionCode`, iOS's `CURRENT_PROJECT_VERSION` via
|
|
317
317
|
`Scripts/build-number.sh`), which makes it reversible.
|
|
318
318
|
|
|
319
|
-
|
|
319
|
+
**`buildNumberOffset` when the count no longer matches.** A repo can be forced off the plain
|
|
320
|
+
convention and be unable to get back: ship from a long branch, squash it onto the release branch,
|
|
321
|
+
and the count lands BELOW versionCodes already uploaded — which Play reserves permanently, so a
|
|
322
|
+
constant is the only way over them. `buildNumberOffset: 100` tells `releases` that
|
|
323
|
+
`build = commits + 100`, and both commands print the offset they applied. It is the one number
|
|
324
|
+
here vydanne cannot verify — the count check validates an index against its own commit and cannot
|
|
325
|
+
tell a right offset from one wrong by five — so it is refused unless it is an integer, and stated
|
|
326
|
+
wherever it is used rather than assumed.
|
|
327
|
+
|
|
328
|
+
**Every other mapping is verified, never assumed.** `rev-list --reverse` is ordered, not counted, so on a
|
|
320
329
|
merged history the Nth line need not be the commit with N ancestors — the candidate's own count has
|
|
321
330
|
to match. An app that does not build this way, or a build number that is not a commit count, gets a
|
|
322
331
|
stated reason instead of a confident wrong answer. That check has already caught a real one: a build
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vydanne",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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/buildCommit.mjs
CHANGED
|
@@ -10,7 +10,22 @@ import { execFileSync } from "node:child_process";
|
|
|
10
10
|
* or reproducing a store binary needs the tree the archive was cut from, and a version string does
|
|
11
11
|
* not identify one.
|
|
12
12
|
*
|
|
13
|
-
* ###
|
|
13
|
+
* ### The offset, and what it costs
|
|
14
|
+
*
|
|
15
|
+
* `buildNumberOffset` shifts the relationship to `count + offset`, because a repo can be forced
|
|
16
|
+
* off the plain count and not be able to get back. Squash a long branch onto `master` after
|
|
17
|
+
* shipping from it and master's count lands BELOW build numbers already uploaded — and Google Play
|
|
18
|
+
* reserves every versionCode it has ever been given, so the only way over them is a constant.
|
|
19
|
+
*
|
|
20
|
+
* **The offset is a declaration this module cannot check**, and that is a real weakening worth
|
|
21
|
+
* stating rather than burying. The count check below verifies an INDEX against its own commit; it
|
|
22
|
+
* cannot tell a correct offset from one that is wrong by five, because the commit five places away
|
|
23
|
+
* verifies just as cleanly. So a wrong offset buys exactly the confident wrong answer everything
|
|
24
|
+
* else here exists to refuse. It is declared once per app, in a committed file, next to the script
|
|
25
|
+
* that stamps the number — and the commands say which offset they applied, so a reader can see the
|
|
26
|
+
* assumption instead of inheriting it.
|
|
27
|
+
*
|
|
28
|
+
* ### Nothing else here assumes the convention holds — it VERIFIES it, per build
|
|
14
29
|
*
|
|
15
30
|
* vydanne is pointed at apps that do not build this way, so a mapping that trusted the convention
|
|
16
31
|
* would confidently name the wrong commit for them. Every answer below is checked
|
|
@@ -59,12 +74,20 @@ export function tagsAt(sha) {
|
|
|
59
74
|
* Returns `{ sha }` when the count checks out, or `{ why }` naming what stopped it — which is the
|
|
60
75
|
* useful half. "This build has no commit" is a finding about the release, not a gap in the tool.
|
|
61
76
|
*/
|
|
62
|
-
export function commitForBuild(order, build) {
|
|
63
|
-
const
|
|
77
|
+
export function commitForBuild(order, build, offset = 0) {
|
|
78
|
+
const raw = Number(build);
|
|
64
79
|
if (!order) return { why: "not a git checkout" };
|
|
65
|
-
if (!Number.isInteger(
|
|
80
|
+
if (!Number.isInteger(raw)) return { why: `build "${build}" is not a number` };
|
|
81
|
+
// Every message below quotes the arithmetic when an offset is in play. A misconfigured offset
|
|
82
|
+
// surfaces here first, as a build that is impossibly far past HEAD, and "490 - 100 = 390, past
|
|
83
|
+
// HEAD (200 commits)" says which of the two numbers to go and look at.
|
|
84
|
+
const n = raw - offset;
|
|
85
|
+
const shown = offset ? `build ${raw} - offset ${offset} = ${n}` : `build ${n}`;
|
|
86
|
+
if (n < 1) {
|
|
87
|
+
return { why: `${shown} is below the first commit — the offset is larger than the build number` };
|
|
88
|
+
}
|
|
66
89
|
if (n > order.length) {
|
|
67
|
-
return { why:
|
|
90
|
+
return { why: `${shown} is past HEAD (${order.length} commits) — built elsewhere, or on an unmerged branch` };
|
|
68
91
|
}
|
|
69
92
|
const sha = order[n - 1];
|
|
70
93
|
let count;
|
|
@@ -42,6 +42,10 @@ export async function run(config, client) {
|
|
|
42
42
|
|
|
43
43
|
const order = commitOrder();
|
|
44
44
|
if (!order) console.log(yellow(" not a git checkout — commit and tag columns unavailable"));
|
|
45
|
+
// Said once, not per row: the commit column is only as true as this number, and a reader who
|
|
46
|
+
// does not know it was applied has no way to tell a mapping from an assumption.
|
|
47
|
+
const offset = config.buildNumberOffset || 0;
|
|
48
|
+
if (order && offset) console.log(` commit column assumes buildNumberOffset=${offset} (build = commits + ${offset})`);
|
|
45
49
|
|
|
46
50
|
// Newest first: the question is almost always about the last one or two.
|
|
47
51
|
versions.sort((a, b) => String(b.attributes.createdDate).localeCompare(String(a.attributes.createdDate)));
|
|
@@ -71,7 +75,7 @@ export async function run(config, client) {
|
|
|
71
75
|
} else if (backwards) {
|
|
72
76
|
note = yellow(backwards);
|
|
73
77
|
} else {
|
|
74
|
-
const got = commitForBuild(order, build);
|
|
78
|
+
const got = commitForBuild(order, build, offset);
|
|
75
79
|
if (got.sha) {
|
|
76
80
|
commit = got.sha.slice(0, 9);
|
|
77
81
|
const tags = tagsAt(got.sha);
|
package/src/config.mjs
CHANGED
|
@@ -8,11 +8,26 @@ import { DEFAULT_PLAY_IMAGES } from "./play/images.mjs";
|
|
|
8
8
|
|
|
9
9
|
// The public config surface — the drift guards assert each key is documented (README/SKILL) and typed
|
|
10
10
|
// (types/index.d.ts). Add a config knob → document + type it, or the guards fail before publish.
|
|
11
|
-
export const CONFIG_KEYS = ["bundleId", "primaryLocale", "asc", "platforms", "uiLocales", "localeMap", "metadataDir", "screenshots", "rating", "ageRating", "categories", "contentRights", "privacy", "iaps", "previews", "export", "ios", "google", "accessibility", "bridge", "push", "reviewContact", "allowCrossStoreTerms"];
|
|
11
|
+
export const CONFIG_KEYS = ["bundleId", "primaryLocale", "asc", "platforms", "uiLocales", "localeMap", "metadataDir", "screenshots", "rating", "ageRating", "categories", "contentRights", "privacy", "iaps", "previews", "export", "ios", "google", "accessibility", "bridge", "push", "reviewContact", "allowCrossStoreTerms", "buildNumberOffset"];
|
|
12
12
|
|
|
13
13
|
// One `vydanne.config.mjs` per app (ESM, like zdymak.config.mjs) — nothing hard-coded. Secrets stay out:
|
|
14
14
|
// credentials resolve from the environment, a gitignored .env, or ~/.appstoreconnect/config.json (see
|
|
15
15
|
// credentials.mjs) and are REFUSED if found in this committed file; review-contact PII stays gitignored.
|
|
16
|
+
/**
|
|
17
|
+
* An integer config value, or the default when absent — and a refusal when it is neither.
|
|
18
|
+
*
|
|
19
|
+
* `buildNumberOffset: "100"` would otherwise coerce through the arithmetic and shift every commit
|
|
20
|
+
* lookup by a string, which resolves to a real commit and reports it with no hint anything is
|
|
21
|
+
* wrong. A value this load-bearing is worth one type check.
|
|
22
|
+
*/
|
|
23
|
+
function integerOr(name, value, fallback) {
|
|
24
|
+
if (value == null) return fallback;
|
|
25
|
+
if (!Number.isInteger(value)) {
|
|
26
|
+
throw new Error(`vydanne: config '${name}' must be an integer, got ${JSON.stringify(value)}`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
export async function loadConfig(p) {
|
|
17
32
|
const file = path.resolve(p || process.env.VYDANNE_CONFIG || "vydanne.config.mjs");
|
|
18
33
|
if (!fs.existsSync(file)) throw new Error(`vydanne: config not found at ${file}`);
|
|
@@ -71,6 +86,13 @@ export async function loadConfig(p) {
|
|
|
71
86
|
bridge: raw.bridge
|
|
72
87
|
? { out: raw.bridge.out || null, apple: raw.bridge.apple || null, play: raw.bridge.play || null }
|
|
73
88
|
: null,
|
|
89
|
+
// `build number = git rev-list --count <commit> + buildNumberOffset`, for a repo that was
|
|
90
|
+
// forced off the plain count and cannot get back — squash a long branch onto the release
|
|
91
|
+
// branch after shipping from it and the count lands below versionCodes already spent, which
|
|
92
|
+
// Play reserves forever. Unlike everything else in buildCommit.mjs this is a DECLARATION the
|
|
93
|
+
// tool cannot verify, so it is refused unless it is an integer and reported wherever it is
|
|
94
|
+
// applied. 0 means the plain convention, which is every app that never had the accident.
|
|
95
|
+
buildNumberOffset: integerOr("buildNumberOffset", raw.buildNumberOffset, 0),
|
|
74
96
|
// Terms the cross-store check must not flag for this app (see src/crossStore.mjs).
|
|
75
97
|
allowCrossStoreTerms: raw.allowCrossStoreTerms || [],
|
|
76
98
|
previews: raw.previews || null,
|
|
@@ -41,6 +41,10 @@ export async function run(config, client) {
|
|
|
41
41
|
|
|
42
42
|
const order = commitOrder();
|
|
43
43
|
if (!order) console.log(yellow(" not a git checkout — commit and tag columns unavailable"));
|
|
44
|
+
// Said once, not per row: the commit column is only as true as this number, and a reader who
|
|
45
|
+
// does not know it was applied has no way to tell a mapping from an assumption.
|
|
46
|
+
const offset = config.buildNumberOffset || 0;
|
|
47
|
+
if (order && offset) console.log(` commit column assumes buildNumberOffset=${offset} (versionCode = commits + ${offset})`);
|
|
44
48
|
|
|
45
49
|
const editId = await client.newEdit();
|
|
46
50
|
let uploaded = [];
|
|
@@ -92,7 +96,7 @@ export async function run(config, client) {
|
|
|
92
96
|
let commit = "-";
|
|
93
97
|
let tag = "-";
|
|
94
98
|
let note = "";
|
|
95
|
-
const got = commitForBuild(order, row.code);
|
|
99
|
+
const got = commitForBuild(order, row.code, offset);
|
|
96
100
|
if (got.sha) {
|
|
97
101
|
commit = got.sha.slice(0, 9);
|
|
98
102
|
const tags = tagsAt(got.sha);
|
package/types/index.d.ts
CHANGED
|
@@ -343,6 +343,18 @@ export interface VydanneConfig {
|
|
|
343
343
|
* "apple"); it is not a way to ship a store name.
|
|
344
344
|
*/
|
|
345
345
|
allowCrossStoreTerms?: string[];
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* `build number = git rev-list --count <commit> + buildNumberOffset`. Defaults to 0, which is
|
|
349
|
+
* the plain convention.
|
|
350
|
+
*
|
|
351
|
+
* Set it only for a repo that was forced off the plain count and cannot get back: ship from a
|
|
352
|
+
* long branch, squash that branch onto the release branch, and the count lands below build
|
|
353
|
+
* numbers already uploaded — which Google Play reserves permanently, so a constant is the only
|
|
354
|
+
* way over them. It is a declaration vydanne cannot verify, so keep it true; a wrong one names
|
|
355
|
+
* a wrong commit as confidently as a right one.
|
|
356
|
+
*/
|
|
357
|
+
buildNumberOffset?: number;
|
|
346
358
|
}
|
|
347
359
|
|
|
348
360
|
/** Thin ASC REST client (native fetch + ES256 JWT). */
|