@intentius/chant-lexicon-terraform 0.102.0 → 0.104.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.
Files changed (270) hide show
  1. package/dist/__fixtures__/terragrunt/affected-cases.d.ts +23 -0
  2. package/dist/__fixtures__/terragrunt/affected-cases.d.ts.map +1 -0
  3. package/dist/__fixtures__/terragrunt/record.d.ts +17 -0
  4. package/dist/__fixtures__/terragrunt/record.d.ts.map +1 -0
  5. package/dist/change-set.d.ts +67 -0
  6. package/dist/change-set.d.ts.map +1 -0
  7. package/dist/changed-roots.d.ts +30 -0
  8. package/dist/changed-roots.d.ts.map +1 -0
  9. package/dist/commands.d.ts +26 -0
  10. package/dist/commands.d.ts.map +1 -0
  11. package/dist/components/capability-plugin.d.ts +54 -0
  12. package/dist/components/capability-plugin.d.ts.map +1 -0
  13. package/dist/composites/terraform-pin-rollout-op.d.ts +44 -0
  14. package/dist/composites/terraform-pin-rollout-op.d.ts.map +1 -0
  15. package/dist/hcl/descend.d.ts +1 -1
  16. package/dist/hcl/descend.d.ts.map +1 -1
  17. package/dist/hcl/edges.d.ts +2 -2
  18. package/dist/hcl/edges.d.ts.map +1 -1
  19. package/dist/hcl/parse.d.ts +29 -1
  20. package/dist/hcl/parse.d.ts.map +1 -1
  21. package/dist/hcl/roots.d.ts +1 -1
  22. package/dist/hcl/roots.d.ts.map +1 -1
  23. package/dist/hcl/suppressions.d.ts.map +1 -1
  24. package/dist/index.d.ts +4 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/integrity.json +13 -4
  27. package/dist/lint/audit-catalog.d.ts.map +1 -1
  28. package/dist/lint/post-synth/index.d.ts.map +1 -1
  29. package/dist/lint/post-synth/module-source.d.ts +27 -1
  30. package/dist/lint/post-synth/module-source.d.ts.map +1 -1
  31. package/dist/lint/post-synth/terragrunt.d.ts +37 -0
  32. package/dist/lint/post-synth/terragrunt.d.ts.map +1 -0
  33. package/dist/lint/post-synth/tf002.d.ts +3 -1
  34. package/dist/lint/post-synth/tf002.d.ts.map +1 -1
  35. package/dist/lint/post-synth/tf038.d.ts +22 -0
  36. package/dist/lint/post-synth/tf038.d.ts.map +1 -0
  37. package/dist/lint/post-synth/tf039.d.ts +23 -0
  38. package/dist/lint/post-synth/tf039.d.ts.map +1 -0
  39. package/dist/lint/post-synth/tf040.d.ts +30 -0
  40. package/dist/lint/post-synth/tf040.d.ts.map +1 -0
  41. package/dist/lint/post-synth/tf041.d.ts +20 -0
  42. package/dist/lint/post-synth/tf041.d.ts.map +1 -0
  43. package/dist/lint/post-synth/tf042.d.ts +18 -0
  44. package/dist/lint/post-synth/tf042.d.ts.map +1 -0
  45. package/dist/lint/post-synth/tf043.d.ts +17 -0
  46. package/dist/lint/post-synth/tf043.d.ts.map +1 -0
  47. package/dist/lint/post-synth/tf044.d.ts +17 -0
  48. package/dist/lint/post-synth/tf044.d.ts.map +1 -0
  49. package/dist/lint/post-synth/tf045.d.ts +22 -0
  50. package/dist/lint/post-synth/tf045.d.ts.map +1 -0
  51. package/dist/manifest.json +1 -1
  52. package/dist/okf/index.md +8 -0
  53. package/dist/okf/rules/TF038.md +11 -0
  54. package/dist/okf/rules/TF039.md +11 -0
  55. package/dist/okf/rules/TF040.md +11 -0
  56. package/dist/okf/rules/TF041.md +11 -0
  57. package/dist/okf/rules/TF042.md +11 -0
  58. package/dist/okf/rules/TF043.md +11 -0
  59. package/dist/okf/rules/TF044.md +11 -0
  60. package/dist/okf/rules/TF045.md +11 -0
  61. package/dist/op/activities/index.d.ts +4 -0
  62. package/dist/op/activities/index.d.ts.map +1 -1
  63. package/dist/op/activities/pin-rollout.d.ts +46 -0
  64. package/dist/op/activities/pin-rollout.d.ts.map +1 -0
  65. package/dist/op/activities/terraform.d.ts +24 -50
  66. package/dist/op/activities/terraform.d.ts.map +1 -1
  67. package/dist/op/activity-contracts.d.ts +48 -0
  68. package/dist/op/activity-contracts.d.ts.map +1 -1
  69. package/dist/op/builders.d.ts +9 -0
  70. package/dist/op/builders.d.ts.map +1 -1
  71. package/dist/pin/edit-ts.d.ts +21 -0
  72. package/dist/pin/edit-ts.d.ts.map +1 -0
  73. package/dist/pin/edit.d.ts +52 -0
  74. package/dist/pin/edit.d.ts.map +1 -0
  75. package/dist/pin/forge.d.ts +39 -0
  76. package/dist/pin/forge.d.ts.map +1 -0
  77. package/dist/pin/index.d.ts +22 -0
  78. package/dist/pin/index.d.ts.map +1 -0
  79. package/dist/pin/rollout.d.ts +116 -0
  80. package/dist/pin/rollout.d.ts.map +1 -0
  81. package/dist/pin/scan.d.ts +54 -0
  82. package/dist/pin/scan.d.ts.map +1 -0
  83. package/dist/pin/source.d.ts +93 -0
  84. package/dist/pin/source.d.ts.map +1 -0
  85. package/dist/pin/waves.d.ts +66 -0
  86. package/dist/pin/waves.d.ts.map +1 -0
  87. package/dist/plan-digest.d.ts +59 -0
  88. package/dist/plan-digest.d.ts.map +1 -0
  89. package/dist/plugin.d.ts.map +1 -1
  90. package/dist/rules/module-source.ts +56 -1
  91. package/dist/rules/terragrunt.ts +76 -0
  92. package/dist/rules/tf002.ts +1 -1
  93. package/dist/rules/tf038.ts +71 -0
  94. package/dist/rules/tf039.ts +64 -0
  95. package/dist/rules/tf040.ts +76 -0
  96. package/dist/rules/tf041.ts +56 -0
  97. package/dist/rules/tf042.ts +42 -0
  98. package/dist/rules/tf043.ts +49 -0
  99. package/dist/rules/tf044.ts +51 -0
  100. package/dist/rules/tf045.ts +47 -0
  101. package/dist/terragrunt/affected.d.ts +119 -0
  102. package/dist/terragrunt/affected.d.ts.map +1 -0
  103. package/dist/terragrunt/index.d.ts +11 -0
  104. package/dist/terragrunt/index.d.ts.map +1 -0
  105. package/dist/terragrunt/mocks.d.ts +147 -0
  106. package/dist/terragrunt/mocks.d.ts.map +1 -0
  107. package/dist/terragrunt/run.d.ts +130 -0
  108. package/dist/terragrunt/run.d.ts.map +1 -0
  109. package/dist/terragrunt/units.d.ts +103 -0
  110. package/dist/terragrunt/units.d.ts.map +1 -0
  111. package/dist/terragrunt/wave.d.ts +89 -0
  112. package/dist/terragrunt/wave.d.ts.map +1 -0
  113. package/package.json +12 -2
  114. package/src/__fixtures__/change-set/choudoufu-set-plan.change-set.golden.json +95 -0
  115. package/src/__fixtures__/change-set/choudoufu-set-plan.json +76 -0
  116. package/src/__fixtures__/change-set/choudoufu.part.golden.json +173 -0
  117. package/src/__fixtures__/change-set/choudoufu.plan.json +610 -0
  118. package/src/__fixtures__/change-set/terraform.import-forget.plan.json +81 -0
  119. package/src/__fixtures__/change-set/terraform.part.golden.json +173 -0
  120. package/src/__fixtures__/change-set/terraform.plan.json +589 -0
  121. package/src/__fixtures__/change-set/tofu.part.golden.json +173 -0
  122. package/src/__fixtures__/change-set/tofu.plan.json +588 -0
  123. package/src/__fixtures__/change-set/v1.tf.txt +21 -0
  124. package/src/__fixtures__/change-set/v2-module-app.tf.txt +4 -0
  125. package/src/__fixtures__/change-set/v2.tf.txt +21 -0
  126. package/src/__fixtures__/pin/choudoufu-live-waves-n5.json +83 -0
  127. package/src/__fixtures__/pin/largeset-oci-n5/apply-order.txt +5 -0
  128. package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/estate.chdf.hcl +6 -0
  129. package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/main.tf +33 -0
  130. package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/estate.chdf.hcl +6 -0
  131. package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/main.tf +45 -0
  132. package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/estate.chdf.hcl +6 -0
  133. package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/main.tf +47 -0
  134. package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/estate.chdf.hcl +6 -0
  135. package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/main.tf +31 -0
  136. package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/estate.chdf.hcl +6 -0
  137. package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/main.tf +27 -0
  138. package/src/__fixtures__/pin/largeset-oci-n5/fixture.json +77 -0
  139. package/src/__fixtures__/plan-summary/largeset-n5-bump.json +2071 -0
  140. package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.json +95 -0
  141. package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.md +22 -0
  142. package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.txt +12 -0
  143. package/src/__fixtures__/terragrunt/affected/catalog/units/web/terragrunt.hcl +3 -0
  144. package/src/__fixtures__/terragrunt/affected/gitignore +3 -0
  145. package/src/__fixtures__/terragrunt/affected/live/dev/app/terragrunt.hcl +3 -0
  146. package/src/__fixtures__/terragrunt/affected/live/dev/vpc/terragrunt.hcl +3 -0
  147. package/src/__fixtures__/terragrunt/affected/live/prod/app/terragrunt.hcl +3 -0
  148. package/src/__fixtures__/terragrunt/affected/live/stg/terragrunt.stack.hcl +5 -0
  149. package/src/__fixtures__/terragrunt/affected/modules/app/main.tf +6 -0
  150. package/src/__fixtures__/terragrunt/affected/modules/app/policy.json +1 -0
  151. package/src/__fixtures__/terragrunt/affected/modules/net/main.tf +2 -0
  152. package/src/__fixtures__/terragrunt/affected/modules/thing/main.tf +3 -0
  153. package/src/__fixtures__/terragrunt/affected/root.hcl +5 -0
  154. package/src/__fixtures__/terragrunt/affected-cases.ts +126 -0
  155. package/src/__fixtures__/terragrunt/five-units/catalog/units/thing/terragrunt.hcl +3 -0
  156. package/src/__fixtures__/terragrunt/five-units/live/dev/app/terragrunt.hcl +13 -0
  157. package/src/__fixtures__/terragrunt/five-units/live/dev/db/terragrunt.hcl +8 -0
  158. package/src/__fixtures__/terragrunt/five-units/live/dev/vpc/terragrunt.hcl +3 -0
  159. package/src/__fixtures__/terragrunt/five-units/live/prod/app/terragrunt.hcl +8 -0
  160. package/src/__fixtures__/terragrunt/five-units/live/prod/vpc/terragrunt.hcl +3 -0
  161. package/src/__fixtures__/terragrunt/five-units/modules/thing/main.tf +7 -0
  162. package/src/__fixtures__/terragrunt/five-units/root.hcl +5 -0
  163. package/src/__fixtures__/terragrunt/record.ts +56 -0
  164. package/src/__fixtures__/terragrunt/recorded/affected-find.json +50 -0
  165. package/src/__fixtures__/terragrunt/recorded/failed-plan-report.json +36 -0
  166. package/src/__fixtures__/terragrunt/recorded/find.json +67 -0
  167. package/src/__fixtures__/terragrunt/recorded/version.txt +1 -0
  168. package/src/__fixtures__/terragrunt/recorded/wave-1/apply-report.json +22 -0
  169. package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/dev/vpc/tfplan.json +1 -0
  170. package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/prod/vpc/tfplan.json +1 -0
  171. package/src/__fixtures__/terragrunt/recorded/wave-1/plan-report.json +22 -0
  172. package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/dev/db/tfplan.json +1 -0
  173. package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/prod/app/tfplan.json +1 -0
  174. package/src/__fixtures__/terragrunt/recorded/wave-2/plan-report.json +22 -0
  175. package/src/change-set-combined-run.test.ts +156 -0
  176. package/src/change-set.test.ts +151 -0
  177. package/src/change-set.ts +273 -0
  178. package/src/changed-roots.test.ts +58 -0
  179. package/src/changed-roots.ts +102 -0
  180. package/src/commands.ts +112 -0
  181. package/src/components/capability-plugin.test.ts +91 -0
  182. package/src/components/capability-plugin.ts +131 -0
  183. package/src/components/pr-loop.tofu.test.ts +244 -0
  184. package/src/composites/terraform-pin-rollout-op.test.ts +69 -0
  185. package/src/composites/terraform-pin-rollout-op.ts +60 -0
  186. package/src/hcl/descend.ts +1 -1
  187. package/src/hcl/edges.ts +2 -2
  188. package/src/hcl/parse.test.ts +25 -0
  189. package/src/hcl/parse.ts +91 -6
  190. package/src/hcl/roots.ts +1 -1
  191. package/src/hcl/suppressions.ts +8 -2
  192. package/src/index.ts +18 -0
  193. package/src/lint/audit-catalog.ts +82 -0
  194. package/src/lint/audit.test.ts +3 -1
  195. package/src/lint/post-synth/fixtures/TF038/positive.tf +3 -0
  196. package/src/lint/post-synth/fixtures/TF039/positive.tf +4 -0
  197. package/src/lint/post-synth/fixtures/TF041/negative/terragrunt.hcl +12 -0
  198. package/src/lint/post-synth/fixtures/TF041/positive/terragrunt.hcl +7 -0
  199. package/src/lint/post-synth/fixtures/TF041/positive-apply/terragrunt.hcl +8 -0
  200. package/src/lint/post-synth/fixtures/TF042/negative/terragrunt.hcl +13 -0
  201. package/src/lint/post-synth/fixtures/TF042/positive/terragrunt.hcl +9 -0
  202. package/src/lint/post-synth/fixtures/TF043/negative/terragrunt.hcl +3 -0
  203. package/src/lint/post-synth/fixtures/TF043/negative-other/terragrunt.hcl +3 -0
  204. package/src/lint/post-synth/fixtures/TF043/positive-git/terragrunt.hcl +3 -0
  205. package/src/lint/post-synth/fixtures/TF043/positive-oci/terragrunt.hcl +3 -0
  206. package/src/lint/post-synth/fixtures/TF043/positive-tfr/terragrunt.hcl +3 -0
  207. package/src/lint/post-synth/fixtures/TF044/negative/terragrunt.hcl +9 -0
  208. package/src/lint/post-synth/fixtures/TF044/positive/terragrunt.hcl +7 -0
  209. package/src/lint/post-synth/fixtures/TF044/positive-generate/terragrunt.hcl +9 -0
  210. package/src/lint/post-synth/fixtures/TF045/negative/root.hcl +1 -0
  211. package/src/lint/post-synth/fixtures/TF045/negative/terragrunt.hcl +3 -0
  212. package/src/lint/post-synth/fixtures/TF045/positive/root.hcl +9 -0
  213. package/src/lint/post-synth/fixtures/TF045/positive/terragrunt.hcl +3 -0
  214. package/src/lint/post-synth/fixtures/TF045/unit-only/terragrunt.hcl +3 -0
  215. package/src/lint/post-synth/index.ts +16 -0
  216. package/src/lint/post-synth/module-source.test.ts +42 -1
  217. package/src/lint/post-synth/module-source.ts +56 -1
  218. package/src/lint/post-synth/terragrunt.ts +76 -0
  219. package/src/lint/post-synth/tf002.ts +1 -1
  220. package/src/lint/post-synth/tf038.test.ts +51 -0
  221. package/src/lint/post-synth/tf038.ts +71 -0
  222. package/src/lint/post-synth/tf039.test.ts +40 -0
  223. package/src/lint/post-synth/tf039.ts +64 -0
  224. package/src/lint/post-synth/tf040.test.ts +55 -0
  225. package/src/lint/post-synth/tf040.ts +76 -0
  226. package/src/lint/post-synth/tf041.test.ts +50 -0
  227. package/src/lint/post-synth/tf041.ts +56 -0
  228. package/src/lint/post-synth/tf042.test.ts +19 -0
  229. package/src/lint/post-synth/tf042.ts +42 -0
  230. package/src/lint/post-synth/tf043.test.ts +65 -0
  231. package/src/lint/post-synth/tf043.ts +49 -0
  232. package/src/lint/post-synth/tf044.test.ts +27 -0
  233. package/src/lint/post-synth/tf044.ts +51 -0
  234. package/src/lint/post-synth/tf045.test.ts +30 -0
  235. package/src/lint/post-synth/tf045.ts +47 -0
  236. package/src/op/activities/index.ts +4 -0
  237. package/src/op/activities/pin-rollout.ts +77 -0
  238. package/src/op/activities/terraform.test.ts +13 -0
  239. package/src/op/activities/terraform.ts +46 -86
  240. package/src/op/activity-contracts.test.ts +4 -1
  241. package/src/op/activity-contracts.ts +42 -0
  242. package/src/op/builders.ts +13 -0
  243. package/src/pin/bundle.test.ts +53 -0
  244. package/src/pin/edit-ts.test.ts +39 -0
  245. package/src/pin/edit-ts.ts +77 -0
  246. package/src/pin/edit.test.ts +199 -0
  247. package/src/pin/edit.ts +157 -0
  248. package/src/pin/forge.ts +85 -0
  249. package/src/pin/index.ts +22 -0
  250. package/src/pin/rollout.test.ts +232 -0
  251. package/src/pin/rollout.ts +451 -0
  252. package/src/pin/scan.ts +309 -0
  253. package/src/pin/source.ts +206 -0
  254. package/src/pin/waves.test.ts +101 -0
  255. package/src/pin/waves.ts +125 -0
  256. package/src/plan-digest.ts +99 -0
  257. package/src/plan-summary.test.ts +154 -0
  258. package/src/plugin.ts +25 -2
  259. package/src/terragrunt/affected.acceptance.test.ts +83 -0
  260. package/src/terragrunt/affected.test.ts +177 -0
  261. package/src/terragrunt/affected.ts +348 -0
  262. package/src/terragrunt/index.ts +11 -0
  263. package/src/terragrunt/mocks.acceptance.test.ts +105 -0
  264. package/src/terragrunt/mocks.test.ts +309 -0
  265. package/src/terragrunt/mocks.ts +324 -0
  266. package/src/terragrunt/run.ts +369 -0
  267. package/src/terragrunt/terragrunt.acceptance.test.ts +122 -0
  268. package/src/terragrunt/terragrunt.test.ts +332 -0
  269. package/src/terragrunt/units.ts +235 -0
  270. package/src/terragrunt/wave.ts +181 -0
@@ -0,0 +1,232 @@
1
+ /**
2
+ * The pin-bump rollout end to end over git, with the forge mocked (#3189).
3
+ *
4
+ * The repository is choudoufu#1750's pinned-OCI variant at N=5, generated at
5
+ * choudoufu 52bef26 with
6
+ * `go run ./tools/largeset-gen -estates 5 -source oci -registry localhost:4890`:
7
+ * five estate roots, each calling `oci://localhost:4890/largeset/shared?tag=1.0.0`,
8
+ * where e02 reads e01 and e03 reads e02. It is pushed to a bare remote, so
9
+ * the rollout fetches, builds wave branches in its own worktree and pushes
10
+ * them for real. Only the forge is a mock: it records each PR, and the test
11
+ * merges one by moving the remote's `main` to the PR's head, the way a
12
+ * fast-forward merge would.
13
+ */
14
+
15
+ import { execFileSync } from "node:child_process";
16
+ import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
20
+ import { loadHcl2json } from "@intentius/chant/terraform/hcl2json";
21
+ import { runPinRollout, type PinRolloutOptions } from "./rollout";
22
+ import type { PinCommitCheck, PinForge, PinPullRequest } from "./forge";
23
+ import { editPinsInTs } from "./edit-ts";
24
+
25
+ const parser = await loadHcl2json();
26
+ const FIXTURE = new URL("../__fixtures__/pin/largeset-oci-n5", import.meta.url).pathname;
27
+ const MODULE = "oci://localhost:4890/largeset/shared";
28
+ const DEPS = [
29
+ { root: "estates/e01" },
30
+ { root: "estates/e02", dependsOn: ["estates/e01"] },
31
+ { root: "estates/e03", dependsOn: ["estates/e02"] },
32
+ { root: "estates/e04" },
33
+ { root: "estates/e05" },
34
+ ];
35
+
36
+ function git(cwd: string, ...args: string[]): string {
37
+ return execFileSync("git", args, { cwd, encoding: "utf-8", env: { ...process.env, GIT_AUTHOR_NAME: "t", GIT_AUTHOR_EMAIL: "t@t", GIT_COMMITTER_NAME: "t", GIT_COMMITTER_EMAIL: "t@t" } });
38
+ }
39
+
40
+ /** An in-memory forge over the bare remote. */
41
+ class MockForge implements PinForge {
42
+ prs = new Map<string, PinPullRequest & { head: string; title: string; base: string }>();
43
+ checks = new Map<string, PinCommitCheck[]>();
44
+ constructor(private remote: string) {}
45
+ async findPullRequest(branch: string) {
46
+ return this.prs.get(branch) ?? null;
47
+ }
48
+ async createPullRequest(input: { base: string; head: string; title: string; body: string }) {
49
+ const url = `https://forge.test/pr/${this.prs.size + 1}`;
50
+ this.prs.set(input.head, { ...input, url, state: "open" });
51
+ return url;
52
+ }
53
+ async commitChecks(sha: string) {
54
+ return this.checks.get(sha) ?? [];
55
+ }
56
+ /** Merge the PR from `branch` by moving the remote's main to its head. */
57
+ merge(branch: string): string {
58
+ const sha = git(this.remote, "rev-parse", `refs/heads/${branch}`).trim();
59
+ git(this.remote, "update-ref", "refs/heads/main", sha);
60
+ const pr = this.prs.get(branch)!;
61
+ pr.state = "merged";
62
+ pr.mergeCommit = sha;
63
+ return sha;
64
+ }
65
+ report(sha: string, roots: string[], state: PinCommitCheck["state"]) {
66
+ this.checks.set(sha, [...(this.checks.get(sha) ?? []), ...roots.map((r) => ({ name: `apply/${r}`, state }))]);
67
+ }
68
+ }
69
+
70
+ let dir: string;
71
+ let remote: string;
72
+ let work: string;
73
+ let forge: MockForge;
74
+
75
+ beforeEach(() => {
76
+ dir = mkdtempSync(join(tmpdir(), "pin-rollout-"));
77
+ remote = join(dir, "remote.git");
78
+ work = join(dir, "work");
79
+ git(dir, "init", "--quiet", "--bare", "--initial-branch=main", remote);
80
+ cpSync(FIXTURE, work, { recursive: true });
81
+ git(work, "init", "--quiet", "--initial-branch=main");
82
+ git(work, "add", ".");
83
+ git(work, "commit", "--quiet", "-m", "fixture");
84
+ git(work, "remote", "add", "origin", remote);
85
+ git(work, "push", "--quiet", "origin", "main");
86
+ git(work, "remote", "set-head", "origin", "main");
87
+ forge = new MockForge(remote);
88
+ });
89
+
90
+ afterEach(() => rmSync(dir, { recursive: true, force: true }));
91
+
92
+ function options(extra: Partial<PinRolloutOptions> = {}): PinRolloutOptions {
93
+ return { cwd: work, module: MODULE, from: "1.0.0", to: "1.1.0", parser, forge, roots: DEPS, canaries: ["estates/e04"], mode: "pull-request", ...extra };
94
+ }
95
+
96
+ /** The roots a path-diff selection picks for a branch: the root directories of the files it changes against main. */
97
+ function pathDiffRoots(branch: string): string[] {
98
+ const files = git(remote, "diff", "--name-only", `main...${branch}`).split("\n").filter(Boolean);
99
+ return [...new Set(files.map((f) => f.split("/").slice(0, 2).join("/")))].sort();
100
+ }
101
+
102
+ describe("runPinRollout (#3189)", () => {
103
+ test("one PR per wave, each opening only after the last merged and applied", async () => {
104
+ const head = git(work, "rev-parse", "HEAD").trim();
105
+
106
+ // Run 1 opens wave 1, the canary, and nothing else.
107
+ const r1 = await runPinRollout(options());
108
+ expect(r1.status).toBe("opened");
109
+ expect(r1.waves.map((w) => [w.wave, w.roots, w.state])).toEqual([
110
+ [1, ["estates/e04"], "opened"],
111
+ [2, ["estates/e01", "estates/e05"], "not-reached"],
112
+ [3, ["estates/e02"], "not-reached"],
113
+ [4, ["estates/e03"], "not-reached"],
114
+ ]);
115
+ expect([...forge.prs.keys()]).toEqual(["chant/pin/localhost-4890-largeset-shared/1.1.0/wave-1"]);
116
+ const pr1 = forge.prs.get(r1.waves[0]!.branch)!;
117
+ expect(pr1.base).toBe("main");
118
+ expect(pr1.body).toContain("`oci://localhost:4890/largeset/shared` pin 1.0.0 -> 1.1.0");
119
+ expect(pr1.body).toContain("wave 1 of 4 (canaries)");
120
+ expect(pr1.body).toContain("- `estates/e04`: estates/e04/main.tf module.shared");
121
+ expect(pathDiffRoots(r1.waves[0]!.branch)).toEqual(["estates/e04"]);
122
+
123
+ // Never the default branch, never the caller's checkout.
124
+ expect(git(remote, "rev-parse", "refs/heads/main").trim()).toBe(head);
125
+ expect(git(work, "rev-parse", "HEAD").trim()).toBe(head);
126
+ expect(git(work, "status", "--porcelain")).toBe("");
127
+
128
+ // Run 2, before the merge: reports where it stands and opens nothing.
129
+ const r2 = await runPinRollout(options());
130
+ expect(r2.status).toBe("waiting");
131
+ expect(r2.waves[0]!.state).toBe("open");
132
+ expect(forge.prs.size).toBe(1);
133
+
134
+ // Merged, apply not reported yet: still nothing opens.
135
+ const merge1 = forge.merge(r1.waves[0]!.branch);
136
+ const r3 = await runPinRollout(options());
137
+ expect(r3.status).toBe("waiting");
138
+ expect(r3.waves[0]).toMatchObject({ state: "waiting-apply", pending: ["estates/e04"] });
139
+ expect(forge.prs.size).toBe(1);
140
+
141
+ // Applied: wave 2 opens, built on the merged main.
142
+ forge.report(merge1, ["estates/e04"], "success");
143
+ const r4 = await runPinRollout(options());
144
+ expect(r4.status).toBe("opened");
145
+ expect(r4.waves.map((w) => w.state)).toEqual(["applied", "opened", "not-reached", "not-reached"]);
146
+ expect(pathDiffRoots(r4.waves[1]!.branch)).toEqual(["estates/e01", "estates/e05"]);
147
+
148
+ // Waves 2 to 4, each merged and applied in turn.
149
+ let last = r4;
150
+ for (const n of [1, 2, 3]) {
151
+ const branch = last.waves[n]!.branch;
152
+ forge.report(forge.merge(branch), last.waves[n]!.roots, "success");
153
+ last = await runPinRollout(options());
154
+ if (n < 3) {
155
+ expect(last.waves[n + 1]!.state).toBe("opened");
156
+ expect(pathDiffRoots(last.waves[n + 1]!.branch)).toEqual(last.waves[n + 1]!.roots);
157
+ }
158
+ }
159
+ expect(last.status).toBe("complete");
160
+ expect(forge.prs.size).toBe(4);
161
+ // Every root is at the new pin on main.
162
+ for (const e of ["e01", "e02", "e03", "e04", "e05"]) {
163
+ expect(git(remote, "show", `main:estates/${e}/main.tf`)).toContain(`${MODULE}?tag=1.1.0`);
164
+ }
165
+ });
166
+
167
+ test("a failed root stops the waves after it, and is named", async () => {
168
+ const r1 = await runPinRollout(options({ canaries: [] }));
169
+ const wave1 = r1.waves[0]!;
170
+ expect(wave1.roots).toEqual(["estates/e01", "estates/e04", "estates/e05"]);
171
+ const sha = forge.merge(wave1.branch);
172
+ forge.report(sha, ["estates/e01", "estates/e04"], "success");
173
+ forge.report(sha, ["estates/e05"], "failure");
174
+ const r2 = await runPinRollout(options({ canaries: [] }));
175
+ expect(r2.status).toBe("stopped");
176
+ expect(r2.stop).toBe("wave 1: estates/e05 failed its apply check apply/estates/e05");
177
+ expect(r2.waves.map((w) => w.state)).toEqual(["failed", "not-reached", "not-reached"]);
178
+ expect(forge.prs.size).toBe(1);
179
+ });
180
+
181
+ test("a PR closed without merging stops the rollout", async () => {
182
+ const r1 = await runPinRollout(options());
183
+ forge.prs.get(r1.waves[0]!.branch)!.state = "closed";
184
+ const r2 = await runPinRollout(options());
185
+ expect(r2.status).toBe("stopped");
186
+ expect(r2.stop).toMatch(/wave 1's PR https:\/\/forge.test\/pr\/1 was closed without merging/);
187
+ });
188
+
189
+ test("report mode opens nothing and pushes nothing", async () => {
190
+ const r = await runPinRollout(options({ mode: "report" }));
191
+ expect(r.status).toBe("would-open");
192
+ expect(r.waves[0]).toMatchObject({ state: "would-open", files: ["estates/e04/main.tf"] });
193
+ expect(forge.prs.size).toBe(0);
194
+ expect(git(remote, "branch", "--list", "chant/*")).toBe("");
195
+ });
196
+
197
+ test("a floating constraint is refused by name and left out of every wave", async () => {
198
+ const tf = join(work, "estates/e05/main.tf");
199
+ writeFileSync(tf, readFileSync(tf, "utf-8").replace(`source = "${MODULE}?tag=1.0.0"`, `source = "app.terraform.io/acme/shared/aws"\n version = "~> 1.0"`));
200
+ git(work, "commit", "--quiet", "-am", "e05 floats");
201
+ git(work, "push", "--quiet", "origin", "main");
202
+ const r = await runPinRollout(options({ module: "app.terraform.io/acme/shared/aws", from: "1.0.0", to: "1.1.0", mode: "report", roots: undefined, canaries: [] }));
203
+ expect(r.roots).toEqual([
204
+ expect.objectContaining({ root: "estates/e05", state: "refused", reason: expect.stringContaining(`version "~> 1.0" is a constraint, not a pin`) }),
205
+ ]);
206
+ expect(r.waves).toEqual([]);
207
+ expect(r.status).toBe("complete");
208
+ });
209
+
210
+ test("roots are found when none are named, and choudoufu's waves can order them", async () => {
211
+ const doc = JSON.parse(readFileSync(new URL("../__fixtures__/pin/choudoufu-live-waves-n5.json", import.meta.url), "utf-8"));
212
+ const { wavesFromChoudoufu } = await import("./waves");
213
+ const r = await runPinRollout(options({ roots: undefined, canaries: undefined, waves: wavesFromChoudoufu(doc), mode: "report" }));
214
+ expect(r.roots.map((x) => x.root)).toEqual(["estates/e01", "estates/e02", "estates/e03", "estates/e04", "estates/e05"]);
215
+ expect(r.waves.map((w) => w.roots)).toEqual([["estates/e04"], ["estates/e01", "estates/e05"], ["estates/e02"], ["estates/e03"]]);
216
+ });
217
+
218
+ test("a generated root's pin moves in its TypeScript source", async () => {
219
+ writeFileSync(join(work, "estates/e04/shared.ts"), `export const shared = { source: "${MODULE}?tag=1.0.0", name: "e04" };\n`);
220
+ git(work, "add", ".");
221
+ git(work, "commit", "--quiet", "-m", "e04 is generated");
222
+ git(work, "push", "--quiet", "origin", "main");
223
+ const roots = DEPS.map((r) => (r.root === "estates/e04" ? { ...r, tsSource: "estates/e04/shared.ts" } : r));
224
+
225
+ const without = await runPinRollout(options({ roots, mode: "report" }));
226
+ expect(without.roots.find((r) => r.root === "estates/e04")).toMatchObject({ state: "refused", reason: expect.stringContaining("no TypeScript editor") });
227
+
228
+ const r = await runPinRollout(options({ roots, editTs: editPinsInTs }));
229
+ expect(r.waves[0]).toMatchObject({ roots: ["estates/e04"], state: "opened", files: ["estates/e04/shared.ts"] });
230
+ expect(git(remote, "show", `${r.waves[0]!.branch}:estates/e04/shared.ts`)).toContain(`${MODULE}?tag=1.1.0`);
231
+ });
232
+ });