@geonosis/release 1.1.0 → 1.3.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
@@ -17,6 +17,10 @@ geonosis-release migrations --since <ref> [--dialect <name>] [--root <dir>] [--j
17
17
 
18
18
  Exit **0** clean · **1** refusals · **2** the run could not be made.
19
19
 
20
+ ## What it reads
21
+
22
+ The files added since `--since <ref>` — committed, **staged, and untracked** — under every configured `release.migrations[].dir`. A migration that exists and narrows is a refusal whatever git thinks of it: the local gate a person runs before committing is the one window in which it is still free to fix, and a gate that only speaks after the commit speaks after the cheap moment has passed. A run that finds no migration file anywhere says `NONE … nothing was read`, never `OK`.
23
+
20
24
  ## `migrations` — expand-only, enforced
21
25
 
22
26
  For every file the diff ADDED under a directory `geonosis.json` names, in the dialect it names it
@@ -97,11 +101,25 @@ An **identifier** argument (`this.addSql(dropOperationChecks)`) and a **template
97
101
  are refused BY NAME with exit 2. Resolving either is name resolution — a TypeScript parser — and
98
102
  this package will not grow one. A migration nobody could read is not a migration that passed.
99
103
 
100
- ### squawk
104
+ ### squawk — an OPTIONAL peer
105
+
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.
101
117
 
102
- `squawk-cli` is a pinned dependency of this package, spawned from its own `node_modules/.bin`. Its
103
- bin is called `squawk`, and `squawk` on a public registry is an unrelated package that exits 0 on
104
- anything a gate that passes because it linted nothing. Nothing here asks a resolver.
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.
105
123
 
106
124
  Exit 0 is clean and exit 1 is findings. **Anything else is exit 2**, one line naming it: a linter
107
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 {
@@ -191,9 +212,9 @@ var arrayTable = (into, path) => {
191
212
  var parseToml = (source) => {
192
213
  const out = {};
193
214
  let here = out;
194
- const lines = source.split("\n");
195
- for (let index = 0; index < lines.length; index += 1) {
196
- const line = (lines[index] ?? "").split("#")[0]?.trim() ?? "";
215
+ const lines2 = source.split("\n");
216
+ for (let index = 0; index < lines2.length; index += 1) {
217
+ const line = (lines2[index] ?? "").split("#")[0]?.trim() ?? "";
197
218
  if (line === "") continue;
198
219
  if (line.startsWith("[[") && line.endsWith("]]")) {
199
220
  here = arrayTable(out, line.slice(2, -2).trim().split("."));
@@ -208,9 +229,9 @@ var parseToml = (source) => {
208
229
  let text = line.slice(at + 1);
209
230
  while ([...text].filter((one) => one === "[").length > [...text].filter((one) => one === "]").length) {
210
231
  index += 1;
211
- if (index >= lines.length) throw new CannotRun("an unterminated array in the TOML config");
232
+ if (index >= lines2.length) throw new CannotRun("an unterminated array in the TOML config");
212
233
  text += `
213
- ${(lines[index] ?? "").split("#")[0] ?? ""}`;
234
+ ${(lines2[index] ?? "").split("#")[0] ?? ""}`;
214
235
  }
215
236
  put(here, line.slice(0, at).trim().split("."), value(text));
216
237
  }
@@ -353,7 +374,13 @@ var git = (root, args) => {
353
374
  throw new CannotRun(`git ${args.join(" ")} failed: ${(error.message ?? "").trim()}`);
354
375
  }
355
376
  };
356
- var addedSince = (root, since) => git(root, ["diff", "--name-only", "--diff-filter=A", `${since}..HEAD`]).split("\n").filter((line) => line !== "");
377
+ var lines = (out) => out.split("\n").filter((line) => line !== "");
378
+ var addedSince = (root, since) => {
379
+ const committed = lines(git(root, ["diff", "--name-only", "--diff-filter=A", `${since}..HEAD`]));
380
+ const staged = lines(git(root, ["diff", "--name-only", "--diff-filter=A", "--cached"]));
381
+ const untracked = lines(git(root, ["ls-files", "--others", "--exclude-standard"]));
382
+ return [.../* @__PURE__ */ new Set([...committed, ...staged, ...untracked])].toSorted();
383
+ };
357
384
  var gitOk = (root, args) => {
358
385
  const run = spawnSync("git", [...args], { cwd: root, encoding: "utf8" });
359
386
  return run.error === void 0 && run.status === 0;
@@ -565,6 +592,7 @@ import { existsSync as existsSync3 } from "fs";
565
592
  import { dirname, resolve as resolve4 } from "path";
566
593
  import { fileURLToPath } from "url";
567
594
  var HERE = dirname(fileURLToPath(import.meta.url));
595
+ var PINNED = "2.63.0";
568
596
  var findSquawk = (from = HERE) => {
569
597
  for (let dir = from; ; dir = dirname(dir)) {
570
598
  const candidate = resolve4(dir, "node_modules/.bin/squawk");
@@ -572,7 +600,11 @@ var findSquawk = (from = HERE) => {
572
600
  if (dirname(dir) === dir) break;
573
601
  }
574
602
  throw new CannotRun(
575
- "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.`
576
608
  );
577
609
  };
578
610
  var squawkOn = (input) => {
@@ -604,12 +636,17 @@ var EXTENSION = {
604
636
  var under = (dir, file) => file === dir || file.startsWith(dir.endsWith("/") ? dir : `${dir}/`);
605
637
  var fromTypeScript = (path, source, entry) => {
606
638
  try {
607
- return addSqlIn(source, entry.phases ?? ["up"]).flatMap(
608
- (one) => refusalsIn(path, one.sql, one.line)
609
- );
639
+ return {
640
+ refusals: addSqlIn(source, entry.phases ?? ["up"]).flatMap(
641
+ (one) => refusalsIn(path, one.sql, one.line)
642
+ )
643
+ };
610
644
  } catch (error) {
611
645
  if (error instanceof Unreadable) {
612
- throw new CannotRun(`${path} holds ${error.message} \u2014 this reader cannot read it`);
646
+ return {
647
+ refusals: [],
648
+ unreadable: `${path} holds ${error.message} \u2014 this reader cannot read it`
649
+ };
613
650
  }
614
651
  throw error;
615
652
  }
@@ -636,7 +673,9 @@ var runMigrations = (input) => {
636
673
  const refusals = [];
637
674
  const markers = [];
638
675
  const squawk = [];
676
+ const unreadable = [];
639
677
  const forSquawk = [];
678
+ const states = /* @__PURE__ */ new Map();
640
679
  for (const { entry, file } of matched) {
641
680
  const source = readFileSync4(resolve5(input.root, file), "utf8");
642
681
  const found = markersIn(source);
@@ -646,11 +685,18 @@ var runMigrations = (input) => {
646
685
  });
647
686
  markers.push(...bad);
648
687
  const excused = found.length > 0 && bad.length === 0;
688
+ states.set(file, excused ? "excused" : "clean");
649
689
  if (!excused) {
650
- refusals.push(
651
- ...entry.dialect === "mikro-orm-ts" ? fromTypeScript(file, source, entry) : refusalsIn(file, source)
652
- );
690
+ const mine = entry.dialect === "mikro-orm-ts" ? fromTypeScript(file, source, entry) : { refusals: refusalsIn(file, source) };
691
+ if (mine.unreadable !== void 0) {
692
+ unreadable.push({ path: file, why: mine.unreadable });
693
+ states.set(file, "unreadable");
694
+ continue;
695
+ }
696
+ refusals.push(...mine.refusals);
697
+ if (mine.refusals.length > 0) states.set(file, "refused");
653
698
  }
699
+ if (bad.length > 0) states.set(file, "refused");
654
700
  if (entry.dialect !== "sqlite")
655
701
  forSquawk.push({ entry, file, sql: sqlFor(file, source, entry) });
656
702
  }
@@ -670,6 +716,7 @@ var runMigrations = (input) => {
670
716
  if (!found.ok) {
671
717
  clean = false;
672
718
  squawk.push({ findings: found.findings, path: one.file });
719
+ if (states.get(one.file) === "clean") states.set(one.file, "findings");
673
720
  }
674
721
  }
675
722
  } finally {
@@ -677,14 +724,18 @@ var runMigrations = (input) => {
677
724
  }
678
725
  }
679
726
  return {
727
+ dirs: entries.map((one) => one.dir),
728
+ files: matched.map(({ file }) => ({ path: file, state: states.get(file) ?? "clean" })),
680
729
  linted: forSquawk.length,
681
730
  markers: markers.toSorted((a, b) => a.path.localeCompare(b.path) || a.line - b.line),
682
- ok: refusals.length === 0 && markers.length === 0 && clean,
683
- read: matched.length,
731
+ ok: refusals.length === 0 && markers.length === 0 && clean && unreadable.length === 0,
732
+ read: matched.length - unreadable.length,
733
+ since: input.since,
684
734
  refusals: refusals.toSorted(
685
735
  (a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.verb.localeCompare(b.verb)
686
736
  ),
687
- squawk
737
+ squawk,
738
+ unreadable
688
739
  };
689
740
  };
690
741
  var WHY = `
@@ -695,21 +746,35 @@ is safe now:
695
746
  -- contract-migration: <what expanded it, and why nothing reads it any more> since:<ref>
696
747
  `;
697
748
  var formatMigrations = (report) => {
749
+ if (report.files.length === 0) {
750
+ return `NONE migrations: no migration file was added since ${report.since} \u2014 committed, staged or untracked \u2014 under ${report.dirs.join(", ")}; nothing was read
751
+ `;
752
+ }
698
753
  if (report.ok) {
699
754
  return `OK migrations: expand-only (${String(report.read)} read, ${String(report.linted)} linted by squawk)
700
755
  `;
701
756
  }
702
757
  const said = report.markers.map((one) => ` ${one.path}: ${one.why} (line ${String(one.line)})
703
758
  `);
704
- const lines = report.refusals.map(
759
+ const lines2 = report.refusals.map(
705
760
  (one) => ` ${one.path}: ${one.verb} (line ${String(one.line)})
706
761
  `
707
762
  );
708
763
  const found = report.squawk.map((one) => ` ${one.path}:
709
764
  ${one.findings}
765
+ `);
766
+ const cannot = report.unreadable.map((one) => ` ${one.why}
710
767
  `);
711
768
  const why = report.markers.length + report.refusals.length === 0 ? "" : WHY;
712
- return `${said.join("")}${lines.join("")}${why}${found.join("")}`;
769
+ return `${said.join("")}${lines2.join("")}${cannot.join("")}${why}${found.join("")}${accounting(report)}`;
770
+ };
771
+ var accounting = (report) => {
772
+ const total = report.files.length;
773
+ const by = (state) => report.files.filter((one) => one.state === state).length;
774
+ const rows = report.files.map((one) => ` ${one.state.padEnd(10)} ${one.path}
775
+ `);
776
+ return `${rows.join("")}migrations: ${String(report.read)} of ${String(total)} files read \u2014 ${String(by("refused"))} refused, ${String(by("findings"))} with squawk findings, ${String(by("excused"))} excused, ${String(by("clean"))} clean${report.unreadable.length === 0 ? "" : `, ${String(report.unreadable.length)} unreadable`}
777
+ `;
713
778
  };
714
779
 
715
780
  // src/plan.ts
@@ -871,7 +936,7 @@ var stubPath = () => {
871
936
  return path;
872
937
  };
873
938
  var prove = async (cwd, stub = stubPath()) => {
874
- const lines = [];
939
+ const lines2 = [];
875
940
  let ok = true;
876
941
  for (const plant of PLANTS) {
877
942
  const verdict = await proveOver({
@@ -881,10 +946,10 @@ var prove = async (cwd, stub = stubPath()) => {
881
946
  timeoutMs: 3e4
882
947
  });
883
948
  if (verdict.ok) ok = false;
884
- lines.push(` ${verdict.ok ? "MISSED" : "PROVEN"} ${plant.says}`);
949
+ lines2.push(` ${verdict.ok ? "MISSED" : "PROVEN"} ${plant.says}`);
885
950
  if (plant.name !== "promoted" && verdict.steps.includes("promote")) {
886
951
  ok = false;
887
- lines.push(" MISSED a refused smoke was followed by a promote request");
952
+ lines2.push(" MISSED a refused smoke was followed by a promote request");
888
953
  }
889
954
  }
890
955
  const honest = await proveOver({
@@ -894,10 +959,10 @@ var prove = async (cwd, stub = stubPath()) => {
894
959
  timeoutMs: 3e4
895
960
  });
896
961
  if (!honest.ok) ok = false;
897
- lines.push(
962
+ lines2.push(
898
963
  ` ${honest.ok ? "PROVEN" : "MISSED"} a smoke that answered the version it overrode to is not refused`
899
964
  );
900
- return { lines, ok };
965
+ return { lines: lines2, ok };
901
966
  };
902
967
  var formatProve = (outcome) => `${outcome.lines.join("\n")}
903
968
 
package/dist/index.d.ts CHANGED
@@ -37,16 +37,30 @@ type MarkerRefusal = {
37
37
  path: string;
38
38
  why: string;
39
39
  };
40
+ type FileState = 'clean' | 'excused' | 'findings' | 'refused' | 'unreadable';
40
41
  type MigrationsReport = {
42
+ /** The configured directories the run looked under. */
43
+ dirs: string[];
44
+ /** Every file the diff added under a configured dir, by name and state — a clean file included. */
45
+ files: {
46
+ path: string;
47
+ state: FileState;
48
+ }[];
41
49
  linted: number;
42
50
  markers: MarkerRefusal[];
43
51
  ok: boolean;
44
52
  read: number;
45
53
  refusals: Refusal[];
54
+ since: string;
46
55
  squawk: {
47
56
  findings: string;
48
57
  path: string;
49
58
  }[];
59
+ /** Files this reader could not read: named, never a stop for the others. */
60
+ unreadable: {
61
+ path: string;
62
+ why: string;
63
+ }[];
50
64
  };
51
65
 
52
66
  declare const parseReleaseConfig: (raw: unknown) => ReleaseConfig;
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  runMigrations,
29
29
  runPlan,
30
30
  statementsOf
31
- } from "./chunk-2JLVQK7Y.js";
31
+ } from "./chunk-D5IC766I.js";
32
32
  export {
33
33
  DEPLOYED_FILE,
34
34
  NARROWING,
@@ -10,7 +10,7 @@ import {
10
10
  runDeployed,
11
11
  runMigrations,
12
12
  runPlan
13
- } from "./chunk-2JLVQK7Y.js";
13
+ } from "./chunk-D5IC766I.js";
14
14
 
15
15
  // src/release-cli.ts
16
16
  import { resolve } from "path";
@@ -93,6 +93,7 @@ var migrations = (parsed, cwd) => {
93
93
  parsed.json ? `${JSON.stringify(report, void 0, 2)}
94
94
  ` : formatMigrations(report)
95
95
  );
96
+ if (report.unreadable.length > 0) return 2;
96
97
  return report.ok ? 0 : 1;
97
98
  };
98
99
  var plan = (parsed, cwd) => {
@@ -133,8 +134,30 @@ var deployed = (parsed, cwd) => {
133
134
  );
134
135
  return report.ok ? 0 : 1;
135
136
  };
137
+ var CONFIG_SHAPE = `geonosis-release reads geonosis.json
138
+
139
+ geonosis.json \u2192 "release" every key optional; a repo that configured nothing gets nothing
140
+ migrations Entry[]? the directories whose migrations a release examines
141
+ dir string required \u2014 the directory, relative to the root
142
+ dialect string required \u2014 one of mikro-orm-ts, postgres, sqlite
143
+ phases string[]? "up" and/or "down" (default ["up"])
144
+ squawk object? { exclude: string[] } \u2014 the rule ids to leave out
145
+ proof object? { mustAssertVersion: boolean }
146
+ secrets string[]? the secret names a deployment carries
147
+ steps string[]? the release steps
148
+ workers string[]? the workers it deploys
149
+ wrangler string[]? the wrangler configs it carries
150
+ wranglerEnv string? the named environment block to read out of them
151
+
152
+ A key outside either list is refused BY NAME, with the accepted set. This shape had to be read out
153
+ of a bundled chunk file by a consumer once (#75).`;
136
154
  var main = async () => {
137
155
  const argv = process.argv.slice(2);
156
+ if (argv.includes("--print-config-shape")) {
157
+ process.stdout.write(`${CONFIG_SHAPE}
158
+ `);
159
+ return 0;
160
+ }
138
161
  if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
139
162
  process.stdout.write(USAGE);
140
163
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/release",
3
- "version": "1.1.0",
3
+ "version": "1.3.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,7 +35,15 @@
35
35
  "bin",
36
36
  "dist"
37
37
  ],
38
- "dependencies": {
38
+ "peerDependencies": {
39
+ "squawk-cli": "2.63.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "squawk-cli": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "devDependencies": {
39
47
  "squawk-cli": "2.63.0"
40
48
  },
41
49
  "engines": {