@intentius/chant-lexicon-terraform 0.124.51 → 0.124.54

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 (338) hide show
  1. package/README.md +7 -6
  2. package/dist/__fixtures__/pr-loop/estate.component.d.ts +7 -0
  3. package/dist/__fixtures__/pr-loop/estate.component.d.ts.map +1 -0
  4. package/dist/__fixtures__/pr-loop/estate.component.js +7 -0
  5. package/dist/__fixtures__/report/generate.d.ts +26 -0
  6. package/dist/__fixtures__/report/generate.d.ts.map +1 -0
  7. package/dist/__fixtures__/report/generate.js +58 -0
  8. package/dist/applied-state.d.ts +27 -0
  9. package/dist/applied-state.d.ts.map +1 -0
  10. package/dist/applied-state.js +44 -0
  11. package/dist/backend-config.d.ts +53 -0
  12. package/dist/backend-config.d.ts.map +1 -0
  13. package/dist/backend-config.js +136 -0
  14. package/dist/behaviour/kinds.d.ts.map +1 -1
  15. package/dist/behaviour/kinds.js +4 -0
  16. package/dist/behaviour/predict.d.ts.map +1 -1
  17. package/dist/behaviour/predict.js +6 -2
  18. package/dist/behaviour/reference-catalog.d.ts +16 -5
  19. package/dist/behaviour/reference-catalog.d.ts.map +1 -1
  20. package/dist/behaviour/reference-catalog.js +16 -5
  21. package/dist/behaviour/request.d.ts.map +1 -1
  22. package/dist/behaviour/request.js +3 -2
  23. package/dist/changed-roots.d.ts +6 -4
  24. package/dist/changed-roots.d.ts.map +1 -1
  25. package/dist/changed-roots.js +14 -18
  26. package/dist/commands.d.ts +5 -0
  27. package/dist/commands.d.ts.map +1 -1
  28. package/dist/commands.js +87 -4
  29. package/dist/components/capability-plugin.d.ts +65 -3
  30. package/dist/components/capability-plugin.d.ts.map +1 -1
  31. package/dist/components/capability-plugin.js +197 -13
  32. package/dist/components/compose-group.d.ts +145 -0
  33. package/dist/components/compose-group.d.ts.map +1 -0
  34. package/dist/components/compose-group.js +407 -0
  35. package/dist/components/placeholder-preview.d.ts +78 -0
  36. package/dist/components/placeholder-preview.d.ts.map +1 -0
  37. package/dist/components/placeholder-preview.js +254 -0
  38. package/dist/compose/apply-events.d.ts +46 -0
  39. package/dist/compose/apply-events.d.ts.map +1 -0
  40. package/dist/compose/apply-events.js +77 -0
  41. package/dist/compose/estate.d.ts +194 -0
  42. package/dist/compose/estate.d.ts.map +1 -0
  43. package/dist/compose/estate.js +363 -0
  44. package/dist/compose/hcl.d.ts +99 -0
  45. package/dist/compose/hcl.d.ts.map +1 -0
  46. package/dist/compose/hcl.js +302 -0
  47. package/dist/compose/module-address.d.ts +24 -0
  48. package/dist/compose/module-address.d.ts.map +1 -0
  49. package/dist/compose/module-address.js +43 -0
  50. package/dist/compose/root.d.ts +127 -0
  51. package/dist/compose/root.d.ts.map +1 -0
  52. package/dist/compose/root.js +586 -0
  53. package/dist/compose/run.d.ts +183 -0
  54. package/dist/compose/run.d.ts.map +1 -0
  55. package/dist/compose/run.js +447 -0
  56. package/dist/compose/slice.d.ts +102 -0
  57. package/dist/compose/slice.d.ts.map +1 -0
  58. package/dist/compose/slice.js +249 -0
  59. package/dist/compose/state-address.d.ts +28 -0
  60. package/dist/compose/state-address.d.ts.map +1 -0
  61. package/dist/compose/state-address.js +154 -0
  62. package/dist/compose/state-merge.d.ts +52 -0
  63. package/dist/compose/state-merge.d.ts.map +1 -0
  64. package/dist/compose/state-merge.js +135 -0
  65. package/dist/compose.d.ts +179 -0
  66. package/dist/compose.d.ts.map +1 -0
  67. package/dist/compose.js +482 -0
  68. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  69. package/dist/composites/terraform-apply-op.js +7 -3
  70. package/dist/config.d.ts +59 -13
  71. package/dist/config.d.ts.map +1 -1
  72. package/dist/config.js +43 -9
  73. package/dist/describe-resources.d.ts +74 -12
  74. package/dist/describe-resources.d.ts.map +1 -1
  75. package/dist/describe-resources.js +374 -66
  76. package/dist/drift-read.d.ts +33 -0
  77. package/dist/drift-read.d.ts.map +1 -0
  78. package/dist/drift-read.js +243 -0
  79. package/dist/drift-record.d.ts +147 -0
  80. package/dist/drift-record.d.ts.map +1 -0
  81. package/dist/drift-record.js +383 -0
  82. package/dist/hcl/edges.d.ts.map +1 -1
  83. package/dist/hcl/edges.js +24 -1
  84. package/dist/hcl/files.d.ts +61 -0
  85. package/dist/hcl/files.d.ts.map +1 -0
  86. package/dist/hcl/files.js +221 -0
  87. package/dist/hcl/instances.d.ts +82 -0
  88. package/dist/hcl/instances.d.ts.map +1 -0
  89. package/dist/hcl/instances.js +390 -0
  90. package/dist/hcl/parse.d.ts +81 -9
  91. package/dist/hcl/parse.d.ts.map +1 -1
  92. package/dist/hcl/parse.js +114 -16
  93. package/dist/hcl/roots.d.ts.map +1 -1
  94. package/dist/hcl/roots.js +5 -0
  95. package/dist/hcl/suppressions.d.ts.map +1 -1
  96. package/dist/hcl/suppressions.js +8 -2
  97. package/dist/index.d.ts +11 -2
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +21 -2
  100. package/dist/integrity.json +3 -3
  101. package/dist/live-plan-reasons.d.ts +21 -0
  102. package/dist/live-plan-reasons.d.ts.map +1 -0
  103. package/dist/live-plan-reasons.js +47 -0
  104. package/dist/lsp/option-keys.d.ts.map +1 -1
  105. package/dist/lsp/option-keys.js +2 -0
  106. package/dist/manifest.json +1 -1
  107. package/dist/op/activities/index.d.ts +6 -2
  108. package/dist/op/activities/index.d.ts.map +1 -1
  109. package/dist/op/activities/index.js +5 -1
  110. package/dist/op/activities/live-detect.d.ts +4 -3
  111. package/dist/op/activities/live-detect.d.ts.map +1 -1
  112. package/dist/op/activities/live-detect.js +57 -32
  113. package/dist/op/activities/pin-rollout.d.ts +2 -0
  114. package/dist/op/activities/pin-rollout.d.ts.map +1 -1
  115. package/dist/op/activities/pin-rollout.js +1 -0
  116. package/dist/op/activities/terraform.d.ts +153 -5
  117. package/dist/op/activities/terraform.d.ts.map +1 -1
  118. package/dist/op/activities/terraform.js +205 -20
  119. package/dist/op/activity-contracts.d.ts +27 -0
  120. package/dist/op/activity-contracts.d.ts.map +1 -1
  121. package/dist/op/activity-contracts.js +19 -1
  122. package/dist/op/builders.d.ts +7 -1
  123. package/dist/op/builders.d.ts.map +1 -1
  124. package/dist/op/builders.js +9 -0
  125. package/dist/pin/edit.d.ts.map +1 -1
  126. package/dist/pin/edit.js +35 -0
  127. package/dist/pin/rollout.d.ts +2 -0
  128. package/dist/pin/rollout.d.ts.map +1 -1
  129. package/dist/pin/rollout.js +14 -11
  130. package/dist/plan-shape.d.ts +30 -0
  131. package/dist/plan-shape.d.ts.map +1 -0
  132. package/dist/plan-shape.js +125 -0
  133. package/dist/plugin.d.ts.map +1 -1
  134. package/dist/plugin.js +32 -18
  135. package/dist/report.d.ts +216 -0
  136. package/dist/report.d.ts.map +1 -0
  137. package/dist/report.js +666 -0
  138. package/dist/skills/chant-terraform.md +1 -1
  139. package/dist/state/azure-blob.d.ts +68 -0
  140. package/dist/state/azure-blob.d.ts.map +1 -0
  141. package/dist/state/azure-blob.js +201 -0
  142. package/dist/state/backend.d.ts +158 -0
  143. package/dist/state/backend.d.ts.map +1 -0
  144. package/dist/state/backend.js +406 -0
  145. package/dist/state/exec.d.ts +39 -0
  146. package/dist/state/exec.d.ts.map +1 -0
  147. package/dist/state/exec.js +80 -0
  148. package/dist/state/gcs.d.ts +62 -0
  149. package/dist/state/gcs.d.ts.map +1 -0
  150. package/dist/state/gcs.js +224 -0
  151. package/dist/state/index.d.ts +15 -0
  152. package/dist/state/index.d.ts.map +1 -0
  153. package/dist/state/index.js +14 -0
  154. package/dist/state/locks.d.ts +105 -0
  155. package/dist/state/locks.d.ts.map +1 -0
  156. package/dist/state/locks.js +130 -0
  157. package/dist/state/object-store.d.ts +37 -0
  158. package/dist/state/object-store.d.ts.map +1 -0
  159. package/dist/state/object-store.js +20 -0
  160. package/dist/state/root-states.d.ts +131 -0
  161. package/dist/state/root-states.d.ts.map +1 -0
  162. package/dist/state/root-states.js +241 -0
  163. package/dist/state/s3.d.ts +68 -0
  164. package/dist/state/s3.d.ts.map +1 -0
  165. package/dist/state/s3.js +170 -0
  166. package/dist/state/store.d.ts +51 -0
  167. package/dist/state/store.d.ts.map +1 -0
  168. package/dist/state/store.js +185 -0
  169. package/dist/terragrunt/affected.d.ts.map +1 -1
  170. package/dist/terragrunt/affected.js +2 -1
  171. package/package.json +7 -3
  172. package/src/__fixtures__/ACCEPTANCE.md +69 -7
  173. package/src/__fixtures__/compose/apply-failed-in-app.jsonl +12 -0
  174. package/src/__fixtures__/compose/state-app.json +66 -0
  175. package/src/__fixtures__/compose/state-edge.json +40 -0
  176. package/src/__fixtures__/compose/state-net.json +58 -0
  177. package/src/__fixtures__/compose/three-app.plan.json +184 -0
  178. package/src/__fixtures__/compose/three-composed.plan.json +509 -0
  179. package/src/__fixtures__/compose/three-edge.plan.json +144 -0
  180. package/src/__fixtures__/compose/three-net.plan.json +174 -0
  181. package/src/__fixtures__/compose/unknown-composed.plan.json +228 -0
  182. package/src/__fixtures__/compose/working-after-apply.json +145 -0
  183. package/src/__fixtures__/config-blocks/app/main.tf +61 -0
  184. package/src/__fixtures__/config-blocks/app/modules/network/main.tf +4 -0
  185. package/src/__fixtures__/config-blocks/app-json/main.tf.json +39 -0
  186. package/src/__fixtures__/config-files/shadow/main.tf +3 -0
  187. package/src/__fixtures__/config-files/shadow/main.tofu +3 -0
  188. package/src/__fixtures__/config-files/shadow/other.tf +3 -0
  189. package/src/__fixtures__/config-files/tf/main.tf +11 -0
  190. package/src/__fixtures__/config-files/tf-json/main.tf.json +18 -0
  191. package/src/__fixtures__/config-files/tofu/main.tofu +11 -0
  192. package/src/__fixtures__/config-files/tofu-json/main.tofu.json +18 -0
  193. package/src/__fixtures__/config-files/tofu-only/main.tofu +3 -0
  194. package/src/__fixtures__/drift-record/README.md +11 -0
  195. package/src/__fixtures__/drift-record/live-plan-sections.json +27 -0
  196. package/src/__fixtures__/instance-forms/app/main.tf +110 -0
  197. package/src/__fixtures__/instance-forms/app/modules/svc/main.tf +3 -0
  198. package/src/__fixtures__/instance-vars/app/main.tf +52 -0
  199. package/src/__fixtures__/instance-vars/app/modules/app/main.tf +12 -0
  200. package/src/__fixtures__/instance-vars/app/modules/edge/main.tf +3 -0
  201. package/src/__fixtures__/instance-vars/app/terraform.tfvars +1 -0
  202. package/src/__fixtures__/live-module-keyed/live-graph.json +259 -0
  203. package/src/__fixtures__/live-module-keyed/live-plan.json +87 -0
  204. package/src/__fixtures__/live-module-keyed/root/estate.chdf.hcl +1 -0
  205. package/src/__fixtures__/live-module-keyed/root/main.tf +21 -0
  206. package/src/__fixtures__/live-module-keyed/root/modules/app/main.tf +17 -0
  207. package/src/__fixtures__/live-module-keyed/root/modules/edge/main.tf +7 -0
  208. package/src/__fixtures__/live-module-keyed/root/versions.tf +30 -0
  209. package/src/__fixtures__/live-module-keyed/show-state.json +146 -0
  210. package/src/__fixtures__/pr-loop/README.md +50 -0
  211. package/src/__fixtures__/pr-loop/chant.config.json +13 -0
  212. package/src/__fixtures__/pr-loop/estate.component.ts +10 -0
  213. package/src/__fixtures__/pr-loop/roots/a/main.tf +11 -0
  214. package/src/__fixtures__/pr-loop/roots/app/main.tf +19 -0
  215. package/src/__fixtures__/pr-loop/roots/b/main.tf +7 -0
  216. package/src/__fixtures__/pr-loop/roots/dns/main.tf +3 -0
  217. package/src/__fixtures__/pr-loop/roots/net/main.tf +7 -0
  218. package/src/__fixtures__/report/applied.show.json +51 -0
  219. package/src/__fixtures__/report/generate.ts +63 -0
  220. package/src/__fixtures__/report/plan.show.json +200 -0
  221. package/src/__fixtures__/report/tf-apply/change-set.json +170 -0
  222. package/src/__fixtures__/report/tf-apply/report.json +338 -0
  223. package/src/__fixtures__/report/tf-plan/change-set.json +170 -0
  224. package/src/__fixtures__/report/tf-plan/report.json +320 -0
  225. package/src/__fixtures__/three-cloud-azure.graph.json +290 -0
  226. package/src/applied-state.ts +52 -0
  227. package/src/backend-config.test.ts +105 -0
  228. package/src/backend-config.ts +133 -0
  229. package/src/behaviour/kinds.ts +4 -0
  230. package/src/behaviour/predict.ts +5 -2
  231. package/src/behaviour/reference-catalog.plugin.test.ts +65 -0
  232. package/src/behaviour/reference-catalog.ts +16 -5
  233. package/src/behaviour/request.ts +3 -2
  234. package/src/change-set-instances.test.ts +98 -0
  235. package/src/changed-roots.ts +14 -16
  236. package/src/commands.ts +84 -4
  237. package/src/components/capability-plugin.test.ts +172 -5
  238. package/src/components/capability-plugin.ts +212 -11
  239. package/src/components/change-set-graph.e2e.test.ts +11 -5
  240. package/src/components/compose-group.test.ts +486 -0
  241. package/src/components/compose-group.ts +489 -0
  242. package/src/components/demo-composed-roots.e2e.test.ts +440 -0
  243. package/src/components/member-outputs.test.ts +76 -0
  244. package/src/components/placeholder-preview.test.ts +147 -0
  245. package/src/components/placeholder-preview.ts +257 -0
  246. package/src/components/pr-loop-compose.tofu.e2e.test.ts +363 -0
  247. package/src/components/pr-loop.tofu.test.ts +112 -56
  248. package/src/compose/apply-events.test.ts +62 -0
  249. package/src/compose/apply-events.ts +100 -0
  250. package/src/compose/estate.choudoufu.e2e.test.ts +112 -0
  251. package/src/compose/estate.test.ts +421 -0
  252. package/src/compose/estate.ts +443 -0
  253. package/src/compose/hcl.ts +311 -0
  254. package/src/compose/module-address.ts +49 -0
  255. package/src/compose/root.test.ts +416 -0
  256. package/src/compose/root.ts +592 -0
  257. package/src/compose/run.test.ts +440 -0
  258. package/src/compose/run.tofu.test.ts +176 -0
  259. package/src/compose/run.ts +527 -0
  260. package/src/compose/slice.test.ts +184 -0
  261. package/src/compose/slice.ts +290 -0
  262. package/src/compose/state-address.ts +157 -0
  263. package/src/compose/state-merge.test.ts +131 -0
  264. package/src/compose/state-merge.ts +148 -0
  265. package/src/compose.test.ts +248 -0
  266. package/src/compose.ts +627 -0
  267. package/src/composites/terraform-apply-op.test.ts +9 -0
  268. package/src/composites/terraform-apply-op.ts +7 -3
  269. package/src/composites/terraform-pin-rollout-op.test.ts +2 -2
  270. package/src/config-file-listers.test.ts +265 -0
  271. package/src/config.ts +44 -9
  272. package/src/describe-resources.engine-facts.test.ts +412 -0
  273. package/src/describe-resources.live-module-keyed.test.ts +290 -0
  274. package/src/describe-resources.ts +416 -70
  275. package/src/drift-read.ts +284 -0
  276. package/src/drift-record.test.ts +223 -0
  277. package/src/drift-record.ts +455 -0
  278. package/src/hcl/config-blocks.test.ts +174 -0
  279. package/src/hcl/edges.ts +21 -1
  280. package/src/hcl/files.ts +234 -0
  281. package/src/hcl/instance-forms.test.ts +94 -0
  282. package/src/hcl/instances.test.ts +137 -0
  283. package/src/hcl/instances.ts +422 -0
  284. package/src/hcl/parse.test.ts +139 -1
  285. package/src/hcl/parse.ts +149 -16
  286. package/src/hcl/roots.ts +6 -0
  287. package/src/hcl/suppressions.ts +8 -2
  288. package/src/index.ts +76 -0
  289. package/src/live-plan-reasons.ts +54 -0
  290. package/src/lsp/completions.test.ts +1 -1
  291. package/src/lsp/option-keys.ts +2 -0
  292. package/src/op/activities/choudoufu-plan-set-op.test.ts +111 -0
  293. package/src/op/activities/index.ts +9 -0
  294. package/src/op/activities/live-detect.ts +55 -26
  295. package/src/op/activities/pin-rollout.ts +3 -0
  296. package/src/op/activities/registry.test.ts +2 -1
  297. package/src/op/activities/terraform.test.ts +55 -0
  298. package/src/op/activities/terraform.ts +266 -22
  299. package/src/op/activity-contracts.test.ts +1 -0
  300. package/src/op/activity-contracts.ts +24 -1
  301. package/src/op/builders.ts +14 -0
  302. package/src/pin/bundle.test.ts +2 -0
  303. package/src/pin/edit.ts +30 -0
  304. package/src/pin/rollout.ts +16 -11
  305. package/src/plan-shape.test.ts +148 -0
  306. package/src/plan-shape.ts +128 -0
  307. package/src/plugin.ts +33 -19
  308. package/src/report.test.ts +423 -0
  309. package/src/report.ts +752 -0
  310. package/src/signal-only-root.test.ts +72 -0
  311. package/src/skills/chant-terraform.md +1 -1
  312. package/src/state/azure-blob.ts +223 -0
  313. package/src/state/backend-verdict.test.ts +187 -0
  314. package/src/state/backend.test.ts +127 -0
  315. package/src/state/backend.ts +488 -0
  316. package/src/state/exec.ts +111 -0
  317. package/src/state/gcs.ts +254 -0
  318. package/src/state/index.ts +15 -0
  319. package/src/state/locks.test.ts +84 -0
  320. package/src/state/locks.ts +201 -0
  321. package/src/state/object-store.ts +35 -0
  322. package/src/state/root-states-floci.e2e.test.ts +66 -0
  323. package/src/state/root-states.test.ts +462 -0
  324. package/src/state/root-states.ts +309 -0
  325. package/src/state/s3.ts +193 -0
  326. package/src/state/store.ts +225 -0
  327. package/src/terragrunt/affected.ts +2 -1
  328. package/src/workspace-kinds.test.ts +4 -17
  329. package/workspace-kinds.json +1 -24
  330. package/dist/workspace-member.d.ts +0 -30
  331. package/dist/workspace-member.d.ts.map +0 -1
  332. package/dist/workspace-member.js +0 -91
  333. package/schemas/pulumi.schema.json +0 -34
  334. package/src/testdata/pulumi-member/Pulumi.yaml +0 -2
  335. package/src/testdata/pulumi-member/stack-export.json +0 -53
  336. package/src/testdata/pulumi-unexported/Pulumi.yaml +0 -2
  337. package/src/workspace-member.test.ts +0 -89
  338. package/src/workspace-member.ts +0 -126
@@ -9,14 +9,17 @@
9
9
  * in a local module it calls (followed through modules that call modules),
10
10
  * or is one of its var files.
11
11
  *
12
- * The module calls are read with a regex over the root's `.tf` and `.tofu`
12
+ * The module calls are read with a regex over the root's HCL configuration
13
13
  * files, the way `./op/activities/live-detect.ts` reads a live block, so
14
- * this stays out of the HCL parser's dependency graph. Only a `source` that
14
+ * this stays out of the HCL parser's dependency graph. Which files those are
15
+ * follows the root's binary through the lexicon's one lister (chant#4457):
16
+ * `.tf`, plus `.tofu` for a tofu or choudoufu root. Only a `source` that
15
17
  * starts with `./` or `../` is followed; a registry or git source is a
16
18
  * version pin in the root's own files, which a change to it already touches.
17
19
  */
18
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
20
+ import { existsSync, readFileSync, statSync } from "node:fs";
19
21
  import { join, posix, relative, resolve, sep } from "node:path";
22
+ import { isJsonConfigFile, listConfigFilesSafe } from "./hcl/files.js";
20
23
  /** `source = "./..."` or `"../..."` inside a module block. Other sources are not on disk here. */
21
24
  const LOCAL_SOURCE = /\bsource\s*=\s*"(\.{1,2}\/[^"]*)"/g;
22
25
  const toPosix = (p) => p.split(sep).join("/");
@@ -25,18 +28,11 @@ function projectRelative(projectRoot, dir) {
25
28
  const rel = toPosix(relative(projectRoot, dir));
26
29
  return rel === "" ? "." : posix.normalize(rel).replace(/\/+$/, "");
27
30
  }
28
- /** Local module directories `dir` calls, read from its HCL files. */
29
- function localModuleDirs(dir) {
30
- let names;
31
- try {
32
- names = readdirSync(dir);
33
- }
34
- catch {
35
- return [];
36
- }
31
+ /** Local module directories `dir` calls, read from the HCL configuration files `binary` reads. */
32
+ function localModuleDirs(dir, binary) {
37
33
  const out = [];
38
- for (const name of names) {
39
- if (!name.endsWith(".tf") && !name.endsWith(".tofu"))
34
+ for (const name of listConfigFilesSafe(dir, binary)) {
35
+ if (isJsonConfigFile(name))
40
36
  continue;
41
37
  let text;
42
38
  try {
@@ -58,7 +54,7 @@ function localModuleDirs(dir) {
58
54
  * Every path, relative to `projectRoot`, whose change touches `root`: its
59
55
  * directory, each local module it reaches, and its var files.
60
56
  */
61
- export function rootWatchPaths(projectRoot, root) {
57
+ export function rootWatchPaths(projectRoot, root, binary) {
62
58
  const rootDir = resolve(projectRoot, root.dir);
63
59
  const seen = new Set();
64
60
  const queue = [rootDir];
@@ -67,7 +63,7 @@ export function rootWatchPaths(projectRoot, root) {
67
63
  if (seen.has(dir))
68
64
  continue;
69
65
  seen.add(dir);
70
- queue.push(...localModuleDirs(dir));
66
+ queue.push(...localModuleDirs(dir, binary));
71
67
  }
72
68
  const paths = [...seen].map((d) => projectRelative(projectRoot, d));
73
69
  for (const file of root.varFiles ?? [])
@@ -85,11 +81,11 @@ function covers(path, file) {
85
81
  * sorted. `changedFiles` are relative to `projectRoot` with `/` separators,
86
82
  * as `git diff --name-only --relative` prints them.
87
83
  */
88
- export function changedRoots(projectRoot, roots, changedFiles) {
84
+ export function changedRoots(projectRoot, roots, changedFiles, binary) {
89
85
  const files = changedFiles.map((f) => posix.normalize(f));
90
86
  const touched = [];
91
87
  for (const [name, root] of Object.entries(roots)) {
92
- const watch = rootWatchPaths(projectRoot, root);
88
+ const watch = rootWatchPaths(projectRoot, root, binary);
93
89
  if (files.some((f) => watch.some((p) => covers(p, f))))
94
90
  touched.push(name);
95
91
  }
@@ -22,5 +22,10 @@ export declare function parsePinRolloutArgs(rawArgs: string[]): {
22
22
  };
23
23
  /** Exit status for a rollout's status. */
24
24
  export declare function pinRolloutExitCode(status: string): number;
25
+ /** Parse `chant terraform report`'s flags. Throws on an unknown flag, a missing value or a missing required flag. */
26
+ export declare function parseReportArgs(rawArgs: string[]): {
27
+ values: Map<string, string>;
28
+ json: boolean;
29
+ };
25
30
  export declare const terraformCommands: CommandGroup;
26
31
  //# sourceMappingURL=commands.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,oCAAoC,CAAC;AAE5F,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAW3E,qGAAqG;AACrG,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG;IAAE,IAAI,EAAE,uBAAuB,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAiDvG;AAED,0CAA0C;AAC1C,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAIzD;AAqBD,eAAO,MAAM,iBAAiB,EAAE,YAI/B,CAAC"}
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,oCAAoC,CAAC;AAE5F,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAW3E,qGAAqG;AACrG,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG;IAAE,IAAI,EAAE,uBAAuB,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAkDvG;AAED,0CAA0C;AAC1C,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAIzD;AA4BD,qHAAqH;AACrH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG;IAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAqBjG;AA+CD,eAAO,MAAM,iBAAiB,EAAE,YAO/B,CAAC"}
package/dist/commands.js CHANGED
@@ -16,8 +16,8 @@
16
16
  import { splitJoinedFlags, unknownFlagError } from "@intentius/chant/cli/command-group";
17
17
  const USAGE = "usage: chant terraform pin-rollout --module <source> --from <pin> --to <pin> [--root <dir>]... [--depends-on <dir>=<dir>]... " +
18
18
  "[--ts-source <dir>=<file>]... [--canary <dir>]... [--waves-from <file> [--waves-prefix <dir>]] [--pull-request] " +
19
- "[--base <branch>] [--remote <name>] [--applied-check <name>] [--json]";
20
- const VALUE_FLAGS = new Set(["--module", "--from", "--to", "--root", "--depends-on", "--ts-source", "--canary", "--waves-from", "--waves-prefix", "--base", "--remote", "--applied-check"]);
19
+ "[--base <branch>] [--remote <name>] [--applied-check <name>] [--binary <terraform|tofu|choudoufu>] [--json]";
20
+ const VALUE_FLAGS = new Set(["--module", "--from", "--to", "--root", "--depends-on", "--ts-source", "--canary", "--waves-from", "--waves-prefix", "--base", "--remote", "--applied-check", "--binary"]);
21
21
  const BOOLEAN_FLAGS = new Set(["--pull-request", "--json"]);
22
22
  /** Parse the verb's flags into the activity's args. Throws on an unknown flag or a missing value. */
23
23
  export function parsePinRolloutArgs(rawArgs) {
@@ -68,6 +68,7 @@ export function parsePinRolloutArgs(rawArgs) {
68
68
  ...(one("--base") ? { base: one("--base") } : {}),
69
69
  ...(one("--remote") ? { remote: one("--remote") } : {}),
70
70
  ...(one("--applied-check") ? { appliedCheck: one("--applied-check") } : {}),
71
+ ...(one("--binary") ? { binary: one("--binary") } : {}),
71
72
  };
72
73
  return { args, json: flags.has("--json") };
73
74
  }
@@ -99,8 +100,90 @@ async function runPinRolloutVerb(ctx) {
99
100
  return 1;
100
101
  }
101
102
  }
103
+ const REPORT_USAGE = "usage: chant terraform report --stage tf-plan|tf-apply --plan <show.json> [--applied <show-after-apply.json>] (--out <dir> | --to <url>) " +
104
+ "[--project <host/path>] [--commit <sha>] [--pull-request <n>] [--binary <name>] [--binary-version <v>] [--root-dir <dir>] [--self-root <dir>] [--repo <dir>] " +
105
+ "[--started <iso>] [--finished <iso>] [--json]";
106
+ const REPORT_VALUE_FLAGS = new Set(["--stage", "--plan", "--applied", "--out", "--to", "--project", "--commit", "--pull-request", "--binary", "--binary-version", "--root-dir", "--self-root", "--repo", "--started", "--finished"]);
107
+ /** Parse `chant terraform report`'s flags. Throws on an unknown flag, a missing value or a missing required flag. */
108
+ export function parseReportArgs(rawArgs) {
109
+ const tokens = splitJoinedFlags(rawArgs, new Set(["--json"]));
110
+ const values = new Map();
111
+ let json = false;
112
+ for (let i = 0; i < tokens.length; i++) {
113
+ const t = tokens[i];
114
+ if (t === "--json") {
115
+ json = true;
116
+ continue;
117
+ }
118
+ if (!REPORT_VALUE_FLAGS.has(t))
119
+ throw t.startsWith("-") ? unknownFlagError(t, REPORT_USAGE) : new Error(`unexpected argument "${t}"\n${REPORT_USAGE}`);
120
+ const v = tokens[++i];
121
+ if (v === undefined || v.startsWith("--"))
122
+ throw new Error(`${t} needs a value\n${REPORT_USAGE}`);
123
+ values.set(t, v);
124
+ }
125
+ const stage = values.get("--stage");
126
+ if (stage !== "tf-plan" && stage !== "tf-apply")
127
+ throw new Error(`--stage is tf-plan or tf-apply\n${REPORT_USAGE}`);
128
+ if (!values.has("--plan"))
129
+ throw new Error(`--plan is required\n${REPORT_USAGE}`);
130
+ if (stage === "tf-apply" && !values.has("--applied"))
131
+ throw new Error(`--stage tf-apply needs --applied, the show -json of a plan made after the apply\n${REPORT_USAGE}`);
132
+ if (values.has("--out") === values.has("--to"))
133
+ throw new Error(`give one of --out <dir> and --to <url>\n${REPORT_USAGE}`);
134
+ return { values, json };
135
+ }
136
+ async function runReportVerb(ctx) {
137
+ try {
138
+ const { values, json } = parseReportArgs(ctx.rawArgs);
139
+ const { readFileSync } = await import("node:fs");
140
+ const { runFactsFromEnv, terraformReport, writeReport, httpStore, dirStore, moduleEstates, atmosStackReads, topModule } = await import("./report.js");
141
+ const read = (flag) => {
142
+ const file = values.get(flag);
143
+ try {
144
+ return JSON.parse(readFileSync(file, "utf8"));
145
+ }
146
+ catch (err) {
147
+ throw new Error(`${flag} ${file}: ${err instanceof Error ? err.message : String(err)}`);
148
+ }
149
+ };
150
+ const run = runFactsFromEnv(process.env, { pullRequest: values.get("--pull-request"), project: values.get("--project"), commit: values.get("--commit") });
151
+ const plan = read("--plan");
152
+ // The plan holds no `live` block: each module's estate is read from its directory, <repo>/<root-dir>/<module>.
153
+ const repo = values.get("--repo") ?? process.env.GITHUB_WORKSPACE ?? process.cwd();
154
+ const modules = [...new Set((plan.resource_changes ?? []).map((r) => topModule(String(r.module_address ?? r.address ?? ""))).filter((m) => m !== undefined))];
155
+ const estates = moduleEstates(repo, values.get("--root-dir") ?? "", modules);
156
+ // An Atmos repo wires its components with !terraform.output in the stack manifests, which the plan does not keep.
157
+ const componentReads = atmosStackReads(repo);
158
+ const result = terraformReport({
159
+ stage: values.get("--stage"),
160
+ plan,
161
+ ...(Object.keys(estates).length > 0 ? { estates } : {}),
162
+ ...(Object.keys(componentReads).length > 0 ? { componentReads } : {}),
163
+ ...(values.has("--applied") ? { applied: read("--applied") } : {}),
164
+ run,
165
+ ...(values.has("--binary") ? { binary: values.get("--binary") } : {}),
166
+ ...(values.has("--binary-version") ? { binaryVersion: values.get("--binary-version") } : {}),
167
+ ...(values.has("--root-dir") ? { rootDir: values.get("--root-dir") } : {}),
168
+ ...(values.has("--self-root") ? { selfRoot: values.get("--self-root") } : {}),
169
+ ...(values.has("--started") ? { started: values.get("--started") } : {}),
170
+ finished: values.get("--finished") ?? new Date().toISOString().replace(/\.\d+Z$/, "Z"),
171
+ });
172
+ const store = values.has("--out") ? dirStore(values.get("--out")) : httpStore(values.get("--to"));
173
+ const keys = await writeReport(store, result);
174
+ console.log(json ? JSON.stringify({ path: result.path, keys, row: result.row }, null, 2) : `wrote ${keys.join(", ")}`);
175
+ return 0;
176
+ }
177
+ catch (err) {
178
+ console.error(err instanceof Error ? err.message : String(err));
179
+ return 1;
180
+ }
181
+ }
102
182
  export const terraformCommands = {
103
183
  name: "terraform",
104
- description: "Terraform and OpenTofu roots: roll a module version out as one pin-bump PR per wave",
105
- commands: [{ name: "pin-rollout", description: "Read a pin-bump rollout's state and open the next wave's PR when it is due", handler: runPinRolloutVerb }],
184
+ description: "Terraform and OpenTofu roots: roll a module version out as one pin-bump PR per wave; write a run report from a saved plan",
185
+ commands: [
186
+ { name: "pin-rollout", description: "Read a pin-bump rollout's state and open the next wave's PR when it is due", handler: runPinRolloutVerb },
187
+ { name: "report", description: "Write a terragucci.report/v1 run report, its change set and its index row from show -json of a composed plan", handler: runReportVerb },
188
+ ],
106
189
  };
@@ -38,9 +38,27 @@
38
38
  * when both roots are in the change.
39
39
  *
40
40
  * A root whose `vars` read an output not known yet (`DeployContext.unresolvedInputs`)
41
- * is not planned: terraform takes no unknown variable. Its member gets a hole
42
- * naming the roots it waits on and no artifact, and the run that applies it
43
- * plans it once their outputs are known.
41
+ * cannot be planned as it stands: terraform takes no unknown variable. It is
42
+ * previewed instead (#4463, `./placeholder-preview.ts`): planned with a
43
+ * stand-in per variable that holds no value, typed from the root's
44
+ * `variable` block, with every attribute and output a stand-in drives marked
45
+ * unknown after apply. The member lists the variables under `placeholders`,
46
+ * and its artifact is the preview's shape, never a plan file. The apply plans
47
+ * the root again once the upstream applied, and core covers that round with
48
+ * the first approval when its shape is within the preview's; a `run` handed
49
+ * the shape itself holds the fresh plan to it. When no preview can be made
50
+ * (an undeclared variable, a `count` or `for_each` over a stand-in, a
51
+ * placeholder plan that fails) the member stays a hole naming the roots it
52
+ * waits on, and the hole's reason says why there is no preview.
53
+ *
54
+ * ## The plan's shape (#4436)
55
+ *
56
+ * `plan` also answers `shape`, `terraformPlanShape` over `show -json`: each
57
+ * resource the plan changes with its action, type, replacement, the
58
+ * attributes that differ and the ones unknown after apply. Core's
59
+ * `coveredRound` compares it with the shape of a later plan of the root. A
60
+ * previewed root's shape lists the upstreams it read as unknown inputs; a
61
+ * root that stays a hole has no plan, so no shape, and stays uncovered.
44
62
  *
45
63
  * ## choudoufu roots in a gated wave (#3459)
46
64
  *
@@ -63,9 +81,30 @@
63
81
  *
64
82
  * chant's waves stay the order. Each root is its own one-root set, so
65
83
  * choudoufu's wave over it is always wave 1.
84
+ *
85
+ * ## Composed runs (#4447)
86
+ *
87
+ * With `terraform.compose.enabled`, a group core forms of `terraform-apply`
88
+ * components that read each other (ws-107) plans as one plan and applies as
89
+ * one apply of a composed root, and a member's read of another member's new
90
+ * output is not a hole: the apply passes the value on. `groupReads`,
91
+ * `groupFallback`, `planGroup` and `runGroup` are in `./compose-group.ts`.
92
+ *
93
+ * ## A choudoufu member's outputs, for another member (#4455)
94
+ *
95
+ * The plugin also contributes {@link choudoufuOutputSource} to core's
96
+ * `member-outputs` verb (`CapabilityPlugin.engineOutputSources`), which is
97
+ * how a chant member's parameters read a choudoufu member of the same
98
+ * workspace: `choudoufu output -json` in the member's directory for the
99
+ * applied values, and `live-plan-set -json` over it for the planned ones,
100
+ * read the way `plan` reads a root's (`terraformPlannedOutputs`). The member
101
+ * is not a `terraform.roots` entry of the reading project; its directory
102
+ * comes from the workspace declaration.
66
103
  */
67
104
  import type { Capability, PlannedOutputs } from "@intentius/chant/components/capability";
105
+ import { type PlanShapeArtifact } from "@intentius/chant/components/covered-round";
68
106
  import { type CapabilityPlugin } from "@intentius/chant/components/capability-plugin";
107
+ import type { EngineOutputSource, EnginePlannedOutputs } from "@intentius/chant/components/verbs/member-outputs";
69
108
  /** The `terraform-apply` step's input. */
70
109
  export interface TerraformApplyStepInput {
71
110
  /** Key into `terraform.roots`. Also the member's name in a wave's set digest. */
@@ -92,6 +131,29 @@ export interface TerraformApplyStepOutput {
92
131
  * only a part of it is.
93
132
  */
94
133
  export declare function terraformPlannedOutputs(json: unknown): PlannedOutputs;
134
+ /**
135
+ * What a choudoufu member's set plan says about its outputs (#4455), for
136
+ * `member-outputs`: {@link terraformPlannedOutputs} of the embedded plan,
137
+ * chant's digest of that plan, and whether it has changes to apply.
138
+ */
139
+ export declare function choudoufuMemberPlannedOutputs(set: {
140
+ planDigest: string;
141
+ changed: boolean;
142
+ json: unknown;
143
+ }): EnginePlannedOutputs;
144
+ /**
145
+ * The choudoufu engine's member outputs (#4455): what core's
146
+ * `member-outputs` reads a workspace member of kind `choudoufu` through.
147
+ * Applied values from the member's root outputs (the record store, for a
148
+ * live estate); planned values from its one-root set plan.
149
+ */
150
+ export declare const choudoufuOutputSource: EngineOutputSource;
151
+ /** The plan file a preview writes, removed once it is read: a plan made with stand-ins must never apply. */
152
+ export declare const PREVIEW_PLAN_FILE = ".chant-preview.tfplan";
153
+ /** What a previewed root hands `run` (#4463): the shape its plan had with stand-in values. Never a plan file. */
154
+ export interface TerraformShapeArtifact extends PlanShapeArtifact {
155
+ kind: "terraform-apply-shape";
156
+ }
95
157
  /**
96
158
  * choudoufu refused the plan `run` was handed by its wave (its exit 3):
97
159
  * the live system moved since the plan, or the plan names another estate.
@@ -1 +1 @@
1
- {"version":3,"file":"capability-plugin.d.ts","sourceRoot":"","sources":["../../src/components/capability-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAiC,cAAc,EAAE,MAAM,wCAAwC,CAAC;AAGxH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,+CAA+C,CAAC;AAWzG,0CAA0C;AAC1C,MAAM,WAAW,uBAAuB;IACtC,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,6FAA6F;AAC7F,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,YAAY,EAAE,OAAO,CAAC;IACtB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AA0CD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,CAerE;AAwBD;;;;;;GAMG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAEpB,IAAI,EAAE,MAAM;IADxC,QAAQ,CAAC,SAAS,QAAQ;IAC1B,YAA4B,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAGxD;CACF;AA+CD,eAAO,MAAM,wBAAwB,EAAE,UAAU,CAAC,uBAAuB,EAAE,wBAAwB,CA0DlG,CAAC;AAEF,eAAO,MAAM,uBAAuB;aAClC,KAAK,YAAG,iBAAiB;CACjB,CAAC;AAEX,eAAO,MAAM,yBAAyB,EAAE,gBAWvC,CAAC"}
1
+ {"version":3,"file":"capability-plugin.d.ts","sourceRoot":"","sources":["../../src/components/capability-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsGG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAiC,cAAc,EAAE,MAAM,wCAAwC,CAAC;AAExH,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,2CAA2C,CAAC;AAGnD,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,+CAA+C,CAAC;AACzG,OAAO,KAAK,EAAmB,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,kDAAkD,CAAC;AAoBlI,0CAA0C;AAC1C,MAAM,WAAW,uBAAuB;IACtC,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,6FAA6F;AAC7F,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,YAAY,EAAE,OAAO,CAAC;IACtB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AA8CD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,CAerE;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG,oBAAoB,CAEhI;AAID;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,kBAInC,CAAC;AA4BF,4GAA4G;AAC5G,eAAO,MAAM,iBAAiB,0BAA0B,CAAC;AAEzD,iHAAiH;AACjH,MAAM,WAAW,sBAAuB,SAAQ,iBAAiB;IAC/D,IAAI,EAAE,uBAAuB,CAAC;CAC/B;AAqFD;;;;;;GAMG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAEpB,IAAI,EAAE,MAAM;IADxC,QAAQ,CAAC,SAAS,QAAQ;IAC1B,YAA4B,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAGxD;CACF;AA+CD,eAAO,MAAM,wBAAwB,EAAE,UAAU,CAAC,uBAAuB,EAAE,wBAAwB,CA8ElG,CAAC;AAEF,eAAO,MAAM,uBAAuB;aAClC,KAAK,YAAG,iBAAiB;CACjB,CAAC;AAEX,eAAO,MAAM,yBAAyB,EAAE,gBAcvC,CAAC"}
@@ -38,9 +38,27 @@
38
38
  * when both roots are in the change.
39
39
  *
40
40
  * A root whose `vars` read an output not known yet (`DeployContext.unresolvedInputs`)
41
- * is not planned: terraform takes no unknown variable. Its member gets a hole
42
- * naming the roots it waits on and no artifact, and the run that applies it
43
- * plans it once their outputs are known.
41
+ * cannot be planned as it stands: terraform takes no unknown variable. It is
42
+ * previewed instead (#4463, `./placeholder-preview.ts`): planned with a
43
+ * stand-in per variable that holds no value, typed from the root's
44
+ * `variable` block, with every attribute and output a stand-in drives marked
45
+ * unknown after apply. The member lists the variables under `placeholders`,
46
+ * and its artifact is the preview's shape, never a plan file. The apply plans
47
+ * the root again once the upstream applied, and core covers that round with
48
+ * the first approval when its shape is within the preview's; a `run` handed
49
+ * the shape itself holds the fresh plan to it. When no preview can be made
50
+ * (an undeclared variable, a `count` or `for_each` over a stand-in, a
51
+ * placeholder plan that fails) the member stays a hole naming the roots it
52
+ * waits on, and the hole's reason says why there is no preview.
53
+ *
54
+ * ## The plan's shape (#4436)
55
+ *
56
+ * `plan` also answers `shape`, `terraformPlanShape` over `show -json`: each
57
+ * resource the plan changes with its action, type, replacement, the
58
+ * attributes that differ and the ones unknown after apply. Core's
59
+ * `coveredRound` compares it with the shape of a later plan of the root. A
60
+ * previewed root's shape lists the upstreams it read as unknown inputs; a
61
+ * root that stays a hole has no plan, so no shape, and stays uncovered.
44
62
  *
45
63
  * ## choudoufu roots in a gated wave (#3459)
46
64
  *
@@ -63,10 +81,39 @@
63
81
  *
64
82
  * chant's waves stay the order. Each root is its own one-root set, so
65
83
  * choudoufu's wave over it is always wave 1.
84
+ *
85
+ * ## Composed runs (#4447)
86
+ *
87
+ * With `terraform.compose.enabled`, a group core forms of `terraform-apply`
88
+ * components that read each other (ws-107) plans as one plan and applies as
89
+ * one apply of a composed root, and a member's read of another member's new
90
+ * output is not a hole: the apply passes the value on. `groupReads`,
91
+ * `groupFallback`, `planGroup` and `runGroup` are in `./compose-group.ts`.
92
+ *
93
+ * ## A choudoufu member's outputs, for another member (#4455)
94
+ *
95
+ * The plugin also contributes {@link choudoufuOutputSource} to core's
96
+ * `member-outputs` verb (`CapabilityPlugin.engineOutputSources`), which is
97
+ * how a chant member's parameters read a choudoufu member of the same
98
+ * workspace: `choudoufu output -json` in the member's directory for the
99
+ * applied values, and `live-plan-set -json` over it for the planned ones,
100
+ * read the way `plan` reads a root's (`terraformPlannedOutputs`). The member
101
+ * is not a `terraform.roots` entry of the reading project; its directory
102
+ * comes from the workspace declaration.
66
103
  */
104
+ import { rmSync } from "node:fs";
105
+ import { dirname, join, resolve } from "node:path";
106
+ import { loadChantConfigUpward } from "@intentius/chant/config";
107
+ import { PlanShapeMovedError, planShapeDifferences, planShapeDigest, } from "@intentius/chant/components/covered-round";
67
108
  import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
68
109
  import { ownPackageVersion } from "@intentius/chant/components/capability-plugin";
69
- import { choudoufuPlanSet, choudoufuWaveApply, choudoufuWaveRoot, terraformApply, terraformInit, terraformOutputs, terraformPlan, } from "../op/activities/terraform.js";
110
+ import { choudoufuMemberOutputs, choudoufuMemberPlanSet, choudoufuPlanSet, choudoufuWaveApply, choudoufuWaveRoot, terraformApply, terraformInit, terraformOutputs, terraformPlan, } from "../op/activities/terraform.js";
111
+ import { terraformPlanShape } from "../plan-shape.js";
112
+ import { terraformChangeSetPart } from "../change-set.js";
113
+ import { rootTexts } from "../compose/hcl.js";
114
+ import { declaredVariables } from "../compose/root.js";
115
+ import { terraformComposeGroup } from "./compose-group.js";
116
+ import { markPlaceholderDriven, placeholderVars, waitingHole } from "./placeholder-preview.js";
70
117
  const rootArgs = (input) => ({ root: input.root, ...(input.cwd ? { cwd: input.cwd } : {}) });
71
118
  function requireRoot(input) {
72
119
  if (typeof input.root !== "string" || input.root === "") {
@@ -76,10 +123,14 @@ function requireRoot(input) {
76
123
  async function planRoot(input, targets) {
77
124
  requireRoot(input);
78
125
  await terraformInit(rootArgs(input));
126
+ // One `show -json` per plan (#4479): its document is the digest, the change
127
+ // set, the planned outputs and the shape. Nothing here reads the
128
+ // `-no-color` render, so the plan skips it.
79
129
  const planned = await terraformPlan({
80
130
  ...rootArgs(input),
81
131
  ...(input.vars ? { vars: input.vars } : {}),
82
132
  ...(targets && targets.length > 0 ? { targets } : {}),
133
+ text: false,
83
134
  });
84
135
  return {
85
136
  planFile: planned.planFile,
@@ -127,12 +178,34 @@ export function terraformPlannedOutputs(json) {
127
178
  return { known, unknown };
128
179
  }
129
180
  /**
130
- * The member of a root whose `vars` read outputs not known yet (#3999): no
131
- * plan, a hole naming whose outputs it waits on, and a digest over the root,
132
- * the vars as resolved and those names.
181
+ * What a choudoufu member's set plan says about its outputs (#4455), for
182
+ * `member-outputs`: {@link terraformPlannedOutputs} of the embedded plan,
183
+ * chant's digest of that plan, and whether it has changes to apply.
184
+ */
185
+ export function choudoufuMemberPlannedOutputs(set) {
186
+ return { ...terraformPlannedOutputs(set.json), planDigest: set.planDigest, changes: set.changed };
187
+ }
188
+ const memberArgs = (ref) => ({ member: ref.member, dir: ref.dir, projectRoot: ref.workspaceRoot });
189
+ /**
190
+ * The choudoufu engine's member outputs (#4455): what core's
191
+ * `member-outputs` reads a workspace member of kind `choudoufu` through.
192
+ * Applied values from the member's root outputs (the record store, for a
193
+ * live estate); planned values from its one-root set plan.
194
+ */
195
+ export const choudoufuOutputSource = {
196
+ kinds: ["choudoufu"],
197
+ applied: (_ctx, ref) => choudoufuMemberOutputs(memberArgs(ref)),
198
+ planned: async (_ctx, ref) => choudoufuMemberPlannedOutputs(await choudoufuMemberPlanSet(memberArgs(ref))),
199
+ };
200
+ const resolvedVars = (input) => Object.fromEntries(Object.entries(input.vars ?? {}).map(([k, v]) => [k, v === undefined ? null : v]));
201
+ /**
202
+ * The member of a root whose `vars` read outputs not known yet and that
203
+ * could not be previewed (#3999, #4463): no plan, a hole naming whose outputs
204
+ * it waits on and, when a preview was tried, why it was not possible, and a
205
+ * digest over the root, the vars as resolved and those names.
133
206
  */
134
- function waitingRoot(ctx, input, unresolved) {
135
- const vars = Object.fromEntries(Object.entries(input.vars ?? {}).map(([k, v]) => [k, v === undefined ? null : v]));
207
+ function waitingRoot(ctx, input, unresolved, noPreview) {
208
+ const vars = resolvedVars(input);
136
209
  const planDigest = computePlanDigest("terraform-apply-unresolved", { env: ctx.env, root: input.root, vars, unresolved });
137
210
  const changeSet = {
138
211
  member: {
@@ -141,12 +214,99 @@ function waitingRoot(ctx, input, unresolved) {
141
214
  planner: "chant",
142
215
  status: "planned",
143
216
  planDigest,
144
- holes: [{ address: input.root, type: "terraform_root", reason: `inputs from ${unresolved.join(", ")}; planned again once they apply` }],
217
+ holes: [waitingHole(input.root, unresolved, noPreview)],
145
218
  },
146
219
  entries: [],
147
220
  };
148
221
  return { member: input.root, planDigest, changeSet, ...(ctx.targets?.length ? { targets: ctx.targets } : {}) };
149
222
  }
223
+ /** The plan file a preview writes, removed once it is read: a plan made with stand-ins must never apply. */
224
+ export const PREVIEW_PLAN_FILE = ".chant-preview.tfplan";
225
+ function isShapeArtifact(value) {
226
+ const v = value;
227
+ return v?.kind === "terraform-apply-shape" && typeof v.digest === "string" && typeof v.shape === "object" && v.shape !== null;
228
+ }
229
+ /** The root's directory, as `terraform.roots` names it from the nearest `chant.config.*`, and the configured binary. */
230
+ async function rootDir(input) {
231
+ const start = resolve(input.cwd ?? process.cwd());
232
+ const { config, configPath } = await loadChantConfigUpward(start);
233
+ const terraform = config.terraform;
234
+ const dir = terraform?.roots?.[input.root]?.dir;
235
+ if (dir === undefined)
236
+ return undefined;
237
+ return { dir: resolve(configPath ? dirname(configPath) : start, dir), ...(terraform?.binary ? { binary: terraform.binary } : {}) };
238
+ }
239
+ const firstLines = (err) => {
240
+ const text = (err instanceof Error ? err.message : String(err)).trim();
241
+ const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
242
+ return lines.slice(0, 4).join(" ").slice(0, 600);
243
+ };
244
+ /**
245
+ * Preview a root whose `vars` read outputs not known yet (#4463): plan it
246
+ * with a typed stand-in per such variable, mark what the stand-ins drive
247
+ * unknown, and answer the change set, the planned outputs and the shape of
248
+ * that plan, with the shape as the artifact. The plan file is removed. When
249
+ * no preview can be made, the member stays a hole saying why.
250
+ */
251
+ async function previewRoot(ctx, input, unresolved, targets) {
252
+ const root = await rootDir(input);
253
+ if (root === undefined)
254
+ return waitingRoot(ctx, input, unresolved, `no root named "${input.root}" in terraform.roots to read its variables from`);
255
+ const { dir } = root;
256
+ // #4457: the same files the binary reads (`.tofu` only for tofu and choudoufu).
257
+ const stand = placeholderVars(input.vars, declaredVariables(rootTexts(dir, root.binary)));
258
+ if (typeof stand === "string")
259
+ return waitingRoot(ctx, input, unresolved, stand);
260
+ let planned;
261
+ try {
262
+ await terraformInit(rootArgs(input));
263
+ planned = await terraformPlan({
264
+ ...rootArgs(input),
265
+ vars: stand.vars,
266
+ planFile: PREVIEW_PLAN_FILE,
267
+ ...(targets ? { targets } : {}),
268
+ text: false,
269
+ });
270
+ }
271
+ catch (err) {
272
+ return waitingRoot(ctx, input, unresolved, `the plan with stand-in values failed: ${firstLines(err)}`);
273
+ }
274
+ finally {
275
+ rmSync(join(dir, PREVIEW_PLAN_FILE), { force: true });
276
+ }
277
+ const marked = markPlaceholderDriven(planned.json, stand.placeholders);
278
+ if ("refused" in marked)
279
+ return waitingRoot(ctx, input, unresolved, marked.refused);
280
+ const placeholders = stand.placeholders.map((name) => `var.${name}`);
281
+ const shape = terraformPlanShape(marked.plan, input.root, { unknownInputs: unresolved });
282
+ const artifact = { kind: "terraform-apply-shape", member: input.root, shape, digest: planShapeDigest(shape) };
283
+ const part = terraformChangeSetPart({
284
+ member: input.root,
285
+ plan: marked.plan,
286
+ ...(planned.changeSet?.member.planner ? { planner: planned.changeSet.member.planner } : {}),
287
+ ...(planned.changeSet?.member.scope ? { scope: planned.changeSet.member.scope } : {}),
288
+ });
289
+ // The stand-ins' values are not real, so the digest is over the preview's shape and entries, never the raw plan.
290
+ const planDigest = computePlanDigest("terraform-apply-preview", {
291
+ env: ctx.env,
292
+ root: input.root,
293
+ vars: resolvedVars(input),
294
+ unresolved,
295
+ placeholders,
296
+ shape: artifact.digest,
297
+ entries: part.entries,
298
+ });
299
+ const changeSet = { ...part, member: { ...part.member, planDigest, placeholders } };
300
+ return {
301
+ member: input.root,
302
+ planDigest,
303
+ artifact,
304
+ changeSet,
305
+ outputs: terraformPlannedOutputs(marked.plan),
306
+ shape,
307
+ ...(targets ? { targets } : {}),
308
+ };
309
+ }
150
310
  /**
151
311
  * choudoufu refused the plan `run` was handed by its wave (its exit 3):
152
312
  * the live system moved since the plan, or the plan names another estate.
@@ -203,9 +363,9 @@ export const terraformApplyCapability = {
203
363
  async plan(ctx, input) {
204
364
  requireRoot(input);
205
365
  const unresolved = [...(ctx.unresolvedInputs ?? [])].sort();
206
- if (unresolved.length > 0)
207
- return waitingRoot(ctx, input, unresolved);
208
366
  const targets = ctx.targets && ctx.targets.length > 0 ? ctx.targets : undefined;
367
+ if (unresolved.length > 0)
368
+ return previewRoot(ctx, input, unresolved, targets);
209
369
  // A targeted root plans with `plan -target`, never as a choudoufu one-root set.
210
370
  const wave = targets ? { wave: false } : await choudoufuWaveRoot({ ...rootArgs(input), ...(input.vars ? { vars: input.vars } : {}) });
211
371
  if (wave.wave) {
@@ -214,7 +374,7 @@ export const terraformApplyCapability = {
214
374
  planDigest: set.planDigest,
215
375
  choudoufu: { planSetFile: set.planSetFile, setDigest: set.setDigest },
216
376
  };
217
- return { member: input.root, planDigest: set.planDigest, artifact, changeSet: set.changeSet, outputs: terraformPlannedOutputs(set.json) };
377
+ return { member: input.root, planDigest: set.planDigest, artifact, changeSet: set.changeSet, outputs: terraformPlannedOutputs(set.json), shape: terraformPlanShape(set.json, input.root) };
218
378
  }
219
379
  const { changeSet, json, ...planned } = await planRoot(input, targets);
220
380
  return {
@@ -223,6 +383,7 @@ export const terraformApplyCapability = {
223
383
  artifact: planned,
224
384
  ...(changeSet ? { changeSet } : {}),
225
385
  outputs: terraformPlannedOutputs(json),
386
+ shape: terraformPlanShape(json, input.root),
226
387
  ...(targets ? { targets } : {}),
227
388
  };
228
389
  },
@@ -233,10 +394,30 @@ export const terraformApplyCapability = {
233
394
  await terraformInit(rootArgs(input));
234
395
  return terraformOutputs(rootArgs(input));
235
396
  },
397
+ // #4447 (ws-107): a group core forms of components that read each other plans and applies as one composed run
398
+ // when `terraform.compose.enabled` is set (./compose-group.ts). Delegates, so the methods are read when called.
399
+ groupReads: (ctx, members) => terraformComposeGroup.groupReads(ctx, members),
400
+ groupFallback: (ctx, members) => terraformComposeGroup.groupFallback(ctx, members),
401
+ planGroup: (ctx, members) => terraformComposeGroup.planGroup(ctx, members),
402
+ runGroup: (ctx, run, members) => terraformComposeGroup.runGroup(ctx, run, members),
236
403
  async run(ctx, input) {
237
404
  const fromWave = ctx.plans?.[input.root];
238
405
  if (isChoudoufuWavePlan(fromWave))
239
406
  return runChoudoufuWave(ctx, input, fromWave);
407
+ if (isShapeArtifact(fromWave)) {
408
+ // A root previewed with stand-ins (#4463): plan it now with the real
409
+ // values, and apply that plan only when it is within the preview's shape.
410
+ const fresh = await planRoot(input, ctx.targets);
411
+ const differences = planShapeDifferences(fromWave.shape, terraformPlanShape(fresh.json, input.root));
412
+ if (differences.length > 0)
413
+ throw new PlanShapeMovedError(input.root, differences);
414
+ const applied = await terraformApply({ ...rootArgs(input), planFile: fresh.planFile });
415
+ if (!applied.applied) {
416
+ throw new Error(`terraform-apply: root "${input.root}" refused the plan it was given (${applied.refused ?? "refused"}). ${applied.refusal ?? ""}`);
417
+ }
418
+ const outputs = await terraformOutputs(rootArgs(input));
419
+ return { root: input.root, planDigest: fresh.planDigest, fromWavePlan: false, outputs };
420
+ }
240
421
  const planned = isPlannedRoot(fromWave)
241
422
  ? fromWave
242
423
  : await planRoot(input, ctx.targets).then(({ planFile, planDigest }) => ({ planFile, planDigest }));
@@ -268,4 +449,7 @@ export const terraformCapabilityPlugin = {
268
449
  families() {
269
450
  return TERRAFORM_VERB_FAMILIES;
270
451
  },
452
+ engineOutputSources() {
453
+ return [choudoufuOutputSource];
454
+ },
271
455
  };