@geonosis/release 1.2.0 → 1.4.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
@@ -101,11 +101,25 @@ An **identifier** argument (`this.addSql(dropOperationChecks)`) and a **template
101
101
  are refused BY NAME with exit 2. Resolving either is name resolution — a TypeScript parser — and
102
102
  this package will not grow one. A migration nobody could read is not a migration that passed.
103
103
 
104
- ### squawk
104
+ ### squawk — an OPTIONAL peer
105
105
 
106
- `squawk-cli` is a pinned dependency of this package, spawned from its own `node_modules/.bin`. Its
107
- bin is called `squawk`, and `squawk` on a public registry is an unrelated package that exits 0 on
108
- anything — a gate that passes because it linted nothing. Nothing here asks a resolver.
106
+ ```bash
107
+ pnpm add -D squawk-cli@2.63.0 # only if release.migrations names postgres or mikro-orm-ts
108
+ ```
109
+
110
+ `squawk-cli` ships platform binaries, and as a hard dependency of this package it grew one
111
+ consumer's lockfile by **395 lines** for a Postgres linter that repo never configured. It is a peer
112
+ marked optional: nothing installs it for you.
113
+
114
+ - a `release.migrations` naming only the **`sqlite`** dialect never calls it, and needs none of it;
115
+ - a `postgres` or `mikro-orm-ts` dialect with squawk absent is **exit 2**, with the install line
116
+ above — the dialect cannot be measured, and a gate that cannot measure has not passed.
117
+
118
+ It is pinned exactly, because a squawk that renamed a rule would change what a release argues from,
119
+ and that is a decision rather than an install. It is spawned from the `node_modules/.bin` beside
120
+ this package: its bin is called `squawk`, and `squawk` on a public registry is an unrelated package
121
+ that exits 0 on anything — a gate that passes because it linted nothing. Nothing here asks a
122
+ resolver.
109
123
 
110
124
  Exit 0 is clean and exit 1 is findings. **Anything else is exit 2**, one line naming it: a linter
111
125
  that could not start has not measured, and a gate that cannot measure has not passed.
@@ -12,26 +12,33 @@ var strings = (value2, where) => {
12
12
  }
13
13
  return value2;
14
14
  };
15
+ var ENTRY_KEYS = "expected { dir, dialect, phases?, squawk? }";
15
16
  var oneDir = (value2, index) => {
16
- if (!isRecord(value2)) throw new CannotRun(`release.migrations[${index}] must be an object`);
17
+ const at = `release.migrations[${index}]`;
18
+ if (!isRecord(value2)) throw new CannotRun(`${at} must be an object \u2014 ${ENTRY_KEYS}`);
19
+ const known = /* @__PURE__ */ new Set(["dialect", "dir", "phases", "squawk"]);
20
+ const unknown = Object.keys(value2).find((key) => !known.has(key));
21
+ if (unknown !== void 0) {
22
+ throw new CannotRun(`${at}.${unknown} is not a key it takes \u2014 ${ENTRY_KEYS}`);
23
+ }
17
24
  const dir = value2["dir"];
18
25
  const dialect = value2["dialect"];
19
26
  if (typeof dir !== "string" || dir === "") {
20
- throw new CannotRun(`release.migrations[${index}].dir must name a directory`);
27
+ throw new CannotRun(`${at}.dir must name a directory \u2014 ${ENTRY_KEYS}`);
21
28
  }
22
29
  if (typeof dialect !== "string" || !DIALECTS.has(dialect)) {
23
30
  throw new CannotRun(
24
- `release.migrations[${index}].dialect must be one of ${[...DIALECTS].toSorted().join(", ")}`
31
+ `${at}.dialect must be one of ${[...DIALECTS].toSorted().join(", ")} \u2014 ${ENTRY_KEYS}`
25
32
  );
26
33
  }
27
- const phases = strings(value2["phases"], `release.migrations[${index}].phases`);
34
+ const phases = strings(value2["phases"], `${at}.phases`);
28
35
  for (const phase of phases) {
29
36
  if (phase !== "up" && phase !== "down") {
30
- throw new CannotRun(`release.migrations[${index}].phases may only hold "up" and "down"`);
37
+ throw new CannotRun(`${at}.phases may only hold "up" and "down"`);
31
38
  }
32
39
  }
33
40
  const squawk = value2["squawk"];
34
- const exclude = isRecord(squawk) ? strings(squawk["exclude"], `release.migrations[${index}].squawk.exclude`) : [];
41
+ const exclude = isRecord(squawk) ? strings(squawk["exclude"], `${at}.squawk.exclude`) : [];
35
42
  return {
36
43
  dialect,
37
44
  dir,
@@ -49,13 +56,27 @@ var EMPTY = {
49
56
  workers: [],
50
57
  wrangler: []
51
58
  };
59
+ var BLOCK_KEYS = "expected { migrations?, proof?, secrets?, steps?, workers?, wrangler?, wranglerEnv? }";
52
60
  var parseReleaseConfig = (raw) => {
53
61
  if (!isRecord(raw)) return EMPTY;
54
62
  const release = raw["release"];
55
63
  if (!isRecord(release)) return EMPTY;
64
+ const known = /* @__PURE__ */ new Set([
65
+ "migrations",
66
+ "proof",
67
+ "secrets",
68
+ "steps",
69
+ "workers",
70
+ "wrangler",
71
+ "wranglerEnv"
72
+ ]);
73
+ const unknown = Object.keys(release).find((key) => !known.has(key));
74
+ if (unknown !== void 0) {
75
+ throw new CannotRun(`release.${unknown} is not a key it reads \u2014 ${BLOCK_KEYS}`);
76
+ }
56
77
  const migrations = release["migrations"];
57
78
  if (migrations !== void 0 && !Array.isArray(migrations)) {
58
- throw new CannotRun("release.migrations must be a list");
79
+ throw new CannotRun(`release.migrations must be a list \u2014 ${ENTRY_KEYS}, per entry`);
59
80
  }
60
81
  const proof = release["proof"];
61
82
  return {
@@ -571,6 +592,7 @@ import { existsSync as existsSync3 } from "fs";
571
592
  import { dirname, resolve as resolve4 } from "path";
572
593
  import { fileURLToPath } from "url";
573
594
  var HERE = dirname(fileURLToPath(import.meta.url));
595
+ var PINNED = "2.63.0";
574
596
  var findSquawk = (from = HERE) => {
575
597
  for (let dir = from; ; dir = dirname(dir)) {
576
598
  const candidate = resolve4(dir, "node_modules/.bin/squawk");
@@ -578,7 +600,11 @@ var findSquawk = (from = HERE) => {
578
600
  if (dirname(dir) === dir) break;
579
601
  }
580
602
  throw new CannotRun(
581
- "squawk-cli is not installed beside this package \u2014 the Postgres dialect cannot be measured, and a gate that cannot measure has not passed"
603
+ `squawk-cli is not installed beside this package, and the postgres and mikro-orm-ts dialects cannot be measured without it \u2014 a gate that cannot measure has not passed. Install it:
604
+
605
+ pnpm add -D squawk-cli@${PINNED}
606
+
607
+ It is an optional peer: a repo whose release.migrations names only the sqlite dialect needs none of this.`
582
608
  );
583
609
  };
584
610
  var squawkOn = (input) => {
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  runMigrations,
29
29
  runPlan,
30
30
  statementsOf
31
- } from "./chunk-BOTEFN3W.js";
31
+ } from "./chunk-D5IC766I.js";
32
32
  export {
33
33
  DEPLOYED_FILE,
34
34
  NARROWING,
@@ -10,10 +10,90 @@ import {
10
10
  runDeployed,
11
11
  runMigrations,
12
12
  runPlan
13
- } from "./chunk-BOTEFN3W.js";
13
+ } from "./chunk-D5IC766I.js";
14
14
 
15
15
  // src/release-cli.ts
16
16
  import { resolve } from "path";
17
+
18
+ // src/envelope.ts
19
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
20
+ import { dirname, join } from "path";
21
+ import { fileURLToPath } from "url";
22
+ var ENVELOPES_DIR = ".geonosis/envelopes";
23
+ var envelopePath = (root, tool) => join(root, ENVELOPES_DIR, `${tool}.json`);
24
+ var UnbalancedEnvelope = class extends Error {
25
+ constructor(message) {
26
+ super(message);
27
+ this.name = "UnbalancedEnvelope";
28
+ }
29
+ };
30
+ var isCount = (value) => Number.isSafeInteger(value) && value >= 0;
31
+ var unbalancedMessage = (envelope, next) => `${envelope.tool}: considered ${envelope.considered} but accounts for ${envelope.read + envelope.refused.length + envelope.excused.length} \u2014 ${envelope.read} read + ${envelope.refused.length} refused + ${envelope.excused.length} excused. A run that has lost count of its own inputs cannot say what it measured, so no verdict was rendered and no envelope was written. Next: ${next}`;
32
+ var writeEnvelope = ({
33
+ envelope,
34
+ next,
35
+ root
36
+ }) => {
37
+ if (envelope.tool.trim() === "") {
38
+ throw new UnbalancedEnvelope(
39
+ `an envelope with no tool name cannot be filed or reported against. Next: ${next}`
40
+ );
41
+ }
42
+ if (envelope.version.trim() === "") {
43
+ throw new UnbalancedEnvelope(
44
+ `${envelope.tool}: an envelope that cannot name the build that wrote it dates nothing, and a stale one reads exactly like a fresh one. Next: ${next}`
45
+ );
46
+ }
47
+ if (!isCount(envelope.considered) || !isCount(envelope.read)) {
48
+ throw new UnbalancedEnvelope(
49
+ `${envelope.tool}: considered ${envelope.considered} and read ${envelope.read} \u2014 a census is a whole number of things, and arithmetic over anything else balances by accident. Next: ${next}`
50
+ );
51
+ }
52
+ if (envelope.considered !== envelope.read + envelope.refused.length + envelope.excused.length) {
53
+ throw new UnbalancedEnvelope(unbalancedMessage(envelope, next));
54
+ }
55
+ const at = envelopePath(root, envelope.tool);
56
+ mkdirSync(dirname(at), { recursive: true });
57
+ writeFileSync(at, `${JSON.stringify(envelope, void 0, 2)}
58
+ `);
59
+ return at;
60
+ };
61
+ var UNKNOWN = "unknown";
62
+ var versionIn = (dir) => {
63
+ try {
64
+ const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
65
+ return typeof manifest.version === "string" ? manifest.version : void 0;
66
+ } catch {
67
+ return void 0;
68
+ }
69
+ };
70
+ var versionOf = (moduleUrl) => {
71
+ let dir = dirname(fileURLToPath(moduleUrl));
72
+ for (; ; ) {
73
+ const found = versionIn(dir);
74
+ if (found !== void 0) return found;
75
+ const up = dirname(dir);
76
+ if (up === dir) return UNKNOWN;
77
+ dir = up;
78
+ }
79
+ };
80
+ var MIGRATIONS_TOOL = "release-migrations";
81
+ var MIGRATIONS_NEXT = "geonosis-release migrations --since <ref> --json and compare `files` against `unreadable` \u2014 every added file leaves by exactly one door, and a file in one list and not the other is the accounting bug this refuses over";
82
+ var migrationsEnvelope = (report, durationMs) => ({
83
+ considered: report.files.length,
84
+ durationMs,
85
+ excused: report.files.filter((one) => one.state === "excused").map((one) => ({
86
+ path: one.path,
87
+ reason: "a contract-migration marker this gate could believe"
88
+ })),
89
+ findings: [...report.refusals, ...report.markers, ...report.squawk],
90
+ read: report.files.filter((one) => one.state !== "excused" && one.state !== "unreadable").length,
91
+ refused: report.unreadable.map((one) => ({ path: one.path, reason: one.why })),
92
+ tool: MIGRATIONS_TOOL,
93
+ version: versionOf(import.meta.url)
94
+ });
95
+
96
+ // src/release-cli.ts
17
97
  var USAGE = `geonosis-release <command> [options]
18
98
 
19
99
  migrations --since <ref> [--dialect <name>] [--root <dir>] [--json]
@@ -84,15 +164,24 @@ var migrations = (parsed, cwd) => {
84
164
  if (dialect !== void 0 && !DIALECTS.has(dialect)) {
85
165
  throw new CannotRun(`--dialect must be one of ${[...DIALECTS].toSorted().join(", ")}`);
86
166
  }
167
+ const root = resolve(cwd, parsed.read["--root"] ?? cwd);
168
+ const startedAt = Date.now();
87
169
  const report = runMigrations({
88
170
  ...dialect === void 0 ? {} : { dialect },
89
- root: resolve(cwd, parsed.read["--root"] ?? cwd),
171
+ root,
90
172
  since
91
173
  });
92
174
  process.stdout.write(
93
175
  parsed.json ? `${JSON.stringify(report, void 0, 2)}
94
176
  ` : formatMigrations(report)
95
177
  );
178
+ const at = writeEnvelope({
179
+ envelope: migrationsEnvelope(report, Date.now() - startedAt),
180
+ next: MIGRATIONS_NEXT,
181
+ root
182
+ });
183
+ if (!parsed.json) process.stdout.write(`envelope: ${at}
184
+ `);
96
185
  if (report.unreadable.length > 0) return 2;
97
186
  return report.ok ? 0 : 1;
98
187
  };
@@ -134,8 +223,30 @@ var deployed = (parsed, cwd) => {
134
223
  );
135
224
  return report.ok ? 0 : 1;
136
225
  };
226
+ var CONFIG_SHAPE = `geonosis-release reads geonosis.json
227
+
228
+ geonosis.json \u2192 "release" every key optional; a repo that configured nothing gets nothing
229
+ migrations Entry[]? the directories whose migrations a release examines
230
+ dir string required \u2014 the directory, relative to the root
231
+ dialect string required \u2014 one of mikro-orm-ts, postgres, sqlite
232
+ phases string[]? "up" and/or "down" (default ["up"])
233
+ squawk object? { exclude: string[] } \u2014 the rule ids to leave out
234
+ proof object? { mustAssertVersion: boolean }
235
+ secrets string[]? the secret names a deployment carries
236
+ steps string[]? the release steps
237
+ workers string[]? the workers it deploys
238
+ wrangler string[]? the wrangler configs it carries
239
+ wranglerEnv string? the named environment block to read out of them
240
+
241
+ A key outside either list is refused BY NAME, with the accepted set. This shape had to be read out
242
+ of a bundled chunk file by a consumer once (#75).`;
137
243
  var main = async () => {
138
244
  const argv = process.argv.slice(2);
245
+ if (argv.includes("--print-config-shape")) {
246
+ process.stdout.write(`${CONFIG_SHAPE}
247
+ `);
248
+ return 0;
249
+ }
139
250
  if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
140
251
  process.stdout.write(USAGE);
141
252
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/release",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "types": "./dist/index.d.ts",
5
5
  "description": "The release contract with its proofs: an expand-only migration gate over three dialects, a smoke that must name the version that answered it, and declared ≠ deployed.",
6
6
  "keywords": [
@@ -35,9 +35,18 @@
35
35
  "bin",
36
36
  "dist"
37
37
  ],
38
- "dependencies": {
38
+ "peerDependencies": {
39
39
  "squawk-cli": "2.63.0"
40
40
  },
41
+ "peerDependenciesMeta": {
42
+ "squawk-cli": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "devDependencies": {
47
+ "squawk-cli": "2.63.0",
48
+ "@geonosis/ratchet": "1.4.0"
49
+ },
41
50
  "engines": {
42
51
  "node": ">=22"
43
52
  },