@geonosis/release 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/{chunk-MZWAN6WT.js → chunk-7H7SV5EP.js} +260 -46
- package/dist/index.d.ts +143 -65
- package/dist/index.js +13 -1
- package/dist/release-cli.js +44 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# @geonosis/release
|
|
2
2
|
|
|
3
|
+
Through the front door: `geonosis release` — the metapackage pins this and every other kit tool at
|
|
4
|
+
ONE version, and passes the exit code through unchanged.
|
|
5
|
+
|
|
3
6
|
The release contract with its proofs.
|
|
4
7
|
|
|
5
8
|
A release proves its **code** before serving it. Its **schema** serves immediately, to 100 % of
|
|
@@ -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";
|
|
@@ -96,17 +99,15 @@ var parseSchema = (value2) => {
|
|
|
96
99
|
};
|
|
97
100
|
var SMOKE_KEYS = "expected { exclude?, snapshots? }";
|
|
98
101
|
var SNAPSHOT_KEYS = "expected { commands?, install?, name }";
|
|
99
|
-
var SMOKE_PHASES =
|
|
102
|
+
var SMOKE_PHASES = PHASES;
|
|
100
103
|
var parseCommands = (value2, at) => {
|
|
101
104
|
if (value2 === void 0) return {};
|
|
102
105
|
if (!isRecord(value2)) {
|
|
103
106
|
throw new CannotRun(
|
|
104
|
-
`${at}.commands must be an object \u2014 expected
|
|
107
|
+
`${at}.commands must be an object \u2014 expected one command per phase: ${PHASES.map((one) => `${one}?`).join(", ")}`
|
|
105
108
|
);
|
|
106
109
|
}
|
|
107
|
-
const unknown = Object.keys(value2).find(
|
|
108
|
-
(key) => !SMOKE_PHASES.includes(key)
|
|
109
|
-
);
|
|
110
|
+
const unknown = Object.keys(value2).find((key) => !SMOKE_PHASES.includes(key));
|
|
110
111
|
if (unknown !== void 0) {
|
|
111
112
|
throw new CannotRun(
|
|
112
113
|
`${at}.commands.${unknown} is not a phase the smoke runs \u2014 it runs ${SMOKE_PHASES.join(", ")}`
|
|
@@ -210,7 +211,7 @@ var readReleaseConfig = (root) => {
|
|
|
210
211
|
|
|
211
212
|
// src/envelope.ts
|
|
212
213
|
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
213
|
-
import { dirname, join } from "path";
|
|
214
|
+
import { dirname, join, resolve as resolve2 } from "path";
|
|
214
215
|
import { fileURLToPath } from "url";
|
|
215
216
|
var ENVELOPES_DIR = ".geonosis/envelopes";
|
|
216
217
|
var envelopePath = (root, tool) => join(root, ENVELOPES_DIR, `${tool}.json`);
|
|
@@ -222,6 +223,14 @@ var UnbalancedEnvelope = class extends Error {
|
|
|
222
223
|
};
|
|
223
224
|
var isCount = (value2) => Number.isSafeInteger(value2) && value2 >= 0;
|
|
224
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
|
+
);
|
|
233
|
+
};
|
|
225
234
|
var writeEnvelope = ({
|
|
226
235
|
envelope,
|
|
227
236
|
next,
|
|
@@ -245,6 +254,7 @@ var writeEnvelope = ({
|
|
|
245
254
|
if (envelope.considered !== envelope.read + envelope.refused.length + envelope.excused.length) {
|
|
246
255
|
throw new UnbalancedEnvelope(unbalancedMessage(envelope, next));
|
|
247
256
|
}
|
|
257
|
+
refuseForbiddenRoot(root);
|
|
248
258
|
const at = envelopePath(root, envelope.tool);
|
|
249
259
|
mkdirSync(dirname(at), { recursive: true });
|
|
250
260
|
writeFileSync(at, `${JSON.stringify(envelope, void 0, 2)}
|
|
@@ -492,11 +502,12 @@ import {
|
|
|
492
502
|
mkdirSync as mkdirSync2,
|
|
493
503
|
readdirSync as readdirSync2,
|
|
494
504
|
readFileSync as readFileSync4,
|
|
505
|
+
renameSync,
|
|
495
506
|
rmSync,
|
|
496
507
|
statSync,
|
|
497
508
|
writeFileSync as writeFileSync2
|
|
498
509
|
} from "fs";
|
|
499
|
-
import { join as join3, relative as relative2, resolve as
|
|
510
|
+
import { join as join3, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
|
|
500
511
|
var SNAPSHOTS_DIR = ".geonosis/consumer-snapshots";
|
|
501
512
|
var LOCKFILES = [
|
|
502
513
|
{ file: "bun.lock", manager: "bun" },
|
|
@@ -509,6 +520,7 @@ var UNMEASURED = [
|
|
|
509
520
|
];
|
|
510
521
|
var EXCLUDED_DIRS = [
|
|
511
522
|
".cache",
|
|
523
|
+
".claude",
|
|
512
524
|
".geonosis",
|
|
513
525
|
".git",
|
|
514
526
|
".next",
|
|
@@ -519,7 +531,6 @@ var EXCLUDED_DIRS = [
|
|
|
519
531
|
"node_modules",
|
|
520
532
|
"storybook-static"
|
|
521
533
|
];
|
|
522
|
-
var PHASES = ["typecheck", "lint", "doctor"];
|
|
523
534
|
var headOf = (from) => {
|
|
524
535
|
const done = spawnSync("git", ["-C", from, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
525
536
|
if (done.error !== void 0) {
|
|
@@ -564,12 +575,17 @@ var scriptsIn = (manifest) => {
|
|
|
564
575
|
var copyInto = (from, to, excluded) => {
|
|
565
576
|
let bytes = 0;
|
|
566
577
|
let files = 0;
|
|
578
|
+
const nested = [];
|
|
567
579
|
const walk = (dir, into) => {
|
|
568
580
|
mkdirSync2(into, { recursive: true });
|
|
569
581
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
570
582
|
if (excluded.has(entry.name)) continue;
|
|
571
583
|
const at = join3(dir, entry.name);
|
|
572
584
|
if (entry.isDirectory()) {
|
|
585
|
+
if (existsSync2(join3(at, ".git"))) {
|
|
586
|
+
nested.push(relative2(from, at).split(sep2).join("/"));
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
573
589
|
walk(at, join3(into, entry.name));
|
|
574
590
|
continue;
|
|
575
591
|
}
|
|
@@ -580,7 +596,40 @@ var copyInto = (from, to, excluded) => {
|
|
|
580
596
|
}
|
|
581
597
|
};
|
|
582
598
|
walk(from, to);
|
|
583
|
-
return { bytes, files };
|
|
599
|
+
return { bytes, files, nested: nested.toSorted() };
|
|
600
|
+
};
|
|
601
|
+
var HISTORY_DEPTH = 50;
|
|
602
|
+
var gitIn = (cwd, args) => {
|
|
603
|
+
const done = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
604
|
+
return { code: done.status ?? -1, out: `${done.stdout ?? ""}${done.stderr ?? ""}` };
|
|
605
|
+
};
|
|
606
|
+
var recordHistoryInto = (from, tree) => {
|
|
607
|
+
const head = gitIn(from, ["rev-parse", "HEAD"]);
|
|
608
|
+
if (head.code !== 0) {
|
|
609
|
+
return {
|
|
610
|
+
why: `${from} is not a git work tree with a commit in it, so this recording carries no history and a tier step that reads git will refuse in it: ${head.out.trim().split("\n")[0] ?? ""}`
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
const staging = `${tree}.history`;
|
|
614
|
+
const cloned = gitIn(from, [
|
|
615
|
+
"clone",
|
|
616
|
+
"--quiet",
|
|
617
|
+
"--no-checkout",
|
|
618
|
+
"--no-hardlinks",
|
|
619
|
+
"--no-single-branch",
|
|
620
|
+
"--depth",
|
|
621
|
+
String(HISTORY_DEPTH),
|
|
622
|
+
`file://${from}`,
|
|
623
|
+
staging
|
|
624
|
+
]);
|
|
625
|
+
if (cloned.code !== 0) {
|
|
626
|
+
rmSync(staging, { force: true, recursive: true });
|
|
627
|
+
return { why: `git clone of ${from} refused: ${cloned.out.trim().split("\n").at(-1) ?? ""}` };
|
|
628
|
+
}
|
|
629
|
+
renameSync(join3(staging, ".git"), join3(tree, ".git"));
|
|
630
|
+
rmSync(staging, { force: true, recursive: true });
|
|
631
|
+
gitIn(tree, ["reset", "--quiet", "--mixed"]);
|
|
632
|
+
return { depth: HISTORY_DEPTH, head: head.out.trim() };
|
|
584
633
|
};
|
|
585
634
|
var manifestsUnder2 = (root) => {
|
|
586
635
|
const found = [];
|
|
@@ -628,11 +677,40 @@ var commandsFor = (manifest, manifests, named) => {
|
|
|
628
677
|
return {
|
|
629
678
|
doctor: phase("doctor", doctorCommand),
|
|
630
679
|
lint: phase("lint", { why: 'the tree has no "lint" script and no --lint command was named' }),
|
|
680
|
+
test: phase("test", { why: 'the tree has no "test" script and no --test command was named' }),
|
|
631
681
|
typecheck: phase("typecheck", {
|
|
632
682
|
why: 'the tree has no "typecheck" script and no --typecheck command was named'
|
|
633
683
|
})
|
|
634
684
|
};
|
|
635
685
|
};
|
|
686
|
+
var asPattern = (glob) => new RegExp(
|
|
687
|
+
`^${glob.split("/").map(
|
|
688
|
+
(part) => part === "**" ? ".*" : part.replaceAll(/[.+^${}()|[\]\\]/g, String.raw`\$&`).replaceAll("*", "[^/]*")
|
|
689
|
+
).join("/").replaceAll(".*/", "(?:.*/)?")}$`
|
|
690
|
+
);
|
|
691
|
+
var filesUnder = (root, at = root) => readdirSync2(at, { withFileTypes: true }).flatMap((entry) => {
|
|
692
|
+
const full = join3(at, entry.name);
|
|
693
|
+
if (entry.isDirectory()) return filesUnder(root, full);
|
|
694
|
+
return entry.isFile() ? [full.slice(root.length + 1)] : [];
|
|
695
|
+
});
|
|
696
|
+
var seamsIn = (tree) => {
|
|
697
|
+
const at = join3(tree, "geonosis.json");
|
|
698
|
+
if (!existsSync2(at)) return void 0;
|
|
699
|
+
let globs;
|
|
700
|
+
try {
|
|
701
|
+
const declared = JSON.parse(readFileSync4(at, "utf8")).adoption?.seams;
|
|
702
|
+
globs = Array.isArray(declared) ? declared.filter((one) => typeof one === "string") : [];
|
|
703
|
+
} catch {
|
|
704
|
+
return void 0;
|
|
705
|
+
}
|
|
706
|
+
if (globs.length === 0) return void 0;
|
|
707
|
+
const patterns = globs.map(asPattern);
|
|
708
|
+
const lines2 = filesUnder(tree).filter((one) => patterns.some((pattern) => pattern.test(one))).reduce((total, one) => {
|
|
709
|
+
const body = readFileSync4(join3(tree, one), "utf8");
|
|
710
|
+
return total + (body === "" ? 0 : body.replace(/\n$/, "").split("\n").length);
|
|
711
|
+
}, 0);
|
|
712
|
+
return { globs, lines: lines2 };
|
|
713
|
+
};
|
|
636
714
|
var snapshotDir = (root, name) => join3(root, SNAPSHOTS_DIR, name);
|
|
637
715
|
var readSnapshot = (root, name) => {
|
|
638
716
|
const at = join3(snapshotDir(root, name), "snapshot.json");
|
|
@@ -655,7 +733,7 @@ var runSnapshot = (input) => {
|
|
|
655
733
|
`"${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`
|
|
656
734
|
);
|
|
657
735
|
}
|
|
658
|
-
const from =
|
|
736
|
+
const from = resolve3(input.from);
|
|
659
737
|
if (!existsSync2(join3(from, "package.json"))) {
|
|
660
738
|
throw new CannotRun(`${from} has no package.json \u2014 that is not a tree a consumer installs`);
|
|
661
739
|
}
|
|
@@ -674,20 +752,25 @@ var runSnapshot = (input) => {
|
|
|
674
752
|
const excluded = [.../* @__PURE__ */ new Set([...EXCLUDED_DIRS, ...input.exclude ?? []])].toSorted();
|
|
675
753
|
rmSync(at, { force: true, recursive: true });
|
|
676
754
|
const tree = join3(at, "tree");
|
|
677
|
-
const { bytes, files } = copyInto(from, tree, new Set(excluded));
|
|
755
|
+
const { bytes, files, nested } = copyInto(from, tree, new Set(excluded));
|
|
756
|
+
const history = recordHistoryInto(from, tree);
|
|
678
757
|
const manifests = manifestsUnder2(tree);
|
|
758
|
+
const seams = seamsIn(tree);
|
|
679
759
|
const record = {
|
|
680
760
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
681
761
|
bytes,
|
|
762
|
+
...seams === void 0 ? {} : { seams },
|
|
682
763
|
commit: headOf(from),
|
|
683
764
|
commands: commandsFor(readManifest(join3(tree, "package.json")), manifests, input.named),
|
|
684
765
|
excluded,
|
|
685
766
|
files,
|
|
686
767
|
from,
|
|
768
|
+
history,
|
|
687
769
|
lockfile,
|
|
688
770
|
manager,
|
|
689
771
|
manifests,
|
|
690
|
-
name: input.name
|
|
772
|
+
name: input.name,
|
|
773
|
+
...nested.length === 0 ? {} : { nested }
|
|
691
774
|
};
|
|
692
775
|
writeFileSync2(join3(at, "snapshot.json"), `${JSON.stringify(record, void 0, 2)}
|
|
693
776
|
`);
|
|
@@ -715,7 +798,7 @@ var formatSnapshot = (record) => [
|
|
|
715
798
|
].join("");
|
|
716
799
|
|
|
717
800
|
// src/adoption.ts
|
|
718
|
-
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
801
|
+
import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "fs";
|
|
719
802
|
import { join as join4 } from "path";
|
|
720
803
|
var groupsOf = (root) => {
|
|
721
804
|
const at = join4(root, ".changeset/config.json");
|
|
@@ -739,6 +822,38 @@ var numberIn = (spec) => {
|
|
|
739
822
|
var declarationsIn = (record, kit) => record.manifests.flatMap(
|
|
740
823
|
(manifest) => Object.entries(manifest.versions).filter(([name]) => kit.has(name)).map(([name, spec]) => ({ name, path: manifest.path, spec }))
|
|
741
824
|
);
|
|
825
|
+
var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
826
|
+
var EXAM_IMPORT = /([$\w]*Conformance)\b/g;
|
|
827
|
+
var NEVER_WALKED2 = /* @__PURE__ */ new Set([".git", "dist", "node_modules"]);
|
|
828
|
+
var examsRunIn = (dir) => {
|
|
829
|
+
const found = /* @__PURE__ */ new Set();
|
|
830
|
+
const stack = [dir];
|
|
831
|
+
while (stack.length > 0) {
|
|
832
|
+
const at = stack.pop();
|
|
833
|
+
let entries;
|
|
834
|
+
try {
|
|
835
|
+
entries = readdirSync3(at, { withFileTypes: true });
|
|
836
|
+
} catch {
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
for (const entry of entries) {
|
|
840
|
+
if (entry.isDirectory()) {
|
|
841
|
+
if (!NEVER_WALKED2.has(entry.name)) stack.push(join4(at, entry.name));
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
if (!TEST_FILE.test(entry.name)) continue;
|
|
845
|
+
let source;
|
|
846
|
+
try {
|
|
847
|
+
source = readFileSync5(join4(at, entry.name), "utf8");
|
|
848
|
+
} catch {
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (!source.includes("@geonosis/")) continue;
|
|
852
|
+
for (const match of source.matchAll(EXAM_IMPORT)) found.add(match[1]);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return [...found].toSorted();
|
|
856
|
+
};
|
|
742
857
|
var groupName = (index, total) => total > 1 ? `fixed group ${index + 1}` : "the fixed group";
|
|
743
858
|
var incoherence = (consumer, groups, declarations) => groups.flatMap((group, index) => {
|
|
744
859
|
const members = new Set(group);
|
|
@@ -822,9 +937,13 @@ var runAdoption = async ({
|
|
|
822
937
|
});
|
|
823
938
|
continue;
|
|
824
939
|
}
|
|
825
|
-
const
|
|
940
|
+
const record = readSnapshot(root, one.name);
|
|
941
|
+
const declarations = declarationsIn(record, new Set(current.keys()));
|
|
942
|
+
const seams = record.seams;
|
|
826
943
|
consumers.push({
|
|
827
944
|
declared: declarations.length,
|
|
945
|
+
exams: examsRunIn(join4(snapshotDir(root, one.name), "tree")),
|
|
946
|
+
...seams === void 0 ? {} : { seams },
|
|
828
947
|
findings: [
|
|
829
948
|
...distance(one.name, declarations, current),
|
|
830
949
|
...incoherence(one.name, groups, declarations),
|
|
@@ -854,7 +973,13 @@ var blockFor = (consumer) => {
|
|
|
854
973
|
const head = faults.length === 0 ? [
|
|
855
974
|
` MATCH ${consumer.name} \u2014 ${consumer.declared} spec(s) on the versions this release is cut at`
|
|
856
975
|
] : [];
|
|
857
|
-
|
|
976
|
+
const exams = consumer.exams.length === 0 ? [
|
|
977
|
+
` EXAMS ${consumer.name} \u2014 none: no test file in this recording runs a floor exam, so a floor bump that broke one would read as a green tier`
|
|
978
|
+
] : [` EXAMS ${consumer.name} \u2014 ${consumer.exams.join(", ")}`];
|
|
979
|
+
const seams = consumer.seams === void 0 ? [] : [
|
|
980
|
+
` 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`
|
|
981
|
+
];
|
|
982
|
+
return [...head, ...exams, ...seams, ...new Set(consumer.findings.map(lineOf))];
|
|
858
983
|
};
|
|
859
984
|
var formatAdoption = (report) => [
|
|
860
985
|
`adoption \u2014 ${report.considered} declared, ${report.consumers.length} read, ${report.excused.length} excused; against ${report.registry ?? "this workspace"}`,
|
|
@@ -877,7 +1002,7 @@ var adoptionEnvelope = (report, durationMs) => ({
|
|
|
877
1002
|
|
|
878
1003
|
// src/wrangler.ts
|
|
879
1004
|
import { readFileSync as readFileSync6 } from "fs";
|
|
880
|
-
import { resolve as
|
|
1005
|
+
import { resolve as resolve4 } from "path";
|
|
881
1006
|
var isRecord3 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
882
1007
|
var parseJsonc = (source) => {
|
|
883
1008
|
let out = "";
|
|
@@ -1053,16 +1178,16 @@ var declaredIn = (config) => {
|
|
|
1053
1178
|
routes: listed.map((route) => isRecord3(route) ? route["pattern"] : route).filter((pattern) => typeof pattern === "string").toSorted()
|
|
1054
1179
|
};
|
|
1055
1180
|
};
|
|
1056
|
-
var readWrangler = (root,
|
|
1057
|
-
const path =
|
|
1181
|
+
var readWrangler = (root, relative6, env) => {
|
|
1182
|
+
const path = resolve4(root, relative6);
|
|
1058
1183
|
let parsed;
|
|
1059
1184
|
try {
|
|
1060
1185
|
const source = readFileSync6(path, "utf8");
|
|
1061
|
-
parsed =
|
|
1186
|
+
parsed = relative6.endsWith(".toml") ? parseToml(source) : parseJsonc(source);
|
|
1062
1187
|
} catch (error) {
|
|
1063
|
-
throw new CannotRun(`${
|
|
1188
|
+
throw new CannotRun(`${relative6} could not be read: ${error.message}`);
|
|
1064
1189
|
}
|
|
1065
|
-
if (!isRecord3(parsed)) throw new CannotRun(`${
|
|
1190
|
+
if (!isRecord3(parsed)) throw new CannotRun(`${relative6} is not a wrangler configuration`);
|
|
1066
1191
|
const environments = parsed["env"];
|
|
1067
1192
|
const block = env !== void 0 && isRecord3(environments) && isRecord3(environments[env]) ? environments[env] : parsed;
|
|
1068
1193
|
return declaredIn(block);
|
|
@@ -1070,13 +1195,13 @@ var readWrangler = (root, relative5, env) => {
|
|
|
1070
1195
|
|
|
1071
1196
|
// src/deployed.ts
|
|
1072
1197
|
import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
|
|
1073
|
-
import { resolve as
|
|
1198
|
+
import { resolve as resolve5 } from "path";
|
|
1074
1199
|
var DEPLOYED_FILE = ".geonosis/deployed.json";
|
|
1075
1200
|
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";
|
|
1076
1201
|
var listOf = (found) => Array.isArray(found) ? found.filter((one) => typeof one === "string") : [];
|
|
1077
1202
|
var isRecord4 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
1078
1203
|
var readDeployed = (root) => {
|
|
1079
|
-
const path =
|
|
1204
|
+
const path = resolve5(root, DEPLOYED_FILE);
|
|
1080
1205
|
if (!existsSync4(path)) throw new CannotRun(NOTHING_SAYS);
|
|
1081
1206
|
let parsed;
|
|
1082
1207
|
try {
|
|
@@ -1359,18 +1484,18 @@ var refusalsIn = (path, body, firstLine = 1) => {
|
|
|
1359
1484
|
// src/migrations.ts
|
|
1360
1485
|
import { mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1361
1486
|
import { tmpdir } from "os";
|
|
1362
|
-
import { basename, join as join5, resolve as
|
|
1487
|
+
import { basename, join as join5, resolve as resolve7 } from "path";
|
|
1363
1488
|
|
|
1364
1489
|
// src/squawk.ts
|
|
1365
1490
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
1366
1491
|
import { existsSync as existsSync5 } from "fs";
|
|
1367
|
-
import { dirname as dirname2, resolve as
|
|
1492
|
+
import { dirname as dirname2, resolve as resolve6 } from "path";
|
|
1368
1493
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1369
1494
|
var HERE = dirname2(fileURLToPath2(import.meta.url));
|
|
1370
1495
|
var PINNED = "2.63.0";
|
|
1371
1496
|
var findSquawk = (from = HERE) => {
|
|
1372
1497
|
for (let dir = from; ; dir = dirname2(dir)) {
|
|
1373
|
-
const candidate =
|
|
1498
|
+
const candidate = resolve6(dir, "node_modules/.bin/squawk");
|
|
1374
1499
|
if (existsSync5(candidate)) return candidate;
|
|
1375
1500
|
if (dirname2(dir) === dir) break;
|
|
1376
1501
|
}
|
|
@@ -1452,7 +1577,7 @@ var runMigrations = (input) => {
|
|
|
1452
1577
|
const forSquawk = [];
|
|
1453
1578
|
const states = /* @__PURE__ */ new Map();
|
|
1454
1579
|
for (const { entry, file } of matched) {
|
|
1455
|
-
const source = readFileSync8(
|
|
1580
|
+
const source = readFileSync8(resolve7(input.root, file), "utf8");
|
|
1456
1581
|
const found = markersIn(source);
|
|
1457
1582
|
const bad = found.flatMap((one) => {
|
|
1458
1583
|
const why = judge(one, input.root, input.since);
|
|
@@ -1480,7 +1605,7 @@ var runMigrations = (input) => {
|
|
|
1480
1605
|
const staging = mkdtempSync(join5(tmpdir(), "geonosis-release-squawk-"));
|
|
1481
1606
|
try {
|
|
1482
1607
|
for (const one of forSquawk) {
|
|
1483
|
-
const path = one.entry.dialect === "mikro-orm-ts" ? join5(staging, `${basename(one.file, ".ts")}.sql`) :
|
|
1608
|
+
const path = one.entry.dialect === "mikro-orm-ts" ? join5(staging, `${basename(one.file, ".ts")}.sql`) : resolve7(input.root, one.file);
|
|
1484
1609
|
if (one.entry.dialect === "mikro-orm-ts") writeFileSync3(path, `${one.sql}
|
|
1485
1610
|
`);
|
|
1486
1611
|
const found = squawkOn({
|
|
@@ -1625,7 +1750,7 @@ var Runner = class {
|
|
|
1625
1750
|
if (this.closed !== void 0) {
|
|
1626
1751
|
throw new CannotRun(`the runner is gone before "${request.step}": ${this.closed}`);
|
|
1627
1752
|
}
|
|
1628
|
-
const line = await new Promise((
|
|
1753
|
+
const line = await new Promise((resolve11, reject) => {
|
|
1629
1754
|
const timer = setTimeout(() => {
|
|
1630
1755
|
reject(
|
|
1631
1756
|
new CannotRun(
|
|
@@ -1635,7 +1760,7 @@ var Runner = class {
|
|
|
1635
1760
|
}, timeoutMs);
|
|
1636
1761
|
this.waiting.push((answer) => {
|
|
1637
1762
|
clearTimeout(timer);
|
|
1638
|
-
|
|
1763
|
+
resolve11(answer);
|
|
1639
1764
|
});
|
|
1640
1765
|
this.child.on("close", () => {
|
|
1641
1766
|
clearTimeout(timer);
|
|
@@ -1748,16 +1873,16 @@ var formatVerdict = (verdict) => verdict.ok ? `OK prove: ${verdict.steps.join
|
|
|
1748
1873
|
`;
|
|
1749
1874
|
|
|
1750
1875
|
// src/schema-walls.ts
|
|
1751
|
-
import { readdirSync as
|
|
1752
|
-
import { join as join6, relative as relative3, resolve as
|
|
1876
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync9 } from "fs";
|
|
1877
|
+
import { join as join6, relative as relative3, resolve as resolve8, sep as sep3 } from "path";
|
|
1753
1878
|
var MIGRATION_FILE = /\.(?:sql|ts)$/;
|
|
1754
1879
|
var CURRENT_SETTING = /current_setting\s*\(\s*'([^']+)'/gi;
|
|
1755
|
-
var
|
|
1880
|
+
var filesUnder2 = (root, dir) => {
|
|
1756
1881
|
const found = [];
|
|
1757
1882
|
const walk = (at) => {
|
|
1758
1883
|
let entries;
|
|
1759
1884
|
try {
|
|
1760
|
-
entries =
|
|
1885
|
+
entries = readdirSync4(at, { withFileTypes: true });
|
|
1761
1886
|
} catch {
|
|
1762
1887
|
return;
|
|
1763
1888
|
}
|
|
@@ -1767,8 +1892,8 @@ var filesUnder = (root, dir) => {
|
|
|
1767
1892
|
else if (MIGRATION_FILE.test(entry.name)) found.push(path);
|
|
1768
1893
|
}
|
|
1769
1894
|
};
|
|
1770
|
-
walk(
|
|
1771
|
-
return found.map((path) => relative3(root, path).split(
|
|
1895
|
+
walk(resolve8(root, dir));
|
|
1896
|
+
return found.map((path) => relative3(root, path).split(sep3).join("/")).toSorted();
|
|
1772
1897
|
};
|
|
1773
1898
|
var lineOf3 = (body, index) => body.slice(0, index).split("\n").length;
|
|
1774
1899
|
var wordFor = (table2) => new RegExp(
|
|
@@ -1820,11 +1945,11 @@ var runSchema = ({ root }) => {
|
|
|
1820
1945
|
);
|
|
1821
1946
|
}
|
|
1822
1947
|
const dirs = schema.domains.map((one) => one.dir);
|
|
1823
|
-
const files = [...new Set(dirs.flatMap((dir) =>
|
|
1948
|
+
const files = [...new Set(dirs.flatMap((dir) => filesUnder2(root, dir)))].toSorted();
|
|
1824
1949
|
const findings = [];
|
|
1825
1950
|
const unreadable = [];
|
|
1826
1951
|
for (const path of files) {
|
|
1827
|
-
const source = readFileSync9(
|
|
1952
|
+
const source = readFileSync9(resolve8(root, path), "utf8");
|
|
1828
1953
|
let sql;
|
|
1829
1954
|
try {
|
|
1830
1955
|
sql = sqlOf(path, source);
|
|
@@ -1863,8 +1988,8 @@ var formatSchema = (report) => {
|
|
|
1863
1988
|
|
|
1864
1989
|
// src/smoke-run.ts
|
|
1865
1990
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
1866
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as
|
|
1867
|
-
import { join as join7, relative as relative4, resolve as
|
|
1991
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync5, readFileSync as readFileSync10, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1992
|
+
import { join as join7, relative as relative4, resolve as resolve9 } from "path";
|
|
1868
1993
|
var BASELINE_FILE = "baseline.json";
|
|
1869
1994
|
var INSTALL = {
|
|
1870
1995
|
bun: ["install"],
|
|
@@ -1938,7 +2063,7 @@ var sourceManifests = (rc) => {
|
|
|
1938
2063
|
const excluded = new Set(EXCLUDED_DIRS);
|
|
1939
2064
|
const found = [];
|
|
1940
2065
|
const walk = (dir) => {
|
|
1941
|
-
for (const entry of
|
|
2066
|
+
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
1942
2067
|
if (excluded.has(entry.name)) continue;
|
|
1943
2068
|
if (entry.isDirectory()) {
|
|
1944
2069
|
walk(join7(dir, entry.name));
|
|
@@ -2064,8 +2189,8 @@ var smokeOver = (input) => {
|
|
|
2064
2189
|
rmSync3(work, { force: true, recursive: true });
|
|
2065
2190
|
copyInto(join7(at, "tree"), work, new Set(EXCLUDED_DIRS));
|
|
2066
2191
|
ownRepository(work);
|
|
2067
|
-
const packed = input.rc === void 0 ? [] : packRc(
|
|
2068
|
-
const findings = input.rc === void 0 ? [] : promisedButNotPacked(
|
|
2192
|
+
const packed = input.rc === void 0 ? [] : packRc(resolve9(input.rc), join7(at, "rc"));
|
|
2193
|
+
const findings = input.rc === void 0 ? [] : promisedButNotPacked(resolve9(input.rc), packed);
|
|
2069
2194
|
const swapped = rewriteManifests(
|
|
2070
2195
|
work,
|
|
2071
2196
|
record.manifests.map((one) => one.path),
|
|
@@ -2113,7 +2238,7 @@ var recordBaseline = (input) => {
|
|
|
2113
2238
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2114
2239
|
packed: packed.map((one) => ({ name: one.name, version: one.version })),
|
|
2115
2240
|
phases: outcomes,
|
|
2116
|
-
rc: input.rc === void 0 ? "the versions the tree names" :
|
|
2241
|
+
rc: input.rc === void 0 ? "the versions the tree names" : resolve9(input.rc)
|
|
2117
2242
|
};
|
|
2118
2243
|
writeFileSync4(baselinePath(input.root, input.name), `${JSON.stringify(baseline, void 0, 2)}
|
|
2119
2244
|
`);
|
|
@@ -2355,7 +2480,91 @@ var smokeEnvelope = (comparison, durationMs) => {
|
|
|
2355
2480
|
};
|
|
2356
2481
|
};
|
|
2357
2482
|
|
|
2483
|
+
// src/changelog.ts
|
|
2484
|
+
import { existsSync as existsSync8, readdirSync as readdirSync6, readFileSync as readFileSync11 } from "fs";
|
|
2485
|
+
import { join as join8, relative as relative5, resolve as resolve10, sep as sep4 } from "path";
|
|
2486
|
+
var CHANGELOG = "CHANGELOG.md";
|
|
2487
|
+
var VERSION_HEADING = /^##\s+v?(\d+\.\d+\.\d+.*)$/;
|
|
2488
|
+
var emptyHeadingsIn = (body, at) => {
|
|
2489
|
+
const lines2 = body.split("\n");
|
|
2490
|
+
const found = [];
|
|
2491
|
+
for (const [index, line] of lines2.entries()) {
|
|
2492
|
+
const heading = VERSION_HEADING.exec(line);
|
|
2493
|
+
if (heading?.[1] === void 0) continue;
|
|
2494
|
+
const under2 = [];
|
|
2495
|
+
for (const next of lines2.slice(index + 1)) {
|
|
2496
|
+
if (next.startsWith("## ")) break;
|
|
2497
|
+
under2.push(next);
|
|
2498
|
+
}
|
|
2499
|
+
if (under2.every((one) => one.trim() === "")) found.push({ at, version: heading[1].trim() });
|
|
2500
|
+
}
|
|
2501
|
+
return found;
|
|
2502
|
+
};
|
|
2503
|
+
var countVersionHeadings = (body) => body.split("\n").filter((line) => VERSION_HEADING.test(line)).length;
|
|
2504
|
+
var checkChangelogs = ({ dirs, root }) => {
|
|
2505
|
+
if (dirs.length === 0) {
|
|
2506
|
+
throw new CannotRun(
|
|
2507
|
+
"no package directories to read a CHANGELOG.md in \u2014 this check needs the workspaces the release covers"
|
|
2508
|
+
);
|
|
2509
|
+
}
|
|
2510
|
+
const empty = [];
|
|
2511
|
+
let read = 0;
|
|
2512
|
+
let versions = 0;
|
|
2513
|
+
for (const dir of dirs) {
|
|
2514
|
+
const at = join8(dir, CHANGELOG);
|
|
2515
|
+
if (!existsSync8(resolve10(root, at))) continue;
|
|
2516
|
+
read += 1;
|
|
2517
|
+
const body = readFileSync11(resolve10(root, at), "utf8");
|
|
2518
|
+
versions += countVersionHeadings(body);
|
|
2519
|
+
empty.push(...emptyHeadingsIn(body, at));
|
|
2520
|
+
}
|
|
2521
|
+
if (read === 0) {
|
|
2522
|
+
throw new CannotRun(
|
|
2523
|
+
`none of the ${dirs.length} package(s) has a ${CHANGELOG} \u2014 a release check that read nothing has not passed`
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
2526
|
+
return { empty, read, versions };
|
|
2527
|
+
};
|
|
2528
|
+
var formatChangelogs = (report) => [
|
|
2529
|
+
...report.empty.map(
|
|
2530
|
+
(one) => `\u2717 ${one.at} \u2192 ${one.version} says a version shipped and says nothing about it`
|
|
2531
|
+
),
|
|
2532
|
+
report.empty.length === 0 ? `changelog PASS \u2014 ${report.versions} version heading(s) over ${report.read} file(s), every one of them says something` : `changelog FAIL \u2014 ${report.empty.length} of ${report.versions} version heading(s) over ${report.read} file(s) are empty`
|
|
2533
|
+
].join("\n");
|
|
2534
|
+
var NEVER_WALKED3 = /* @__PURE__ */ new Set(["build", "coverage", "dist", "node_modules", "storybook-static"]);
|
|
2535
|
+
var publishableDirs = (root) => {
|
|
2536
|
+
const found = [];
|
|
2537
|
+
const walk = (dir) => {
|
|
2538
|
+
let entries;
|
|
2539
|
+
try {
|
|
2540
|
+
entries = readdirSync6(dir, { withFileTypes: true });
|
|
2541
|
+
} catch {
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
for (const entry of entries) {
|
|
2545
|
+
if (entry.isDirectory()) {
|
|
2546
|
+
if (!entry.name.startsWith(".") && !NEVER_WALKED3.has(entry.name)) {
|
|
2547
|
+
walk(join8(dir, entry.name));
|
|
2548
|
+
}
|
|
2549
|
+
continue;
|
|
2550
|
+
}
|
|
2551
|
+
if (entry.name !== "package.json") continue;
|
|
2552
|
+
let manifest;
|
|
2553
|
+
try {
|
|
2554
|
+
manifest = JSON.parse(readFileSync11(join8(dir, entry.name), "utf8"));
|
|
2555
|
+
} catch {
|
|
2556
|
+
continue;
|
|
2557
|
+
}
|
|
2558
|
+
if (manifest.private === true || typeof manifest.name !== "string") continue;
|
|
2559
|
+
found.push(relative5(root, dir).split(sep4).join("/"));
|
|
2560
|
+
}
|
|
2561
|
+
};
|
|
2562
|
+
walk(root);
|
|
2563
|
+
return found.filter((one) => one !== "").toSorted();
|
|
2564
|
+
};
|
|
2565
|
+
|
|
2358
2566
|
export {
|
|
2567
|
+
PHASES,
|
|
2359
2568
|
CannotRun,
|
|
2360
2569
|
parseReleaseConfig,
|
|
2361
2570
|
readReleaseConfig,
|
|
@@ -2375,7 +2584,6 @@ export {
|
|
|
2375
2584
|
formatPublished,
|
|
2376
2585
|
SNAPSHOTS_DIR,
|
|
2377
2586
|
EXCLUDED_DIRS,
|
|
2378
|
-
PHASES,
|
|
2379
2587
|
headOf,
|
|
2380
2588
|
snapshotDir,
|
|
2381
2589
|
readSnapshot,
|
|
@@ -2432,5 +2640,11 @@ export {
|
|
|
2432
2640
|
formatBaseline,
|
|
2433
2641
|
SMOKE_TOOL,
|
|
2434
2642
|
SMOKE_NEXT,
|
|
2435
|
-
smokeEnvelope
|
|
2643
|
+
smokeEnvelope,
|
|
2644
|
+
CHANGELOG,
|
|
2645
|
+
emptyHeadingsIn,
|
|
2646
|
+
countVersionHeadings,
|
|
2647
|
+
checkChangelogs,
|
|
2648
|
+
formatChangelogs,
|
|
2649
|
+
publishableDirs
|
|
2436
2650
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,107 @@
|
|
|
1
1
|
import { ReportEnvelope } from '@geonosis/ratchet';
|
|
2
2
|
|
|
3
|
+
/** What a phase runs: a script of theirs, a binary their install put there, or nothing at all. */
|
|
4
|
+
type Command = {
|
|
5
|
+
exec: string;
|
|
6
|
+
} | {
|
|
7
|
+
run: string;
|
|
8
|
+
} | {
|
|
9
|
+
why: string;
|
|
10
|
+
};
|
|
11
|
+
type SmokePhase = 'doctor' | 'lint' | 'test' | 'typecheck';
|
|
12
|
+
/**
|
|
13
|
+
* The gates a release is read against, in the order they run.
|
|
14
|
+
*
|
|
15
|
+
* Its own module because the runner and the config reader both need it and each imports the other:
|
|
16
|
+
* read through `smoke.ts`, `PHASES` was still undefined when `config.ts` initialised, and the
|
|
17
|
+
* config refused every `geonosis.json` with `Cannot read properties of undefined`.
|
|
18
|
+
*
|
|
19
|
+
* #272: `test` is here because a floor change can keep a consumer's types and their linter green
|
|
20
|
+
* and still redden their suites — the one thing only their own tests can say. A recording taken
|
|
21
|
+
* before it existed has no baseline for the phase, and a phase with nothing to compare against is
|
|
22
|
+
* reported as uncomparable rather than as a pass.
|
|
23
|
+
*/
|
|
24
|
+
declare const PHASES: SmokePhase[];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Where a consumer's bytes live while the release is being read against them.
|
|
28
|
+
*
|
|
29
|
+
* D-048: a consumer tree is private, and a copy of one that reaches a commit cannot be taken back.
|
|
30
|
+
* The directory is under `.geonosis/`, which every repo in this kit's orbit already ignores, and
|
|
31
|
+
* `smoke snapshot` asks git — in the tree it is about to write into — whether that is still true
|
|
32
|
+
* before it copies a single file.
|
|
33
|
+
*/
|
|
34
|
+
declare const SNAPSHOTS_DIR = ".geonosis/consumer-snapshots";
|
|
35
|
+
/** The managers a snapshot can be installed with here, each measured on this machine. */
|
|
36
|
+
type Manager = 'bun' | 'pnpm';
|
|
37
|
+
/**
|
|
38
|
+
* Directories a consumer builds or installs into. They are excluded by name at every depth: the
|
|
39
|
+
* snapshot is the SOURCE of a tree, and the install the smoke runs is the thing under test.
|
|
40
|
+
*/
|
|
41
|
+
declare const EXCLUDED_DIRS: string[];
|
|
42
|
+
/** Where the source tree stood, or the sentence saying why nothing here could say. */
|
|
43
|
+
type RecordedCommit = {
|
|
44
|
+
sha: string;
|
|
45
|
+
} | {
|
|
46
|
+
why: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The seam a consumer declared, measured on the bytes this recording holds (#280).
|
|
50
|
+
*
|
|
51
|
+
* The user's rule for a floor: it is adopted the day it deletes more than it adds. The sweep can
|
|
52
|
+
* only say so about trees it has recorded, so the number is taken WHERE the tree is read — a
|
|
53
|
+
* `.geonosis/` of theirs never reaches a snapshot (it is an excluded directory), so their own bump
|
|
54
|
+
* report is not available here and this is the figure that is.
|
|
55
|
+
*/
|
|
56
|
+
type RecordedSeams = {
|
|
57
|
+
globs: string[];
|
|
58
|
+
lines: number;
|
|
59
|
+
};
|
|
60
|
+
type SnapshotRecord = {
|
|
61
|
+
at: string;
|
|
62
|
+
bytes: number;
|
|
63
|
+
/** Absent in a recording taken before this was recorded at all. */
|
|
64
|
+
commit?: RecordedCommit;
|
|
65
|
+
commands: Record<SmokePhase, Command>;
|
|
66
|
+
excluded: string[];
|
|
67
|
+
files: number;
|
|
68
|
+
from: string;
|
|
69
|
+
/** Absent in a recording taken before history was recorded at all. */
|
|
70
|
+
history?: RecordedHistory;
|
|
71
|
+
lockfile: string;
|
|
72
|
+
manager: Manager;
|
|
73
|
+
manifests: {
|
|
74
|
+
path: string;
|
|
75
|
+
versions: Record<string, string>;
|
|
76
|
+
}[];
|
|
77
|
+
name: string;
|
|
78
|
+
/** Directories skipped because they carry their own `.git`: a different tree inside this one. */
|
|
79
|
+
nested?: string[];
|
|
80
|
+
/** Absent when this consumer declares no `adoption.seams`, or the recording predates the block. */
|
|
81
|
+
seams?: RecordedSeams;
|
|
82
|
+
};
|
|
83
|
+
/** git asked in THEIR tree, and only ever asked: law 5 forbids this tool writing a byte there. */
|
|
84
|
+
declare const headOf: (from: string) => RecordedCommit;
|
|
85
|
+
/** What the recording can answer about where it came from, or why it can answer nothing. */
|
|
86
|
+
type RecordedHistory = {
|
|
87
|
+
depth: number;
|
|
88
|
+
head: string;
|
|
89
|
+
} | {
|
|
90
|
+
why: string;
|
|
91
|
+
};
|
|
92
|
+
declare const snapshotDir: (root: string, name: string) => string;
|
|
93
|
+
declare const readSnapshot: (root: string, name: string) => SnapshotRecord;
|
|
94
|
+
type SnapshotInput = {
|
|
95
|
+
exclude?: string[];
|
|
96
|
+
from: string;
|
|
97
|
+
name: string;
|
|
98
|
+
named: Partial<Record<SmokePhase, string>>;
|
|
99
|
+
replace: boolean;
|
|
100
|
+
root: string;
|
|
101
|
+
};
|
|
102
|
+
declare const runSnapshot: (input: SnapshotInput) => SnapshotRecord;
|
|
103
|
+
declare const formatSnapshot: (record: SnapshotRecord) => string;
|
|
104
|
+
|
|
3
105
|
type AdoptionVerdict = 'BEHIND' | 'FLOOR UNDECLARED' | 'INCOHERENT' | 'UNJUDGED';
|
|
4
106
|
type AdoptionFinding = {
|
|
5
107
|
consumer: string;
|
|
@@ -10,8 +112,11 @@ type AdoptionFinding = {
|
|
|
10
112
|
type AdoptionConsumer = {
|
|
11
113
|
/** How many specs naming a package of this workspace that consumer's manifests declare. */
|
|
12
114
|
declared: number;
|
|
115
|
+
/** The floor exams a test file in the recording runs. A floor nothing exams is a floor whose break their tier cannot see (#293). */
|
|
116
|
+
exams: string[];
|
|
13
117
|
findings: AdoptionFinding[];
|
|
14
118
|
name: string;
|
|
119
|
+
seams?: RecordedSeams;
|
|
15
120
|
};
|
|
16
121
|
type AdoptionReport = {
|
|
17
122
|
considered: number;
|
|
@@ -412,70 +517,6 @@ declare class Runner {
|
|
|
412
517
|
close(): void;
|
|
413
518
|
}
|
|
414
519
|
|
|
415
|
-
/**
|
|
416
|
-
* Where a consumer's bytes live while the release is being read against them.
|
|
417
|
-
*
|
|
418
|
-
* D-048: a consumer tree is private, and a copy of one that reaches a commit cannot be taken back.
|
|
419
|
-
* The directory is under `.geonosis/`, which every repo in this kit's orbit already ignores, and
|
|
420
|
-
* `smoke snapshot` asks git — in the tree it is about to write into — whether that is still true
|
|
421
|
-
* before it copies a single file.
|
|
422
|
-
*/
|
|
423
|
-
declare const SNAPSHOTS_DIR = ".geonosis/consumer-snapshots";
|
|
424
|
-
/** The managers a snapshot can be installed with here, each measured on this machine. */
|
|
425
|
-
type Manager = 'bun' | 'pnpm';
|
|
426
|
-
/**
|
|
427
|
-
* Directories a consumer builds or installs into. They are excluded by name at every depth: the
|
|
428
|
-
* snapshot is the SOURCE of a tree, and the install the smoke runs is the thing under test.
|
|
429
|
-
*/
|
|
430
|
-
declare const EXCLUDED_DIRS: string[];
|
|
431
|
-
/** What a phase runs: a script of theirs, a binary their install put there, or nothing at all. */
|
|
432
|
-
type Command = {
|
|
433
|
-
exec: string;
|
|
434
|
-
} | {
|
|
435
|
-
run: string;
|
|
436
|
-
} | {
|
|
437
|
-
why: string;
|
|
438
|
-
};
|
|
439
|
-
type SmokePhase = 'doctor' | 'lint' | 'typecheck';
|
|
440
|
-
declare const PHASES: SmokePhase[];
|
|
441
|
-
/** Where the source tree stood, or the sentence saying why nothing here could say. */
|
|
442
|
-
type RecordedCommit = {
|
|
443
|
-
sha: string;
|
|
444
|
-
} | {
|
|
445
|
-
why: string;
|
|
446
|
-
};
|
|
447
|
-
type SnapshotRecord = {
|
|
448
|
-
at: string;
|
|
449
|
-
bytes: number;
|
|
450
|
-
/** Absent in a recording taken before this was recorded at all. */
|
|
451
|
-
commit?: RecordedCommit;
|
|
452
|
-
commands: Record<SmokePhase, Command>;
|
|
453
|
-
excluded: string[];
|
|
454
|
-
files: number;
|
|
455
|
-
from: string;
|
|
456
|
-
lockfile: string;
|
|
457
|
-
manager: Manager;
|
|
458
|
-
manifests: {
|
|
459
|
-
path: string;
|
|
460
|
-
versions: Record<string, string>;
|
|
461
|
-
}[];
|
|
462
|
-
name: string;
|
|
463
|
-
};
|
|
464
|
-
/** git asked in THEIR tree, and only ever asked: law 5 forbids this tool writing a byte there. */
|
|
465
|
-
declare const headOf: (from: string) => RecordedCommit;
|
|
466
|
-
declare const snapshotDir: (root: string, name: string) => string;
|
|
467
|
-
declare const readSnapshot: (root: string, name: string) => SnapshotRecord;
|
|
468
|
-
type SnapshotInput = {
|
|
469
|
-
exclude?: string[];
|
|
470
|
-
from: string;
|
|
471
|
-
name: string;
|
|
472
|
-
named: Partial<Record<SmokePhase, string>>;
|
|
473
|
-
replace: boolean;
|
|
474
|
-
root: string;
|
|
475
|
-
};
|
|
476
|
-
declare const runSnapshot: (input: SnapshotInput) => SnapshotRecord;
|
|
477
|
-
declare const formatSnapshot: (record: SnapshotRecord) => string;
|
|
478
|
-
|
|
479
520
|
/** What a phase did, or the sentence saying there was nothing of theirs to do it with. */
|
|
480
521
|
type PhaseOutcome = {
|
|
481
522
|
code: number;
|
|
@@ -656,4 +697,41 @@ declare const statementsOf: (sql: string) => string;
|
|
|
656
697
|
/** One refusal per verb per file, at the line of that verb's first occurrence. */
|
|
657
698
|
declare const refusalsIn: (path: string, body: string, firstLine?: number) => Refusal[];
|
|
658
699
|
|
|
659
|
-
|
|
700
|
+
declare const CHANGELOG = "CHANGELOG.md";
|
|
701
|
+
type EmptyHeading = {
|
|
702
|
+
at: string;
|
|
703
|
+
version: string;
|
|
704
|
+
};
|
|
705
|
+
type ChangelogReport = {
|
|
706
|
+
/** Every `## <version>` heading with nothing under it. */
|
|
707
|
+
empty: EmptyHeading[];
|
|
708
|
+
/** How many changelogs were read. The denominator a "no findings" line is worth reading over. */
|
|
709
|
+
read: number;
|
|
710
|
+
/** Version headings seen across all of them. */
|
|
711
|
+
versions: number;
|
|
712
|
+
};
|
|
713
|
+
/**
|
|
714
|
+
* A version heading with nothing under it, in a file whose whole job is to say what a version
|
|
715
|
+
* changed. The audit found fourteen of twenty-two packages carrying at least one — a reader told
|
|
716
|
+
* that 1.2.0 shipped and told nothing else, in a tree that is published.
|
|
717
|
+
*
|
|
718
|
+
* A fixed publish group makes them by construction: every package moves to one number, and one that
|
|
719
|
+
* did not change has no changeset to render. That is a reason and not an excuse — a line saying so
|
|
720
|
+
* is a sentence, and a blank is a question.
|
|
721
|
+
*/
|
|
722
|
+
declare const emptyHeadingsIn: (body: string, at: string) => EmptyHeading[];
|
|
723
|
+
declare const countVersionHeadings: (body: string) => number;
|
|
724
|
+
type ChangelogInput = {
|
|
725
|
+
dirs: readonly string[];
|
|
726
|
+
root: string;
|
|
727
|
+
};
|
|
728
|
+
declare const checkChangelogs: ({ dirs, root }: ChangelogInput) => ChangelogReport;
|
|
729
|
+
declare const formatChangelogs: (report: ChangelogReport) => string;
|
|
730
|
+
/**
|
|
731
|
+
* Every publishable package's directory, by MANIFEST rather than by a glob: a package a workspace
|
|
732
|
+
* glob no longer claims still publishes a changelog, and a check whose denominator is a glob would
|
|
733
|
+
* pass it by not looking.
|
|
734
|
+
*/
|
|
735
|
+
declare const publishableDirs: (root: string) => string[];
|
|
736
|
+
|
|
737
|
+
export { ADOPTION_NEXT, ADOPTION_TOOL, type AdoptionConsumer, type AdoptionFinding, type AdoptionReport, type AdoptionVerdict, BASELINE_FILE, CHANGELOG, CONSUMER_TREES_FILE, type ChangelogInput, type ChangelogReport, type Command, DEFAULT_REGISTRY, DEPLOYED_FILE, type Declared, type Deployed, type DeployedReport, type Dialect, type Drift, EXCLUDED_DIRS, EXISTS_BUT, type EmptyHeading, type Manager, type MigrationDir, type MigrationsReport, NARROWING, NOTHING_SAYS, PHASES, PLANTS, type Packed, type Phase, type PhaseOutcome, type PlanReport, type ProveOutcome, type PublishedLine, type PublishedReport, type PublishedVerdict, type RecordedCommit, type RecordedSeams, type Refusal, type RegistryAnswer, type ReleaseConfig, Runner, SMOKE_NEXT, SMOKE_TOOL, SNAPSHOTS_DIR, STEPS, type SchemaConfig, type SchemaDomain, type SchemaFinding, type SchemaReport, type SmokeBaseline, type SmokeComparison, type SmokePhase, type SmokeSweep, type SnapshotRecord, type SweepEntry, type SweepNote, type Verdict, WHY_VERSION_IDENTITY, type WantedSnapshot, addSqlIn, adoptionEnvelope, askRegistry, censusOf, checkChangelogs, compareToBaseline, comparisonsIn, countVersionHeadings, declaredIn, driftBetween, emptyHeadingsIn, formatAdoption, formatBaseline, formatChangelogs, formatDeployed, formatMigrations, formatPlan, formatProve, formatPublished, formatSchema, formatSmoke, formatSnapshot, formatSweep, formatVerdict, headOf, markersIn, packRc, parseJsonc, parseReleaseConfig, parseToml, promisedButNotPacked, prove, proveOver, publishableDirs, readBaseline, readDeployed, readReleaseConfig, readSnapshot, readWrangler, recordBaseline, refusalsIn, registryPathOf, rewriteManifests, runAdoption, runDeployed, runMigrations, runPlan, runPublished, runSchema, runSmoke, runSnapshot, smokeEnvelope, snapshotDir, statementsOf, sweepEnvelope, sweepSmoke };
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
ADOPTION_NEXT,
|
|
3
3
|
ADOPTION_TOOL,
|
|
4
4
|
BASELINE_FILE,
|
|
5
|
+
CHANGELOG,
|
|
5
6
|
CONSUMER_TREES_FILE,
|
|
6
7
|
DEFAULT_REGISTRY,
|
|
7
8
|
DEPLOYED_FILE,
|
|
@@ -21,12 +22,16 @@ import {
|
|
|
21
22
|
adoptionEnvelope,
|
|
22
23
|
askRegistry,
|
|
23
24
|
censusOf,
|
|
25
|
+
checkChangelogs,
|
|
24
26
|
compareToBaseline,
|
|
25
27
|
comparisonsIn,
|
|
28
|
+
countVersionHeadings,
|
|
26
29
|
declaredIn,
|
|
27
30
|
driftBetween,
|
|
31
|
+
emptyHeadingsIn,
|
|
28
32
|
formatAdoption,
|
|
29
33
|
formatBaseline,
|
|
34
|
+
formatChangelogs,
|
|
30
35
|
formatDeployed,
|
|
31
36
|
formatMigrations,
|
|
32
37
|
formatPlan,
|
|
@@ -46,6 +51,7 @@ import {
|
|
|
46
51
|
promisedButNotPacked,
|
|
47
52
|
prove,
|
|
48
53
|
proveOver,
|
|
54
|
+
publishableDirs,
|
|
49
55
|
readBaseline,
|
|
50
56
|
readDeployed,
|
|
51
57
|
readReleaseConfig,
|
|
@@ -68,11 +74,12 @@ import {
|
|
|
68
74
|
statementsOf,
|
|
69
75
|
sweepEnvelope,
|
|
70
76
|
sweepSmoke
|
|
71
|
-
} from "./chunk-
|
|
77
|
+
} from "./chunk-7H7SV5EP.js";
|
|
72
78
|
export {
|
|
73
79
|
ADOPTION_NEXT,
|
|
74
80
|
ADOPTION_TOOL,
|
|
75
81
|
BASELINE_FILE,
|
|
82
|
+
CHANGELOG,
|
|
76
83
|
CONSUMER_TREES_FILE,
|
|
77
84
|
DEFAULT_REGISTRY,
|
|
78
85
|
DEPLOYED_FILE,
|
|
@@ -92,12 +99,16 @@ export {
|
|
|
92
99
|
adoptionEnvelope,
|
|
93
100
|
askRegistry,
|
|
94
101
|
censusOf,
|
|
102
|
+
checkChangelogs,
|
|
95
103
|
compareToBaseline,
|
|
96
104
|
comparisonsIn,
|
|
105
|
+
countVersionHeadings,
|
|
97
106
|
declaredIn,
|
|
98
107
|
driftBetween,
|
|
108
|
+
emptyHeadingsIn,
|
|
99
109
|
formatAdoption,
|
|
100
110
|
formatBaseline,
|
|
111
|
+
formatChangelogs,
|
|
101
112
|
formatDeployed,
|
|
102
113
|
formatMigrations,
|
|
103
114
|
formatPlan,
|
|
@@ -117,6 +128,7 @@ export {
|
|
|
117
128
|
promisedButNotPacked,
|
|
118
129
|
prove,
|
|
119
130
|
proveOver,
|
|
131
|
+
publishableDirs,
|
|
120
132
|
readBaseline,
|
|
121
133
|
readDeployed,
|
|
122
134
|
readReleaseConfig,
|
package/dist/release-cli.js
CHANGED
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
SCHEMA_NEXT,
|
|
8
8
|
SMOKE_NEXT,
|
|
9
9
|
adoptionEnvelope,
|
|
10
|
+
checkChangelogs,
|
|
10
11
|
comparisonsIn,
|
|
11
12
|
formatAdoption,
|
|
12
13
|
formatBaseline,
|
|
14
|
+
formatChangelogs,
|
|
13
15
|
formatDeployed,
|
|
14
16
|
formatMigrations,
|
|
15
17
|
formatPlan,
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
migrationsEnvelope,
|
|
23
25
|
prove,
|
|
24
26
|
proveOver,
|
|
27
|
+
publishableDirs,
|
|
25
28
|
publishedEnvelope,
|
|
26
29
|
readReleaseConfig,
|
|
27
30
|
recordBaseline,
|
|
@@ -36,11 +39,24 @@ import {
|
|
|
36
39
|
sweepEnvelope,
|
|
37
40
|
sweepSmoke,
|
|
38
41
|
writeEnvelope
|
|
39
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-7H7SV5EP.js";
|
|
40
43
|
|
|
41
44
|
// src/release-cli.ts
|
|
42
45
|
import { existsSync } from "fs";
|
|
43
46
|
import { join, resolve } from "path";
|
|
47
|
+
|
|
48
|
+
// src/since.ts
|
|
49
|
+
var MEANS = {
|
|
50
|
+
date: {
|
|
51
|
+
is: (value) => /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(Date.parse(value)),
|
|
52
|
+
shape: "a YYYY-MM-DD date"
|
|
53
|
+
},
|
|
54
|
+
plan: { is: (value) => /^\d+$/.test(value), shape: "a plan number" },
|
|
55
|
+
ref: { is: (value) => value !== "" && !value.startsWith("-"), shape: "a git ref" }
|
|
56
|
+
};
|
|
57
|
+
var unreadableSince = (value, kind) => MEANS[kind].is(value) ? void 0 : `--since must be ${MEANS[kind].shape} \u2014 "${value}" is not one`;
|
|
58
|
+
|
|
59
|
+
// src/release-cli.ts
|
|
44
60
|
var USAGE = `geonosis-release <command> [options]
|
|
45
61
|
|
|
46
62
|
migrations --since <ref> [--dialect <name>] [--root <dir>] [--json]
|
|
@@ -57,6 +73,15 @@ var USAGE = `geonosis-release <command> [options]
|
|
|
57
73
|
|
|
58
74
|
Exit 0 clean, 1 refusals, 2 the run could not be made.
|
|
59
75
|
|
|
76
|
+
changelog [--root <dir>] [--json]
|
|
77
|
+
|
|
78
|
+
Every publishable package's CHANGELOG.md, and every "## <version>" heading with
|
|
79
|
+
nothing under it. A reader told that 1.2.0 shipped and told nothing else has been
|
|
80
|
+
told less than nothing; a fixed publish group makes those headings by construction,
|
|
81
|
+
which is a reason to write the sentence and not a reason to leave the blank.
|
|
82
|
+
|
|
83
|
+
Exit 0 clean, 1 an empty heading, 2 no changelog to read at all.
|
|
84
|
+
|
|
60
85
|
schema [--root <dir>] [--json]
|
|
61
86
|
|
|
62
87
|
The wall no oxlint rule can see. Over every migration under every directory
|
|
@@ -119,7 +144,7 @@ var USAGE = `geonosis-release <command> [options]
|
|
|
119
144
|
from it (D-061).
|
|
120
145
|
|
|
121
146
|
smoke snapshot <name> --from <their tree> [--replace] [--root <dir>] [--json]
|
|
122
|
-
[--typecheck <cmd>] [--lint <cmd>] [--doctor <cmd>]
|
|
147
|
+
[--typecheck <cmd>] [--lint <cmd>] [--test <cmd>] [--doctor <cmd>]
|
|
123
148
|
|
|
124
149
|
Copy a consumer tree, read-only, into .geonosis/consumer-snapshots/<name>/ \u2014 the
|
|
125
150
|
thing a release must be read against. Installed packages, build output and the git
|
|
@@ -137,9 +162,9 @@ var USAGE = `geonosis-release <command> [options]
|
|
|
137
162
|
what a consumer installs, and a release that shipped no declarations shipped them in
|
|
138
163
|
the tree and not in the tarball), point a COPY of the snapshot's manifests at those
|
|
139
164
|
tarballs, install with the snapshot's own manager, and run the snapshot's own
|
|
140
|
-
typecheck, lint and doctor.
|
|
165
|
+
typecheck, lint, tests and doctor.
|
|
141
166
|
|
|
142
|
-
\`baseline\` records what those
|
|
167
|
+
\`baseline\` records what those four answered at the versions the consumer is on.
|
|
143
168
|
\`run\` compares against it and refuses only what THIS release broke, in their
|
|
144
169
|
compiler's and their doctor's own lines. Exit 0 nothing new, 1 a new failure,
|
|
145
170
|
2 the run could not be made \u2014 including a phase the baseline cannot be compared to.
|
|
@@ -163,6 +188,7 @@ var VALUED = /* @__PURE__ */ new Set([
|
|
|
163
188
|
"--runner",
|
|
164
189
|
"--since",
|
|
165
190
|
"--snapshot",
|
|
191
|
+
"--test",
|
|
166
192
|
"--typecheck",
|
|
167
193
|
"--wait"
|
|
168
194
|
]);
|
|
@@ -198,6 +224,8 @@ var rootOf = (parsed, cwd) => resolve(cwd, parsed.read["--root"] ?? cwd);
|
|
|
198
224
|
var migrations = (parsed, cwd) => {
|
|
199
225
|
const since = parsed.read["--since"];
|
|
200
226
|
if (since === void 0) throw new CannotRun("migrations needs --since <ref>");
|
|
227
|
+
const unreadable = unreadableSince(since, "ref");
|
|
228
|
+
if (unreadable !== void 0) throw new CannotRun(unreadable);
|
|
201
229
|
const dialect = parsed.read["--dialect"];
|
|
202
230
|
if (dialect !== void 0 && !DIALECTS.has(dialect)) {
|
|
203
231
|
throw new CannotRun(`--dialect must be one of ${[...DIALECTS].toSorted().join(", ")}`);
|
|
@@ -241,6 +269,16 @@ var schema = (parsed, cwd) => {
|
|
|
241
269
|
if (report.unreadable.length > 0) return 2;
|
|
242
270
|
return report.ok ? 0 : 1;
|
|
243
271
|
};
|
|
272
|
+
var changelog = (parsed, cwd) => {
|
|
273
|
+
const root = rootOf(parsed, cwd);
|
|
274
|
+
const report = checkChangelogs({ dirs: publishableDirs(root), root });
|
|
275
|
+
process.stdout.write(
|
|
276
|
+
parsed.json ? `${JSON.stringify(report, void 0, 2)}
|
|
277
|
+
` : `${formatChangelogs(report)}
|
|
278
|
+
`
|
|
279
|
+
);
|
|
280
|
+
return report.empty.length === 0 ? 0 : 1;
|
|
281
|
+
};
|
|
244
282
|
var plan = (parsed, cwd) => {
|
|
245
283
|
const report = runPlan({
|
|
246
284
|
accept: parsed.flags.has("--i-accept-an-unproven-smoke"),
|
|
@@ -333,6 +371,7 @@ var adoption = async (parsed, cwd) => {
|
|
|
333
371
|
var namedCommands = (parsed) => ({
|
|
334
372
|
...parsed.read["--doctor"] === void 0 ? {} : { doctor: parsed.read["--doctor"] },
|
|
335
373
|
...parsed.read["--lint"] === void 0 ? {} : { lint: parsed.read["--lint"] },
|
|
374
|
+
...parsed.read["--test"] === void 0 ? {} : { test: parsed.read["--test"] },
|
|
336
375
|
...parsed.read["--typecheck"] === void 0 ? {} : { typecheck: parsed.read["--typecheck"] }
|
|
337
376
|
});
|
|
338
377
|
var snapshot = (parsed, root) => {
|
|
@@ -485,6 +524,7 @@ var main = async () => {
|
|
|
485
524
|
if (stray !== void 0) throw new CannotRun(`unknown argument "${stray}"`);
|
|
486
525
|
if (parsed.command === "migrations") return migrations(parsed, process.cwd());
|
|
487
526
|
if (parsed.command === "plan") return plan(parsed, process.cwd());
|
|
527
|
+
if (parsed.command === "changelog") return changelog(parsed, process.cwd());
|
|
488
528
|
if (parsed.command === "schema") return schema(parsed, process.cwd());
|
|
489
529
|
if (parsed.command === "prove") return proving(parsed, process.cwd());
|
|
490
530
|
if (parsed.command === "deployed") return deployed(parsed, process.cwd());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geonosis/release",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"types": "./dist/index.d.ts",
|
|
5
5
|
"description": "The release contract with its proofs: an expand-only migration gate over three dialects, a smoke that must name the version that answered it, and declared ≠ deployed.",
|
|
6
6
|
"keywords": [
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"squawk-cli": "2.63.0",
|
|
48
|
-
"@geonosis/ratchet": "2.
|
|
48
|
+
"@geonosis/ratchet": "2.3.0"
|
|
49
49
|
},
|
|
50
50
|
"engines": {
|
|
51
51
|
"node": ">=22"
|