@geonosis/release 2.0.0 → 2.2.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/dist/{chunk-KQIWTXQP.js → chunk-IPCIF5DO.js} +1275 -926
- package/dist/index.d.ts +169 -64
- package/dist/index.js +17 -1
- package/dist/release-cli.js +73 -12
- package/package.json +2 -2
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/phases.ts
|
|
2
|
+
var PHASES = ["typecheck", "lint", "test", "doctor"];
|
|
3
|
+
|
|
1
4
|
// src/config.ts
|
|
2
5
|
import { existsSync, readFileSync } from "fs";
|
|
3
6
|
import { resolve } from "path";
|
|
@@ -95,12 +98,39 @@ var parseSchema = (value2) => {
|
|
|
95
98
|
};
|
|
96
99
|
};
|
|
97
100
|
var SMOKE_KEYS = "expected { exclude?, snapshots? }";
|
|
101
|
+
var SNAPSHOT_KEYS = "expected { commands?, install?, name }";
|
|
102
|
+
var SMOKE_PHASES = PHASES;
|
|
103
|
+
var parseCommands = (value2, at) => {
|
|
104
|
+
if (value2 === void 0) return {};
|
|
105
|
+
if (!isRecord(value2)) {
|
|
106
|
+
throw new CannotRun(
|
|
107
|
+
`${at}.commands must be an object \u2014 expected one command per phase: ${PHASES.map((one) => `${one}?`).join(", ")}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const unknown = Object.keys(value2).find((key) => !SMOKE_PHASES.includes(key));
|
|
111
|
+
if (unknown !== void 0) {
|
|
112
|
+
throw new CannotRun(
|
|
113
|
+
`${at}.commands.${unknown} is not a phase the smoke runs \u2014 it runs ${SMOKE_PHASES.join(", ")}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const commands = {};
|
|
117
|
+
for (const phase of SMOKE_PHASES) {
|
|
118
|
+
const command = value2[phase];
|
|
119
|
+
if (command === void 0) continue;
|
|
120
|
+
if (typeof command !== "string" || command === "") {
|
|
121
|
+
throw new CannotRun(`${at}.commands.${phase} must be the command that runs their ${phase}`);
|
|
122
|
+
}
|
|
123
|
+
commands[phase] = command;
|
|
124
|
+
}
|
|
125
|
+
return commands;
|
|
126
|
+
};
|
|
98
127
|
var oneSnapshot = (value2, index) => {
|
|
99
128
|
const at = `release.smoke.snapshots[${index}]`;
|
|
100
|
-
if (!isRecord(value2)) throw new CannotRun(`${at} must be an object \u2014
|
|
101
|
-
const
|
|
129
|
+
if (!isRecord(value2)) throw new CannotRun(`${at} must be an object \u2014 ${SNAPSHOT_KEYS}`);
|
|
130
|
+
const known = /* @__PURE__ */ new Set(["commands", "install", "name"]);
|
|
131
|
+
const unknown = Object.keys(value2).find((key) => !known.has(key));
|
|
102
132
|
if (unknown !== void 0) {
|
|
103
|
-
throw new CannotRun(`${at}.${unknown} is not a key it takes \u2014
|
|
133
|
+
throw new CannotRun(`${at}.${unknown} is not a key it takes \u2014 ${SNAPSHOT_KEYS}`);
|
|
104
134
|
}
|
|
105
135
|
const name = value2["name"];
|
|
106
136
|
if (typeof name !== "string" || name === "") {
|
|
@@ -110,7 +140,11 @@ var oneSnapshot = (value2, index) => {
|
|
|
110
140
|
if (install !== void 0 && typeof install !== "string") {
|
|
111
141
|
throw new CannotRun(`${at}.install must be the command that installs that tree`);
|
|
112
142
|
}
|
|
113
|
-
return {
|
|
143
|
+
return {
|
|
144
|
+
commands: parseCommands(value2["commands"], at),
|
|
145
|
+
...install === void 0 ? {} : { install },
|
|
146
|
+
name
|
|
147
|
+
};
|
|
114
148
|
};
|
|
115
149
|
var parseSmoke = (value2) => {
|
|
116
150
|
if (value2 === void 0) return { exclude: [], snapshots: [] };
|
|
@@ -121,7 +155,7 @@ var parseSmoke = (value2) => {
|
|
|
121
155
|
}
|
|
122
156
|
const snapshots = value2["snapshots"];
|
|
123
157
|
if (snapshots !== void 0 && !Array.isArray(snapshots)) {
|
|
124
|
-
throw new CannotRun(
|
|
158
|
+
throw new CannotRun(`release.smoke.snapshots must be a list \u2014 ${SNAPSHOT_KEYS}, per entry`);
|
|
125
159
|
}
|
|
126
160
|
return {
|
|
127
161
|
exclude: strings(value2["exclude"], "release.smoke.exclude"),
|
|
@@ -175,239 +209,952 @@ var readReleaseConfig = (root) => {
|
|
|
175
209
|
}
|
|
176
210
|
};
|
|
177
211
|
|
|
178
|
-
// src/
|
|
179
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
180
|
-
import { resolve as resolve2 } from "path";
|
|
181
|
-
|
|
182
|
-
var
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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;
|
|
212
|
+
// src/envelope.ts
|
|
213
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
214
|
+
import { dirname, join, resolve as resolve2 } from "path";
|
|
215
|
+
import { fileURLToPath } from "url";
|
|
216
|
+
var ENVELOPES_DIR = ".geonosis/envelopes";
|
|
217
|
+
var envelopePath = (root, tool) => join(root, ENVELOPES_DIR, `${tool}.json`);
|
|
218
|
+
var UnbalancedEnvelope = class extends Error {
|
|
219
|
+
constructor(message) {
|
|
220
|
+
super(message);
|
|
221
|
+
this.name = "UnbalancedEnvelope";
|
|
210
222
|
}
|
|
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
223
|
};
|
|
221
|
-
var
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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() !== "");
|
|
224
|
+
var isCount = (value2) => Number.isSafeInteger(value2) && value2 >= 0;
|
|
225
|
+
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}`;
|
|
226
|
+
var FORBIDDEN_ROOT = "GEONOSIS_ENVELOPES_FORBIDDEN_ROOT";
|
|
227
|
+
var refuseForbiddenRoot = (root) => {
|
|
228
|
+
const forbidden = process.env[FORBIDDEN_ROOT];
|
|
229
|
+
if (forbidden === void 0 || resolve2(forbidden) !== resolve2(root)) return;
|
|
230
|
+
throw new UnbalancedEnvelope(
|
|
231
|
+
`${root} is off limits to envelope writers in this process (${FORBIDDEN_ROOT}) \u2014 a run that writes one into a shared root races every other run reading it, and leaves a file the next one takes for real. Point this at a scratch root of its own: tooling/scratch-dir.ts.`
|
|
232
|
+
);
|
|
244
233
|
};
|
|
245
|
-
var
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
})
|
|
234
|
+
var writeEnvelope = ({
|
|
235
|
+
envelope,
|
|
236
|
+
next,
|
|
237
|
+
root
|
|
238
|
+
}) => {
|
|
239
|
+
if (envelope.tool.trim() === "") {
|
|
240
|
+
throw new UnbalancedEnvelope(
|
|
241
|
+
`an envelope with no tool name cannot be filed or reported against. Next: ${next}`
|
|
254
242
|
);
|
|
255
243
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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];
|
|
244
|
+
if (envelope.version.trim() === "") {
|
|
245
|
+
throw new UnbalancedEnvelope(
|
|
246
|
+
`${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}`
|
|
247
|
+
);
|
|
271
248
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
for (const key of path.slice(0, -1)) {
|
|
277
|
-
if (!isRecord2(here[key])) here[key] = {};
|
|
278
|
-
here = here[key];
|
|
249
|
+
if (!isCount(envelope.considered) || !isCount(envelope.read)) {
|
|
250
|
+
throw new UnbalancedEnvelope(
|
|
251
|
+
`${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}`
|
|
252
|
+
);
|
|
279
253
|
}
|
|
280
|
-
|
|
281
|
-
|
|
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));
|
|
254
|
+
if (envelope.considered !== envelope.read + envelope.refused.length + envelope.excused.length) {
|
|
255
|
+
throw new UnbalancedEnvelope(unbalancedMessage(envelope, next));
|
|
312
256
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
};
|
|
257
|
+
refuseForbiddenRoot(root);
|
|
258
|
+
const at = envelopePath(root, envelope.tool);
|
|
259
|
+
mkdirSync(dirname(at), { recursive: true });
|
|
260
|
+
writeFileSync(at, `${JSON.stringify(envelope, void 0, 2)}
|
|
261
|
+
`);
|
|
262
|
+
return at;
|
|
355
263
|
};
|
|
356
|
-
var
|
|
357
|
-
|
|
358
|
-
let parsed;
|
|
264
|
+
var UNKNOWN = "unknown";
|
|
265
|
+
var versionIn = (dir) => {
|
|
359
266
|
try {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
} catch
|
|
363
|
-
|
|
267
|
+
const manifest = JSON.parse(readFileSync2(join(dir, "package.json"), "utf8"));
|
|
268
|
+
return typeof manifest.version === "string" ? manifest.version : void 0;
|
|
269
|
+
} catch {
|
|
270
|
+
return void 0;
|
|
364
271
|
}
|
|
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
272
|
};
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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}`);
|
|
273
|
+
var versionOf = (moduleUrl) => {
|
|
274
|
+
let dir = dirname(fileURLToPath(moduleUrl));
|
|
275
|
+
for (; ; ) {
|
|
276
|
+
const found = versionIn(dir);
|
|
277
|
+
if (found !== void 0) return found;
|
|
278
|
+
const up = dirname(dir);
|
|
279
|
+
if (up === dir) return UNKNOWN;
|
|
280
|
+
dir = up;
|
|
386
281
|
}
|
|
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
282
|
};
|
|
404
|
-
var
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
283
|
+
var MIGRATIONS_TOOL = "release-migrations";
|
|
284
|
+
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";
|
|
285
|
+
var migrationsEnvelope = (report, durationMs) => ({
|
|
286
|
+
considered: report.files.length,
|
|
287
|
+
durationMs,
|
|
288
|
+
excused: report.files.filter((one) => one.state === "excused").map((one) => ({
|
|
289
|
+
path: one.path,
|
|
290
|
+
reason: "a contract-migration marker this gate could believe"
|
|
291
|
+
})),
|
|
292
|
+
findings: [...report.refusals, ...report.markers, ...report.squawk],
|
|
293
|
+
read: report.files.filter((one) => one.state !== "excused" && one.state !== "unreadable").length,
|
|
294
|
+
refused: report.unreadable.map((one) => ({ path: one.path, reason: one.why })),
|
|
295
|
+
tool: MIGRATIONS_TOOL,
|
|
296
|
+
version: versionOf(import.meta.url)
|
|
297
|
+
});
|
|
298
|
+
var SCHEMA_TOOL = "release-schema";
|
|
299
|
+
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)";
|
|
300
|
+
var schemaEnvelope = (report, durationMs) => ({
|
|
301
|
+
considered: report.files.length,
|
|
302
|
+
durationMs,
|
|
303
|
+
excused: [],
|
|
304
|
+
findings: report.findings,
|
|
305
|
+
read: report.read,
|
|
306
|
+
refused: report.unreadable.map((one) => ({ path: one.path, reason: one.why })),
|
|
307
|
+
tool: SCHEMA_TOOL,
|
|
308
|
+
version: versionOf(import.meta.url)
|
|
309
|
+
});
|
|
310
|
+
var PUBLISHED_TOOL = "release-published";
|
|
311
|
+
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)";
|
|
312
|
+
var publishedEnvelope = (report, durationMs) => ({
|
|
313
|
+
considered: report.considered,
|
|
314
|
+
durationMs,
|
|
315
|
+
excused: report.excused,
|
|
316
|
+
findings: report.lines.filter((one) => one.verdict !== "MATCH"),
|
|
317
|
+
read: report.lines.filter((one) => one.verdict !== "UNREACHABLE").length,
|
|
318
|
+
refused: report.lines.filter((one) => one.verdict === "UNREACHABLE").map((one) => ({ path: one.at, reason: one.why })),
|
|
319
|
+
tool: PUBLISHED_TOOL,
|
|
320
|
+
version: versionOf(import.meta.url)
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// src/registry.ts
|
|
324
|
+
var DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
325
|
+
var TIMEOUT_MS = 15e3;
|
|
326
|
+
var registryPathOf = (name) => name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : encodeURIComponent(name);
|
|
327
|
+
var versionsIn = (body) => {
|
|
328
|
+
const versions = body.versions;
|
|
329
|
+
return typeof versions === "object" && versions !== null ? Object.keys(versions) : [];
|
|
330
|
+
};
|
|
331
|
+
var latestIn = (body) => {
|
|
332
|
+
const tags = body["dist-tags"];
|
|
333
|
+
const latest = typeof tags === "object" && tags !== null ? tags.latest : void 0;
|
|
334
|
+
return typeof latest === "string" ? latest : void 0;
|
|
335
|
+
};
|
|
336
|
+
var askRegistry = async ({
|
|
337
|
+
name,
|
|
338
|
+
registry = DEFAULT_REGISTRY,
|
|
339
|
+
timeoutMs = TIMEOUT_MS
|
|
340
|
+
}) => {
|
|
341
|
+
const url = `${registry.replace(/\/+$/, "")}/${registryPathOf(name)}`;
|
|
342
|
+
let response;
|
|
343
|
+
try {
|
|
344
|
+
response = await fetch(url, {
|
|
345
|
+
headers: { accept: "application/json" },
|
|
346
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
347
|
+
});
|
|
348
|
+
} catch (error) {
|
|
349
|
+
return { kind: "unreachable", name, why: `GET ${url} \u2014 ${error.message}` };
|
|
350
|
+
}
|
|
351
|
+
if (response.status === 404) return { kind: "absent", name };
|
|
352
|
+
if (!response.ok) {
|
|
353
|
+
return { kind: "unreachable", name, why: `GET ${url} \u2014 HTTP ${response.status}` };
|
|
354
|
+
}
|
|
355
|
+
let body;
|
|
356
|
+
try {
|
|
357
|
+
body = await response.json();
|
|
358
|
+
} catch (error) {
|
|
359
|
+
return {
|
|
360
|
+
kind: "unreachable",
|
|
361
|
+
name,
|
|
362
|
+
why: `GET ${url} answered ${response.status} with something that is not JSON \u2014 ${error.message}`
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return { kind: "present", latest: latestIn(body), name, versions: versionsIn(body) };
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// src/published.ts
|
|
369
|
+
import { readdirSync, readFileSync as readFileSync3 } from "fs";
|
|
370
|
+
import { join as join2, relative, sep } from "path";
|
|
371
|
+
var NEVER_WALKED = /* @__PURE__ */ new Set(["build", "coverage", "dist", "node_modules", "storybook-static"]);
|
|
372
|
+
var pathOf = (root, path) => relative(root, path).split(sep).join("/");
|
|
373
|
+
var manifestsUnder = (root) => {
|
|
374
|
+
const found = [];
|
|
375
|
+
const walk = (dir) => {
|
|
376
|
+
let entries;
|
|
377
|
+
try {
|
|
378
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
379
|
+
} catch {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
for (const entry of entries) {
|
|
383
|
+
if (entry.isDirectory()) {
|
|
384
|
+
if (!entry.name.startsWith(".") && !NEVER_WALKED.has(entry.name))
|
|
385
|
+
walk(join2(dir, entry.name));
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (entry.name === "package.json") found.push(join2(dir, entry.name));
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
walk(root);
|
|
392
|
+
return found;
|
|
393
|
+
};
|
|
394
|
+
var censusOf = (root) => {
|
|
395
|
+
const excused = [];
|
|
396
|
+
const locals = [];
|
|
397
|
+
for (const path of manifestsUnder(root)) {
|
|
398
|
+
const at = pathOf(root, path);
|
|
399
|
+
let manifest;
|
|
400
|
+
try {
|
|
401
|
+
manifest = JSON.parse(readFileSync3(path, "utf8"));
|
|
402
|
+
} catch (error) {
|
|
403
|
+
excused.push({ path: at, reason: `it does not parse: ${error.message}` });
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (manifest.private === true) {
|
|
407
|
+
excused.push({ path: at, reason: "private: true \u2014 nothing publishes it" });
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (typeof manifest.name !== "string" || manifest.name === "") {
|
|
411
|
+
excused.push({ path: at, reason: "it names no package" });
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (typeof manifest.version !== "string" || manifest.version === "") {
|
|
415
|
+
excused.push({ path: at, reason: "it declares no version" });
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
locals.push({ at, name: manifest.name, version: manifest.version });
|
|
419
|
+
}
|
|
420
|
+
return { excused, locals: locals.toSorted((a, b) => a.name.localeCompare(b.name)) };
|
|
421
|
+
};
|
|
422
|
+
var EXISTS_BUT = "exists but no version matching";
|
|
423
|
+
var lineFor = async (local, registry) => {
|
|
424
|
+
const answer = await askRegistry({ name: local.name, registry });
|
|
425
|
+
const base = { at: local.at, local: local.version, name: local.name };
|
|
426
|
+
if (answer.kind === "unreachable") {
|
|
427
|
+
return { ...base, latest: void 0, verdict: "UNREACHABLE", why: answer.why };
|
|
428
|
+
}
|
|
429
|
+
if (answer.kind === "absent") {
|
|
430
|
+
return {
|
|
431
|
+
...base,
|
|
432
|
+
latest: void 0,
|
|
433
|
+
verdict: "ABSENT",
|
|
434
|
+
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"
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
if (answer.latest === local.version) {
|
|
438
|
+
return { ...base, latest: answer.latest, verdict: "MATCH", why: "on npm" };
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
...base,
|
|
442
|
+
latest: answer.latest,
|
|
443
|
+
verdict: "BEHIND",
|
|
444
|
+
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`
|
|
445
|
+
};
|
|
446
|
+
};
|
|
447
|
+
var sleep = (ms) => new Promise((done) => {
|
|
448
|
+
setTimeout(done, ms);
|
|
449
|
+
});
|
|
450
|
+
var POLL_MS = 5e3;
|
|
451
|
+
var runPublished = async ({
|
|
452
|
+
pollMs = POLL_MS,
|
|
453
|
+
registry = DEFAULT_REGISTRY,
|
|
454
|
+
root,
|
|
455
|
+
waitSeconds = 0
|
|
456
|
+
}) => {
|
|
457
|
+
const { excused, locals } = censusOf(root);
|
|
458
|
+
const startedAt = Date.now();
|
|
459
|
+
const budgetMs = Math.max(0, waitSeconds * 1e3);
|
|
460
|
+
let lines2 = [];
|
|
461
|
+
let sweeps = 0;
|
|
462
|
+
for (; ; ) {
|
|
463
|
+
lines2 = [];
|
|
464
|
+
for (const local of locals) lines2.push(await lineFor(local, registry));
|
|
465
|
+
sweeps += 1;
|
|
466
|
+
const settled = lines2.every((one) => one.verdict === "MATCH");
|
|
467
|
+
const stuck = lines2.some((one) => one.verdict === "UNREACHABLE");
|
|
468
|
+
const left = budgetMs - (Date.now() - startedAt);
|
|
469
|
+
if (settled || stuck || left <= 0) break;
|
|
470
|
+
await sleep(Math.min(pollMs, left));
|
|
471
|
+
}
|
|
472
|
+
const unreachable = lines2.filter((one) => one.verdict === "UNREACHABLE").length;
|
|
473
|
+
return {
|
|
474
|
+
considered: lines2.length + excused.length,
|
|
475
|
+
excused: excused.toSorted((a, b) => a.path.localeCompare(b.path)),
|
|
476
|
+
lines: lines2,
|
|
477
|
+
ok: lines2.length > 0 && lines2.every((one) => one.verdict === "MATCH"),
|
|
478
|
+
registry,
|
|
479
|
+
sweeps,
|
|
480
|
+
unreachable,
|
|
481
|
+
waitedMs: Date.now() - startedAt
|
|
482
|
+
};
|
|
483
|
+
};
|
|
484
|
+
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";
|
|
485
|
+
var formatPublished = (report) => {
|
|
486
|
+
const width = Math.max(1, ...report.lines.map((one) => one.name.length));
|
|
487
|
+
const body = report.lines.map((one) => ` ${one.verdict.padEnd(11)} ${one.name.padEnd(width)} ${one.local} ${one.why}`).join("\n");
|
|
488
|
+
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}`;
|
|
489
|
+
const tail = [
|
|
490
|
+
`considered ${report.considered} manifests: ${report.lines.length} read, ${report.excused.length} excused`,
|
|
491
|
+
report.sweeps > 1 ? `swept ${report.sweeps} times over ${report.waitedMs} ms` : ""
|
|
492
|
+
].filter((one) => one !== "");
|
|
493
|
+
return `${[head, body, ...tail].filter((one) => one !== "").join("\n")}
|
|
494
|
+
`;
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// src/smoke.ts
|
|
498
|
+
import { spawnSync } from "child_process";
|
|
499
|
+
import {
|
|
500
|
+
cpSync,
|
|
501
|
+
existsSync as existsSync2,
|
|
502
|
+
mkdirSync as mkdirSync2,
|
|
503
|
+
readdirSync as readdirSync2,
|
|
504
|
+
readFileSync as readFileSync4,
|
|
505
|
+
rmSync,
|
|
506
|
+
statSync,
|
|
507
|
+
writeFileSync as writeFileSync2
|
|
508
|
+
} from "fs";
|
|
509
|
+
import { join as join3, relative as relative2, resolve as resolve3 } from "path";
|
|
510
|
+
var SNAPSHOTS_DIR = ".geonosis/consumer-snapshots";
|
|
511
|
+
var LOCKFILES = [
|
|
512
|
+
{ file: "bun.lock", manager: "bun" },
|
|
513
|
+
{ file: "bun.lockb", manager: "bun" },
|
|
514
|
+
{ file: "pnpm-lock.yaml", manager: "pnpm" }
|
|
515
|
+
];
|
|
516
|
+
var UNMEASURED = [
|
|
517
|
+
{ file: "package-lock.json", manager: "npm" },
|
|
518
|
+
{ file: "yarn.lock", manager: "yarn" }
|
|
519
|
+
];
|
|
520
|
+
var EXCLUDED_DIRS = [
|
|
521
|
+
".cache",
|
|
522
|
+
".claude",
|
|
523
|
+
".geonosis",
|
|
524
|
+
".git",
|
|
525
|
+
".next",
|
|
526
|
+
".turbo",
|
|
527
|
+
".wrangler",
|
|
528
|
+
"coverage",
|
|
529
|
+
"dist",
|
|
530
|
+
"node_modules",
|
|
531
|
+
"storybook-static"
|
|
532
|
+
];
|
|
533
|
+
var headOf = (from) => {
|
|
534
|
+
const done = spawnSync("git", ["-C", from, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
535
|
+
if (done.error !== void 0) {
|
|
536
|
+
return { why: `git could not be run here: ${done.error.message}` };
|
|
537
|
+
}
|
|
538
|
+
if (done.status !== 0) {
|
|
539
|
+
return { why: `git says nothing about ${from}: ${done.stderr.trim()}` };
|
|
540
|
+
}
|
|
541
|
+
return { sha: done.stdout.trim() };
|
|
542
|
+
};
|
|
543
|
+
var DOCTOR_PACKAGE = "@geonosis/doctor";
|
|
544
|
+
var DOCTOR_BIN = "geonosis-doctor";
|
|
545
|
+
var DOCTOR_DOORS = ["@geonosis/cli", "geonosis"];
|
|
546
|
+
var DOOR_BIN = "geonosis doctor";
|
|
547
|
+
var isRecord2 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
548
|
+
var readManifest = (path) => {
|
|
549
|
+
try {
|
|
550
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
551
|
+
return isRecord2(parsed) ? parsed : {};
|
|
552
|
+
} catch (error) {
|
|
553
|
+
throw new CannotRun(`${path} is not readable JSON: ${error.message}`);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
var versionsIn2 = (manifest) => {
|
|
557
|
+
const found = {};
|
|
558
|
+
for (const key of ["dependencies", "devDependencies"]) {
|
|
559
|
+
const block = manifest[key];
|
|
560
|
+
if (!isRecord2(block)) continue;
|
|
561
|
+
for (const [name, range] of Object.entries(block)) {
|
|
562
|
+
if (typeof range === "string") found[name] = range;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return Object.fromEntries(Object.entries(found).toSorted(([a], [b]) => a.localeCompare(b)));
|
|
566
|
+
};
|
|
567
|
+
var scriptsIn = (manifest) => {
|
|
568
|
+
const scripts = manifest["scripts"];
|
|
569
|
+
if (!isRecord2(scripts)) return {};
|
|
570
|
+
return Object.fromEntries(
|
|
571
|
+
Object.entries(scripts).filter((one) => typeof one[1] === "string")
|
|
572
|
+
);
|
|
573
|
+
};
|
|
574
|
+
var copyInto = (from, to, excluded) => {
|
|
575
|
+
let bytes = 0;
|
|
576
|
+
let files = 0;
|
|
577
|
+
const walk = (dir, into) => {
|
|
578
|
+
mkdirSync2(into, { recursive: true });
|
|
579
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
580
|
+
if (excluded.has(entry.name)) continue;
|
|
581
|
+
const at = join3(dir, entry.name);
|
|
582
|
+
if (entry.isDirectory()) {
|
|
583
|
+
walk(at, join3(into, entry.name));
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (!entry.isFile()) continue;
|
|
587
|
+
cpSync(at, join3(into, entry.name));
|
|
588
|
+
bytes += statSync(at).size;
|
|
589
|
+
files += 1;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
walk(from, to);
|
|
593
|
+
return { bytes, files };
|
|
594
|
+
};
|
|
595
|
+
var manifestsUnder2 = (root) => {
|
|
596
|
+
const found = [];
|
|
597
|
+
const walk = (dir) => {
|
|
598
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true }).toSorted(
|
|
599
|
+
(a, b) => a.name.localeCompare(b.name)
|
|
600
|
+
)) {
|
|
601
|
+
const at = join3(dir, entry.name);
|
|
602
|
+
if (entry.isDirectory()) {
|
|
603
|
+
walk(at);
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (entry.name !== "package.json") continue;
|
|
607
|
+
found.push({ path: relative2(root, at), versions: versionsIn2(readManifest(at)) });
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
walk(root);
|
|
611
|
+
return found.toSorted((a, b) => a.path.localeCompare(b.path));
|
|
612
|
+
};
|
|
613
|
+
var managerOf = (from) => {
|
|
614
|
+
const found = LOCKFILES.find((one) => existsSync2(join3(from, one.file)));
|
|
615
|
+
if (found !== void 0) return { lockfile: found.file, manager: found.manager };
|
|
616
|
+
const unmeasured = UNMEASURED.find((one) => existsSync2(join3(from, one.file)));
|
|
617
|
+
if (unmeasured !== void 0) {
|
|
618
|
+
throw new CannotRun(
|
|
619
|
+
`${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(", ")}.`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
throw new CannotRun(
|
|
623
|
+
`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.`
|
|
624
|
+
);
|
|
625
|
+
};
|
|
626
|
+
var commandsFor = (manifest, manifests, named) => {
|
|
627
|
+
const scripts = scriptsIn(manifest);
|
|
628
|
+
const installs = (name) => manifests.some((one) => one.versions[name] !== void 0);
|
|
629
|
+
const doctorCommand = installs(DOCTOR_PACKAGE) ? { exec: DOCTOR_BIN } : DOCTOR_DOORS.some(installs) ? { exec: DOOR_BIN } : {
|
|
630
|
+
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}`
|
|
631
|
+
};
|
|
632
|
+
const phase = (name, fallback) => {
|
|
633
|
+
const override = named[name];
|
|
634
|
+
if (override !== void 0) return { exec: override };
|
|
635
|
+
if (scripts[name] !== void 0) return { run: name };
|
|
636
|
+
return fallback;
|
|
637
|
+
};
|
|
638
|
+
return {
|
|
639
|
+
doctor: phase("doctor", doctorCommand),
|
|
640
|
+
lint: phase("lint", { why: 'the tree has no "lint" script and no --lint command was named' }),
|
|
641
|
+
test: phase("test", { why: 'the tree has no "test" script and no --test command was named' }),
|
|
642
|
+
typecheck: phase("typecheck", {
|
|
643
|
+
why: 'the tree has no "typecheck" script and no --typecheck command was named'
|
|
644
|
+
})
|
|
645
|
+
};
|
|
646
|
+
};
|
|
647
|
+
var asPattern = (glob) => new RegExp(
|
|
648
|
+
`^${glob.split("/").map(
|
|
649
|
+
(part) => part === "**" ? ".*" : part.replaceAll(/[.+^${}()|[\]\\]/g, String.raw`\$&`).replaceAll("*", "[^/]*")
|
|
650
|
+
).join("/").replaceAll(".*/", "(?:.*/)?")}$`
|
|
651
|
+
);
|
|
652
|
+
var filesUnder = (root, at = root) => readdirSync2(at, { withFileTypes: true }).flatMap((entry) => {
|
|
653
|
+
const full = join3(at, entry.name);
|
|
654
|
+
if (entry.isDirectory()) return filesUnder(root, full);
|
|
655
|
+
return entry.isFile() ? [full.slice(root.length + 1)] : [];
|
|
656
|
+
});
|
|
657
|
+
var seamsIn = (tree) => {
|
|
658
|
+
const at = join3(tree, "geonosis.json");
|
|
659
|
+
if (!existsSync2(at)) return void 0;
|
|
660
|
+
let globs;
|
|
661
|
+
try {
|
|
662
|
+
const declared = JSON.parse(readFileSync4(at, "utf8")).adoption?.seams;
|
|
663
|
+
globs = Array.isArray(declared) ? declared.filter((one) => typeof one === "string") : [];
|
|
664
|
+
} catch {
|
|
665
|
+
return void 0;
|
|
666
|
+
}
|
|
667
|
+
if (globs.length === 0) return void 0;
|
|
668
|
+
const patterns = globs.map(asPattern);
|
|
669
|
+
const lines2 = filesUnder(tree).filter((one) => patterns.some((pattern) => pattern.test(one))).reduce((total, one) => {
|
|
670
|
+
const body = readFileSync4(join3(tree, one), "utf8");
|
|
671
|
+
return total + (body === "" ? 0 : body.replace(/\n$/, "").split("\n").length);
|
|
672
|
+
}, 0);
|
|
673
|
+
return { globs, lines: lines2 };
|
|
674
|
+
};
|
|
675
|
+
var snapshotDir = (root, name) => join3(root, SNAPSHOTS_DIR, name);
|
|
676
|
+
var readSnapshot = (root, name) => {
|
|
677
|
+
const at = join3(snapshotDir(root, name), "snapshot.json");
|
|
678
|
+
if (!existsSync2(at)) {
|
|
679
|
+
throw new CannotRun(
|
|
680
|
+
`there is no snapshot called "${name}" here \u2014 ${at} does not exist. Record one with: geonosis-release smoke snapshot ${name} --from <their tree>`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
return JSON.parse(readFileSync4(at, "utf8"));
|
|
684
|
+
};
|
|
685
|
+
var wouldBeCommitted = (root, path) => {
|
|
686
|
+
const inside = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root });
|
|
687
|
+
if (inside.error !== void 0 || inside.status !== 0) return false;
|
|
688
|
+
return spawnSync("git", ["check-ignore", "-q", path], { cwd: root }).status !== 0;
|
|
689
|
+
};
|
|
690
|
+
var NAME_SHAPE = /^[a-z0-9][\w.-]*$/i;
|
|
691
|
+
var runSnapshot = (input) => {
|
|
692
|
+
if (!NAME_SHAPE.test(input.name)) {
|
|
693
|
+
throw new CannotRun(
|
|
694
|
+
`"${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`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
const from = resolve3(input.from);
|
|
698
|
+
if (!existsSync2(join3(from, "package.json"))) {
|
|
699
|
+
throw new CannotRun(`${from} has no package.json \u2014 that is not a tree a consumer installs`);
|
|
700
|
+
}
|
|
701
|
+
const at = snapshotDir(input.root, input.name);
|
|
702
|
+
if (wouldBeCommitted(input.root, join3(SNAPSHOTS_DIR, input.name))) {
|
|
703
|
+
throw new CannotRun(
|
|
704
|
+
`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.`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
if (existsSync2(at) && !input.replace) {
|
|
708
|
+
throw new CannotRun(
|
|
709
|
+
`${at} is already a snapshot, and a baseline recorded against other bytes is worse than none \u2014 pass --replace to overwrite it`
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
const { lockfile, manager } = managerOf(from);
|
|
713
|
+
const excluded = [.../* @__PURE__ */ new Set([...EXCLUDED_DIRS, ...input.exclude ?? []])].toSorted();
|
|
714
|
+
rmSync(at, { force: true, recursive: true });
|
|
715
|
+
const tree = join3(at, "tree");
|
|
716
|
+
const { bytes, files } = copyInto(from, tree, new Set(excluded));
|
|
717
|
+
const manifests = manifestsUnder2(tree);
|
|
718
|
+
const seams = seamsIn(tree);
|
|
719
|
+
const record = {
|
|
720
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
721
|
+
bytes,
|
|
722
|
+
...seams === void 0 ? {} : { seams },
|
|
723
|
+
commit: headOf(from),
|
|
724
|
+
commands: commandsFor(readManifest(join3(tree, "package.json")), manifests, input.named),
|
|
725
|
+
excluded,
|
|
726
|
+
files,
|
|
727
|
+
from,
|
|
728
|
+
lockfile,
|
|
729
|
+
manager,
|
|
730
|
+
manifests,
|
|
731
|
+
name: input.name
|
|
732
|
+
};
|
|
733
|
+
writeFileSync2(join3(at, "snapshot.json"), `${JSON.stringify(record, void 0, 2)}
|
|
734
|
+
`);
|
|
735
|
+
return record;
|
|
736
|
+
};
|
|
737
|
+
var describeCommand = (command) => {
|
|
738
|
+
if ("run" in command) return `run ${command.run}`;
|
|
739
|
+
if ("exec" in command) return command.exec;
|
|
740
|
+
return `nothing \u2014 ${command.why}`;
|
|
741
|
+
};
|
|
742
|
+
var describeCommit = (commit) => {
|
|
743
|
+
if (commit === void 0) return "nothing recorded its commit";
|
|
744
|
+
if ("sha" in commit) return commit.sha;
|
|
745
|
+
return commit.why;
|
|
746
|
+
};
|
|
747
|
+
var formatSnapshot = (record) => [
|
|
748
|
+
`OK snapshot ${record.name}: ${record.files} files, ${record.bytes} bytes, ${record.manifests.length} manifests
|
|
749
|
+
`,
|
|
750
|
+
` from ${record.from} \u2014 ${record.manager} (${record.lockfile})
|
|
751
|
+
`,
|
|
752
|
+
` at ${describeCommit(record.commit)}
|
|
753
|
+
`,
|
|
754
|
+
...PHASES.map((phase) => ` ${phase}: ${describeCommand(record.commands[phase])}
|
|
755
|
+
`)
|
|
756
|
+
].join("");
|
|
757
|
+
|
|
758
|
+
// src/adoption.ts
|
|
759
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
760
|
+
import { join as join4 } from "path";
|
|
761
|
+
var groupsOf = (root) => {
|
|
762
|
+
const at = join4(root, ".changeset/config.json");
|
|
763
|
+
if (!existsSync3(at)) return [];
|
|
764
|
+
let parsed;
|
|
765
|
+
try {
|
|
766
|
+
parsed = JSON.parse(readFileSync5(at, "utf8"));
|
|
767
|
+
} catch (error) {
|
|
768
|
+
throw new CannotRun(`${at} is not readable JSON: ${error.message}`);
|
|
769
|
+
}
|
|
770
|
+
const fixed = parsed.fixed;
|
|
771
|
+
return Array.isArray(fixed) ? fixed.filter((one) => Array.isArray(one) && one.every(isString)) : [];
|
|
772
|
+
};
|
|
773
|
+
var isString = (value2) => typeof value2 === "string";
|
|
774
|
+
var RANGE_PREFIX = /^[\^~>=<v\s]+/;
|
|
775
|
+
var RELEASE_NUMBER = /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/;
|
|
776
|
+
var numberIn = (spec) => {
|
|
777
|
+
const bare = spec.replace(RANGE_PREFIX, "").trim();
|
|
778
|
+
return RELEASE_NUMBER.test(bare) ? bare : void 0;
|
|
779
|
+
};
|
|
780
|
+
var declarationsIn = (record, kit) => record.manifests.flatMap(
|
|
781
|
+
(manifest) => Object.entries(manifest.versions).filter(([name]) => kit.has(name)).map(([name, spec]) => ({ name, path: manifest.path, spec }))
|
|
782
|
+
);
|
|
783
|
+
var groupName = (index, total) => total > 1 ? `fixed group ${index + 1}` : "the fixed group";
|
|
784
|
+
var incoherence = (consumer, groups, declarations) => groups.flatMap((group, index) => {
|
|
785
|
+
const members = new Set(group);
|
|
786
|
+
const versions = [
|
|
787
|
+
...new Set(
|
|
788
|
+
declarations.filter((one) => members.has(one.name)).flatMap((one) => {
|
|
789
|
+
const number = numberIn(one.spec);
|
|
790
|
+
return number === void 0 ? [] : [number];
|
|
791
|
+
})
|
|
792
|
+
)
|
|
793
|
+
].toSorted();
|
|
794
|
+
if (versions.length < 2) return [];
|
|
795
|
+
const name = groupName(index, groups.length);
|
|
796
|
+
return [
|
|
797
|
+
{
|
|
798
|
+
consumer,
|
|
799
|
+
detail: `${name} at ${versions.join(", ")} \u2014 they are published as one number, so this tree is a bump that landed halfway`,
|
|
800
|
+
name,
|
|
801
|
+
verdict: "INCOHERENT"
|
|
802
|
+
}
|
|
803
|
+
];
|
|
804
|
+
});
|
|
805
|
+
var distance = (consumer, declarations, current) => declarations.flatMap((one) => {
|
|
806
|
+
const now = current.get(one.name);
|
|
807
|
+
if (now === void 0) return [];
|
|
808
|
+
const declared = numberIn(one.spec);
|
|
809
|
+
if (declared === void 0) {
|
|
810
|
+
return [
|
|
811
|
+
{
|
|
812
|
+
consumer,
|
|
813
|
+
detail: `${one.path} names it "${one.spec}", which is not a release number this can compare`,
|
|
814
|
+
name: one.name,
|
|
815
|
+
verdict: "UNJUDGED"
|
|
816
|
+
}
|
|
817
|
+
];
|
|
818
|
+
}
|
|
819
|
+
if (declared === now) return [];
|
|
820
|
+
return [{ consumer, detail: `${declared} \u2192 ${now}`, name: one.name, verdict: "BEHIND" }];
|
|
821
|
+
});
|
|
822
|
+
var undeclaredFloors = (consumer, floors, declarations) => {
|
|
823
|
+
const declared = new Set(declarations.map((one) => one.name));
|
|
824
|
+
return floors.filter((name) => !declared.has(name)).map((name) => ({
|
|
825
|
+
consumer,
|
|
826
|
+
detail: "nothing in this tree declares it \u2014 informational, a floor is opted into",
|
|
827
|
+
name,
|
|
828
|
+
verdict: "FLOOR UNDECLARED"
|
|
829
|
+
}));
|
|
830
|
+
};
|
|
831
|
+
var currentVersions = async (root, registry) => {
|
|
832
|
+
const locals = censusOf(root).locals;
|
|
833
|
+
const current = new Map(locals.map((one) => [one.name, one.version]));
|
|
834
|
+
if (registry === void 0) return current;
|
|
835
|
+
for (const one of locals) {
|
|
836
|
+
const answer = await askRegistry({ name: one.name, registry });
|
|
837
|
+
if (answer.kind === "present" && answer.latest !== void 0)
|
|
838
|
+
current.set(one.name, answer.latest);
|
|
839
|
+
}
|
|
840
|
+
return current;
|
|
841
|
+
};
|
|
842
|
+
var runAdoption = async ({
|
|
843
|
+
declared,
|
|
844
|
+
registry,
|
|
845
|
+
root
|
|
846
|
+
}) => {
|
|
847
|
+
if (declared.length === 0) {
|
|
848
|
+
throw new CannotRun(
|
|
849
|
+
"nothing here names a consumer to measure \u2014 geonosis.json \u2192 release.smoke.snapshots names them. An adoption sweep over zero consumers is not a scoreboard."
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
const current = await currentVersions(root, registry);
|
|
853
|
+
const groups = groupsOf(root);
|
|
854
|
+
const grouped = new Set(groups.flat());
|
|
855
|
+
const floors = [...current.keys()].filter((name) => !grouped.has(name)).toSorted();
|
|
856
|
+
const consumers = [];
|
|
857
|
+
const excused = [];
|
|
858
|
+
for (const one of declared) {
|
|
859
|
+
if (!existsSync3(join4(snapshotDir(root, one.name), "snapshot.json"))) {
|
|
860
|
+
excused.push({
|
|
861
|
+
path: one.name,
|
|
862
|
+
reason: `no recording of it here \u2014 record one with: geonosis-release smoke snapshot ${one.name} --from <their tree>`
|
|
863
|
+
});
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
const record = readSnapshot(root, one.name);
|
|
867
|
+
const declarations = declarationsIn(record, new Set(current.keys()));
|
|
868
|
+
const seams = record.seams;
|
|
869
|
+
consumers.push({
|
|
870
|
+
declared: declarations.length,
|
|
871
|
+
...seams === void 0 ? {} : { seams },
|
|
872
|
+
findings: [
|
|
873
|
+
...distance(one.name, declarations, current),
|
|
874
|
+
...incoherence(one.name, groups, declarations),
|
|
875
|
+
...undeclaredFloors(one.name, floors, declarations)
|
|
876
|
+
],
|
|
877
|
+
name: one.name
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
considered: declared.length,
|
|
882
|
+
consumers,
|
|
883
|
+
excused,
|
|
884
|
+
registry,
|
|
885
|
+
versions: Object.fromEntries([...current.entries()].toSorted(([a], [b]) => a.localeCompare(b)))
|
|
886
|
+
};
|
|
887
|
+
};
|
|
888
|
+
var NOT_A_FAULT = /* @__PURE__ */ new Set(["FLOOR UNDECLARED"]);
|
|
889
|
+
var lineOf = (finding) => {
|
|
890
|
+
if (finding.verdict === "INCOHERENT") return ` INCOHERENT ${finding.consumer}: ${finding.detail}`;
|
|
891
|
+
if (finding.verdict === "FLOOR UNDECLARED") {
|
|
892
|
+
return ` FLOOR UNDECLARED ${finding.consumer} ${finding.name}`;
|
|
893
|
+
}
|
|
894
|
+
return ` ${finding.verdict} ${finding.consumer} ${finding.name} ${finding.detail}`;
|
|
895
|
+
};
|
|
896
|
+
var blockFor = (consumer) => {
|
|
897
|
+
const faults = consumer.findings.filter((one) => !NOT_A_FAULT.has(one.verdict));
|
|
898
|
+
const head = faults.length === 0 ? [
|
|
899
|
+
` MATCH ${consumer.name} \u2014 ${consumer.declared} spec(s) on the versions this release is cut at`
|
|
900
|
+
] : [];
|
|
901
|
+
const seams = consumer.seams === void 0 ? [] : [
|
|
902
|
+
` SEAMS ${consumer.name} \u2014 ${consumer.seams.lines} lines in ${consumer.seams.globs.join(", ")} at this recording; a floor is adopted the day it deletes more than it adds`
|
|
903
|
+
];
|
|
904
|
+
return [...head, ...seams, ...new Set(consumer.findings.map(lineOf))];
|
|
905
|
+
};
|
|
906
|
+
var formatAdoption = (report) => [
|
|
907
|
+
`adoption \u2014 ${report.considered} declared, ${report.consumers.length} read, ${report.excused.length} excused; against ${report.registry ?? "this workspace"}`,
|
|
908
|
+
...report.consumers.flatMap(blockFor),
|
|
909
|
+
...report.excused.map((one) => ` SKIP ${one.path}: ${one.reason}`),
|
|
910
|
+
""
|
|
911
|
+
].join("\n");
|
|
912
|
+
var ADOPTION_TOOL = "release-adoption";
|
|
913
|
+
var ADOPTION_NEXT = "geonosis-release adoption --json and read `consumers` \u2014 every declared consumer leaves by exactly one door, read or excused, and a release review carries one row per consumer from it (D-061)";
|
|
914
|
+
var adoptionEnvelope = (report, durationMs) => ({
|
|
915
|
+
considered: report.considered,
|
|
916
|
+
durationMs,
|
|
917
|
+
excused: report.excused,
|
|
918
|
+
findings: report.consumers.flatMap((one) => one.findings),
|
|
919
|
+
read: report.consumers.length,
|
|
920
|
+
refused: [],
|
|
921
|
+
tool: ADOPTION_TOOL,
|
|
922
|
+
version: versionOf(import.meta.url)
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
// src/wrangler.ts
|
|
926
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
927
|
+
import { resolve as resolve4 } from "path";
|
|
928
|
+
var isRecord3 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
929
|
+
var parseJsonc = (source) => {
|
|
930
|
+
let out = "";
|
|
931
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
932
|
+
const char = source[index] ?? "";
|
|
933
|
+
if (char === '"') {
|
|
934
|
+
const start = index;
|
|
935
|
+
index += 1;
|
|
936
|
+
for (; index < source.length; index += 1) {
|
|
937
|
+
if (source[index] === "\\") {
|
|
938
|
+
index += 1;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (source[index] === '"') break;
|
|
942
|
+
}
|
|
943
|
+
out += source.slice(start, index + 1);
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (char === "/" && source[index + 1] === "/") {
|
|
947
|
+
const end = source.indexOf("\n", index);
|
|
948
|
+
index = end === -1 ? source.length : end - 1;
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
if (char === "/" && source[index + 1] === "*") {
|
|
952
|
+
const end = source.indexOf("*/", index + 2);
|
|
953
|
+
index = end === -1 ? source.length : end + 1;
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
out += char;
|
|
957
|
+
}
|
|
958
|
+
return JSON.parse(out.replaceAll(/,(\s*[\]}])/g, "$1"));
|
|
959
|
+
};
|
|
960
|
+
var literal = (text) => {
|
|
961
|
+
const value2 = text.trim();
|
|
962
|
+
if (value2.startsWith('"') || value2.startsWith("'")) return value2.slice(1, -1);
|
|
963
|
+
if (value2 === "true") return true;
|
|
964
|
+
if (value2 === "false") return false;
|
|
965
|
+
const number = Number(value2);
|
|
966
|
+
return Number.isNaN(number) ? value2 : number;
|
|
967
|
+
};
|
|
968
|
+
var split = (text, delimiter) => {
|
|
969
|
+
const parts = [];
|
|
970
|
+
let depth = 0;
|
|
971
|
+
let quote = "";
|
|
972
|
+
let current = "";
|
|
973
|
+
for (const char of text) {
|
|
974
|
+
if (quote !== "") {
|
|
975
|
+
current += char;
|
|
976
|
+
if (char === quote) quote = "";
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
if (char === '"' || char === "'") quote = char;
|
|
980
|
+
if (char === "[" || char === "{") depth += 1;
|
|
981
|
+
if (char === "]" || char === "}") depth -= 1;
|
|
982
|
+
if (char === delimiter && depth === 0) {
|
|
983
|
+
parts.push(current);
|
|
984
|
+
current = "";
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
current += char;
|
|
988
|
+
}
|
|
989
|
+
parts.push(current);
|
|
990
|
+
return parts.filter((one) => one.trim() !== "");
|
|
991
|
+
};
|
|
992
|
+
var value = (text) => {
|
|
993
|
+
const trimmed = text.trim();
|
|
994
|
+
if (trimmed.startsWith("[")) return split(trimmed.slice(1, -1), ",").map(value);
|
|
995
|
+
if (trimmed.startsWith("{")) {
|
|
996
|
+
return Object.fromEntries(
|
|
997
|
+
split(trimmed.slice(1, -1), ",").map((pair) => {
|
|
998
|
+
const at = pair.indexOf("=");
|
|
999
|
+
return [pair.slice(0, at).trim(), value(pair.slice(at + 1))];
|
|
1000
|
+
})
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
return literal(trimmed);
|
|
1004
|
+
};
|
|
1005
|
+
var put = (into, path, leaf) => {
|
|
1006
|
+
let here = into;
|
|
1007
|
+
for (const key of path.slice(0, -1)) {
|
|
1008
|
+
if (!isRecord3(here[key])) here[key] = {};
|
|
1009
|
+
here = here[key];
|
|
1010
|
+
}
|
|
1011
|
+
here[path.at(-1) ?? ""] = leaf;
|
|
1012
|
+
};
|
|
1013
|
+
var table = (into, path) => {
|
|
1014
|
+
let here = into;
|
|
1015
|
+
for (const key of path) {
|
|
1016
|
+
if (!isRecord3(here[key])) here[key] = {};
|
|
1017
|
+
here = here[key];
|
|
1018
|
+
}
|
|
1019
|
+
return here;
|
|
1020
|
+
};
|
|
1021
|
+
var arrayTable = (into, path) => {
|
|
1022
|
+
let here = into;
|
|
1023
|
+
for (const key of path.slice(0, -1)) {
|
|
1024
|
+
if (!isRecord3(here[key])) here[key] = {};
|
|
1025
|
+
here = here[key];
|
|
1026
|
+
}
|
|
1027
|
+
const last = path.at(-1) ?? "";
|
|
1028
|
+
if (!Array.isArray(here[last])) here[last] = [];
|
|
1029
|
+
const list = here[last];
|
|
1030
|
+
const entry = {};
|
|
1031
|
+
list.push(entry);
|
|
1032
|
+
return entry;
|
|
1033
|
+
};
|
|
1034
|
+
var parseToml = (source) => {
|
|
1035
|
+
const out = {};
|
|
1036
|
+
let here = out;
|
|
1037
|
+
const lines2 = source.split("\n");
|
|
1038
|
+
for (let index = 0; index < lines2.length; index += 1) {
|
|
1039
|
+
const line = (lines2[index] ?? "").split("#")[0]?.trim() ?? "";
|
|
1040
|
+
if (line === "") continue;
|
|
1041
|
+
if (line.startsWith("[[") && line.endsWith("]]")) {
|
|
1042
|
+
here = arrayTable(out, line.slice(2, -2).trim().split("."));
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
if (line.startsWith("[") && line.endsWith("]")) {
|
|
1046
|
+
here = table(out, line.slice(1, -1).trim().split("."));
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
const at = line.indexOf("=");
|
|
1050
|
+
if (at === -1) continue;
|
|
1051
|
+
let text = line.slice(at + 1);
|
|
1052
|
+
while ([...text].filter((one) => one === "[").length > [...text].filter((one) => one === "]").length) {
|
|
1053
|
+
index += 1;
|
|
1054
|
+
if (index >= lines2.length) throw new CannotRun("an unterminated array in the TOML config");
|
|
1055
|
+
text += `
|
|
1056
|
+
${(lines2[index] ?? "").split("#")[0] ?? ""}`;
|
|
1057
|
+
}
|
|
1058
|
+
put(here, line.slice(0, at).trim().split("."), value(text));
|
|
1059
|
+
}
|
|
1060
|
+
return out;
|
|
1061
|
+
};
|
|
1062
|
+
var BINDING_LISTS = [
|
|
1063
|
+
"ai",
|
|
1064
|
+
"analytics_engine_datasets",
|
|
1065
|
+
"browser",
|
|
1066
|
+
"d1_databases",
|
|
1067
|
+
"dispatch_namespaces",
|
|
1068
|
+
"durable_objects",
|
|
1069
|
+
"hyperdrive",
|
|
1070
|
+
"kv_namespaces",
|
|
1071
|
+
"mtls_certificates",
|
|
1072
|
+
"queues",
|
|
1073
|
+
"r2_buckets",
|
|
1074
|
+
"send_email",
|
|
1075
|
+
"services",
|
|
1076
|
+
"vectorize",
|
|
1077
|
+
"version_metadata",
|
|
1078
|
+
"workflows"
|
|
1079
|
+
];
|
|
1080
|
+
var bindingsOf = (found) => {
|
|
1081
|
+
if (Array.isArray(found)) return found.flatMap(bindingsOf);
|
|
1082
|
+
if (!isRecord3(found)) return [];
|
|
1083
|
+
const named = found["binding"] ?? found["name"];
|
|
1084
|
+
const here = typeof named === "string" ? [named] : [];
|
|
1085
|
+
const nested = Object.entries(found).filter(([key]) => key === "bindings" || key === "producers" || key === "consumers").flatMap(([, one]) => bindingsOf(one));
|
|
1086
|
+
return [...here, ...nested];
|
|
1087
|
+
};
|
|
1088
|
+
var declaredIn = (config) => {
|
|
1089
|
+
const triggers = config["triggers"];
|
|
1090
|
+
const crons = isRecord3(triggers) && Array.isArray(triggers["crons"]) ? triggers["crons"] : [];
|
|
1091
|
+
const routes = config["routes"];
|
|
1092
|
+
const one = config["route"];
|
|
1093
|
+
const listed = [
|
|
1094
|
+
...Array.isArray(routes) ? routes : [],
|
|
1095
|
+
...typeof one === "string" ? [one] : []
|
|
1096
|
+
];
|
|
1097
|
+
return {
|
|
1098
|
+
bindings: BINDING_LISTS.flatMap((key) => bindingsOf(config[key])).toSorted(),
|
|
1099
|
+
crons: crons.filter((cron) => typeof cron === "string").toSorted(),
|
|
1100
|
+
routes: listed.map((route) => isRecord3(route) ? route["pattern"] : route).filter((pattern) => typeof pattern === "string").toSorted()
|
|
1101
|
+
};
|
|
1102
|
+
};
|
|
1103
|
+
var readWrangler = (root, relative5, env) => {
|
|
1104
|
+
const path = resolve4(root, relative5);
|
|
1105
|
+
let parsed;
|
|
1106
|
+
try {
|
|
1107
|
+
const source = readFileSync6(path, "utf8");
|
|
1108
|
+
parsed = relative5.endsWith(".toml") ? parseToml(source) : parseJsonc(source);
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
throw new CannotRun(`${relative5} could not be read: ${error.message}`);
|
|
1111
|
+
}
|
|
1112
|
+
if (!isRecord3(parsed)) throw new CannotRun(`${relative5} is not a wrangler configuration`);
|
|
1113
|
+
const environments = parsed["env"];
|
|
1114
|
+
const block = env !== void 0 && isRecord3(environments) && isRecord3(environments[env]) ? environments[env] : parsed;
|
|
1115
|
+
return declaredIn(block);
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
// src/deployed.ts
|
|
1119
|
+
import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
|
|
1120
|
+
import { resolve as resolve5 } from "path";
|
|
1121
|
+
var DEPLOYED_FILE = ".geonosis/deployed.json";
|
|
1122
|
+
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";
|
|
1123
|
+
var listOf = (found) => Array.isArray(found) ? found.filter((one) => typeof one === "string") : [];
|
|
1124
|
+
var isRecord4 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
1125
|
+
var readDeployed = (root) => {
|
|
1126
|
+
const path = resolve5(root, DEPLOYED_FILE);
|
|
1127
|
+
if (!existsSync4(path)) throw new CannotRun(NOTHING_SAYS);
|
|
1128
|
+
let parsed;
|
|
1129
|
+
try {
|
|
1130
|
+
parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
throw new CannotRun(`${DEPLOYED_FILE} is not readable JSON: ${error.message}`);
|
|
1133
|
+
}
|
|
1134
|
+
if (!isRecord4(parsed)) throw new CannotRun(`${DEPLOYED_FILE} is not an object`);
|
|
1135
|
+
const triggers = isRecord4(parsed["triggers"]) ? parsed["triggers"] : {};
|
|
1136
|
+
return {
|
|
1137
|
+
...typeof parsed["at"] === "string" ? { at: parsed["at"] } : {},
|
|
1138
|
+
deployed: {
|
|
1139
|
+
bindings: listOf(parsed["bindings"]),
|
|
1140
|
+
crons: listOf(triggers["crons"]),
|
|
1141
|
+
routes: listOf(triggers["routes"]),
|
|
1142
|
+
secrets: listOf(parsed["secrets"])
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
};
|
|
1146
|
+
var missingBetween = (kind, declared, deployed) => {
|
|
1147
|
+
const missing = declared.filter((one) => !deployed.includes(one));
|
|
1148
|
+
const extra = deployed.filter((one) => !declared.includes(one));
|
|
1149
|
+
return missing.length === 0 && extra.length === 0 ? [] : [{ extra, kind, missing }];
|
|
1150
|
+
};
|
|
1151
|
+
var driftBetween = (declared, deployed) => [
|
|
1152
|
+
...missingBetween("crons", declared.crons, deployed.crons),
|
|
1153
|
+
...missingBetween("routes", declared.routes, deployed.routes),
|
|
1154
|
+
...missingBetween("bindings", declared.bindings, deployed.bindings),
|
|
1155
|
+
...missingBetween("secrets", declared.secrets, deployed.secrets)
|
|
1156
|
+
];
|
|
1157
|
+
var runDeployed = (input) => {
|
|
411
1158
|
const config = readReleaseConfig(input.root);
|
|
412
1159
|
const configs = config.wrangler ?? [];
|
|
413
1160
|
const secrets = config.secrets ?? [];
|
|
@@ -441,7 +1188,7 @@ var formatDeployed = (report) => {
|
|
|
441
1188
|
};
|
|
442
1189
|
|
|
443
1190
|
// src/added.ts
|
|
444
|
-
import { execFileSync, spawnSync } from "child_process";
|
|
1191
|
+
import { execFileSync, spawnSync as spawnSync2 } from "child_process";
|
|
445
1192
|
var git = (root, args) => {
|
|
446
1193
|
try {
|
|
447
1194
|
return execFileSync("git", [...args], { cwd: root, encoding: "utf8", stdio: "pipe" });
|
|
@@ -457,7 +1204,7 @@ var addedSince = (root, since) => {
|
|
|
457
1204
|
return [.../* @__PURE__ */ new Set([...committed, ...staged, ...untracked])].toSorted();
|
|
458
1205
|
};
|
|
459
1206
|
var gitOk = (root, args) => {
|
|
460
|
-
const run =
|
|
1207
|
+
const run = spawnSync2("git", [...args], { cwd: root, encoding: "utf8" });
|
|
461
1208
|
return run.error === void 0 && run.status === 0;
|
|
462
1209
|
};
|
|
463
1210
|
|
|
@@ -647,32 +1394,32 @@ var NARROWING = [
|
|
|
647
1394
|
["SET NOT NULL", /\bSET\s+NOT\s+NULL\b/i]
|
|
648
1395
|
];
|
|
649
1396
|
var statementsOf = (sql) => sql.replaceAll(/'[^']*'/g, (found) => `'${found.slice(1, -1).replaceAll(/[^\n]/g, " ")}'`);
|
|
650
|
-
var
|
|
1397
|
+
var lineOf2 = (body, index) => body.slice(0, index).split("\n").length;
|
|
651
1398
|
var refusalsIn = (path, body, firstLine = 1) => {
|
|
652
1399
|
const stripped = statementsOf(body);
|
|
653
1400
|
return NARROWING.flatMap(([verb, pattern]) => {
|
|
654
1401
|
const found = pattern.exec(stripped);
|
|
655
|
-
return found === null ? [] : [{ line:
|
|
1402
|
+
return found === null ? [] : [{ line: lineOf2(stripped, found.index) + firstLine - 1, path, verb }];
|
|
656
1403
|
});
|
|
657
1404
|
};
|
|
658
1405
|
|
|
659
1406
|
// src/migrations.ts
|
|
660
|
-
import { mkdtempSync, readFileSync as
|
|
1407
|
+
import { mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
661
1408
|
import { tmpdir } from "os";
|
|
662
|
-
import { basename, join, resolve as
|
|
1409
|
+
import { basename, join as join5, resolve as resolve7 } from "path";
|
|
663
1410
|
|
|
664
1411
|
// src/squawk.ts
|
|
665
|
-
import { spawnSync as
|
|
666
|
-
import { existsSync as
|
|
667
|
-
import { dirname, resolve as
|
|
668
|
-
import { fileURLToPath } from "url";
|
|
669
|
-
var HERE =
|
|
1412
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
1413
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1414
|
+
import { dirname as dirname2, resolve as resolve6 } from "path";
|
|
1415
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1416
|
+
var HERE = dirname2(fileURLToPath2(import.meta.url));
|
|
670
1417
|
var PINNED = "2.63.0";
|
|
671
1418
|
var findSquawk = (from = HERE) => {
|
|
672
|
-
for (let dir = from; ; dir =
|
|
673
|
-
const candidate =
|
|
674
|
-
if (
|
|
675
|
-
if (
|
|
1419
|
+
for (let dir = from; ; dir = dirname2(dir)) {
|
|
1420
|
+
const candidate = resolve6(dir, "node_modules/.bin/squawk");
|
|
1421
|
+
if (existsSync5(candidate)) return candidate;
|
|
1422
|
+
if (dirname2(dir) === dir) break;
|
|
676
1423
|
}
|
|
677
1424
|
throw new CannotRun(
|
|
678
1425
|
`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:
|
|
@@ -685,7 +1432,7 @@ It is an optional peer: a repo whose release.migrations names only the sqlite di
|
|
|
685
1432
|
var squawkOn = (input) => {
|
|
686
1433
|
const { cwd, exclude, files } = input;
|
|
687
1434
|
if (files.length === 0) return { findings: "", ok: true };
|
|
688
|
-
const run =
|
|
1435
|
+
const run = spawnSync3(
|
|
689
1436
|
input.bin ?? findSquawk(),
|
|
690
1437
|
[...exclude.length === 0 ? [] : [`--exclude=${exclude.join(",")}`], ...files],
|
|
691
1438
|
{ cwd, encoding: "utf8" }
|
|
@@ -752,7 +1499,7 @@ var runMigrations = (input) => {
|
|
|
752
1499
|
const forSquawk = [];
|
|
753
1500
|
const states = /* @__PURE__ */ new Map();
|
|
754
1501
|
for (const { entry, file } of matched) {
|
|
755
|
-
const source =
|
|
1502
|
+
const source = readFileSync8(resolve7(input.root, file), "utf8");
|
|
756
1503
|
const found = markersIn(source);
|
|
757
1504
|
const bad = found.flatMap((one) => {
|
|
758
1505
|
const why = judge(one, input.root, input.since);
|
|
@@ -777,11 +1524,11 @@ var runMigrations = (input) => {
|
|
|
777
1524
|
}
|
|
778
1525
|
let clean = true;
|
|
779
1526
|
if (forSquawk.length > 0) {
|
|
780
|
-
const staging = mkdtempSync(
|
|
1527
|
+
const staging = mkdtempSync(join5(tmpdir(), "geonosis-release-squawk-"));
|
|
781
1528
|
try {
|
|
782
1529
|
for (const one of forSquawk) {
|
|
783
|
-
const path = one.entry.dialect === "mikro-orm-ts" ?
|
|
784
|
-
if (one.entry.dialect === "mikro-orm-ts")
|
|
1530
|
+
const path = one.entry.dialect === "mikro-orm-ts" ? join5(staging, `${basename(one.file, ".ts")}.sql`) : resolve7(input.root, one.file);
|
|
1531
|
+
if (one.entry.dialect === "mikro-orm-ts") writeFileSync3(path, `${one.sql}
|
|
785
1532
|
`);
|
|
786
1533
|
const found = squawkOn({
|
|
787
1534
|
cwd: input.root,
|
|
@@ -795,7 +1542,7 @@ var runMigrations = (input) => {
|
|
|
795
1542
|
}
|
|
796
1543
|
}
|
|
797
1544
|
} finally {
|
|
798
|
-
|
|
1545
|
+
rmSync2(staging, { force: true, recursive: true });
|
|
799
1546
|
}
|
|
800
1547
|
}
|
|
801
1548
|
return {
|
|
@@ -925,7 +1672,7 @@ var Runner = class {
|
|
|
925
1672
|
if (this.closed !== void 0) {
|
|
926
1673
|
throw new CannotRun(`the runner is gone before "${request.step}": ${this.closed}`);
|
|
927
1674
|
}
|
|
928
|
-
const line = await new Promise((
|
|
1675
|
+
const line = await new Promise((resolve10, reject) => {
|
|
929
1676
|
const timer = setTimeout(() => {
|
|
930
1677
|
reject(
|
|
931
1678
|
new CannotRun(
|
|
@@ -935,7 +1682,7 @@ var Runner = class {
|
|
|
935
1682
|
}, timeoutMs);
|
|
936
1683
|
this.waiting.push((answer) => {
|
|
937
1684
|
clearTimeout(timer);
|
|
938
|
-
|
|
1685
|
+
resolve10(answer);
|
|
939
1686
|
});
|
|
940
1687
|
this.child.on("close", () => {
|
|
941
1688
|
clearTimeout(timer);
|
|
@@ -959,8 +1706,8 @@ var Runner = class {
|
|
|
959
1706
|
};
|
|
960
1707
|
|
|
961
1708
|
// src/prove.ts
|
|
962
|
-
import { existsSync as
|
|
963
|
-
import { fileURLToPath as
|
|
1709
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1710
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
964
1711
|
var stringAt = (reply, key) => typeof reply[key] === "string" ? reply[key] : void 0;
|
|
965
1712
|
var proveOver = async (input) => {
|
|
966
1713
|
const runner = new Runner(input.command, input.args ?? [], input.cwd);
|
|
@@ -987,264 +1734,90 @@ var proveOver = async (input) => {
|
|
|
987
1734
|
}
|
|
988
1735
|
steps.push("promote");
|
|
989
1736
|
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));
|
|
1737
|
+
if (promoted["ok"] !== true) return refuse(`the promote of ${versionId} did not pass`);
|
|
1738
|
+
const went = stringAt(promoted, "promotedVersionId");
|
|
1739
|
+
if (went !== void 0 && went !== versionId) {
|
|
1740
|
+
return refuse(`${versionId} was proved and ${went} was promoted \u2014 nothing proved ${went}`);
|
|
1741
|
+
}
|
|
1742
|
+
return { ok: true, steps };
|
|
1743
|
+
} finally {
|
|
1744
|
+
runner.close();
|
|
1198
1745
|
}
|
|
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
1746
|
};
|
|
1211
|
-
var
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1747
|
+
var PLANTS = [
|
|
1748
|
+
{
|
|
1749
|
+
name: "answered",
|
|
1750
|
+
says: "a smoke that answered a version other than the one it overrode to is refused"
|
|
1751
|
+
},
|
|
1752
|
+
{ name: "failed", says: "a smoke that did not pass is refused" },
|
|
1753
|
+
{ name: "promoted", says: "a promote of a version nothing proved is refused" }
|
|
1754
|
+
];
|
|
1755
|
+
var stubPath = () => {
|
|
1756
|
+
const path = fileURLToPath3(new URL("./stub-runner.js", import.meta.url));
|
|
1757
|
+
if (!existsSync6(path)) throw new CannotRun(`${path} is missing \u2014 run pnpm build first`);
|
|
1758
|
+
return path;
|
|
1759
|
+
};
|
|
1760
|
+
var prove = async (cwd, stub = stubPath()) => {
|
|
1761
|
+
const lines2 = [];
|
|
1762
|
+
let ok = true;
|
|
1763
|
+
for (const plant of PLANTS) {
|
|
1764
|
+
const verdict = await proveOver({
|
|
1765
|
+
args: [stub, `--plant=${plant.name}`],
|
|
1766
|
+
command: process.execPath,
|
|
1767
|
+
cwd,
|
|
1768
|
+
timeoutMs: 3e4
|
|
1769
|
+
});
|
|
1770
|
+
if (verdict.ok) ok = false;
|
|
1771
|
+
lines2.push(` ${verdict.ok ? "MISSED" : "PROVEN"} ${plant.says}`);
|
|
1772
|
+
if (plant.name !== "promoted" && verdict.steps.includes("promote")) {
|
|
1773
|
+
ok = false;
|
|
1774
|
+
lines2.push(" MISSED a refused smoke was followed by a promote request");
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
const honest = await proveOver({
|
|
1778
|
+
args: [stub, "--plant=good"],
|
|
1779
|
+
command: process.execPath,
|
|
1780
|
+
cwd,
|
|
1781
|
+
timeoutMs: 3e4
|
|
1782
|
+
});
|
|
1783
|
+
if (!honest.ok) ok = false;
|
|
1784
|
+
lines2.push(
|
|
1785
|
+
` ${honest.ok ? "PROVEN" : "MISSED"} a smoke that answered the version it overrode to is not refused`
|
|
1786
|
+
);
|
|
1787
|
+
return { lines: lines2, ok };
|
|
1222
1788
|
};
|
|
1789
|
+
var formatProve = (outcome) => `${outcome.lines.join("\n")}
|
|
1790
|
+
|
|
1791
|
+
prove ${outcome.ok ? "PASS" : "FAIL"} \u2014 ${outcome.ok ? "every plant was refused, and the honest runner was not" : "a plant went through"}.
|
|
1792
|
+
`;
|
|
1793
|
+
var formatVerdict = (verdict) => verdict.ok ? `OK prove: ${verdict.steps.join(" \u2192 ")}
|
|
1794
|
+
` : ` REFUSED after ${verdict.steps.join(" \u2192 ")}: ${verdict.why ?? ""}
|
|
1795
|
+
`;
|
|
1223
1796
|
|
|
1224
1797
|
// src/schema-walls.ts
|
|
1225
|
-
import { readdirSync as
|
|
1226
|
-
import { join as
|
|
1798
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
|
|
1799
|
+
import { join as join6, relative as relative3, resolve as resolve8, sep as sep2 } from "path";
|
|
1227
1800
|
var MIGRATION_FILE = /\.(?:sql|ts)$/;
|
|
1228
1801
|
var CURRENT_SETTING = /current_setting\s*\(\s*'([^']+)'/gi;
|
|
1229
|
-
var
|
|
1802
|
+
var filesUnder2 = (root, dir) => {
|
|
1230
1803
|
const found = [];
|
|
1231
1804
|
const walk = (at) => {
|
|
1232
1805
|
let entries;
|
|
1233
1806
|
try {
|
|
1234
|
-
entries =
|
|
1807
|
+
entries = readdirSync3(at, { withFileTypes: true });
|
|
1235
1808
|
} catch {
|
|
1236
1809
|
return;
|
|
1237
1810
|
}
|
|
1238
1811
|
for (const entry of entries) {
|
|
1239
|
-
const path =
|
|
1812
|
+
const path = join6(at, entry.name);
|
|
1240
1813
|
if (entry.isDirectory()) walk(path);
|
|
1241
1814
|
else if (MIGRATION_FILE.test(entry.name)) found.push(path);
|
|
1242
1815
|
}
|
|
1243
1816
|
};
|
|
1244
|
-
walk(
|
|
1245
|
-
return found.map((path) =>
|
|
1817
|
+
walk(resolve8(root, dir));
|
|
1818
|
+
return found.map((path) => relative3(root, path).split(sep2).join("/")).toSorted();
|
|
1246
1819
|
};
|
|
1247
|
-
var
|
|
1820
|
+
var lineOf3 = (body, index) => body.slice(0, index).split("\n").length;
|
|
1248
1821
|
var wordFor = (table2) => new RegExp(
|
|
1249
1822
|
String.raw`(?<![\w.])${table2.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)}\b`,
|
|
1250
1823
|
"i"
|
|
@@ -1258,7 +1831,7 @@ var foreignTables = (path, sql, schema) => {
|
|
|
1258
1831
|
const found = wordFor(table2).exec(sql);
|
|
1259
1832
|
return found === null ? [] : [
|
|
1260
1833
|
{
|
|
1261
|
-
line:
|
|
1834
|
+
line: lineOf3(sql, found.index),
|
|
1262
1835
|
path,
|
|
1263
1836
|
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
1837
|
}
|
|
@@ -1274,404 +1847,98 @@ var undeclaredSettings = (path, source, schema) => {
|
|
|
1274
1847
|
const name = match[1] ?? "";
|
|
1275
1848
|
if (allowed.has(name)) continue;
|
|
1276
1849
|
found.push({
|
|
1277
|
-
line:
|
|
1850
|
+
line: lineOf3(source, match.index),
|
|
1278
1851
|
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
|
-
);
|
|
1852
|
+
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`
|
|
1853
|
+
});
|
|
1484
1854
|
}
|
|
1485
|
-
return
|
|
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;
|
|
1855
|
+
return found;
|
|
1491
1856
|
};
|
|
1492
|
-
var
|
|
1493
|
-
|
|
1494
|
-
if (
|
|
1857
|
+
var runSchema = ({ root }) => {
|
|
1858
|
+
const schema = readReleaseConfig(root).schema;
|
|
1859
|
+
if (schema === void 0) {
|
|
1495
1860
|
throw new CannotRun(
|
|
1496
|
-
|
|
1861
|
+
"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)"
|
|
1497
1862
|
);
|
|
1498
1863
|
}
|
|
1499
|
-
|
|
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))) {
|
|
1864
|
+
if (schema.domains.length === 0 && schema.sessionVariables.length === 0) {
|
|
1505
1865
|
throw new CannotRun(
|
|
1506
|
-
|
|
1866
|
+
"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"
|
|
1507
1867
|
);
|
|
1508
1868
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1869
|
+
const dirs = schema.domains.map((one) => one.dir);
|
|
1870
|
+
const files = [...new Set(dirs.flatMap((dir) => filesUnder2(root, dir)))].toSorted();
|
|
1871
|
+
const findings = [];
|
|
1872
|
+
const unreadable = [];
|
|
1873
|
+
for (const path of files) {
|
|
1874
|
+
const source = readFileSync9(resolve8(root, path), "utf8");
|
|
1875
|
+
let sql;
|
|
1876
|
+
try {
|
|
1877
|
+
sql = sqlOf(path, source);
|
|
1878
|
+
} catch (error) {
|
|
1879
|
+
if (!(error instanceof Unreadable)) throw error;
|
|
1880
|
+
unreadable.push({ path, why: `${path} holds ${error.message} \u2014 this reader cannot read it` });
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
findings.push(...foreignTables(path, sql, schema), ...undeclaredSettings(path, source, schema));
|
|
1513
1884
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
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,
|
|
1885
|
+
return {
|
|
1886
|
+
dirs,
|
|
1525
1887
|
files,
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
name: input.name
|
|
1888
|
+
findings: findings.toSorted((a, b) => a.path.localeCompare(b.path) || a.line - b.line),
|
|
1889
|
+
ok: findings.length === 0 && unreadable.length === 0,
|
|
1890
|
+
read: files.length - unreadable.length,
|
|
1891
|
+
unreadable
|
|
1531
1892
|
};
|
|
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
1893
|
};
|
|
1562
|
-
var
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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
|
-
);
|
|
1894
|
+
var formatSchema = (report) => {
|
|
1895
|
+
if (report.files.length === 0) {
|
|
1896
|
+
return `NONE schema: no migration file under ${report.dirs.join(", ")}; nothing was read
|
|
1897
|
+
`;
|
|
1583
1898
|
}
|
|
1584
|
-
if (
|
|
1585
|
-
|
|
1899
|
+
if (report.ok) {
|
|
1900
|
+
return `OK schema: ${report.read} migration(s) read under ${report.dirs.length} declared domain(s), no wall crossed
|
|
1901
|
+
`;
|
|
1586
1902
|
}
|
|
1587
|
-
const
|
|
1588
|
-
mkdirSync2(dirname2(at), { recursive: true });
|
|
1589
|
-
writeFileSync3(at, `${JSON.stringify(envelope, void 0, 2)}
|
|
1903
|
+
const said = report.findings.map((one) => ` ${one.path}:${one.line}: ${one.why}
|
|
1590
1904
|
`);
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
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
|
-
}
|
|
1905
|
+
const cannot = report.unreadable.map((one) => ` ${one.why}
|
|
1906
|
+
`);
|
|
1907
|
+
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`}
|
|
1908
|
+
`;
|
|
1611
1909
|
};
|
|
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
1910
|
|
|
1652
1911
|
// src/smoke-run.ts
|
|
1653
1912
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
1654
|
-
import { existsSync as
|
|
1655
|
-
import { join as
|
|
1913
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync10, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1914
|
+
import { join as join7, relative as relative4, resolve as resolve9 } from "path";
|
|
1656
1915
|
var BASELINE_FILE = "baseline.json";
|
|
1657
1916
|
var INSTALL = {
|
|
1658
1917
|
bun: ["install"],
|
|
1659
1918
|
pnpm: ["install", "--no-frozen-lockfile"]
|
|
1660
1919
|
};
|
|
1920
|
+
var MANAGER_CONFIG = [".npmrc", "bunfig.toml", "pnpm-workspace.yaml"];
|
|
1921
|
+
var configBytesOf = (work) => Object.fromEntries(
|
|
1922
|
+
MANAGER_CONFIG.filter((file) => existsSync7(join7(work, file))).map((file) => [
|
|
1923
|
+
file,
|
|
1924
|
+
readFileSync10(join7(work, file), "utf8")
|
|
1925
|
+
])
|
|
1926
|
+
);
|
|
1927
|
+
var editedByInstall = (before, after) => [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter((file) => before[file] !== after[file]).toSorted();
|
|
1661
1928
|
var isRecord5 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
1662
1929
|
var readJson = (path) => {
|
|
1663
1930
|
try {
|
|
1664
|
-
return JSON.parse(
|
|
1931
|
+
return JSON.parse(readFileSync10(path, "utf8"));
|
|
1665
1932
|
} catch (error) {
|
|
1666
1933
|
throw new CannotRun(`${path} is not readable JSON: ${error.message}`);
|
|
1667
1934
|
}
|
|
1668
1935
|
};
|
|
1669
1936
|
var packRc = (rc, into) => {
|
|
1670
|
-
if (!
|
|
1937
|
+
if (!existsSync7(join7(rc, "package.json"))) {
|
|
1671
1938
|
throw new CannotRun(`${rc} has no package.json \u2014 that is not a workspace to pack`);
|
|
1672
1939
|
}
|
|
1673
1940
|
mkdirSync3(into, { recursive: true });
|
|
1674
|
-
const root = readJson(
|
|
1941
|
+
const root = readJson(join7(rc, "package.json"));
|
|
1675
1942
|
const isPrivateRoot = isRecord5(root) && root["private"] === true;
|
|
1676
1943
|
const done = spawnSync4(
|
|
1677
1944
|
"pnpm",
|
|
@@ -1721,11 +1988,11 @@ var sourceManifests = (rc) => {
|
|
|
1721
1988
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
1722
1989
|
if (excluded.has(entry.name)) continue;
|
|
1723
1990
|
if (entry.isDirectory()) {
|
|
1724
|
-
walk(
|
|
1991
|
+
walk(join7(dir, entry.name));
|
|
1725
1992
|
continue;
|
|
1726
1993
|
}
|
|
1727
1994
|
if (entry.name !== "package.json") continue;
|
|
1728
|
-
const manifest = readJson(
|
|
1995
|
+
const manifest = readJson(join7(dir, entry.name));
|
|
1729
1996
|
if (!isRecord5(manifest) || typeof manifest["name"] !== "string") continue;
|
|
1730
1997
|
found.push({ manifest, name: manifest["name"] });
|
|
1731
1998
|
}
|
|
@@ -1747,9 +2014,9 @@ var OVERRIDES = "overrides:";
|
|
|
1747
2014
|
var TOP_LEVEL = /^\S/;
|
|
1748
2015
|
var KEYED = /^\s+["']?(.+?)["']?\s*:/;
|
|
1749
2016
|
var pinsInWorkspaceYaml = (work, pins) => {
|
|
1750
|
-
const at =
|
|
2017
|
+
const at = join7(work, "pnpm-workspace.yaml");
|
|
1751
2018
|
const written = Object.entries(pins).map(([name, spec]) => ` '${name}': '${spec}'`);
|
|
1752
|
-
const lines2 =
|
|
2019
|
+
const lines2 = existsSync7(at) ? readFileSync10(at, "utf8").split("\n") : [];
|
|
1753
2020
|
const kept = [];
|
|
1754
2021
|
let inside = false;
|
|
1755
2022
|
let found = false;
|
|
@@ -1768,8 +2035,8 @@ var pinsInWorkspaceYaml = (work, pins) => {
|
|
|
1768
2035
|
writeFileSync4(at, body.join("\n"));
|
|
1769
2036
|
};
|
|
1770
2037
|
var pinsInManifest = (work, pins) => {
|
|
1771
|
-
const at =
|
|
1772
|
-
if (!
|
|
2038
|
+
const at = join7(work, "package.json");
|
|
2039
|
+
if (!existsSync7(at)) return;
|
|
1773
2040
|
const manifest = readJson(at);
|
|
1774
2041
|
if (!isRecord5(manifest)) return;
|
|
1775
2042
|
manifest["overrides"] = {
|
|
@@ -1789,8 +2056,8 @@ var rewriteManifests = (tree, manifests, packed) => {
|
|
|
1789
2056
|
const byName = new Map(packed.map((one) => [one.name, one.tarball]));
|
|
1790
2057
|
const swapped = [];
|
|
1791
2058
|
for (const path of manifests) {
|
|
1792
|
-
const at =
|
|
1793
|
-
if (!
|
|
2059
|
+
const at = join7(tree, path);
|
|
2060
|
+
if (!existsSync7(at)) continue;
|
|
1794
2061
|
const manifest = readJson(at);
|
|
1795
2062
|
if (!isRecord5(manifest)) continue;
|
|
1796
2063
|
let touched = false;
|
|
@@ -1826,20 +2093,26 @@ var runPhase = (phase, command, cwd, manager) => {
|
|
|
1826
2093
|
const line = commandLine(command, manager);
|
|
1827
2094
|
const env = {
|
|
1828
2095
|
...process.env,
|
|
1829
|
-
PATH: `${
|
|
2096
|
+
PATH: `${join7(cwd, "node_modules/.bin")}:${process.env["PATH"] ?? ""}`
|
|
1830
2097
|
};
|
|
1831
2098
|
const { code, output } = shellRun(line, cwd, env);
|
|
1832
2099
|
return { code, command: line, lines: quotable(output), ok: code === 0, phase };
|
|
1833
2100
|
};
|
|
2101
|
+
var commandsToRun = (record, declared) => Object.fromEntries(
|
|
2102
|
+
PHASES.map((phase) => {
|
|
2103
|
+
const named = declared[phase];
|
|
2104
|
+
return [phase, named === void 0 ? record.commands[phase] : { exec: named }];
|
|
2105
|
+
})
|
|
2106
|
+
);
|
|
1834
2107
|
var smokeOver = (input) => {
|
|
1835
2108
|
const record = readSnapshot(input.root, input.name);
|
|
1836
2109
|
const at = snapshotDir(input.root, input.name);
|
|
1837
|
-
const work =
|
|
2110
|
+
const work = join7(at, "run");
|
|
1838
2111
|
rmSync3(work, { force: true, recursive: true });
|
|
1839
|
-
copyInto(
|
|
2112
|
+
copyInto(join7(at, "tree"), work, new Set(EXCLUDED_DIRS));
|
|
1840
2113
|
ownRepository(work);
|
|
1841
|
-
const packed = input.rc === void 0 ? [] : packRc(
|
|
1842
|
-
const findings = input.rc === void 0 ? [] : promisedButNotPacked(
|
|
2114
|
+
const packed = input.rc === void 0 ? [] : packRc(resolve9(input.rc), join7(at, "rc"));
|
|
2115
|
+
const findings = input.rc === void 0 ? [] : promisedButNotPacked(resolve9(input.rc), packed);
|
|
1843
2116
|
const swapped = rewriteManifests(
|
|
1844
2117
|
work,
|
|
1845
2118
|
record.manifests.map((one) => one.path),
|
|
@@ -1852,6 +2125,7 @@ var smokeOver = (input) => {
|
|
|
1852
2125
|
}
|
|
1853
2126
|
overrideEveryCopy(work, record.manager, packed);
|
|
1854
2127
|
const install = input.install ?? `${record.manager} ${INSTALL[record.manager].join(" ")}`;
|
|
2128
|
+
const configBefore = configBytesOf(work);
|
|
1855
2129
|
const installed = shellRun(install, work, process.env);
|
|
1856
2130
|
if (installed.code !== 0) {
|
|
1857
2131
|
throw new CannotRun(
|
|
@@ -1859,9 +2133,15 @@ var smokeOver = (input) => {
|
|
|
1859
2133
|
${quotable(installed.output).join("\n")}`
|
|
1860
2134
|
);
|
|
1861
2135
|
}
|
|
2136
|
+
for (const file of editedByInstall(configBefore, configBytesOf(work))) {
|
|
2137
|
+
findings.push(
|
|
2138
|
+
`\`${install}\` rewrote ${file} in the copy \u2014 every phase below ran against a tree that is no longer byte-for-byte "${input.name}", and the same install in their own tree would edit theirs`
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
2141
|
+
const commands = commandsToRun(record, input.commands ?? {});
|
|
1862
2142
|
return {
|
|
1863
2143
|
findings,
|
|
1864
|
-
outcomes: PHASES.map((phase) => runPhase(phase,
|
|
2144
|
+
outcomes: PHASES.map((phase) => runPhase(phase, commands[phase], work, record.manager)),
|
|
1865
2145
|
packed
|
|
1866
2146
|
};
|
|
1867
2147
|
};
|
|
@@ -1873,14 +2153,14 @@ var ownRepository = (work) => {
|
|
|
1873
2153
|
);
|
|
1874
2154
|
}
|
|
1875
2155
|
};
|
|
1876
|
-
var baselinePath = (root, name) =>
|
|
2156
|
+
var baselinePath = (root, name) => join7(snapshotDir(root, name), BASELINE_FILE);
|
|
1877
2157
|
var recordBaseline = (input) => {
|
|
1878
2158
|
const { outcomes, packed } = smokeOver(input);
|
|
1879
2159
|
const baseline = {
|
|
1880
2160
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1881
2161
|
packed: packed.map((one) => ({ name: one.name, version: one.version })),
|
|
1882
2162
|
phases: outcomes,
|
|
1883
|
-
rc: input.rc === void 0 ? "the versions the tree names" :
|
|
2163
|
+
rc: input.rc === void 0 ? "the versions the tree names" : resolve9(input.rc)
|
|
1884
2164
|
};
|
|
1885
2165
|
writeFileSync4(baselinePath(input.root, input.name), `${JSON.stringify(baseline, void 0, 2)}
|
|
1886
2166
|
`);
|
|
@@ -1888,7 +2168,7 @@ var recordBaseline = (input) => {
|
|
|
1888
2168
|
};
|
|
1889
2169
|
var readBaseline = (root, name) => {
|
|
1890
2170
|
const at = baselinePath(root, name);
|
|
1891
|
-
if (!
|
|
2171
|
+
if (!existsSync7(at)) {
|
|
1892
2172
|
throw new CannotRun(
|
|
1893
2173
|
`"${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
2174
|
);
|
|
@@ -1942,57 +2222,118 @@ var runSmoke = (input) => {
|
|
|
1942
2222
|
const { findings, outcomes } = smokeOver(input);
|
|
1943
2223
|
return compareToBaseline(input.name, baseline, outcomes, findings);
|
|
1944
2224
|
};
|
|
2225
|
+
var CONSUMER_TREES_FILE = ".geonosis/consumer-trees.local.json";
|
|
2226
|
+
var localTrees = (root) => {
|
|
2227
|
+
const at = join7(root, CONSUMER_TREES_FILE);
|
|
2228
|
+
if (!existsSync7(at)) return {};
|
|
2229
|
+
const parsed = readJson(at);
|
|
2230
|
+
if (!isRecord5(parsed)) {
|
|
2231
|
+
throw new CannotRun(`${at} must be an object of { "<snapshot name>": "<absolute path>" }`);
|
|
2232
|
+
}
|
|
2233
|
+
return Object.fromEntries(
|
|
2234
|
+
Object.entries(parsed).filter((one) => typeof one[1] === "string")
|
|
2235
|
+
);
|
|
2236
|
+
};
|
|
2237
|
+
var recordedAlready = (root, name) => existsSync7(join7(snapshotDir(root, name), "snapshot.json"));
|
|
2238
|
+
var unrecordedButHere = (root, wanted, absent) => {
|
|
2239
|
+
const trees = localTrees(root);
|
|
2240
|
+
return wanted.filter((one) => !absent.includes(one.name) && !recordedAlready(root, one.name)).flatMap((one) => {
|
|
2241
|
+
const path = trees[one.name];
|
|
2242
|
+
if (path === void 0 || !existsSync7(path)) return [];
|
|
2243
|
+
return [
|
|
2244
|
+
`${one.name} is declared and its tree is here (${path}) \u2014 record it: geonosis-release smoke snapshot ${one.name} --from ${path}`
|
|
2245
|
+
];
|
|
2246
|
+
});
|
|
2247
|
+
};
|
|
2248
|
+
var dating = (root, name) => {
|
|
2249
|
+
const record = readSnapshot(root, name);
|
|
2250
|
+
const commit = record.commit;
|
|
2251
|
+
if (commit === void 0) {
|
|
2252
|
+
return {
|
|
2253
|
+
undated: `UNDATED ${name}: recorded before source commits were kept \u2014 re-record it: geonosis-release smoke snapshot ${name} --from ${record.from}`
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
if (!("sha" in commit) || !existsSync7(record.from)) return void 0;
|
|
2257
|
+
const now = headOf(record.from);
|
|
2258
|
+
if (!("sha" in now) || now.sha === commit.sha) return void 0;
|
|
2259
|
+
return { stale: `STALE ${name}: recorded at ${commit.sha}, the tree is at ${now.sha}` };
|
|
2260
|
+
};
|
|
1945
2261
|
var sweepSmoke = (input) => {
|
|
1946
|
-
const
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
if (
|
|
1950
|
-
|
|
2262
|
+
const absent = input.absent ?? [];
|
|
2263
|
+
if (input.skipMissing) {
|
|
2264
|
+
const owed = unrecordedButHere(input.root, input.wanted, absent);
|
|
2265
|
+
if (owed.length > 0) throw new CannotRun(owed.join("\n"));
|
|
2266
|
+
}
|
|
2267
|
+
const entries = input.wanted.map((one) => {
|
|
2268
|
+
if (absent.includes(one.name)) {
|
|
2269
|
+
return {
|
|
2270
|
+
name: one.name,
|
|
2271
|
+
why: "--absent named it, so this release was not read against it"
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
if (input.skipMissing && !recordedAlready(input.root, one.name)) {
|
|
2275
|
+
return {
|
|
1951
2276
|
name: one.name,
|
|
1952
2277
|
why: `no snapshot of it here \u2014 record one with: geonosis-release smoke snapshot ${one.name} --from <their tree>`
|
|
1953
|
-
}
|
|
1954
|
-
continue;
|
|
2278
|
+
};
|
|
1955
2279
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
2280
|
+
const note = dating(input.root, one.name);
|
|
2281
|
+
return {
|
|
2282
|
+
comparison: runSmoke({
|
|
2283
|
+
...one.commands === void 0 ? {} : { commands: one.commands },
|
|
1958
2284
|
...one.install === void 0 ? {} : { install: one.install },
|
|
1959
2285
|
name: one.name,
|
|
1960
2286
|
...input.rc === void 0 ? {} : { rc: input.rc },
|
|
1961
2287
|
root: input.root
|
|
1962
|
-
})
|
|
1963
|
-
|
|
2288
|
+
}),
|
|
2289
|
+
name: one.name,
|
|
2290
|
+
...note === void 0 ? {} : { note }
|
|
2291
|
+
};
|
|
2292
|
+
});
|
|
2293
|
+
return { entries };
|
|
2294
|
+
};
|
|
2295
|
+
var comparisonsIn = (sweep) => sweep.entries.flatMap((one) => "comparison" in one ? [one.comparison] : []);
|
|
2296
|
+
var EXCUSED = "EXCUSED";
|
|
2297
|
+
var noteLine = (note) => "stale" in note ? note.stale : note.undated;
|
|
2298
|
+
var entryBlock = (entry) => {
|
|
2299
|
+
if (!("comparison" in entry)) {
|
|
2300
|
+
const head = entry.why.startsWith("--absent") ? EXCUSED : "SKIP ";
|
|
2301
|
+
return `${head} smoke ${entry.name}: ${entry.why}
|
|
2302
|
+
`;
|
|
1964
2303
|
}
|
|
1965
|
-
return {
|
|
2304
|
+
return `${formatSmoke(entry.comparison)}${entry.note === void 0 ? "" : `${noteLine(entry.note)}
|
|
2305
|
+
`}`;
|
|
1966
2306
|
};
|
|
1967
|
-
var formatSweep = (sweep) =>
|
|
1968
|
-
...sweep.comparisons.map(formatSmoke),
|
|
1969
|
-
...sweep.skipped.map((one) => `SKIP smoke ${one.name}: ${one.why}
|
|
1970
|
-
`)
|
|
1971
|
-
].join("");
|
|
2307
|
+
var formatSweep = (sweep) => sweep.entries.map(entryBlock).join("");
|
|
1972
2308
|
var sweepEnvelope = (sweep, durationMs) => {
|
|
1973
|
-
const
|
|
2309
|
+
const excused = [];
|
|
2310
|
+
const findings = [];
|
|
2311
|
+
const refused = [];
|
|
2312
|
+
let considered = 0;
|
|
2313
|
+
let read = 0;
|
|
2314
|
+
for (const entry of sweep.entries) {
|
|
2315
|
+
if (!("comparison" in entry)) {
|
|
2316
|
+
considered += PHASES.length;
|
|
2317
|
+
excused.push(
|
|
2318
|
+
...PHASES.map((phase) => ({ path: `${entry.name}/${phase}`, reason: entry.why }))
|
|
2319
|
+
);
|
|
2320
|
+
continue;
|
|
2321
|
+
}
|
|
2322
|
+
const part = smokeEnvelope(entry.comparison, 0);
|
|
2323
|
+
considered += part.considered;
|
|
2324
|
+
read += part.read;
|
|
2325
|
+
excused.push(...part.excused.map((one) => ({ ...one, path: `${entry.name}/${one.path}` })));
|
|
2326
|
+
refused.push(...part.refused.map((one) => ({ ...one, path: `${entry.name}/${one.path}` })));
|
|
2327
|
+
findings.push(...part.findings);
|
|
2328
|
+
if (entry.note !== void 0) findings.push(entry.note);
|
|
2329
|
+
}
|
|
1974
2330
|
return {
|
|
1975
|
-
considered
|
|
2331
|
+
considered,
|
|
1976
2332
|
durationMs,
|
|
1977
|
-
excused
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
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
|
-
),
|
|
2333
|
+
excused,
|
|
2334
|
+
findings,
|
|
2335
|
+
read,
|
|
2336
|
+
refused,
|
|
1996
2337
|
tool: SMOKE_TOOL,
|
|
1997
2338
|
version: versionOf(import.meta.url)
|
|
1998
2339
|
};
|
|
@@ -2062,9 +2403,36 @@ var smokeEnvelope = (comparison, durationMs) => {
|
|
|
2062
2403
|
};
|
|
2063
2404
|
|
|
2064
2405
|
export {
|
|
2406
|
+
PHASES,
|
|
2065
2407
|
CannotRun,
|
|
2066
2408
|
parseReleaseConfig,
|
|
2067
2409
|
readReleaseConfig,
|
|
2410
|
+
writeEnvelope,
|
|
2411
|
+
MIGRATIONS_NEXT,
|
|
2412
|
+
migrationsEnvelope,
|
|
2413
|
+
SCHEMA_NEXT,
|
|
2414
|
+
schemaEnvelope,
|
|
2415
|
+
PUBLISHED_NEXT,
|
|
2416
|
+
publishedEnvelope,
|
|
2417
|
+
DEFAULT_REGISTRY,
|
|
2418
|
+
registryPathOf,
|
|
2419
|
+
askRegistry,
|
|
2420
|
+
censusOf,
|
|
2421
|
+
EXISTS_BUT,
|
|
2422
|
+
runPublished,
|
|
2423
|
+
formatPublished,
|
|
2424
|
+
SNAPSHOTS_DIR,
|
|
2425
|
+
EXCLUDED_DIRS,
|
|
2426
|
+
headOf,
|
|
2427
|
+
snapshotDir,
|
|
2428
|
+
readSnapshot,
|
|
2429
|
+
runSnapshot,
|
|
2430
|
+
formatSnapshot,
|
|
2431
|
+
runAdoption,
|
|
2432
|
+
formatAdoption,
|
|
2433
|
+
ADOPTION_TOOL,
|
|
2434
|
+
ADOPTION_NEXT,
|
|
2435
|
+
adoptionEnvelope,
|
|
2068
2436
|
parseJsonc,
|
|
2069
2437
|
parseToml,
|
|
2070
2438
|
declaredIn,
|
|
@@ -2092,29 +2460,8 @@ export {
|
|
|
2092
2460
|
prove,
|
|
2093
2461
|
formatProve,
|
|
2094
2462
|
formatVerdict,
|
|
2095
|
-
DEFAULT_REGISTRY,
|
|
2096
|
-
registryPathOf,
|
|
2097
|
-
askRegistry,
|
|
2098
|
-
censusOf,
|
|
2099
|
-
EXISTS_BUT,
|
|
2100
|
-
runPublished,
|
|
2101
|
-
formatPublished,
|
|
2102
2463
|
runSchema,
|
|
2103
2464
|
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
2465
|
BASELINE_FILE,
|
|
2119
2466
|
packRc,
|
|
2120
2467
|
promisedButNotPacked,
|
|
@@ -2123,7 +2470,9 @@ export {
|
|
|
2123
2470
|
readBaseline,
|
|
2124
2471
|
compareToBaseline,
|
|
2125
2472
|
runSmoke,
|
|
2473
|
+
CONSUMER_TREES_FILE,
|
|
2126
2474
|
sweepSmoke,
|
|
2475
|
+
comparisonsIn,
|
|
2127
2476
|
formatSweep,
|
|
2128
2477
|
sweepEnvelope,
|
|
2129
2478
|
formatSmoke,
|