@intentius/chant 0.34.0 → 0.34.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.34.0",
3
+ "version": "0.34.1",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Publishing a package runs its prepack, and a prepack builds against and
3
+ * imports its workspace dependencies' *generated* output — output that only
4
+ * exists once that dependency has been published. So publish order has to be a
5
+ * topological order, and for a long time directory order stood in for one.
6
+ *
7
+ * It held until it did not. chant-v0.34.0 published twelve of fourteen packages
8
+ * and stranded lexicon-forgejo and lexicon-helm a version behind: forgejo needs
9
+ * github's `src/generated/index` and helm needs k8s's `dist/generated`, but
10
+ * `forgejo` < `github` and `helm` < `k8s`, so both ran before the thing they
11
+ * import existed. Half a release shipped and the failure only surfaced from
12
+ * npm, after the tag.
13
+ *
14
+ * publish-packages.sh now derives the order instead of assuming one. This
15
+ * asserts the derivation is actually topological, so the next lexicon that
16
+ * depends on an alphabetically earlier one fails here rather than mid-release.
17
+ */
18
+
19
+ import { describe, expect, it } from "vitest";
20
+ import { execFileSync } from "node:child_process";
21
+ import { readFileSync } from "node:fs";
22
+ import { join, dirname } from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+
25
+ const REPO = join(dirname(fileURLToPath(import.meta.url)), "../../../..");
26
+
27
+ /** Run one of publish-packages.sh's own functions and read back what it prints. */
28
+ function ask(fn: string): string[] {
29
+ const script = readFileSync(join(REPO, "scripts/publish-packages.sh"), "utf8");
30
+ const body = script.match(new RegExp(`^${fn}\\(\\) \\{$.*?^\\}$`, "ms"));
31
+ if (!body) throw new Error(`${fn}() not found in scripts/publish-packages.sh`);
32
+ return execFileSync("bash", ["-c", `${body[0]}\n${fn}`], { cwd: REPO, encoding: "utf8" })
33
+ .split("\n")
34
+ .filter(Boolean);
35
+ }
36
+
37
+ const publishOrder = () => ask("publishable_dirs");
38
+
39
+ function manifest(dir: string) {
40
+ return JSON.parse(readFileSync(join(REPO, dir, "package.json"), "utf8"));
41
+ }
42
+
43
+ describe("publish order", () => {
44
+ const order = publishOrder();
45
+
46
+ it("covers every publishable workspace package", () => {
47
+ // A package missing from the order never publishes at all, which is the
48
+ // same stranding by a different route.
49
+ const all = execFileSync(
50
+ "bash",
51
+ ["-c", 'for d in packages/*/ lexicons/*/; do [ -f "$d/package.json" ] && echo "${d%/}"; done'],
52
+ { cwd: REPO, encoding: "utf8" },
53
+ )
54
+ .split("\n")
55
+ .filter(Boolean)
56
+ .filter((d) => !manifest(d).private);
57
+
58
+ expect([...order].sort()).toEqual([...all].sort());
59
+ });
60
+
61
+ it("places every package after the workspace packages it depends on", () => {
62
+ const position = new Map(order.map((d, i) => [d, i]));
63
+ const owner = new Map(order.map((d) => [manifest(d).name as string, d]));
64
+
65
+ for (const dir of order) {
66
+ const pkg = manifest(dir);
67
+ const deps = Object.keys({
68
+ ...pkg.dependencies,
69
+ ...pkg.peerDependencies,
70
+ ...pkg.optionalDependencies,
71
+ });
72
+ for (const name of deps) {
73
+ const depDir = owner.get(name);
74
+ if (!depDir || depDir === dir) continue;
75
+ expect(
76
+ position.get(depDir)!,
77
+ `${pkg.name} (${dir}) publishes before its dependency ${name} (${depDir}), ` +
78
+ `so ${name}'s generated output will not exist when ${pkg.name} prepacks`,
79
+ ).toBeLessThan(position.get(dir)!);
80
+ }
81
+ }
82
+ });
83
+
84
+ it("puts the two packages that stranded chant-v0.34.0 after what they import", () => {
85
+ // The specific regression, named, so the failure says what broke rather
86
+ // than only that some invariant did.
87
+ expect(order.indexOf("lexicons/github")).toBeLessThan(order.indexOf("lexicons/forgejo"));
88
+ expect(order.indexOf("lexicons/k8s")).toBeLessThan(order.indexOf("lexicons/helm"));
89
+ });
90
+ });
91
+
92
+ /**
93
+ * Ordering alone did not fix the release. The rerun published nothing, found
94
+ * k8s already at 0.34.0, skipped it — and skipping the publish skipped the
95
+ * prepack that builds `dist/generated`, so helm failed on the same missing
96
+ * module. Anything another package compiles against has to be built whether or
97
+ * not it needs publishing.
98
+ */
99
+ describe("packages built even when their publish is skipped", () => {
100
+ const built = new Set(ask("depended_on_dirs"));
101
+ const order = publishOrder();
102
+
103
+ it("covers every workspace dependency, transitively", () => {
104
+ for (const dir of order) {
105
+ const pkg = manifest(dir);
106
+ const owner = new Map(order.map((d) => [manifest(d).name as string, d]));
107
+ for (const name of Object.keys({
108
+ ...pkg.dependencies,
109
+ ...pkg.peerDependencies,
110
+ ...pkg.optionalDependencies,
111
+ })) {
112
+ const depDir = owner.get(name);
113
+ if (!depDir || depDir === dir) continue;
114
+ expect(
115
+ built.has(depDir),
116
+ `${name} (${depDir}) is a dependency of ${pkg.name} but would not be built when ` +
117
+ `its own publish is skipped, so ${pkg.name} compiles against nothing`,
118
+ ).toBe(true);
119
+ }
120
+ }
121
+ });
122
+
123
+ it("includes the two whose skipped build failed the chant-v0.34.0 rerun", () => {
124
+ expect(built.has("lexicons/k8s")).toBe(true);
125
+ expect(built.has("lexicons/github")).toBe(true);
126
+ });
127
+
128
+ it("does not build a leaf nothing depends on", () => {
129
+ // The set is the reason a rerun is not a full 14-package rebuild.
130
+ expect(built.has("lexicons/helm")).toBe(false);
131
+ expect(built.has("lexicons/forgejo")).toBe(false);
132
+ });
133
+ });