@geonosis/release 1.4.0 → 2.0.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.
@@ -0,0 +1,2134 @@
1
+ // src/config.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { resolve } from "path";
4
+ var CannotRun = class extends Error {
5
+ };
6
+ var DIALECTS = /* @__PURE__ */ new Set(["mikro-orm-ts", "postgres", "sqlite"]);
7
+ var isRecord = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
8
+ var strings = (value2, where) => {
9
+ if (value2 === void 0) return [];
10
+ if (!Array.isArray(value2) || value2.some((one) => typeof one !== "string")) {
11
+ throw new CannotRun(`${where} must be a list of strings`);
12
+ }
13
+ return value2;
14
+ };
15
+ var ENTRY_KEYS = "expected { dir, dialect, phases?, squawk? }";
16
+ var oneDir = (value2, index) => {
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
+ }
24
+ const dir = value2["dir"];
25
+ const dialect = value2["dialect"];
26
+ if (typeof dir !== "string" || dir === "") {
27
+ throw new CannotRun(`${at}.dir must name a directory \u2014 ${ENTRY_KEYS}`);
28
+ }
29
+ if (typeof dialect !== "string" || !DIALECTS.has(dialect)) {
30
+ throw new CannotRun(
31
+ `${at}.dialect must be one of ${[...DIALECTS].toSorted().join(", ")} \u2014 ${ENTRY_KEYS}`
32
+ );
33
+ }
34
+ const phases = strings(value2["phases"], `${at}.phases`);
35
+ for (const phase of phases) {
36
+ if (phase !== "up" && phase !== "down") {
37
+ throw new CannotRun(`${at}.phases may only hold "up" and "down"`);
38
+ }
39
+ }
40
+ const squawk = value2["squawk"];
41
+ const exclude = isRecord(squawk) ? strings(squawk["exclude"], `${at}.squawk.exclude`) : [];
42
+ return {
43
+ dialect,
44
+ dir,
45
+ // `down()` holds the drop in every MikroORM migration in the corpus this was built from, and a
46
+ // down that is never run against a live database cannot narrow a schema anyone is serving.
47
+ phases: phases.length === 0 ? ["up"] : phases,
48
+ squawk: { exclude }
49
+ };
50
+ };
51
+ var EMPTY = {
52
+ migrations: [],
53
+ proof: {},
54
+ secrets: [],
55
+ smoke: { exclude: [], snapshots: [] },
56
+ steps: [],
57
+ workers: [],
58
+ wrangler: []
59
+ };
60
+ var BLOCK_KEYS = "expected { migrations?, proof?, schema?, secrets?, smoke?, steps?, workers?, wrangler?, wranglerEnv? }";
61
+ var SCHEMA_KEYS = "expected { domains?, sessionVariables? }";
62
+ var DOMAIN_KEYS = "expected { dir, name, tables }";
63
+ var oneSchemaDomain = (value2, index) => {
64
+ const at = `release.schema.domains[${index}]`;
65
+ if (!isRecord(value2)) throw new CannotRun(`${at} must be an object \u2014 ${DOMAIN_KEYS}`);
66
+ const known = /* @__PURE__ */ new Set(["dir", "name", "tables"]);
67
+ const unknown = Object.keys(value2).find((key) => !known.has(key));
68
+ if (unknown !== void 0) {
69
+ throw new CannotRun(`${at}.${unknown} is not a key it takes \u2014 ${DOMAIN_KEYS}`);
70
+ }
71
+ const dir = value2["dir"];
72
+ const name = value2["name"];
73
+ if (typeof dir !== "string" || dir === "") {
74
+ throw new CannotRun(`${at}.dir must name the directory holding that domain's migrations`);
75
+ }
76
+ if (typeof name !== "string" || name === "") {
77
+ throw new CannotRun(`${at}.name must name the domain those tables belong to`);
78
+ }
79
+ return { dir, name, tables: strings(value2["tables"], `${at}.tables`) };
80
+ };
81
+ var parseSchema = (value2) => {
82
+ if (value2 === void 0) return void 0;
83
+ if (!isRecord(value2)) throw new CannotRun(`release.schema must be an object \u2014 ${SCHEMA_KEYS}`);
84
+ const unknown = Object.keys(value2).find((key) => key !== "domains" && key !== "sessionVariables");
85
+ if (unknown !== void 0) {
86
+ throw new CannotRun(`release.schema.${unknown} is not a key it reads \u2014 ${SCHEMA_KEYS}`);
87
+ }
88
+ const domains = value2["domains"];
89
+ if (domains !== void 0 && !Array.isArray(domains)) {
90
+ throw new CannotRun(`release.schema.domains must be a list \u2014 ${DOMAIN_KEYS}, per entry`);
91
+ }
92
+ return {
93
+ domains: (domains ?? []).map(oneSchemaDomain),
94
+ sessionVariables: strings(value2["sessionVariables"], "release.schema.sessionVariables")
95
+ };
96
+ };
97
+ var SMOKE_KEYS = "expected { exclude?, snapshots? }";
98
+ var oneSnapshot = (value2, index) => {
99
+ const at = `release.smoke.snapshots[${index}]`;
100
+ if (!isRecord(value2)) throw new CannotRun(`${at} must be an object \u2014 expected { name, install? }`);
101
+ const unknown = Object.keys(value2).find((key) => key !== "install" && key !== "name");
102
+ if (unknown !== void 0) {
103
+ throw new CannotRun(`${at}.${unknown} is not a key it takes \u2014 expected { name, install? }`);
104
+ }
105
+ const name = value2["name"];
106
+ if (typeof name !== "string" || name === "") {
107
+ throw new CannotRun(`${at}.name must name a snapshot recorded under .geonosis/`);
108
+ }
109
+ const install = value2["install"];
110
+ if (install !== void 0 && typeof install !== "string") {
111
+ throw new CannotRun(`${at}.install must be the command that installs that tree`);
112
+ }
113
+ return { ...install === void 0 ? {} : { install }, name };
114
+ };
115
+ var parseSmoke = (value2) => {
116
+ if (value2 === void 0) return { exclude: [], snapshots: [] };
117
+ if (!isRecord(value2)) throw new CannotRun(`release.smoke must be an object \u2014 ${SMOKE_KEYS}`);
118
+ const unknown = Object.keys(value2).find((key) => key !== "exclude" && key !== "snapshots");
119
+ if (unknown !== void 0) {
120
+ throw new CannotRun(`release.smoke.${unknown} is not a key it reads \u2014 ${SMOKE_KEYS}`);
121
+ }
122
+ const snapshots = value2["snapshots"];
123
+ if (snapshots !== void 0 && !Array.isArray(snapshots)) {
124
+ throw new CannotRun("release.smoke.snapshots must be a list of { name, install? }");
125
+ }
126
+ return {
127
+ exclude: strings(value2["exclude"], "release.smoke.exclude"),
128
+ snapshots: (snapshots ?? []).map(oneSnapshot)
129
+ };
130
+ };
131
+ var parseReleaseConfig = (raw) => {
132
+ if (!isRecord(raw)) return EMPTY;
133
+ const release = raw["release"];
134
+ if (!isRecord(release)) return EMPTY;
135
+ const known = /* @__PURE__ */ new Set([
136
+ "migrations",
137
+ "proof",
138
+ "schema",
139
+ "secrets",
140
+ "smoke",
141
+ "steps",
142
+ "workers",
143
+ "wrangler",
144
+ "wranglerEnv"
145
+ ]);
146
+ const unknown = Object.keys(release).find((key) => !known.has(key));
147
+ if (unknown !== void 0) {
148
+ throw new CannotRun(`release.${unknown} is not a key it reads \u2014 ${BLOCK_KEYS}`);
149
+ }
150
+ const migrations = release["migrations"];
151
+ if (migrations !== void 0 && !Array.isArray(migrations)) {
152
+ throw new CannotRun(`release.migrations must be a list \u2014 ${ENTRY_KEYS}, per entry`);
153
+ }
154
+ const proof = release["proof"];
155
+ return {
156
+ migrations: (migrations ?? []).map(oneDir),
157
+ proof: isRecord(proof) ? { mustAssertVersion: proof["mustAssertVersion"] !== false } : {},
158
+ schema: parseSchema(release["schema"]),
159
+ secrets: strings(release["secrets"], "release.secrets"),
160
+ smoke: parseSmoke(release["smoke"]),
161
+ steps: strings(release["steps"], "release.steps"),
162
+ workers: strings(release["workers"], "release.workers"),
163
+ wrangler: strings(release["wrangler"], "release.wrangler"),
164
+ wranglerEnv: typeof release["wranglerEnv"] === "string" ? release["wranglerEnv"] : void 0
165
+ };
166
+ };
167
+ var readReleaseConfig = (root) => {
168
+ const path = resolve(root, "geonosis.json");
169
+ if (!existsSync(path)) return EMPTY;
170
+ try {
171
+ return parseReleaseConfig(JSON.parse(readFileSync(path, "utf8")));
172
+ } catch (error) {
173
+ if (error instanceof CannotRun) throw error;
174
+ throw new CannotRun(`${path} is not readable JSON: ${error.message}`);
175
+ }
176
+ };
177
+
178
+ // src/wrangler.ts
179
+ import { readFileSync as readFileSync2 } from "fs";
180
+ import { resolve as resolve2 } from "path";
181
+ var isRecord2 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
182
+ var parseJsonc = (source) => {
183
+ let out = "";
184
+ for (let index = 0; index < source.length; index += 1) {
185
+ const char = source[index] ?? "";
186
+ if (char === '"') {
187
+ const start = index;
188
+ index += 1;
189
+ for (; index < source.length; index += 1) {
190
+ if (source[index] === "\\") {
191
+ index += 1;
192
+ continue;
193
+ }
194
+ if (source[index] === '"') break;
195
+ }
196
+ out += source.slice(start, index + 1);
197
+ continue;
198
+ }
199
+ if (char === "/" && source[index + 1] === "/") {
200
+ const end = source.indexOf("\n", index);
201
+ index = end === -1 ? source.length : end - 1;
202
+ continue;
203
+ }
204
+ if (char === "/" && source[index + 1] === "*") {
205
+ const end = source.indexOf("*/", index + 2);
206
+ index = end === -1 ? source.length : end + 1;
207
+ continue;
208
+ }
209
+ out += char;
210
+ }
211
+ return JSON.parse(out.replaceAll(/,(\s*[\]}])/g, "$1"));
212
+ };
213
+ var literal = (text) => {
214
+ const value2 = text.trim();
215
+ if (value2.startsWith('"') || value2.startsWith("'")) return value2.slice(1, -1);
216
+ if (value2 === "true") return true;
217
+ if (value2 === "false") return false;
218
+ const number = Number(value2);
219
+ return Number.isNaN(number) ? value2 : number;
220
+ };
221
+ var split = (text, delimiter) => {
222
+ const parts = [];
223
+ let depth = 0;
224
+ let quote = "";
225
+ let current = "";
226
+ for (const char of text) {
227
+ if (quote !== "") {
228
+ current += char;
229
+ if (char === quote) quote = "";
230
+ continue;
231
+ }
232
+ if (char === '"' || char === "'") quote = char;
233
+ if (char === "[" || char === "{") depth += 1;
234
+ if (char === "]" || char === "}") depth -= 1;
235
+ if (char === delimiter && depth === 0) {
236
+ parts.push(current);
237
+ current = "";
238
+ continue;
239
+ }
240
+ current += char;
241
+ }
242
+ parts.push(current);
243
+ return parts.filter((one) => one.trim() !== "");
244
+ };
245
+ var value = (text) => {
246
+ const trimmed = text.trim();
247
+ if (trimmed.startsWith("[")) return split(trimmed.slice(1, -1), ",").map(value);
248
+ if (trimmed.startsWith("{")) {
249
+ return Object.fromEntries(
250
+ split(trimmed.slice(1, -1), ",").map((pair) => {
251
+ const at = pair.indexOf("=");
252
+ return [pair.slice(0, at).trim(), value(pair.slice(at + 1))];
253
+ })
254
+ );
255
+ }
256
+ return literal(trimmed);
257
+ };
258
+ var put = (into, path, leaf) => {
259
+ let here = into;
260
+ for (const key of path.slice(0, -1)) {
261
+ if (!isRecord2(here[key])) here[key] = {};
262
+ here = here[key];
263
+ }
264
+ here[path.at(-1) ?? ""] = leaf;
265
+ };
266
+ var table = (into, path) => {
267
+ let here = into;
268
+ for (const key of path) {
269
+ if (!isRecord2(here[key])) here[key] = {};
270
+ here = here[key];
271
+ }
272
+ return here;
273
+ };
274
+ var arrayTable = (into, path) => {
275
+ let here = into;
276
+ for (const key of path.slice(0, -1)) {
277
+ if (!isRecord2(here[key])) here[key] = {};
278
+ here = here[key];
279
+ }
280
+ const last = path.at(-1) ?? "";
281
+ if (!Array.isArray(here[last])) here[last] = [];
282
+ const list = here[last];
283
+ const entry = {};
284
+ list.push(entry);
285
+ return entry;
286
+ };
287
+ var parseToml = (source) => {
288
+ const out = {};
289
+ let here = out;
290
+ const lines2 = source.split("\n");
291
+ for (let index = 0; index < lines2.length; index += 1) {
292
+ const line = (lines2[index] ?? "").split("#")[0]?.trim() ?? "";
293
+ if (line === "") continue;
294
+ if (line.startsWith("[[") && line.endsWith("]]")) {
295
+ here = arrayTable(out, line.slice(2, -2).trim().split("."));
296
+ continue;
297
+ }
298
+ if (line.startsWith("[") && line.endsWith("]")) {
299
+ here = table(out, line.slice(1, -1).trim().split("."));
300
+ continue;
301
+ }
302
+ const at = line.indexOf("=");
303
+ if (at === -1) continue;
304
+ let text = line.slice(at + 1);
305
+ while ([...text].filter((one) => one === "[").length > [...text].filter((one) => one === "]").length) {
306
+ index += 1;
307
+ if (index >= lines2.length) throw new CannotRun("an unterminated array in the TOML config");
308
+ text += `
309
+ ${(lines2[index] ?? "").split("#")[0] ?? ""}`;
310
+ }
311
+ put(here, line.slice(0, at).trim().split("."), value(text));
312
+ }
313
+ return out;
314
+ };
315
+ var BINDING_LISTS = [
316
+ "ai",
317
+ "analytics_engine_datasets",
318
+ "browser",
319
+ "d1_databases",
320
+ "dispatch_namespaces",
321
+ "durable_objects",
322
+ "hyperdrive",
323
+ "kv_namespaces",
324
+ "mtls_certificates",
325
+ "queues",
326
+ "r2_buckets",
327
+ "send_email",
328
+ "services",
329
+ "vectorize",
330
+ "version_metadata",
331
+ "workflows"
332
+ ];
333
+ var bindingsOf = (found) => {
334
+ if (Array.isArray(found)) return found.flatMap(bindingsOf);
335
+ if (!isRecord2(found)) return [];
336
+ const named = found["binding"] ?? found["name"];
337
+ const here = typeof named === "string" ? [named] : [];
338
+ const nested = Object.entries(found).filter(([key]) => key === "bindings" || key === "producers" || key === "consumers").flatMap(([, one]) => bindingsOf(one));
339
+ return [...here, ...nested];
340
+ };
341
+ var declaredIn = (config) => {
342
+ const triggers = config["triggers"];
343
+ const crons = isRecord2(triggers) && Array.isArray(triggers["crons"]) ? triggers["crons"] : [];
344
+ const routes = config["routes"];
345
+ const one = config["route"];
346
+ const listed = [
347
+ ...Array.isArray(routes) ? routes : [],
348
+ ...typeof one === "string" ? [one] : []
349
+ ];
350
+ return {
351
+ bindings: BINDING_LISTS.flatMap((key) => bindingsOf(config[key])).toSorted(),
352
+ crons: crons.filter((cron) => typeof cron === "string").toSorted(),
353
+ routes: listed.map((route) => isRecord2(route) ? route["pattern"] : route).filter((pattern) => typeof pattern === "string").toSorted()
354
+ };
355
+ };
356
+ var readWrangler = (root, relative5, env) => {
357
+ const path = resolve2(root, relative5);
358
+ let parsed;
359
+ try {
360
+ const source = readFileSync2(path, "utf8");
361
+ parsed = relative5.endsWith(".toml") ? parseToml(source) : parseJsonc(source);
362
+ } catch (error) {
363
+ throw new CannotRun(`${relative5} could not be read: ${error.message}`);
364
+ }
365
+ if (!isRecord2(parsed)) throw new CannotRun(`${relative5} is not a wrangler configuration`);
366
+ const environments = parsed["env"];
367
+ const block = env !== void 0 && isRecord2(environments) && isRecord2(environments[env]) ? environments[env] : parsed;
368
+ return declaredIn(block);
369
+ };
370
+
371
+ // src/deployed.ts
372
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
373
+ import { resolve as resolve3 } from "path";
374
+ var DEPLOYED_FILE = ".geonosis/deployed.json";
375
+ var NOTHING_SAYS = "nothing here says what is deployed \u2014 .geonosis/deployed.json is written by the pipeline after promote, and its absence is not a pass";
376
+ var listOf = (found) => Array.isArray(found) ? found.filter((one) => typeof one === "string") : [];
377
+ var isRecord3 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
378
+ var readDeployed = (root) => {
379
+ const path = resolve3(root, DEPLOYED_FILE);
380
+ if (!existsSync2(path)) throw new CannotRun(NOTHING_SAYS);
381
+ let parsed;
382
+ try {
383
+ parsed = JSON.parse(readFileSync3(path, "utf8"));
384
+ } catch (error) {
385
+ throw new CannotRun(`${DEPLOYED_FILE} is not readable JSON: ${error.message}`);
386
+ }
387
+ if (!isRecord3(parsed)) throw new CannotRun(`${DEPLOYED_FILE} is not an object`);
388
+ const triggers = isRecord3(parsed["triggers"]) ? parsed["triggers"] : {};
389
+ return {
390
+ ...typeof parsed["at"] === "string" ? { at: parsed["at"] } : {},
391
+ deployed: {
392
+ bindings: listOf(parsed["bindings"]),
393
+ crons: listOf(triggers["crons"]),
394
+ routes: listOf(triggers["routes"]),
395
+ secrets: listOf(parsed["secrets"])
396
+ }
397
+ };
398
+ };
399
+ var missingBetween = (kind, declared, deployed) => {
400
+ const missing = declared.filter((one) => !deployed.includes(one));
401
+ const extra = deployed.filter((one) => !declared.includes(one));
402
+ return missing.length === 0 && extra.length === 0 ? [] : [{ extra, kind, missing }];
403
+ };
404
+ var driftBetween = (declared, deployed) => [
405
+ ...missingBetween("crons", declared.crons, deployed.crons),
406
+ ...missingBetween("routes", declared.routes, deployed.routes),
407
+ ...missingBetween("bindings", declared.bindings, deployed.bindings),
408
+ ...missingBetween("secrets", declared.secrets, deployed.secrets)
409
+ ];
410
+ var runDeployed = (input) => {
411
+ const config = readReleaseConfig(input.root);
412
+ const configs = config.wrangler ?? [];
413
+ const secrets = config.secrets ?? [];
414
+ if (configs.length === 0 && secrets.length === 0) {
415
+ throw new CannotRun(
416
+ "geonosis.json names no release.wrangler configs and no release.secrets \u2014 nothing here declares what a deployment is supposed to carry"
417
+ );
418
+ }
419
+ const found = configs.map((one) => readWrangler(input.root, one, config.wranglerEnv));
420
+ const declared = {
421
+ bindings: [...new Set(found.flatMap((one) => one.bindings))].toSorted(),
422
+ crons: [...new Set(found.flatMap((one) => one.crons))].toSorted(),
423
+ routes: [...new Set(found.flatMap((one) => one.routes))].toSorted(),
424
+ secrets: [...secrets].toSorted()
425
+ };
426
+ const { at, deployed } = readDeployed(input.root);
427
+ const drift = driftBetween(declared, deployed);
428
+ return { ...at === void 0 ? {} : { at }, drift, ok: drift.length === 0 };
429
+ };
430
+ var formatDeployed = (report) => {
431
+ if (report.ok) {
432
+ return `OK deployed: what the tree declares is what the pipeline reported${report.at === void 0 ? "" : ` at ${report.at}`}
433
+ `;
434
+ }
435
+ return report.drift.flatMap((one) => [
436
+ ...one.missing.length === 0 ? [] : [` declared but not deployed ${one.kind}: ${one.missing.join(", ")}
437
+ `],
438
+ ...one.extra.length === 0 ? [] : [` deployed but not declared ${one.kind}: ${one.extra.join(", ")}
439
+ `]
440
+ ]).join("");
441
+ };
442
+
443
+ // src/added.ts
444
+ import { execFileSync, spawnSync } from "child_process";
445
+ var git = (root, args) => {
446
+ try {
447
+ return execFileSync("git", [...args], { cwd: root, encoding: "utf8", stdio: "pipe" });
448
+ } catch (error) {
449
+ throw new CannotRun(`git ${args.join(" ")} failed: ${(error.message ?? "").trim()}`);
450
+ }
451
+ };
452
+ var lines = (out) => out.split("\n").filter((line) => line !== "");
453
+ var addedSince = (root, since) => {
454
+ const committed = lines(git(root, ["diff", "--name-only", "--diff-filter=A", `${since}..HEAD`]));
455
+ const staged = lines(git(root, ["diff", "--name-only", "--diff-filter=A", "--cached"]));
456
+ const untracked = lines(git(root, ["ls-files", "--others", "--exclude-standard"]));
457
+ return [.../* @__PURE__ */ new Set([...committed, ...staged, ...untracked])].toSorted();
458
+ };
459
+ var gitOk = (root, args) => {
460
+ const run = spawnSync("git", [...args], { cwd: root, encoding: "utf8" });
461
+ return run.error === void 0 && run.status === 0;
462
+ };
463
+
464
+ // src/marker.ts
465
+ var MARKER = /^[^\S\n]*(?:--|\/\/)[^\S\n]*contract-migration:[^\S\n]*(.*)$/gim;
466
+ var SINCE = /(?:^|\s)since:(\S+)/;
467
+ var markersIn = (source) => {
468
+ const out = [];
469
+ MARKER.lastIndex = 0;
470
+ for (let found = MARKER.exec(source); found !== null; found = MARKER.exec(source)) {
471
+ const tail = found[1] ?? "";
472
+ const since = SINCE.exec(tail)?.[1];
473
+ out.push({
474
+ line: source.slice(0, found.index).split("\n").length,
475
+ reason: (since === void 0 ? tail : tail.replace(SINCE, " ")).trim(),
476
+ ...since === void 0 ? {} : { since }
477
+ });
478
+ }
479
+ return out;
480
+ };
481
+ var judge = (marker, root, base) => {
482
+ if (marker.reason === "") {
483
+ return "the marker gives no reason \u2014 a marker with nothing after the colon is a comment";
484
+ }
485
+ if (marker.since === void 0) {
486
+ return "the marker has no since:<ref> \u2014 an expand nobody can date excuses nothing";
487
+ }
488
+ if (!gitOk(root, ["rev-parse", "--verify", "--quiet", `${marker.since}^{commit}`])) {
489
+ return `since:${marker.since} is a ref git does not know`;
490
+ }
491
+ if (!gitOk(root, ["merge-base", "--is-ancestor", marker.since, base])) {
492
+ return `since:${marker.since} is not an ancestor of ${base} \u2014 the expand it names is inside this release, so nothing has drained`;
493
+ }
494
+ return void 0;
495
+ };
496
+
497
+ // src/mikro.ts
498
+ var Unreadable = class extends Error {
499
+ };
500
+ var isSpace = (char) => char === " " || char === " " || char === "\n" || char === "\r";
501
+ var advance = (source, at, to) => {
502
+ for (let index = at.index; index < to; index += 1) {
503
+ if (source[index] === "\n") at.line += 1;
504
+ }
505
+ at.index = to;
506
+ };
507
+ var skipTrivia = (source, at) => {
508
+ for (; ; ) {
509
+ while (at.index < source.length && isSpace(source[at.index] ?? "")) {
510
+ if (source[at.index] === "\n") at.line += 1;
511
+ at.index += 1;
512
+ }
513
+ if (source.startsWith("//", at.index)) {
514
+ const end = source.indexOf("\n", at.index);
515
+ advance(source, at, end === -1 ? source.length : end);
516
+ continue;
517
+ }
518
+ if (source.startsWith("/*", at.index)) {
519
+ const end = source.indexOf("*/", at.index + 2);
520
+ if (end === -1) throw new Unreadable("an unterminated block comment");
521
+ advance(source, at, end + 2);
522
+ continue;
523
+ }
524
+ return;
525
+ }
526
+ };
527
+ var readLiteral = (source, at) => {
528
+ const quote = source[at.index];
529
+ if (quote !== "`" && quote !== "'" && quote !== '"') {
530
+ throw new Unreadable(`an argument that is not a string literal: ${nameOf(source, at.index)}`);
531
+ }
532
+ let out = "";
533
+ let index = at.index + 1;
534
+ for (; index < source.length; index += 1) {
535
+ const char = source[index] ?? "";
536
+ if (char === "\\") {
537
+ out += source[index + 1] ?? "";
538
+ index += 1;
539
+ continue;
540
+ }
541
+ if (quote === "`" && char === "$" && source[index + 1] === "{") {
542
+ throw new Unreadable("a template literal with an interpolation");
543
+ }
544
+ if (char === quote) {
545
+ advance(source, at, index + 1);
546
+ return out;
547
+ }
548
+ out += char;
549
+ }
550
+ throw new Unreadable("an unterminated string literal");
551
+ };
552
+ var nameOf = (source, index) => {
553
+ const rest = source.slice(index);
554
+ const end = rest.search(/[),\n]/);
555
+ return (end === -1 ? rest : rest.slice(0, end)).trim();
556
+ };
557
+ var addSqlIn = (source, phases) => {
558
+ const out = [];
559
+ for (const phase of phases) {
560
+ const body = phaseBody(source, phase);
561
+ if (body === void 0) continue;
562
+ const at = { index: 0, line: body.line };
563
+ const calls = /(?:^|[^\w$.])(?:this\.)?addSql\s*\(/g;
564
+ for (let found = calls.exec(body.text); found !== null; found = calls.exec(body.text)) {
565
+ advance(body.text, at, found.index + found[0].length);
566
+ skipTrivia(body.text, at);
567
+ const line = at.line;
568
+ let sql = readLiteral(body.text, at);
569
+ for (; ; ) {
570
+ skipTrivia(body.text, at);
571
+ if (body.text[at.index] !== "+") break;
572
+ advance(body.text, at, at.index + 1);
573
+ skipTrivia(body.text, at);
574
+ sql += readLiteral(body.text, at);
575
+ }
576
+ out.push({ line, sql });
577
+ calls.lastIndex = at.index;
578
+ }
579
+ }
580
+ return out.toSorted((a, b) => a.line - b.line);
581
+ };
582
+ var phaseBody = (source, phase) => {
583
+ const blanked = blank(source);
584
+ const opener = new RegExp(`\\b${phase}\\s*\\([^)]*\\)[^{;]*\\{`);
585
+ const found = opener.exec(blanked);
586
+ if (found === null) return void 0;
587
+ const open = found.index + found[0].length - 1;
588
+ let depth = 0;
589
+ for (let index = open; index < blanked.length; index += 1) {
590
+ if (blanked[index] === "{") depth += 1;
591
+ if (blanked[index] === "}") {
592
+ depth -= 1;
593
+ if (depth === 0) {
594
+ return {
595
+ line: source.slice(0, open + 1).split("\n").length,
596
+ text: source.slice(open + 1, index)
597
+ };
598
+ }
599
+ }
600
+ }
601
+ throw new Unreadable(`an unbalanced ${phase}() body`);
602
+ };
603
+ var blank = (source) => {
604
+ const out = [...source];
605
+ const hide = (from, to) => {
606
+ for (let index = from; index < to && index < out.length; index += 1) {
607
+ if (out[index] !== "\n") out[index] = " ";
608
+ }
609
+ };
610
+ for (let index = 0; index < source.length; index += 1) {
611
+ const char = source[index] ?? "";
612
+ if (char === "/" && source[index + 1] === "/") {
613
+ const end2 = source.indexOf("\n", index);
614
+ const stop = end2 === -1 ? source.length : end2;
615
+ hide(index, stop);
616
+ index = stop;
617
+ continue;
618
+ }
619
+ if (char === "/" && source[index + 1] === "*") {
620
+ const end2 = source.indexOf("*/", index + 2);
621
+ const stop = end2 === -1 ? source.length : end2 + 2;
622
+ hide(index, stop);
623
+ index = stop - 1;
624
+ continue;
625
+ }
626
+ if (char !== "`" && char !== "'" && char !== '"') continue;
627
+ let end = index + 1;
628
+ for (; end < source.length; end += 1) {
629
+ if (source[end] === "\\") {
630
+ end += 1;
631
+ continue;
632
+ }
633
+ if (source[end] === char) break;
634
+ }
635
+ hide(index + 1, end);
636
+ index = end;
637
+ }
638
+ return out.join("");
639
+ };
640
+
641
+ // src/sql.ts
642
+ var NARROWING = [
643
+ ["DROP TABLE", /\bDROP\s+TABLE\b/i],
644
+ ["DROP COLUMN", /\bDROP\s+COLUMN\b/i],
645
+ ["RENAME", /\bRENAME\b/i],
646
+ ["ALTER COLUMN \u2026 TYPE", /\bALTER\s+COLUMN\b[^;]*\bTYPE\b/i],
647
+ ["SET NOT NULL", /\bSET\s+NOT\s+NULL\b/i]
648
+ ];
649
+ var statementsOf = (sql) => sql.replaceAll(/'[^']*'/g, (found) => `'${found.slice(1, -1).replaceAll(/[^\n]/g, " ")}'`);
650
+ var lineOf = (body, index) => body.slice(0, index).split("\n").length;
651
+ var refusalsIn = (path, body, firstLine = 1) => {
652
+ const stripped = statementsOf(body);
653
+ return NARROWING.flatMap(([verb, pattern]) => {
654
+ const found = pattern.exec(stripped);
655
+ return found === null ? [] : [{ line: lineOf(stripped, found.index) + firstLine - 1, path, verb }];
656
+ });
657
+ };
658
+
659
+ // src/migrations.ts
660
+ import { mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync } from "fs";
661
+ import { tmpdir } from "os";
662
+ import { basename, join, resolve as resolve5 } from "path";
663
+
664
+ // src/squawk.ts
665
+ import { spawnSync as spawnSync2 } from "child_process";
666
+ import { existsSync as existsSync3 } from "fs";
667
+ import { dirname, resolve as resolve4 } from "path";
668
+ import { fileURLToPath } from "url";
669
+ var HERE = dirname(fileURLToPath(import.meta.url));
670
+ var PINNED = "2.63.0";
671
+ var findSquawk = (from = HERE) => {
672
+ for (let dir = from; ; dir = dirname(dir)) {
673
+ const candidate = resolve4(dir, "node_modules/.bin/squawk");
674
+ if (existsSync3(candidate)) return candidate;
675
+ if (dirname(dir) === dir) break;
676
+ }
677
+ throw new CannotRun(
678
+ `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:
679
+
680
+ pnpm add -D squawk-cli@${PINNED}
681
+
682
+ It is an optional peer: a repo whose release.migrations names only the sqlite dialect needs none of this.`
683
+ );
684
+ };
685
+ var squawkOn = (input) => {
686
+ const { cwd, exclude, files } = input;
687
+ if (files.length === 0) return { findings: "", ok: true };
688
+ const run = spawnSync2(
689
+ input.bin ?? findSquawk(),
690
+ [...exclude.length === 0 ? [] : [`--exclude=${exclude.join(",")}`], ...files],
691
+ { cwd, encoding: "utf8" }
692
+ );
693
+ if (run.error !== void 0) {
694
+ throw new CannotRun(`squawk could not be started: ${run.error.message}`);
695
+ }
696
+ const findings = `${run.stdout}${run.stderr}`;
697
+ if (run.status === 0) return { findings, ok: true };
698
+ if (run.status === 1) return { findings, ok: false };
699
+ throw new CannotRun(
700
+ `squawk exited ${String(run.status)}, which is neither clean nor findings:
701
+ ${findings}`
702
+ );
703
+ };
704
+
705
+ // src/migrations.ts
706
+ var EXTENSION = {
707
+ "mikro-orm-ts": ".ts",
708
+ postgres: ".sql",
709
+ sqlite: ".sql"
710
+ };
711
+ var under = (dir, file) => file === dir || file.startsWith(dir.endsWith("/") ? dir : `${dir}/`);
712
+ var fromTypeScript = (path, source, entry) => {
713
+ try {
714
+ return {
715
+ refusals: addSqlIn(source, entry.phases ?? ["up"]).flatMap(
716
+ (one) => refusalsIn(path, one.sql, one.line)
717
+ )
718
+ };
719
+ } catch (error) {
720
+ if (error instanceof Unreadable) {
721
+ return {
722
+ refusals: [],
723
+ unreadable: `${path} holds ${error.message} \u2014 this reader cannot read it`
724
+ };
725
+ }
726
+ throw error;
727
+ }
728
+ };
729
+ var sqlFor = (path, source, entry) => entry.dialect === "mikro-orm-ts" ? addSqlIn(source, entry.phases ?? ["up"]).map((one) => one.sql.trim()).map((one) => one.endsWith(";") ? one : `${one};`).join("\n") : source;
730
+ var runMigrations = (input) => {
731
+ const config = readReleaseConfig(input.root);
732
+ const declared = config.migrations ?? [];
733
+ if (declared.length === 0) {
734
+ throw new CannotRun(
735
+ "geonosis.json names no release.migrations \u2014 nothing here says which directories hold migrations, or in which dialect"
736
+ );
737
+ }
738
+ const entries = input.dialect === void 0 ? declared : declared.filter((one) => one.dialect === input.dialect);
739
+ if (entries.length === 0) {
740
+ throw new CannotRun(`release.migrations names no directory with dialect "${input.dialect}"`);
741
+ }
742
+ const matched = addedSince(input.root, input.since).flatMap((file) => {
743
+ const entry = entries.find(
744
+ (one) => under(one.dir, file) && file.endsWith(EXTENSION[one.dialect])
745
+ );
746
+ return entry === void 0 ? [] : [{ entry, file }];
747
+ });
748
+ const refusals = [];
749
+ const markers = [];
750
+ const squawk = [];
751
+ const unreadable = [];
752
+ const forSquawk = [];
753
+ const states = /* @__PURE__ */ new Map();
754
+ for (const { entry, file } of matched) {
755
+ const source = readFileSync4(resolve5(input.root, file), "utf8");
756
+ const found = markersIn(source);
757
+ const bad = found.flatMap((one) => {
758
+ const why = judge(one, input.root, input.since);
759
+ return why === void 0 ? [] : [{ line: one.line, path: file, why }];
760
+ });
761
+ markers.push(...bad);
762
+ const excused = found.length > 0 && bad.length === 0;
763
+ states.set(file, excused ? "excused" : "clean");
764
+ if (!excused) {
765
+ const mine = entry.dialect === "mikro-orm-ts" ? fromTypeScript(file, source, entry) : { refusals: refusalsIn(file, source) };
766
+ if (mine.unreadable !== void 0) {
767
+ unreadable.push({ path: file, why: mine.unreadable });
768
+ states.set(file, "unreadable");
769
+ continue;
770
+ }
771
+ refusals.push(...mine.refusals);
772
+ if (mine.refusals.length > 0) states.set(file, "refused");
773
+ }
774
+ if (bad.length > 0) states.set(file, "refused");
775
+ if (entry.dialect !== "sqlite")
776
+ forSquawk.push({ entry, file, sql: sqlFor(file, source, entry) });
777
+ }
778
+ let clean = true;
779
+ if (forSquawk.length > 0) {
780
+ const staging = mkdtempSync(join(tmpdir(), "geonosis-release-squawk-"));
781
+ try {
782
+ for (const one of forSquawk) {
783
+ const path = one.entry.dialect === "mikro-orm-ts" ? join(staging, `${basename(one.file, ".ts")}.sql`) : resolve5(input.root, one.file);
784
+ if (one.entry.dialect === "mikro-orm-ts") writeFileSync(path, `${one.sql}
785
+ `);
786
+ const found = squawkOn({
787
+ cwd: input.root,
788
+ exclude: one.entry.squawk?.exclude ?? [],
789
+ files: [path]
790
+ });
791
+ if (!found.ok) {
792
+ clean = false;
793
+ squawk.push({ findings: found.findings, path: one.file });
794
+ if (states.get(one.file) === "clean") states.set(one.file, "findings");
795
+ }
796
+ }
797
+ } finally {
798
+ rmSync(staging, { force: true, recursive: true });
799
+ }
800
+ }
801
+ return {
802
+ dirs: entries.map((one) => one.dir),
803
+ files: matched.map(({ file }) => ({ path: file, state: states.get(file) ?? "clean" })),
804
+ linted: forSquawk.length,
805
+ markers: markers.toSorted((a, b) => a.path.localeCompare(b.path) || a.line - b.line),
806
+ ok: refusals.length === 0 && markers.length === 0 && clean && unreadable.length === 0,
807
+ read: matched.length - unreadable.length,
808
+ since: input.since,
809
+ refusals: refusals.toSorted(
810
+ (a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.verb.localeCompare(b.verb)
811
+ ),
812
+ squawk,
813
+ unreadable
814
+ };
815
+ };
816
+ var WHY = `
817
+ A release may only WIDEN the schema. Migrations run against live databases while the previous
818
+ version is still serving, and no rollback undoes them. Contract in a later release, or say why it
819
+ is safe now:
820
+
821
+ -- contract-migration: <what expanded it, and why nothing reads it any more> since:<ref>
822
+ `;
823
+ var formatMigrations = (report) => {
824
+ if (report.files.length === 0) {
825
+ return `NONE migrations: no migration file was added since ${report.since} \u2014 committed, staged or untracked \u2014 under ${report.dirs.join(", ")}; nothing was read
826
+ `;
827
+ }
828
+ if (report.ok) {
829
+ return `OK migrations: expand-only (${String(report.read)} read, ${String(report.linted)} linted by squawk)
830
+ `;
831
+ }
832
+ const said = report.markers.map((one) => ` ${one.path}: ${one.why} (line ${String(one.line)})
833
+ `);
834
+ const lines2 = report.refusals.map(
835
+ (one) => ` ${one.path}: ${one.verb} (line ${String(one.line)})
836
+ `
837
+ );
838
+ const found = report.squawk.map((one) => ` ${one.path}:
839
+ ${one.findings}
840
+ `);
841
+ const cannot = report.unreadable.map((one) => ` ${one.why}
842
+ `);
843
+ const why = report.markers.length + report.refusals.length === 0 ? "" : WHY;
844
+ return `${said.join("")}${lines2.join("")}${cannot.join("")}${why}${found.join("")}${accounting(report)}`;
845
+ };
846
+ var accounting = (report) => {
847
+ const total = report.files.length;
848
+ const by = (state) => report.files.filter((one) => one.state === state).length;
849
+ const rows = report.files.map((one) => ` ${one.state.padEnd(10)} ${one.path}
850
+ `);
851
+ 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`}
852
+ `;
853
+ };
854
+
855
+ // src/plan.ts
856
+ var STEPS = ["upload", "park", "prove", "promote"];
857
+ var WHY_VERSION_IDENTITY = "An override that was not applied routes by the configured percentages, to the OLD version \u2014 so a smoke that does not assert which version answered can pass by testing the code it was replacing.";
858
+ var runPlan = (input) => {
859
+ const config = readReleaseConfig(input.root);
860
+ const steps = config.steps ?? [];
861
+ if (steps.length === 0) {
862
+ throw new CannotRun(
863
+ "geonosis.json names no release.steps \u2014 nothing here says what this repo does to put a version in front of a user"
864
+ );
865
+ }
866
+ const problems = [];
867
+ const prove2 = steps.indexOf("prove");
868
+ const promote = steps.indexOf("promote");
869
+ if (prove2 === -1 || promote === -1 || prove2 > promote) {
870
+ problems.push(
871
+ "prove must come before promote \u2014 a promote that has not been proved serves code nothing answered for"
872
+ );
873
+ }
874
+ if (steps.length !== STEPS.length || steps.some((step, at) => step !== STEPS[at])) {
875
+ problems.push(`release.steps must be exactly ${STEPS.join(" \u2192 ")}, and is ${steps.join(" \u2192 ")}`);
876
+ }
877
+ const mustAssert = config.proof?.mustAssertVersion !== false;
878
+ if (!mustAssert && !input.accept) {
879
+ problems.push(
880
+ `release.proof.mustAssertVersion is false. ${WHY_VERSION_IDENTITY} Pass --i-accept-an-unproven-smoke to say so on purpose.`
881
+ );
882
+ }
883
+ return {
884
+ accepted: !mustAssert && input.accept,
885
+ ok: problems.length === 0,
886
+ problems,
887
+ steps: [...steps]
888
+ };
889
+ };
890
+ var formatPlan = (report) => {
891
+ if (!report.ok) return `${report.problems.map((one) => ` ${one}
892
+ `).join("")}`;
893
+ const note = report.accepted ? ` the smoke need not say which version answered. ${WHY_VERSION_IDENTITY}
894
+ ` : " the smoke asserts the version that answered it\n";
895
+ return `OK release: ${report.steps.join(" \u2192 ")}
896
+ ${note}`;
897
+ };
898
+
899
+ // src/runner.ts
900
+ import { spawn } from "child_process";
901
+ var Runner = class {
902
+ buffered = "";
903
+ closed;
904
+ child;
905
+ waiting = [];
906
+ constructor(command, args, cwd) {
907
+ this.child = spawn(command, [...args], { cwd, stdio: ["pipe", "pipe", "pipe"] });
908
+ this.child.stdout.setEncoding("utf8");
909
+ this.child.stdout.on("data", (chunk) => {
910
+ this.buffered += chunk;
911
+ for (let at = this.buffered.indexOf("\n"); at !== -1; at = this.buffered.indexOf("\n")) {
912
+ const line = this.buffered.slice(0, at);
913
+ this.buffered = this.buffered.slice(at + 1);
914
+ this.waiting.shift()?.(line);
915
+ }
916
+ });
917
+ this.child.on("error", (error) => {
918
+ this.closed = error.message;
919
+ });
920
+ this.child.on("close", () => {
921
+ this.closed ??= "the runner exited without answering";
922
+ });
923
+ }
924
+ async ask(request, timeoutMs = 6e4) {
925
+ if (this.closed !== void 0) {
926
+ throw new CannotRun(`the runner is gone before "${request.step}": ${this.closed}`);
927
+ }
928
+ const line = await new Promise((resolve9, reject) => {
929
+ const timer = setTimeout(() => {
930
+ reject(
931
+ new CannotRun(
932
+ `the runner did not answer "${request.step}" within ${String(timeoutMs)}ms`
933
+ )
934
+ );
935
+ }, timeoutMs);
936
+ this.waiting.push((answer) => {
937
+ clearTimeout(timer);
938
+ resolve9(answer);
939
+ });
940
+ this.child.on("close", () => {
941
+ clearTimeout(timer);
942
+ reject(new CannotRun(`the runner closed before answering "${request.step}"`));
943
+ });
944
+ this.child.stdin.write(`${JSON.stringify(request)}
945
+ `);
946
+ });
947
+ try {
948
+ return JSON.parse(line);
949
+ } catch {
950
+ throw new CannotRun(
951
+ `the runner answered "${request.step}" with something that is not JSON: ${line}`
952
+ );
953
+ }
954
+ }
955
+ close() {
956
+ this.child.stdin.end();
957
+ this.child.kill();
958
+ }
959
+ };
960
+
961
+ // src/prove.ts
962
+ import { existsSync as existsSync4 } from "fs";
963
+ import { fileURLToPath as fileURLToPath2 } from "url";
964
+ var stringAt = (reply, key) => typeof reply[key] === "string" ? reply[key] : void 0;
965
+ var proveOver = async (input) => {
966
+ const runner = new Runner(input.command, input.args ?? [], input.cwd);
967
+ const steps = [];
968
+ const refuse = (why) => ({ ok: false, steps, why });
969
+ try {
970
+ steps.push("upload");
971
+ const uploaded = await runner.ask({ step: "upload" }, input.timeoutMs);
972
+ const versionId = stringAt(uploaded, "versionId");
973
+ if (versionId === void 0) {
974
+ return refuse("the runner\u2019s upload answered no versionId, so there is nothing to override to");
975
+ }
976
+ steps.push("smoke");
977
+ const smoked = await runner.ask({ step: "smoke", versionId }, input.timeoutMs);
978
+ if (smoked["ok"] !== true) return refuse(`the smoke of ${versionId} did not pass`);
979
+ const answered = stringAt(smoked, "answeredVersionId");
980
+ if (answered === void 0) {
981
+ return refuse(`the smoke of ${versionId} named no version. ${WHY_VERSION_IDENTITY}`);
982
+ }
983
+ if (answered !== versionId) {
984
+ return refuse(
985
+ `the smoke overrode to ${versionId} and ${answered} answered. ${WHY_VERSION_IDENTITY}`
986
+ );
987
+ }
988
+ steps.push("promote");
989
+ const promoted = await runner.ask({ step: "promote", versionId }, input.timeoutMs);
990
+ if (promoted["ok"] !== true) return refuse(`the promote of ${versionId} did not pass`);
991
+ const went = stringAt(promoted, "promotedVersionId");
992
+ if (went !== void 0 && went !== versionId) {
993
+ return refuse(`${versionId} was proved and ${went} was promoted \u2014 nothing proved ${went}`);
994
+ }
995
+ return { ok: true, steps };
996
+ } finally {
997
+ runner.close();
998
+ }
999
+ };
1000
+ var PLANTS = [
1001
+ {
1002
+ name: "answered",
1003
+ says: "a smoke that answered a version other than the one it overrode to is refused"
1004
+ },
1005
+ { name: "failed", says: "a smoke that did not pass is refused" },
1006
+ { name: "promoted", says: "a promote of a version nothing proved is refused" }
1007
+ ];
1008
+ var stubPath = () => {
1009
+ const path = fileURLToPath2(new URL("./stub-runner.js", import.meta.url));
1010
+ if (!existsSync4(path)) throw new CannotRun(`${path} is missing \u2014 run pnpm build first`);
1011
+ return path;
1012
+ };
1013
+ var prove = async (cwd, stub = stubPath()) => {
1014
+ const lines2 = [];
1015
+ let ok = true;
1016
+ for (const plant of PLANTS) {
1017
+ const verdict = await proveOver({
1018
+ args: [stub, `--plant=${plant.name}`],
1019
+ command: process.execPath,
1020
+ cwd,
1021
+ timeoutMs: 3e4
1022
+ });
1023
+ if (verdict.ok) ok = false;
1024
+ lines2.push(` ${verdict.ok ? "MISSED" : "PROVEN"} ${plant.says}`);
1025
+ if (plant.name !== "promoted" && verdict.steps.includes("promote")) {
1026
+ ok = false;
1027
+ lines2.push(" MISSED a refused smoke was followed by a promote request");
1028
+ }
1029
+ }
1030
+ const honest = await proveOver({
1031
+ args: [stub, "--plant=good"],
1032
+ command: process.execPath,
1033
+ cwd,
1034
+ timeoutMs: 3e4
1035
+ });
1036
+ if (!honest.ok) ok = false;
1037
+ lines2.push(
1038
+ ` ${honest.ok ? "PROVEN" : "MISSED"} a smoke that answered the version it overrode to is not refused`
1039
+ );
1040
+ return { lines: lines2, ok };
1041
+ };
1042
+ var formatProve = (outcome) => `${outcome.lines.join("\n")}
1043
+
1044
+ prove ${outcome.ok ? "PASS" : "FAIL"} \u2014 ${outcome.ok ? "every plant was refused, and the honest runner was not" : "a plant went through"}.
1045
+ `;
1046
+ var formatVerdict = (verdict) => verdict.ok ? `OK prove: ${verdict.steps.join(" \u2192 ")}
1047
+ ` : ` REFUSED after ${verdict.steps.join(" \u2192 ")}: ${verdict.why ?? ""}
1048
+ `;
1049
+
1050
+ // src/registry.ts
1051
+ var DEFAULT_REGISTRY = "https://registry.npmjs.org";
1052
+ var TIMEOUT_MS = 15e3;
1053
+ var registryPathOf = (name) => name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : encodeURIComponent(name);
1054
+ var versionsIn = (body) => {
1055
+ const versions = body.versions;
1056
+ return typeof versions === "object" && versions !== null ? Object.keys(versions) : [];
1057
+ };
1058
+ var latestIn = (body) => {
1059
+ const tags = body["dist-tags"];
1060
+ const latest = typeof tags === "object" && tags !== null ? tags.latest : void 0;
1061
+ return typeof latest === "string" ? latest : void 0;
1062
+ };
1063
+ var askRegistry = async ({
1064
+ name,
1065
+ registry = DEFAULT_REGISTRY,
1066
+ timeoutMs = TIMEOUT_MS
1067
+ }) => {
1068
+ const url = `${registry.replace(/\/+$/, "")}/${registryPathOf(name)}`;
1069
+ let response;
1070
+ try {
1071
+ response = await fetch(url, {
1072
+ headers: { accept: "application/json" },
1073
+ signal: AbortSignal.timeout(timeoutMs)
1074
+ });
1075
+ } catch (error) {
1076
+ return { kind: "unreachable", name, why: `GET ${url} \u2014 ${error.message}` };
1077
+ }
1078
+ if (response.status === 404) return { kind: "absent", name };
1079
+ if (!response.ok) {
1080
+ return { kind: "unreachable", name, why: `GET ${url} \u2014 HTTP ${response.status}` };
1081
+ }
1082
+ let body;
1083
+ try {
1084
+ body = await response.json();
1085
+ } catch (error) {
1086
+ return {
1087
+ kind: "unreachable",
1088
+ name,
1089
+ why: `GET ${url} answered ${response.status} with something that is not JSON \u2014 ${error.message}`
1090
+ };
1091
+ }
1092
+ return { kind: "present", latest: latestIn(body), name, versions: versionsIn(body) };
1093
+ };
1094
+
1095
+ // src/published.ts
1096
+ import { readdirSync, readFileSync as readFileSync5 } from "fs";
1097
+ import { join as join2, relative, sep } from "path";
1098
+ var NEVER_WALKED = /* @__PURE__ */ new Set(["build", "coverage", "dist", "node_modules", "storybook-static"]);
1099
+ var pathOf = (root, path) => relative(root, path).split(sep).join("/");
1100
+ var manifestsUnder = (root) => {
1101
+ const found = [];
1102
+ const walk = (dir) => {
1103
+ let entries;
1104
+ try {
1105
+ entries = readdirSync(dir, { withFileTypes: true });
1106
+ } catch {
1107
+ return;
1108
+ }
1109
+ for (const entry of entries) {
1110
+ if (entry.isDirectory()) {
1111
+ if (!entry.name.startsWith(".") && !NEVER_WALKED.has(entry.name))
1112
+ walk(join2(dir, entry.name));
1113
+ continue;
1114
+ }
1115
+ if (entry.name === "package.json") found.push(join2(dir, entry.name));
1116
+ }
1117
+ };
1118
+ walk(root);
1119
+ return found;
1120
+ };
1121
+ var censusOf = (root) => {
1122
+ const excused = [];
1123
+ const locals = [];
1124
+ for (const path of manifestsUnder(root)) {
1125
+ const at = pathOf(root, path);
1126
+ let manifest;
1127
+ try {
1128
+ manifest = JSON.parse(readFileSync5(path, "utf8"));
1129
+ } catch (error) {
1130
+ excused.push({ path: at, reason: `it does not parse: ${error.message}` });
1131
+ continue;
1132
+ }
1133
+ if (manifest.private === true) {
1134
+ excused.push({ path: at, reason: "private: true \u2014 nothing publishes it" });
1135
+ continue;
1136
+ }
1137
+ if (typeof manifest.name !== "string" || manifest.name === "") {
1138
+ excused.push({ path: at, reason: "it names no package" });
1139
+ continue;
1140
+ }
1141
+ if (typeof manifest.version !== "string" || manifest.version === "") {
1142
+ excused.push({ path: at, reason: "it declares no version" });
1143
+ continue;
1144
+ }
1145
+ locals.push({ at, name: manifest.name, version: manifest.version });
1146
+ }
1147
+ return { excused, locals: locals.toSorted((a, b) => a.name.localeCompare(b.name)) };
1148
+ };
1149
+ var EXISTS_BUT = "exists but no version matching";
1150
+ var lineFor = async (local, registry) => {
1151
+ const answer = await askRegistry({ name: local.name, registry });
1152
+ const base = { at: local.at, local: local.version, name: local.name };
1153
+ if (answer.kind === "unreachable") {
1154
+ return { ...base, latest: void 0, verdict: "UNREACHABLE", why: answer.why };
1155
+ }
1156
+ if (answer.kind === "absent") {
1157
+ return {
1158
+ ...base,
1159
+ latest: void 0,
1160
+ verdict: "ABSENT",
1161
+ why: "npm has never heard of this name \u2014 a name published moments ago 404s for a few minutes; --wait <seconds> polls through that window"
1162
+ };
1163
+ }
1164
+ if (answer.latest === local.version) {
1165
+ return { ...base, latest: answer.latest, verdict: "MATCH", why: "on npm" };
1166
+ }
1167
+ return {
1168
+ ...base,
1169
+ latest: answer.latest,
1170
+ verdict: "BEHIND",
1171
+ why: answer.versions.includes(local.version) ? `npm has ${local.version}, and its latest tag is ${answer.latest ?? "nothing"} \u2014 the group has not landed on one version` : `the package ${EXISTS_BUT} ${local.version}; npm answers ${answer.latest ?? "nothing"}. Minutes after a publish, suspect the package manager's manifest cache before the registry`
1172
+ };
1173
+ };
1174
+ var sleep = (ms) => new Promise((done) => {
1175
+ setTimeout(done, ms);
1176
+ });
1177
+ var POLL_MS = 5e3;
1178
+ var runPublished = async ({
1179
+ pollMs = POLL_MS,
1180
+ registry = DEFAULT_REGISTRY,
1181
+ root,
1182
+ waitSeconds = 0
1183
+ }) => {
1184
+ const { excused, locals } = censusOf(root);
1185
+ const startedAt = Date.now();
1186
+ const budgetMs = Math.max(0, waitSeconds * 1e3);
1187
+ let lines2 = [];
1188
+ let sweeps = 0;
1189
+ for (; ; ) {
1190
+ lines2 = [];
1191
+ for (const local of locals) lines2.push(await lineFor(local, registry));
1192
+ sweeps += 1;
1193
+ const settled = lines2.every((one) => one.verdict === "MATCH");
1194
+ const stuck = lines2.some((one) => one.verdict === "UNREACHABLE");
1195
+ const left = budgetMs - (Date.now() - startedAt);
1196
+ if (settled || stuck || left <= 0) break;
1197
+ await sleep(Math.min(pollMs, left));
1198
+ }
1199
+ const unreachable = lines2.filter((one) => one.verdict === "UNREACHABLE").length;
1200
+ return {
1201
+ considered: lines2.length + excused.length,
1202
+ excused: excused.toSorted((a, b) => a.path.localeCompare(b.path)),
1203
+ lines: lines2,
1204
+ ok: lines2.length > 0 && lines2.every((one) => one.verdict === "MATCH"),
1205
+ registry,
1206
+ sweeps,
1207
+ unreachable,
1208
+ waitedMs: Date.now() - startedAt
1209
+ };
1210
+ };
1211
+ var NOTHING_TO_CHECK = "published FAIL \u2014 no workspace in this tree declares a package a publish could be about, and a check that considered nothing has not passed";
1212
+ var formatPublished = (report) => {
1213
+ const width = Math.max(1, ...report.lines.map((one) => one.name.length));
1214
+ const body = report.lines.map((one) => ` ${one.verdict.padEnd(11)} ${one.name.padEnd(width)} ${one.local} ${one.why}`).join("\n");
1215
+ const head = report.lines.length === 0 ? NOTHING_TO_CHECK : `published ${report.ok ? "PASS" : "FAIL"} \u2014 ${report.lines.filter((one) => one.verdict === "MATCH").length}/${report.lines.length} of the group on ${report.registry}`;
1216
+ const tail = [
1217
+ `considered ${report.considered} manifests: ${report.lines.length} read, ${report.excused.length} excused`,
1218
+ report.sweeps > 1 ? `swept ${report.sweeps} times over ${report.waitedMs} ms` : ""
1219
+ ].filter((one) => one !== "");
1220
+ return `${[head, body, ...tail].filter((one) => one !== "").join("\n")}
1221
+ `;
1222
+ };
1223
+
1224
+ // src/schema-walls.ts
1225
+ import { readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
1226
+ import { join as join3, relative as relative2, resolve as resolve6, sep as sep2 } from "path";
1227
+ var MIGRATION_FILE = /\.(?:sql|ts)$/;
1228
+ var CURRENT_SETTING = /current_setting\s*\(\s*'([^']+)'/gi;
1229
+ var filesUnder = (root, dir) => {
1230
+ const found = [];
1231
+ const walk = (at) => {
1232
+ let entries;
1233
+ try {
1234
+ entries = readdirSync2(at, { withFileTypes: true });
1235
+ } catch {
1236
+ return;
1237
+ }
1238
+ for (const entry of entries) {
1239
+ const path = join3(at, entry.name);
1240
+ if (entry.isDirectory()) walk(path);
1241
+ else if (MIGRATION_FILE.test(entry.name)) found.push(path);
1242
+ }
1243
+ };
1244
+ walk(resolve6(root, dir));
1245
+ return found.map((path) => relative2(root, path).split(sep2).join("/")).toSorted();
1246
+ };
1247
+ var lineOf2 = (body, index) => body.slice(0, index).split("\n").length;
1248
+ var wordFor = (table2) => new RegExp(
1249
+ String.raw`(?<![\w.])${table2.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)}\b`,
1250
+ "i"
1251
+ );
1252
+ var sqlOf = (path, source) => path.endsWith(".ts") ? addSqlIn(source, ["up", "down"]).map((one) => `${"\n".repeat(Math.max(0, one.line - 1))}${statementsOf(one.sql)}`).join("") : statementsOf(source);
1253
+ var foreignTables = (path, sql, schema) => {
1254
+ const mine = schema.domains.find((one) => path.startsWith(`${one.dir}/`));
1255
+ if (mine === void 0) return [];
1256
+ return schema.domains.filter((one) => one.name !== mine.name).flatMap(
1257
+ (other) => other.tables.flatMap((table2) => {
1258
+ const found = wordFor(table2).exec(sql);
1259
+ return found === null ? [] : [
1260
+ {
1261
+ line: lineOf2(sql, found.index),
1262
+ path,
1263
+ why: `"${mine.name}" names "${table2}", which "${other.name}" declares. A domain's migrations may only touch its own tables \u2014 cross-domain talk goes through events, and a foreign key here is a coupling no import graph shows`
1264
+ }
1265
+ ];
1266
+ })
1267
+ );
1268
+ };
1269
+ var undeclaredSettings = (path, source, schema) => {
1270
+ if (schema.sessionVariables.length === 0) return [];
1271
+ const allowed = new Set(schema.sessionVariables);
1272
+ const found = [];
1273
+ for (const match of source.matchAll(CURRENT_SETTING)) {
1274
+ const name = match[1] ?? "";
1275
+ if (allowed.has(name)) continue;
1276
+ found.push({
1277
+ line: lineOf2(source, match.index),
1278
+ path,
1279
+ why: `current_setting('${name}') names a session variable this repo has not declared. release.schema.sessionVariables holds ${schema.sessionVariables.join(", ")} \u2014 add it there, or take the name as an option so the DDL is not written for one consumer's session`
1280
+ });
1281
+ }
1282
+ return found;
1283
+ };
1284
+ var runSchema = ({ root }) => {
1285
+ const schema = readReleaseConfig(root).schema;
1286
+ if (schema === void 0) {
1287
+ throw new CannotRun(
1288
+ "geonosis.json names no release.schema \u2014 nothing here says which tables belong to which domain, or which session variables the DDL may name (#168)"
1289
+ );
1290
+ }
1291
+ if (schema.domains.length === 0 && schema.sessionVariables.length === 0) {
1292
+ throw new CannotRun(
1293
+ "release.schema is declared and holds neither domains nor sessionVariables \u2014 say which tables belong to which domain and which session variables the DDL may name, or remove the block: a check with nothing to compare against reads exactly like a clean schema"
1294
+ );
1295
+ }
1296
+ const dirs = schema.domains.map((one) => one.dir);
1297
+ const files = [...new Set(dirs.flatMap((dir) => filesUnder(root, dir)))].toSorted();
1298
+ const findings = [];
1299
+ const unreadable = [];
1300
+ for (const path of files) {
1301
+ const source = readFileSync6(resolve6(root, path), "utf8");
1302
+ let sql;
1303
+ try {
1304
+ sql = sqlOf(path, source);
1305
+ } catch (error) {
1306
+ if (!(error instanceof Unreadable)) throw error;
1307
+ unreadable.push({ path, why: `${path} holds ${error.message} \u2014 this reader cannot read it` });
1308
+ continue;
1309
+ }
1310
+ findings.push(...foreignTables(path, sql, schema), ...undeclaredSettings(path, source, schema));
1311
+ }
1312
+ return {
1313
+ dirs,
1314
+ files,
1315
+ findings: findings.toSorted((a, b) => a.path.localeCompare(b.path) || a.line - b.line),
1316
+ ok: findings.length === 0 && unreadable.length === 0,
1317
+ read: files.length - unreadable.length,
1318
+ unreadable
1319
+ };
1320
+ };
1321
+ var formatSchema = (report) => {
1322
+ if (report.files.length === 0) {
1323
+ return `NONE schema: no migration file under ${report.dirs.join(", ")}; nothing was read
1324
+ `;
1325
+ }
1326
+ if (report.ok) {
1327
+ return `OK schema: ${report.read} migration(s) read under ${report.dirs.length} declared domain(s), no wall crossed
1328
+ `;
1329
+ }
1330
+ const said = report.findings.map((one) => ` ${one.path}:${one.line}: ${one.why}
1331
+ `);
1332
+ const cannot = report.unreadable.map((one) => ` ${one.why}
1333
+ `);
1334
+ return `${said.join("")}${cannot.join("")}schema: ${report.read} of ${report.files.length} files read \u2014 ${report.findings.length} finding(s)${report.unreadable.length === 0 ? "" : `, ${report.unreadable.length} unreadable`}
1335
+ `;
1336
+ };
1337
+
1338
+ // src/smoke.ts
1339
+ import { spawnSync as spawnSync3 } from "child_process";
1340
+ import {
1341
+ cpSync,
1342
+ existsSync as existsSync5,
1343
+ mkdirSync,
1344
+ readdirSync as readdirSync3,
1345
+ readFileSync as readFileSync7,
1346
+ rmSync as rmSync2,
1347
+ statSync,
1348
+ writeFileSync as writeFileSync2
1349
+ } from "fs";
1350
+ import { join as join4, relative as relative3, resolve as resolve7 } from "path";
1351
+ var SNAPSHOTS_DIR = ".geonosis/consumer-snapshots";
1352
+ var LOCKFILES = [
1353
+ { file: "bun.lock", manager: "bun" },
1354
+ { file: "bun.lockb", manager: "bun" },
1355
+ { file: "pnpm-lock.yaml", manager: "pnpm" }
1356
+ ];
1357
+ var UNMEASURED = [
1358
+ { file: "package-lock.json", manager: "npm" },
1359
+ { file: "yarn.lock", manager: "yarn" }
1360
+ ];
1361
+ var EXCLUDED_DIRS = [
1362
+ ".cache",
1363
+ ".geonosis",
1364
+ ".git",
1365
+ ".next",
1366
+ ".turbo",
1367
+ ".wrangler",
1368
+ "coverage",
1369
+ "dist",
1370
+ "node_modules",
1371
+ "storybook-static"
1372
+ ];
1373
+ var PHASES = ["typecheck", "lint", "doctor"];
1374
+ var DOCTOR_PACKAGE = "@geonosis/doctor";
1375
+ var DOCTOR_BIN = "geonosis-doctor";
1376
+ var DOCTOR_DOORS = ["@geonosis/cli", "geonosis"];
1377
+ var DOOR_BIN = "geonosis doctor";
1378
+ var isRecord4 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
1379
+ var readManifest = (path) => {
1380
+ try {
1381
+ const parsed = JSON.parse(readFileSync7(path, "utf8"));
1382
+ return isRecord4(parsed) ? parsed : {};
1383
+ } catch (error) {
1384
+ throw new CannotRun(`${path} is not readable JSON: ${error.message}`);
1385
+ }
1386
+ };
1387
+ var versionsIn2 = (manifest) => {
1388
+ const found = {};
1389
+ for (const key of ["dependencies", "devDependencies"]) {
1390
+ const block = manifest[key];
1391
+ if (!isRecord4(block)) continue;
1392
+ for (const [name, range] of Object.entries(block)) {
1393
+ if (typeof range === "string") found[name] = range;
1394
+ }
1395
+ }
1396
+ return Object.fromEntries(Object.entries(found).toSorted(([a], [b]) => a.localeCompare(b)));
1397
+ };
1398
+ var scriptsIn = (manifest) => {
1399
+ const scripts = manifest["scripts"];
1400
+ if (!isRecord4(scripts)) return {};
1401
+ return Object.fromEntries(
1402
+ Object.entries(scripts).filter((one) => typeof one[1] === "string")
1403
+ );
1404
+ };
1405
+ var copyInto = (from, to, excluded) => {
1406
+ let bytes = 0;
1407
+ let files = 0;
1408
+ const walk = (dir, into) => {
1409
+ mkdirSync(into, { recursive: true });
1410
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
1411
+ if (excluded.has(entry.name)) continue;
1412
+ const at = join4(dir, entry.name);
1413
+ if (entry.isDirectory()) {
1414
+ walk(at, join4(into, entry.name));
1415
+ continue;
1416
+ }
1417
+ if (!entry.isFile()) continue;
1418
+ cpSync(at, join4(into, entry.name));
1419
+ bytes += statSync(at).size;
1420
+ files += 1;
1421
+ }
1422
+ };
1423
+ walk(from, to);
1424
+ return { bytes, files };
1425
+ };
1426
+ var manifestsUnder2 = (root) => {
1427
+ const found = [];
1428
+ const walk = (dir) => {
1429
+ for (const entry of readdirSync3(dir, { withFileTypes: true }).toSorted(
1430
+ (a, b) => a.name.localeCompare(b.name)
1431
+ )) {
1432
+ const at = join4(dir, entry.name);
1433
+ if (entry.isDirectory()) {
1434
+ walk(at);
1435
+ continue;
1436
+ }
1437
+ if (entry.name !== "package.json") continue;
1438
+ found.push({ path: relative3(root, at), versions: versionsIn2(readManifest(at)) });
1439
+ }
1440
+ };
1441
+ walk(root);
1442
+ return found.toSorted((a, b) => a.path.localeCompare(b.path));
1443
+ };
1444
+ var managerOf = (from) => {
1445
+ const found = LOCKFILES.find((one) => existsSync5(join4(from, one.file)));
1446
+ if (found !== void 0) return { lockfile: found.file, manager: found.manager };
1447
+ const unmeasured = UNMEASURED.find((one) => existsSync5(join4(from, one.file)));
1448
+ if (unmeasured !== void 0) {
1449
+ throw new CannotRun(
1450
+ `${from} is a ${unmeasured.manager} tree (${unmeasured.file}), and the pack \u2192 rewrite \u2192 install path has never been measured under ${unmeasured.manager} here. It reads ${LOCKFILES.map((one) => one.file).join(", ")}.`
1451
+ );
1452
+ }
1453
+ throw new CannotRun(
1454
+ `nothing in ${from} says which runtime installs it \u2014 looked for ${[...LOCKFILES, ...UNMEASURED].map((one) => one.file).join(", ")}. The smoke installs the way the consumer installs, so the manager is read, never assumed.`
1455
+ );
1456
+ };
1457
+ var commandsFor = (manifest, manifests, named) => {
1458
+ const scripts = scriptsIn(manifest);
1459
+ const installs = (name) => manifests.some((one) => one.versions[name] !== void 0);
1460
+ const doctorCommand = installs(DOCTOR_PACKAGE) ? { exec: DOCTOR_BIN } : DOCTOR_DOORS.some(installs) ? { exec: DOOR_BIN } : {
1461
+ why: `no manifest in this tree installs ${DOCTOR_PACKAGE} or a door that brings it (${DOCTOR_DOORS.join(", ")}), so nothing here can run ${DOCTOR_BIN}`
1462
+ };
1463
+ const phase = (name, fallback) => {
1464
+ const override = named[name];
1465
+ if (override !== void 0) return { exec: override };
1466
+ if (scripts[name] !== void 0) return { run: name };
1467
+ return fallback;
1468
+ };
1469
+ return {
1470
+ doctor: phase("doctor", doctorCommand),
1471
+ lint: phase("lint", { why: 'the tree has no "lint" script and no --lint command was named' }),
1472
+ typecheck: phase("typecheck", {
1473
+ why: 'the tree has no "typecheck" script and no --typecheck command was named'
1474
+ })
1475
+ };
1476
+ };
1477
+ var snapshotDir = (root, name) => join4(root, SNAPSHOTS_DIR, name);
1478
+ var readSnapshot = (root, name) => {
1479
+ const at = join4(snapshotDir(root, name), "snapshot.json");
1480
+ if (!existsSync5(at)) {
1481
+ throw new CannotRun(
1482
+ `there is no snapshot called "${name}" here \u2014 ${at} does not exist. Record one with: geonosis-release smoke snapshot ${name} --from <their tree>`
1483
+ );
1484
+ }
1485
+ return JSON.parse(readFileSync7(at, "utf8"));
1486
+ };
1487
+ var wouldBeCommitted = (root, path) => {
1488
+ const inside = spawnSync3("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root });
1489
+ if (inside.error !== void 0 || inside.status !== 0) return false;
1490
+ return spawnSync3("git", ["check-ignore", "-q", path], { cwd: root }).status !== 0;
1491
+ };
1492
+ var NAME_SHAPE = /^[a-z0-9][\w.-]*$/i;
1493
+ var runSnapshot = (input) => {
1494
+ if (!NAME_SHAPE.test(input.name)) {
1495
+ throw new CannotRun(
1496
+ `"${input.name}" is not a snapshot name \u2014 a name is letters, digits, dots, dashes and underscores, so that ${SNAPSHOTS_DIR}/<name> is the only place the bytes can land`
1497
+ );
1498
+ }
1499
+ const from = resolve7(input.from);
1500
+ if (!existsSync5(join4(from, "package.json"))) {
1501
+ throw new CannotRun(`${from} has no package.json \u2014 that is not a tree a consumer installs`);
1502
+ }
1503
+ const at = snapshotDir(input.root, input.name);
1504
+ if (wouldBeCommitted(input.root, join4(SNAPSHOTS_DIR, input.name))) {
1505
+ throw new CannotRun(
1506
+ `git here does not ignore ${SNAPSHOTS_DIR}/ \u2014 a consumer's tree is private (D-048) and a copy that reaches a commit cannot be taken back. Add ".geonosis/" to .gitignore and run this again.`
1507
+ );
1508
+ }
1509
+ if (existsSync5(at) && !input.replace) {
1510
+ throw new CannotRun(
1511
+ `${at} is already a snapshot, and a baseline recorded against other bytes is worse than none \u2014 pass --replace to overwrite it`
1512
+ );
1513
+ }
1514
+ const { lockfile, manager } = managerOf(from);
1515
+ const excluded = [.../* @__PURE__ */ new Set([...EXCLUDED_DIRS, ...input.exclude ?? []])].toSorted();
1516
+ rmSync2(at, { force: true, recursive: true });
1517
+ const tree = join4(at, "tree");
1518
+ const { bytes, files } = copyInto(from, tree, new Set(excluded));
1519
+ const manifests = manifestsUnder2(tree);
1520
+ const record = {
1521
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1522
+ bytes,
1523
+ commands: commandsFor(readManifest(join4(tree, "package.json")), manifests, input.named),
1524
+ excluded,
1525
+ files,
1526
+ from,
1527
+ lockfile,
1528
+ manager,
1529
+ manifests,
1530
+ name: input.name
1531
+ };
1532
+ writeFileSync2(join4(at, "snapshot.json"), `${JSON.stringify(record, void 0, 2)}
1533
+ `);
1534
+ return record;
1535
+ };
1536
+ var describeCommand = (command) => {
1537
+ if ("run" in command) return `run ${command.run}`;
1538
+ if ("exec" in command) return command.exec;
1539
+ return `nothing \u2014 ${command.why}`;
1540
+ };
1541
+ var formatSnapshot = (record) => [
1542
+ `OK snapshot ${record.name}: ${record.files} files, ${record.bytes} bytes, ${record.manifests.length} manifests
1543
+ `,
1544
+ ` from ${record.from} \u2014 ${record.manager} (${record.lockfile})
1545
+ `,
1546
+ ...PHASES.map((phase) => ` ${phase}: ${describeCommand(record.commands[phase])}
1547
+ `)
1548
+ ].join("");
1549
+
1550
+ // src/envelope.ts
1551
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
1552
+ import { dirname as dirname2, join as join5 } from "path";
1553
+ import { fileURLToPath as fileURLToPath3 } from "url";
1554
+ var ENVELOPES_DIR = ".geonosis/envelopes";
1555
+ var envelopePath = (root, tool) => join5(root, ENVELOPES_DIR, `${tool}.json`);
1556
+ var UnbalancedEnvelope = class extends Error {
1557
+ constructor(message) {
1558
+ super(message);
1559
+ this.name = "UnbalancedEnvelope";
1560
+ }
1561
+ };
1562
+ var isCount = (value2) => Number.isSafeInteger(value2) && value2 >= 0;
1563
+ 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}`;
1564
+ var writeEnvelope = ({
1565
+ envelope,
1566
+ next,
1567
+ root
1568
+ }) => {
1569
+ if (envelope.tool.trim() === "") {
1570
+ throw new UnbalancedEnvelope(
1571
+ `an envelope with no tool name cannot be filed or reported against. Next: ${next}`
1572
+ );
1573
+ }
1574
+ if (envelope.version.trim() === "") {
1575
+ throw new UnbalancedEnvelope(
1576
+ `${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}`
1577
+ );
1578
+ }
1579
+ if (!isCount(envelope.considered) || !isCount(envelope.read)) {
1580
+ throw new UnbalancedEnvelope(
1581
+ `${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}`
1582
+ );
1583
+ }
1584
+ if (envelope.considered !== envelope.read + envelope.refused.length + envelope.excused.length) {
1585
+ throw new UnbalancedEnvelope(unbalancedMessage(envelope, next));
1586
+ }
1587
+ const at = envelopePath(root, envelope.tool);
1588
+ mkdirSync2(dirname2(at), { recursive: true });
1589
+ writeFileSync3(at, `${JSON.stringify(envelope, void 0, 2)}
1590
+ `);
1591
+ return at;
1592
+ };
1593
+ var UNKNOWN = "unknown";
1594
+ var versionIn = (dir) => {
1595
+ try {
1596
+ const manifest = JSON.parse(readFileSync8(join5(dir, "package.json"), "utf8"));
1597
+ return typeof manifest.version === "string" ? manifest.version : void 0;
1598
+ } catch {
1599
+ return void 0;
1600
+ }
1601
+ };
1602
+ var versionOf = (moduleUrl) => {
1603
+ let dir = dirname2(fileURLToPath3(moduleUrl));
1604
+ for (; ; ) {
1605
+ const found = versionIn(dir);
1606
+ if (found !== void 0) return found;
1607
+ const up = dirname2(dir);
1608
+ if (up === dir) return UNKNOWN;
1609
+ dir = up;
1610
+ }
1611
+ };
1612
+ var MIGRATIONS_TOOL = "release-migrations";
1613
+ 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";
1614
+ var migrationsEnvelope = (report, durationMs) => ({
1615
+ considered: report.files.length,
1616
+ durationMs,
1617
+ excused: report.files.filter((one) => one.state === "excused").map((one) => ({
1618
+ path: one.path,
1619
+ reason: "a contract-migration marker this gate could believe"
1620
+ })),
1621
+ findings: [...report.refusals, ...report.markers, ...report.squawk],
1622
+ read: report.files.filter((one) => one.state !== "excused" && one.state !== "unreadable").length,
1623
+ refused: report.unreadable.map((one) => ({ path: one.path, reason: one.why })),
1624
+ tool: MIGRATIONS_TOOL,
1625
+ version: versionOf(import.meta.url)
1626
+ });
1627
+ var SCHEMA_TOOL = "release-schema";
1628
+ var SCHEMA_NEXT = "geonosis-release schema --json and compare `files` against `unreadable` \u2014 the denominator is every migration under every declared domain directory, and a file in neither list is one nothing read (#168)";
1629
+ var schemaEnvelope = (report, durationMs) => ({
1630
+ considered: report.files.length,
1631
+ durationMs,
1632
+ excused: [],
1633
+ findings: report.findings,
1634
+ read: report.read,
1635
+ refused: report.unreadable.map((one) => ({ path: one.path, reason: one.why })),
1636
+ tool: SCHEMA_TOOL,
1637
+ version: versionOf(import.meta.url)
1638
+ });
1639
+ var PUBLISHED_TOOL = "release-published";
1640
+ var PUBLISHED_NEXT = "geonosis-release published --group --json and compare `considered` against `lines` + `excused` \u2014 every manifest in the tree leaves by exactly one door, and a group announced off a sample is the failure this counts against (#147)";
1641
+ var publishedEnvelope = (report, durationMs) => ({
1642
+ considered: report.considered,
1643
+ durationMs,
1644
+ excused: report.excused,
1645
+ findings: report.lines.filter((one) => one.verdict !== "MATCH"),
1646
+ read: report.lines.filter((one) => one.verdict !== "UNREACHABLE").length,
1647
+ refused: report.lines.filter((one) => one.verdict === "UNREACHABLE").map((one) => ({ path: one.at, reason: one.why })),
1648
+ tool: PUBLISHED_TOOL,
1649
+ version: versionOf(import.meta.url)
1650
+ });
1651
+
1652
+ // src/smoke-run.ts
1653
+ import { spawnSync as spawnSync4 } from "child_process";
1654
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync9, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
1655
+ import { join as join6, relative as relative4, resolve as resolve8 } from "path";
1656
+ var BASELINE_FILE = "baseline.json";
1657
+ var INSTALL = {
1658
+ bun: ["install"],
1659
+ pnpm: ["install", "--no-frozen-lockfile"]
1660
+ };
1661
+ var isRecord5 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
1662
+ var readJson = (path) => {
1663
+ try {
1664
+ return JSON.parse(readFileSync9(path, "utf8"));
1665
+ } catch (error) {
1666
+ throw new CannotRun(`${path} is not readable JSON: ${error.message}`);
1667
+ }
1668
+ };
1669
+ var packRc = (rc, into) => {
1670
+ if (!existsSync6(join6(rc, "package.json"))) {
1671
+ throw new CannotRun(`${rc} has no package.json \u2014 that is not a workspace to pack`);
1672
+ }
1673
+ mkdirSync3(into, { recursive: true });
1674
+ const root = readJson(join6(rc, "package.json"));
1675
+ const isPrivateRoot = isRecord5(root) && root["private"] === true;
1676
+ const done = spawnSync4(
1677
+ "pnpm",
1678
+ [
1679
+ "pack",
1680
+ "--recursive",
1681
+ ...isPrivateRoot ? ["--filter", "!."] : [],
1682
+ "--pack-destination",
1683
+ into,
1684
+ "--json"
1685
+ ],
1686
+ { cwd: rc, encoding: "utf8" }
1687
+ );
1688
+ if (done.error !== void 0) {
1689
+ throw new CannotRun(`pnpm pack could not be run in ${rc}: ${done.error.message}`);
1690
+ }
1691
+ if (done.status !== 0) {
1692
+ throw new CannotRun(
1693
+ `pnpm pack exited ${done.status ?? -1} in ${rc} \u2014 nothing was installed anywhere:
1694
+ ${(done.stdout + done.stderr).trim()}`
1695
+ );
1696
+ }
1697
+ const parsed = JSON.parse(done.stdout.slice(done.stdout.search(/[[{]/)));
1698
+ const entries = Array.isArray(parsed) ? parsed : [parsed];
1699
+ return entries.map((one) => ({
1700
+ files: (one.files ?? []).map((file) => file.path),
1701
+ name: one.name,
1702
+ tarball: one.filename,
1703
+ version: one.version
1704
+ }));
1705
+ };
1706
+ var declaredTypes = (manifest) => {
1707
+ const found = [];
1708
+ const walk = (value2) => {
1709
+ if (!isRecord5(value2)) return;
1710
+ const types = value2["types"];
1711
+ if (typeof types === "string") found.push(types);
1712
+ for (const one of Object.values(value2)) walk(one);
1713
+ };
1714
+ walk({ exports: manifest["exports"], types: manifest["types"] });
1715
+ return [...new Set(found.map((one) => one.replace(/^\.\//, "")))];
1716
+ };
1717
+ var sourceManifests = (rc) => {
1718
+ const excluded = new Set(EXCLUDED_DIRS);
1719
+ const found = [];
1720
+ const walk = (dir) => {
1721
+ for (const entry of readdirSync4(dir, { withFileTypes: true })) {
1722
+ if (excluded.has(entry.name)) continue;
1723
+ if (entry.isDirectory()) {
1724
+ walk(join6(dir, entry.name));
1725
+ continue;
1726
+ }
1727
+ if (entry.name !== "package.json") continue;
1728
+ const manifest = readJson(join6(dir, entry.name));
1729
+ if (!isRecord5(manifest) || typeof manifest["name"] !== "string") continue;
1730
+ found.push({ manifest, name: manifest["name"] });
1731
+ }
1732
+ };
1733
+ walk(rc);
1734
+ return found;
1735
+ };
1736
+ var promisedButNotPacked = (rc, packed) => {
1737
+ const manifests = new Map(sourceManifests(rc).map((one) => [one.name, one.manifest]));
1738
+ return packed.flatMap((one) => {
1739
+ const manifest = manifests.get(one.name);
1740
+ if (manifest === void 0) return [];
1741
+ return declaredTypes(manifest).filter((path) => !one.files.includes(path)).map(
1742
+ (path) => `${one.name}@${one.version}: the manifest promises ${path} and the tarball does not carry it`
1743
+ );
1744
+ });
1745
+ };
1746
+ var OVERRIDES = "overrides:";
1747
+ var TOP_LEVEL = /^\S/;
1748
+ var KEYED = /^\s+["']?(.+?)["']?\s*:/;
1749
+ var pinsInWorkspaceYaml = (work, pins) => {
1750
+ const at = join6(work, "pnpm-workspace.yaml");
1751
+ const written = Object.entries(pins).map(([name, spec]) => ` '${name}': '${spec}'`);
1752
+ const lines2 = existsSync6(at) ? readFileSync9(at, "utf8").split("\n") : [];
1753
+ const kept = [];
1754
+ let inside = false;
1755
+ let found = false;
1756
+ for (const line of lines2) {
1757
+ if (TOP_LEVEL.test(line)) inside = line.startsWith(OVERRIDES);
1758
+ if (inside && line.startsWith(OVERRIDES)) {
1759
+ kept.push(line, ...written);
1760
+ found = true;
1761
+ continue;
1762
+ }
1763
+ const key = inside ? KEYED.exec(line)?.[1] : void 0;
1764
+ if (key !== void 0 && pins[key] !== void 0) continue;
1765
+ kept.push(line);
1766
+ }
1767
+ const body = found ? kept : [...kept, OVERRIDES, ...written, ""];
1768
+ writeFileSync4(at, body.join("\n"));
1769
+ };
1770
+ var pinsInManifest = (work, pins) => {
1771
+ const at = join6(work, "package.json");
1772
+ if (!existsSync6(at)) return;
1773
+ const manifest = readJson(at);
1774
+ if (!isRecord5(manifest)) return;
1775
+ manifest["overrides"] = {
1776
+ ...isRecord5(manifest["overrides"]) ? manifest["overrides"] : {},
1777
+ ...pins
1778
+ };
1779
+ writeFileSync4(at, `${JSON.stringify(manifest, void 0, 2)}
1780
+ `);
1781
+ };
1782
+ var overrideEveryCopy = (work, manager, packed) => {
1783
+ if (packed.length === 0) return;
1784
+ const pins = Object.fromEntries(packed.map((one) => [one.name, `file:${one.tarball}`]));
1785
+ if (manager === "pnpm") pinsInWorkspaceYaml(work, pins);
1786
+ else pinsInManifest(work, pins);
1787
+ };
1788
+ var rewriteManifests = (tree, manifests, packed) => {
1789
+ const byName = new Map(packed.map((one) => [one.name, one.tarball]));
1790
+ const swapped = [];
1791
+ for (const path of manifests) {
1792
+ const at = join6(tree, path);
1793
+ if (!existsSync6(at)) continue;
1794
+ const manifest = readJson(at);
1795
+ if (!isRecord5(manifest)) continue;
1796
+ let touched = false;
1797
+ for (const key of ["dependencies", "devDependencies"]) {
1798
+ const block = manifest[key];
1799
+ if (!isRecord5(block)) continue;
1800
+ for (const name of Object.keys(block)) {
1801
+ const tarball = byName.get(name);
1802
+ if (tarball === void 0) continue;
1803
+ block[name] = `file:${tarball}`;
1804
+ swapped.push({ name, path });
1805
+ touched = true;
1806
+ }
1807
+ }
1808
+ if (touched) writeFileSync4(at, `${JSON.stringify(manifest, void 0, 2)}
1809
+ `);
1810
+ }
1811
+ return swapped;
1812
+ };
1813
+ var shellRun = (command, cwd, env) => {
1814
+ const done = spawnSync4(command, { cwd, encoding: "utf8", env, shell: true });
1815
+ return { code: done.status ?? -1, output: `${done.stdout ?? ""}${done.stderr ?? ""}` };
1816
+ };
1817
+ var INTERESTING = /\b(error|Error|ERROR|FAIL|failed|refused|✗|✘)\b/;
1818
+ var quotable = (output) => {
1819
+ const lines2 = output.split("\n").map((one) => one.trimEnd()).filter((one) => one.trim() !== "");
1820
+ const named = lines2.filter((one) => INTERESTING.test(one));
1821
+ return (named.length > 0 ? named : lines2.slice(-10)).slice(0, 20).map((one) => one.slice(0, 300));
1822
+ };
1823
+ var commandLine = (command, manager) => "run" in command ? `${manager} run ${command.run}` : "exec" in command ? command.exec : "";
1824
+ var runPhase = (phase, command, cwd, manager) => {
1825
+ if ("why" in command) return { phase, why: command.why };
1826
+ const line = commandLine(command, manager);
1827
+ const env = {
1828
+ ...process.env,
1829
+ PATH: `${join6(cwd, "node_modules/.bin")}:${process.env["PATH"] ?? ""}`
1830
+ };
1831
+ const { code, output } = shellRun(line, cwd, env);
1832
+ return { code, command: line, lines: quotable(output), ok: code === 0, phase };
1833
+ };
1834
+ var smokeOver = (input) => {
1835
+ const record = readSnapshot(input.root, input.name);
1836
+ const at = snapshotDir(input.root, input.name);
1837
+ const work = join6(at, "run");
1838
+ rmSync3(work, { force: true, recursive: true });
1839
+ copyInto(join6(at, "tree"), work, new Set(EXCLUDED_DIRS));
1840
+ ownRepository(work);
1841
+ const packed = input.rc === void 0 ? [] : packRc(resolve8(input.rc), join6(at, "rc"));
1842
+ const findings = input.rc === void 0 ? [] : promisedButNotPacked(resolve8(input.rc), packed);
1843
+ const swapped = rewriteManifests(
1844
+ work,
1845
+ record.manifests.map((one) => one.path),
1846
+ packed
1847
+ );
1848
+ if (input.rc !== void 0 && swapped.length === 0) {
1849
+ throw new CannotRun(
1850
+ `no manifest in "${input.name}" depends on anything ${input.rc} packs \u2014 this run would have installed the published versions and called them the release candidate`
1851
+ );
1852
+ }
1853
+ overrideEveryCopy(work, record.manager, packed);
1854
+ const install = input.install ?? `${record.manager} ${INSTALL[record.manager].join(" ")}`;
1855
+ const installed = shellRun(install, work, process.env);
1856
+ if (installed.code !== 0) {
1857
+ throw new CannotRun(
1858
+ `\`${install}\` exited ${installed.code} in the snapshot \u2014 the release was never installed, so nothing below it was measured:
1859
+ ${quotable(installed.output).join("\n")}`
1860
+ );
1861
+ }
1862
+ return {
1863
+ findings,
1864
+ outcomes: PHASES.map((phase) => runPhase(phase, record.commands[phase], work, record.manager)),
1865
+ packed
1866
+ };
1867
+ };
1868
+ var ownRepository = (work) => {
1869
+ const done = spawnSync4("git", ["init", "-q"], { cwd: work });
1870
+ if (done.error !== void 0 || done.status !== 0) {
1871
+ throw new CannotRun(
1872
+ `git init failed in ${work} \u2014 without a repository of its own, this copy's install would find the repository this tool is running inside and write into it: ${done.error?.message ?? done.stderr}`
1873
+ );
1874
+ }
1875
+ };
1876
+ var baselinePath = (root, name) => join6(snapshotDir(root, name), BASELINE_FILE);
1877
+ var recordBaseline = (input) => {
1878
+ const { outcomes, packed } = smokeOver(input);
1879
+ const baseline = {
1880
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1881
+ packed: packed.map((one) => ({ name: one.name, version: one.version })),
1882
+ phases: outcomes,
1883
+ rc: input.rc === void 0 ? "the versions the tree names" : resolve8(input.rc)
1884
+ };
1885
+ writeFileSync4(baselinePath(input.root, input.name), `${JSON.stringify(baseline, void 0, 2)}
1886
+ `);
1887
+ return baseline;
1888
+ };
1889
+ var readBaseline = (root, name) => {
1890
+ const at = baselinePath(root, name);
1891
+ if (!existsSync6(at)) {
1892
+ throw new CannotRun(
1893
+ `"${name}" has no baseline \u2014 a run that took its own answer as the standard would certify anything. Record one against the versions this consumer is on: geonosis-release smoke baseline --snapshot ${name}`
1894
+ );
1895
+ }
1896
+ return readJson(at);
1897
+ };
1898
+ var compareToBaseline = (name, baseline, outcomes, findings) => {
1899
+ const before = new Map(baseline.phases.map((one) => [one.phase, one]));
1900
+ const broken = [];
1901
+ const known = [];
1902
+ const mended = [];
1903
+ const refused = [];
1904
+ for (const now of outcomes) {
1905
+ const then = before.get(now.phase);
1906
+ if (then === void 0) {
1907
+ refused.push({
1908
+ phase: now.phase,
1909
+ why: "the baseline never considered this phase, so there is nothing to compare it against"
1910
+ });
1911
+ continue;
1912
+ }
1913
+ if ("why" in now) continue;
1914
+ if ("why" in then) {
1915
+ refused.push({
1916
+ phase: now.phase,
1917
+ why: `the baseline excused this phase (${then.why}) and this run ran it \u2014 the two are not comparable`
1918
+ });
1919
+ continue;
1920
+ }
1921
+ if (now.code < 0) {
1922
+ refused.push({ phase: now.phase, why: `\`${now.command}\` could not be run at all` });
1923
+ continue;
1924
+ }
1925
+ if (!now.ok && then.ok) broken.push({ lines: now.lines, phase: now.phase });
1926
+ if (!now.ok && !then.ok) known.push(now.phase);
1927
+ if (now.ok && !then.ok) mended.push(now.phase);
1928
+ }
1929
+ return {
1930
+ broken,
1931
+ findings,
1932
+ known,
1933
+ mended,
1934
+ name,
1935
+ ok: broken.length === 0 && findings.length === 0,
1936
+ outcomes,
1937
+ refused
1938
+ };
1939
+ };
1940
+ var runSmoke = (input) => {
1941
+ const baseline = readBaseline(input.root, input.name);
1942
+ const { findings, outcomes } = smokeOver(input);
1943
+ return compareToBaseline(input.name, baseline, outcomes, findings);
1944
+ };
1945
+ var sweepSmoke = (input) => {
1946
+ const comparisons = [];
1947
+ const skipped = [];
1948
+ for (const one of input.wanted) {
1949
+ if (input.skipMissing && !existsSync6(join6(snapshotDir(input.root, one.name), "snapshot.json"))) {
1950
+ skipped.push({
1951
+ name: one.name,
1952
+ why: `no snapshot of it here \u2014 record one with: geonosis-release smoke snapshot ${one.name} --from <their tree>`
1953
+ });
1954
+ continue;
1955
+ }
1956
+ comparisons.push(
1957
+ runSmoke({
1958
+ ...one.install === void 0 ? {} : { install: one.install },
1959
+ name: one.name,
1960
+ ...input.rc === void 0 ? {} : { rc: input.rc },
1961
+ root: input.root
1962
+ })
1963
+ );
1964
+ }
1965
+ return { comparisons, skipped };
1966
+ };
1967
+ var formatSweep = (sweep) => [
1968
+ ...sweep.comparisons.map(formatSmoke),
1969
+ ...sweep.skipped.map((one) => `SKIP smoke ${one.name}: ${one.why}
1970
+ `)
1971
+ ].join("");
1972
+ var sweepEnvelope = (sweep, durationMs) => {
1973
+ const parts = sweep.comparisons.map((one) => smokeEnvelope(one, 0));
1974
+ return {
1975
+ considered: parts.reduce((sum, one) => sum + one.considered, 0) + sweep.skipped.length * 3,
1976
+ durationMs,
1977
+ excused: [
1978
+ ...parts.flatMap(
1979
+ (one, index) => one.excused.map((entry) => ({
1980
+ path: `${sweep.comparisons[index]?.name ?? ""}/${entry.path}`,
1981
+ reason: entry.reason
1982
+ }))
1983
+ ),
1984
+ ...sweep.skipped.flatMap(
1985
+ (one) => PHASES.map((phase) => ({ path: `${one.name}/${phase}`, reason: one.why }))
1986
+ )
1987
+ ],
1988
+ findings: parts.flatMap((one) => one.findings),
1989
+ read: parts.reduce((sum, one) => sum + one.read, 0),
1990
+ refused: parts.flatMap(
1991
+ (one, index) => one.refused.map((entry) => ({
1992
+ path: `${sweep.comparisons[index]?.name ?? ""}/${entry.path}`,
1993
+ reason: entry.reason
1994
+ }))
1995
+ ),
1996
+ tool: SMOKE_TOOL,
1997
+ version: versionOf(import.meta.url)
1998
+ };
1999
+ };
2000
+ var outcomeLine = (outcome) => "why" in outcome ? ` ${outcome.phase}: nothing to run \u2014 ${outcome.why}
2001
+ ` : ` ${outcome.phase}: \`${outcome.command}\` exited ${outcome.code}
2002
+ `;
2003
+ var headline = (comparison) => {
2004
+ if (comparison.ok) {
2005
+ return `OK smoke ${comparison.name}: nothing this release changed reddened their tree
2006
+ `;
2007
+ }
2008
+ if (comparison.broken.length === 0) {
2009
+ return `FAIL smoke ${comparison.name}: their gates are unmoved, and this release ships ${comparison.findings.length} promise(s) its tarballs do not carry
2010
+ `;
2011
+ }
2012
+ return `FAIL smoke ${comparison.name}: the release broke ${comparison.broken.length} of their gates
2013
+ `;
2014
+ };
2015
+ var formatSmoke = (comparison) => [
2016
+ headline(comparison),
2017
+ ...comparison.outcomes.map(outcomeLine),
2018
+ ...comparison.broken.flatMap((one) => [
2019
+ ` ${one.phase} passed at the versions they are on and fails under this release:
2020
+ `,
2021
+ ...one.lines.map((line) => ` ${line}
2022
+ `)
2023
+ ]),
2024
+ ...comparison.findings.map((one) => ` packed: ${one}
2025
+ `),
2026
+ ...comparison.known.map(
2027
+ (one) => ` ${one} was already failing at their versions \u2014 not this release's doing
2028
+ `
2029
+ ),
2030
+ ...comparison.mended.map(
2031
+ (one) => ` ${one} was failing at their versions and passes under this release
2032
+ `
2033
+ ),
2034
+ ...comparison.refused.map((one) => ` ${one.phase} could not be compared: ${one.why}
2035
+ `)
2036
+ ].join("");
2037
+ var formatBaseline = (name, baseline) => [
2038
+ `OK baseline ${name}: recorded against ${baseline.rc}
2039
+ `,
2040
+ ...baseline.phases.map(outcomeLine)
2041
+ ].join("");
2042
+ var SMOKE_TOOL = "release-smoke";
2043
+ var SMOKE_NEXT = "geonosis-release smoke run --snapshot <name> --json and read `outcomes` \u2014 every phase the snapshot named leaves by exactly one door: read, excused with the sentence saying there was nothing of theirs to run, or refused because this run and the baseline are not comparable";
2044
+ var smokeEnvelope = (comparison, durationMs) => {
2045
+ const refused = new Set(comparison.refused.map((one) => one.phase));
2046
+ const excused = comparison.outcomes.filter(
2047
+ (one) => "why" in one && !refused.has(one.phase)
2048
+ );
2049
+ return {
2050
+ considered: comparison.outcomes.length,
2051
+ durationMs,
2052
+ excused: excused.map((one) => ({ path: one.phase, reason: one.why })),
2053
+ findings: [
2054
+ ...comparison.broken.map((one) => ({ lines: one.lines, phase: one.phase })),
2055
+ ...comparison.findings.map((one) => ({ packed: one }))
2056
+ ],
2057
+ read: comparison.outcomes.length - excused.length - comparison.refused.length,
2058
+ refused: comparison.refused.map((one) => ({ path: one.phase, reason: one.why })),
2059
+ tool: SMOKE_TOOL,
2060
+ version: versionOf(import.meta.url)
2061
+ };
2062
+ };
2063
+
2064
+ export {
2065
+ CannotRun,
2066
+ parseReleaseConfig,
2067
+ readReleaseConfig,
2068
+ parseJsonc,
2069
+ parseToml,
2070
+ declaredIn,
2071
+ readWrangler,
2072
+ DEPLOYED_FILE,
2073
+ NOTHING_SAYS,
2074
+ readDeployed,
2075
+ driftBetween,
2076
+ runDeployed,
2077
+ formatDeployed,
2078
+ markersIn,
2079
+ addSqlIn,
2080
+ NARROWING,
2081
+ statementsOf,
2082
+ refusalsIn,
2083
+ runMigrations,
2084
+ formatMigrations,
2085
+ STEPS,
2086
+ WHY_VERSION_IDENTITY,
2087
+ runPlan,
2088
+ formatPlan,
2089
+ Runner,
2090
+ proveOver,
2091
+ PLANTS,
2092
+ prove,
2093
+ formatProve,
2094
+ formatVerdict,
2095
+ DEFAULT_REGISTRY,
2096
+ registryPathOf,
2097
+ askRegistry,
2098
+ censusOf,
2099
+ EXISTS_BUT,
2100
+ runPublished,
2101
+ formatPublished,
2102
+ runSchema,
2103
+ formatSchema,
2104
+ SNAPSHOTS_DIR,
2105
+ EXCLUDED_DIRS,
2106
+ PHASES,
2107
+ snapshotDir,
2108
+ readSnapshot,
2109
+ runSnapshot,
2110
+ formatSnapshot,
2111
+ writeEnvelope,
2112
+ MIGRATIONS_NEXT,
2113
+ migrationsEnvelope,
2114
+ SCHEMA_NEXT,
2115
+ schemaEnvelope,
2116
+ PUBLISHED_NEXT,
2117
+ publishedEnvelope,
2118
+ BASELINE_FILE,
2119
+ packRc,
2120
+ promisedButNotPacked,
2121
+ rewriteManifests,
2122
+ recordBaseline,
2123
+ readBaseline,
2124
+ compareToBaseline,
2125
+ runSmoke,
2126
+ sweepSmoke,
2127
+ formatSweep,
2128
+ sweepEnvelope,
2129
+ formatSmoke,
2130
+ formatBaseline,
2131
+ SMOKE_TOOL,
2132
+ SMOKE_NEXT,
2133
+ smokeEnvelope
2134
+ };