@intentius/chant-lexicon-terraform 0.124.52 → 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 (336) 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.map +1 -1
  27. package/dist/commands.js +4 -3
  28. package/dist/components/capability-plugin.d.ts +65 -3
  29. package/dist/components/capability-plugin.d.ts.map +1 -1
  30. package/dist/components/capability-plugin.js +197 -13
  31. package/dist/components/compose-group.d.ts +145 -0
  32. package/dist/components/compose-group.d.ts.map +1 -0
  33. package/dist/components/compose-group.js +407 -0
  34. package/dist/components/placeholder-preview.d.ts +78 -0
  35. package/dist/components/placeholder-preview.d.ts.map +1 -0
  36. package/dist/components/placeholder-preview.js +254 -0
  37. package/dist/compose/apply-events.d.ts +46 -0
  38. package/dist/compose/apply-events.d.ts.map +1 -0
  39. package/dist/compose/apply-events.js +77 -0
  40. package/dist/compose/estate.d.ts +194 -0
  41. package/dist/compose/estate.d.ts.map +1 -0
  42. package/dist/compose/estate.js +363 -0
  43. package/dist/compose/hcl.d.ts +99 -0
  44. package/dist/compose/hcl.d.ts.map +1 -0
  45. package/dist/compose/hcl.js +302 -0
  46. package/dist/compose/module-address.d.ts +24 -0
  47. package/dist/compose/module-address.d.ts.map +1 -0
  48. package/dist/compose/module-address.js +43 -0
  49. package/dist/compose/root.d.ts +127 -0
  50. package/dist/compose/root.d.ts.map +1 -0
  51. package/dist/compose/root.js +586 -0
  52. package/dist/compose/run.d.ts +183 -0
  53. package/dist/compose/run.d.ts.map +1 -0
  54. package/dist/compose/run.js +447 -0
  55. package/dist/compose/slice.d.ts +102 -0
  56. package/dist/compose/slice.d.ts.map +1 -0
  57. package/dist/compose/slice.js +249 -0
  58. package/dist/compose/state-address.d.ts +28 -0
  59. package/dist/compose/state-address.d.ts.map +1 -0
  60. package/dist/compose/state-address.js +154 -0
  61. package/dist/compose/state-merge.d.ts +52 -0
  62. package/dist/compose/state-merge.d.ts.map +1 -0
  63. package/dist/compose/state-merge.js +135 -0
  64. package/dist/compose.d.ts +179 -0
  65. package/dist/compose.d.ts.map +1 -0
  66. package/dist/compose.js +482 -0
  67. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  68. package/dist/composites/terraform-apply-op.js +7 -3
  69. package/dist/config.d.ts +59 -13
  70. package/dist/config.d.ts.map +1 -1
  71. package/dist/config.js +43 -9
  72. package/dist/describe-resources.d.ts +74 -12
  73. package/dist/describe-resources.d.ts.map +1 -1
  74. package/dist/describe-resources.js +374 -66
  75. package/dist/drift-read.d.ts +33 -0
  76. package/dist/drift-read.d.ts.map +1 -0
  77. package/dist/drift-read.js +243 -0
  78. package/dist/drift-record.d.ts +147 -0
  79. package/dist/drift-record.d.ts.map +1 -0
  80. package/dist/drift-record.js +383 -0
  81. package/dist/hcl/edges.d.ts.map +1 -1
  82. package/dist/hcl/edges.js +24 -1
  83. package/dist/hcl/files.d.ts +61 -0
  84. package/dist/hcl/files.d.ts.map +1 -0
  85. package/dist/hcl/files.js +221 -0
  86. package/dist/hcl/instances.d.ts +82 -0
  87. package/dist/hcl/instances.d.ts.map +1 -0
  88. package/dist/hcl/instances.js +390 -0
  89. package/dist/hcl/parse.d.ts +81 -9
  90. package/dist/hcl/parse.d.ts.map +1 -1
  91. package/dist/hcl/parse.js +114 -16
  92. package/dist/hcl/roots.d.ts.map +1 -1
  93. package/dist/hcl/roots.js +5 -0
  94. package/dist/hcl/suppressions.d.ts.map +1 -1
  95. package/dist/hcl/suppressions.js +8 -2
  96. package/dist/index.d.ts +11 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +21 -2
  99. package/dist/integrity.json +3 -3
  100. package/dist/live-plan-reasons.d.ts +21 -0
  101. package/dist/live-plan-reasons.d.ts.map +1 -0
  102. package/dist/live-plan-reasons.js +47 -0
  103. package/dist/lsp/option-keys.d.ts.map +1 -1
  104. package/dist/lsp/option-keys.js +2 -0
  105. package/dist/manifest.json +1 -1
  106. package/dist/op/activities/index.d.ts +6 -2
  107. package/dist/op/activities/index.d.ts.map +1 -1
  108. package/dist/op/activities/index.js +5 -1
  109. package/dist/op/activities/live-detect.d.ts +4 -3
  110. package/dist/op/activities/live-detect.d.ts.map +1 -1
  111. package/dist/op/activities/live-detect.js +57 -32
  112. package/dist/op/activities/pin-rollout.d.ts +2 -0
  113. package/dist/op/activities/pin-rollout.d.ts.map +1 -1
  114. package/dist/op/activities/pin-rollout.js +1 -0
  115. package/dist/op/activities/terraform.d.ts +153 -5
  116. package/dist/op/activities/terraform.d.ts.map +1 -1
  117. package/dist/op/activities/terraform.js +205 -20
  118. package/dist/op/activity-contracts.d.ts +27 -0
  119. package/dist/op/activity-contracts.d.ts.map +1 -1
  120. package/dist/op/activity-contracts.js +19 -1
  121. package/dist/op/builders.d.ts +7 -1
  122. package/dist/op/builders.d.ts.map +1 -1
  123. package/dist/op/builders.js +9 -0
  124. package/dist/pin/edit.d.ts.map +1 -1
  125. package/dist/pin/edit.js +35 -0
  126. package/dist/pin/rollout.d.ts +2 -0
  127. package/dist/pin/rollout.d.ts.map +1 -1
  128. package/dist/pin/rollout.js +14 -11
  129. package/dist/plan-shape.d.ts +30 -0
  130. package/dist/plan-shape.d.ts.map +1 -0
  131. package/dist/plan-shape.js +125 -0
  132. package/dist/plugin.d.ts.map +1 -1
  133. package/dist/plugin.js +32 -18
  134. package/dist/report.d.ts +15 -6
  135. package/dist/report.d.ts.map +1 -1
  136. package/dist/report.js +28 -55
  137. package/dist/skills/chant-terraform.md +1 -1
  138. package/dist/state/azure-blob.d.ts +68 -0
  139. package/dist/state/azure-blob.d.ts.map +1 -0
  140. package/dist/state/azure-blob.js +201 -0
  141. package/dist/state/backend.d.ts +158 -0
  142. package/dist/state/backend.d.ts.map +1 -0
  143. package/dist/state/backend.js +406 -0
  144. package/dist/state/exec.d.ts +39 -0
  145. package/dist/state/exec.d.ts.map +1 -0
  146. package/dist/state/exec.js +80 -0
  147. package/dist/state/gcs.d.ts +62 -0
  148. package/dist/state/gcs.d.ts.map +1 -0
  149. package/dist/state/gcs.js +224 -0
  150. package/dist/state/index.d.ts +15 -0
  151. package/dist/state/index.d.ts.map +1 -0
  152. package/dist/state/index.js +14 -0
  153. package/dist/state/locks.d.ts +105 -0
  154. package/dist/state/locks.d.ts.map +1 -0
  155. package/dist/state/locks.js +130 -0
  156. package/dist/state/object-store.d.ts +37 -0
  157. package/dist/state/object-store.d.ts.map +1 -0
  158. package/dist/state/object-store.js +20 -0
  159. package/dist/state/root-states.d.ts +131 -0
  160. package/dist/state/root-states.d.ts.map +1 -0
  161. package/dist/state/root-states.js +241 -0
  162. package/dist/state/s3.d.ts +68 -0
  163. package/dist/state/s3.d.ts.map +1 -0
  164. package/dist/state/s3.js +170 -0
  165. package/dist/state/store.d.ts +51 -0
  166. package/dist/state/store.d.ts.map +1 -0
  167. package/dist/state/store.js +185 -0
  168. package/dist/terragrunt/affected.d.ts.map +1 -1
  169. package/dist/terragrunt/affected.js +2 -1
  170. package/package.json +7 -3
  171. package/src/__fixtures__/ACCEPTANCE.md +69 -7
  172. package/src/__fixtures__/compose/apply-failed-in-app.jsonl +12 -0
  173. package/src/__fixtures__/compose/state-app.json +66 -0
  174. package/src/__fixtures__/compose/state-edge.json +40 -0
  175. package/src/__fixtures__/compose/state-net.json +58 -0
  176. package/src/__fixtures__/compose/three-app.plan.json +184 -0
  177. package/src/__fixtures__/compose/three-composed.plan.json +509 -0
  178. package/src/__fixtures__/compose/three-edge.plan.json +144 -0
  179. package/src/__fixtures__/compose/three-net.plan.json +174 -0
  180. package/src/__fixtures__/compose/unknown-composed.plan.json +228 -0
  181. package/src/__fixtures__/compose/working-after-apply.json +145 -0
  182. package/src/__fixtures__/config-blocks/app/main.tf +61 -0
  183. package/src/__fixtures__/config-blocks/app/modules/network/main.tf +4 -0
  184. package/src/__fixtures__/config-blocks/app-json/main.tf.json +39 -0
  185. package/src/__fixtures__/config-files/shadow/main.tf +3 -0
  186. package/src/__fixtures__/config-files/shadow/main.tofu +3 -0
  187. package/src/__fixtures__/config-files/shadow/other.tf +3 -0
  188. package/src/__fixtures__/config-files/tf/main.tf +11 -0
  189. package/src/__fixtures__/config-files/tf-json/main.tf.json +18 -0
  190. package/src/__fixtures__/config-files/tofu/main.tofu +11 -0
  191. package/src/__fixtures__/config-files/tofu-json/main.tofu.json +18 -0
  192. package/src/__fixtures__/config-files/tofu-only/main.tofu +3 -0
  193. package/src/__fixtures__/drift-record/README.md +11 -0
  194. package/src/__fixtures__/drift-record/live-plan-sections.json +27 -0
  195. package/src/__fixtures__/instance-forms/app/main.tf +110 -0
  196. package/src/__fixtures__/instance-forms/app/modules/svc/main.tf +3 -0
  197. package/src/__fixtures__/instance-vars/app/main.tf +52 -0
  198. package/src/__fixtures__/instance-vars/app/modules/app/main.tf +12 -0
  199. package/src/__fixtures__/instance-vars/app/modules/edge/main.tf +3 -0
  200. package/src/__fixtures__/instance-vars/app/terraform.tfvars +1 -0
  201. package/src/__fixtures__/live-module-keyed/live-graph.json +259 -0
  202. package/src/__fixtures__/live-module-keyed/live-plan.json +87 -0
  203. package/src/__fixtures__/live-module-keyed/root/estate.chdf.hcl +1 -0
  204. package/src/__fixtures__/live-module-keyed/root/main.tf +21 -0
  205. package/src/__fixtures__/live-module-keyed/root/modules/app/main.tf +17 -0
  206. package/src/__fixtures__/live-module-keyed/root/modules/edge/main.tf +7 -0
  207. package/src/__fixtures__/live-module-keyed/root/versions.tf +30 -0
  208. package/src/__fixtures__/live-module-keyed/show-state.json +146 -0
  209. package/src/__fixtures__/pr-loop/README.md +50 -0
  210. package/src/__fixtures__/pr-loop/chant.config.json +13 -0
  211. package/src/__fixtures__/pr-loop/estate.component.ts +10 -0
  212. package/src/__fixtures__/pr-loop/roots/a/main.tf +11 -0
  213. package/src/__fixtures__/pr-loop/roots/app/main.tf +19 -0
  214. package/src/__fixtures__/pr-loop/roots/b/main.tf +7 -0
  215. package/src/__fixtures__/pr-loop/roots/dns/main.tf +3 -0
  216. package/src/__fixtures__/pr-loop/roots/net/main.tf +7 -0
  217. package/src/__fixtures__/report/applied.show.json +51 -0
  218. package/src/__fixtures__/report/generate.ts +63 -0
  219. package/src/__fixtures__/report/plan.show.json +200 -0
  220. package/src/__fixtures__/report/tf-apply/change-set.json +170 -0
  221. package/src/__fixtures__/report/tf-apply/report.json +338 -0
  222. package/src/__fixtures__/report/tf-plan/change-set.json +170 -0
  223. package/src/__fixtures__/report/tf-plan/report.json +320 -0
  224. package/src/__fixtures__/three-cloud-azure.graph.json +290 -0
  225. package/src/applied-state.ts +52 -0
  226. package/src/backend-config.test.ts +105 -0
  227. package/src/backend-config.ts +133 -0
  228. package/src/behaviour/kinds.ts +4 -0
  229. package/src/behaviour/predict.ts +5 -2
  230. package/src/behaviour/reference-catalog.plugin.test.ts +65 -0
  231. package/src/behaviour/reference-catalog.ts +16 -5
  232. package/src/behaviour/request.ts +3 -2
  233. package/src/change-set-instances.test.ts +98 -0
  234. package/src/changed-roots.ts +14 -16
  235. package/src/commands.ts +4 -3
  236. package/src/components/capability-plugin.test.ts +172 -5
  237. package/src/components/capability-plugin.ts +212 -11
  238. package/src/components/change-set-graph.e2e.test.ts +11 -5
  239. package/src/components/compose-group.test.ts +486 -0
  240. package/src/components/compose-group.ts +489 -0
  241. package/src/components/demo-composed-roots.e2e.test.ts +440 -0
  242. package/src/components/member-outputs.test.ts +76 -0
  243. package/src/components/placeholder-preview.test.ts +147 -0
  244. package/src/components/placeholder-preview.ts +257 -0
  245. package/src/components/pr-loop-compose.tofu.e2e.test.ts +363 -0
  246. package/src/components/pr-loop.tofu.test.ts +112 -56
  247. package/src/compose/apply-events.test.ts +62 -0
  248. package/src/compose/apply-events.ts +100 -0
  249. package/src/compose/estate.choudoufu.e2e.test.ts +112 -0
  250. package/src/compose/estate.test.ts +421 -0
  251. package/src/compose/estate.ts +443 -0
  252. package/src/compose/hcl.ts +311 -0
  253. package/src/compose/module-address.ts +49 -0
  254. package/src/compose/root.test.ts +416 -0
  255. package/src/compose/root.ts +592 -0
  256. package/src/compose/run.test.ts +440 -0
  257. package/src/compose/run.tofu.test.ts +176 -0
  258. package/src/compose/run.ts +527 -0
  259. package/src/compose/slice.test.ts +184 -0
  260. package/src/compose/slice.ts +290 -0
  261. package/src/compose/state-address.ts +157 -0
  262. package/src/compose/state-merge.test.ts +131 -0
  263. package/src/compose/state-merge.ts +148 -0
  264. package/src/compose.test.ts +248 -0
  265. package/src/compose.ts +627 -0
  266. package/src/composites/terraform-apply-op.test.ts +9 -0
  267. package/src/composites/terraform-apply-op.ts +7 -3
  268. package/src/config-file-listers.test.ts +265 -0
  269. package/src/config.ts +44 -9
  270. package/src/describe-resources.engine-facts.test.ts +412 -0
  271. package/src/describe-resources.live-module-keyed.test.ts +290 -0
  272. package/src/describe-resources.ts +416 -70
  273. package/src/drift-read.ts +284 -0
  274. package/src/drift-record.test.ts +223 -0
  275. package/src/drift-record.ts +455 -0
  276. package/src/hcl/config-blocks.test.ts +174 -0
  277. package/src/hcl/edges.ts +21 -1
  278. package/src/hcl/files.ts +234 -0
  279. package/src/hcl/instance-forms.test.ts +94 -0
  280. package/src/hcl/instances.test.ts +137 -0
  281. package/src/hcl/instances.ts +422 -0
  282. package/src/hcl/parse.test.ts +139 -1
  283. package/src/hcl/parse.ts +149 -16
  284. package/src/hcl/roots.ts +6 -0
  285. package/src/hcl/suppressions.ts +8 -2
  286. package/src/index.ts +76 -0
  287. package/src/live-plan-reasons.ts +54 -0
  288. package/src/lsp/completions.test.ts +1 -1
  289. package/src/lsp/option-keys.ts +2 -0
  290. package/src/op/activities/choudoufu-plan-set-op.test.ts +111 -0
  291. package/src/op/activities/index.ts +9 -0
  292. package/src/op/activities/live-detect.ts +55 -26
  293. package/src/op/activities/pin-rollout.ts +3 -0
  294. package/src/op/activities/registry.test.ts +2 -1
  295. package/src/op/activities/terraform.test.ts +55 -0
  296. package/src/op/activities/terraform.ts +266 -22
  297. package/src/op/activity-contracts.test.ts +1 -0
  298. package/src/op/activity-contracts.ts +24 -1
  299. package/src/op/builders.ts +14 -0
  300. package/src/pin/bundle.test.ts +2 -0
  301. package/src/pin/edit.ts +30 -0
  302. package/src/pin/rollout.ts +16 -11
  303. package/src/plan-shape.test.ts +148 -0
  304. package/src/plan-shape.ts +128 -0
  305. package/src/plugin.ts +33 -19
  306. package/src/report.test.ts +82 -1
  307. package/src/report.ts +31 -54
  308. package/src/signal-only-root.test.ts +72 -0
  309. package/src/skills/chant-terraform.md +1 -1
  310. package/src/state/azure-blob.ts +223 -0
  311. package/src/state/backend-verdict.test.ts +187 -0
  312. package/src/state/backend.test.ts +127 -0
  313. package/src/state/backend.ts +488 -0
  314. package/src/state/exec.ts +111 -0
  315. package/src/state/gcs.ts +254 -0
  316. package/src/state/index.ts +15 -0
  317. package/src/state/locks.test.ts +84 -0
  318. package/src/state/locks.ts +201 -0
  319. package/src/state/object-store.ts +35 -0
  320. package/src/state/root-states-floci.e2e.test.ts +66 -0
  321. package/src/state/root-states.test.ts +462 -0
  322. package/src/state/root-states.ts +309 -0
  323. package/src/state/s3.ts +193 -0
  324. package/src/state/store.ts +225 -0
  325. package/src/terragrunt/affected.ts +2 -1
  326. package/src/workspace-kinds.test.ts +4 -17
  327. package/workspace-kinds.json +1 -24
  328. package/dist/workspace-member.d.ts +0 -30
  329. package/dist/workspace-member.d.ts.map +0 -1
  330. package/dist/workspace-member.js +0 -91
  331. package/schemas/pulumi.schema.json +0 -34
  332. package/src/testdata/pulumi-member/Pulumi.yaml +0 -2
  333. package/src/testdata/pulumi-member/stack-export.json +0 -53
  334. package/src/testdata/pulumi-unexported/Pulumi.yaml +0 -2
  335. package/src/workspace-member.test.ts +0 -89
  336. 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
  }
@@ -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;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"}
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
  }
@@ -183,6 +184,6 @@ export const terraformCommands = {
183
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",
184
185
  commands: [
185
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 },
186
- { name: "report", description: "Write a terragucci.report/v1 run report and its index row from show -json of a composed plan", handler: runReportVerb },
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 },
187
188
  ],
188
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
  };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * `terraform-apply`'s group methods (chant#4447, ws-107): core forms a group
3
+ * of `terraform-apply` components that read each other, and these plan it
4
+ * as one plan and apply it as one apply of a composed root, so a value one
5
+ * root creates reaches its readers inside that apply.
6
+ *
7
+ * - `groupReads` reports to core each member's `terraform_remote_state` and
8
+ * `terraform_estate_outputs` reads of another, as the composition finds
9
+ * them, so roots wired in HCL alone form a group. Nothing while
10
+ * `terraform.compose.enabled` is unset.
11
+ * - `groupFallback` answers `off` while `terraform.compose.enabled` is unset,
12
+ * else builds the {@link Composition} (#4438) from the files, the config
13
+ * and each member's cloud identity, and answers its fallback, if any. A
14
+ * member the composition leaves out is dropped with its own kind and
15
+ * reason, and core forms the group again from the rest (#4419 ruling 1).
16
+ * A composition of estates also asks `choudoufu version` (#4445).
17
+ * Nothing plans.
18
+ * - `planGroup` builds the composition again, plans it once with
19
+ * `ComposedRun` (#4444) or, for estates, `EstateRun` (#4445), and answers
20
+ * each member's part (#4442): its change-set part, planned outputs and
21
+ * shape, with its own plan's digest. The group's digest is the composed
22
+ * plan's with the generated files. The run is released before it returns,
23
+ * with no lock taken, so a plan that core then refuses or makes wait
24
+ * leaves no lock held and no file changed.
25
+ * - `runGroup` plans once more, this time under each state's lock, and
26
+ * applies only when that plan's digest is the one `planGroup` answered;
27
+ * otherwise it throws a moved plan and nothing applies. Whatever happens,
28
+ * it releases the run. Each member's outputs come from the run: its part
29
+ * of the working state's `show -json` for roots with state, the post-apply
30
+ * plan's `prior_state` for estates.
31
+ *
32
+ * Each member's identity is `ci.identity.cloud` resolved for its root as
33
+ * the target (`resolveTargetCloudIdentity`, #4332), by the root's key or its
34
+ * directory, whichever a rule's target glob matches, compared by
35
+ * `identityKey`, so members that apply under different identities do not
36
+ * compose (#4438's `identity`).
37
+ */
38
+ import type { CapabilityGroupOutcome, CapabilityGroupPlan, GroupContext, GroupFallback, GroupMember, GroupRun, ReportedGroupRead } from "@intentius/chant/components/capability";
39
+ import type { DriverComponent } from "@intentius/chant/components/driver";
40
+ import { type TargetCloudIdentityLookup } from "@intentius/chant/workspace/ci-cloud-identity";
41
+ import { type Composition } from "../compose.js";
42
+ import type { TerraformConfig } from "../config.js";
43
+ import { type BinaryExec } from "../state/exec.js";
44
+ import { type ComposedRunner } from "../compose/estate.js";
45
+ /** The step input as the group methods read it; `TerraformApplyStepInput` in ./capability-plugin.ts. */
46
+ interface StepInput {
47
+ root: string;
48
+ vars?: Record<string, unknown>;
49
+ cwd?: string;
50
+ }
51
+ /** The project a group's roots belong to. */
52
+ export interface ComposeProject {
53
+ /** Where `chant.config.*` was found: roots resolve against it and `.chant-compose/` is written there. */
54
+ projectRoot: string;
55
+ terraform: TerraformConfig;
56
+ }
57
+ /** What a runner is built from. */
58
+ export interface GroupRunnerOptions extends ComposeProject {
59
+ composition: Composition;
60
+ /** Take each state's lock: `runGroup` does, `planGroup` does not. */
61
+ lock: boolean;
62
+ /** Each member's step `vars` as written, by component. */
63
+ vars: Record<string, Record<string, unknown> | undefined>;
64
+ /** The outputs of components outside the group that a member's `vars` read. */
65
+ componentOutputs: Record<string, Record<string, unknown>>;
66
+ env: NodeJS.ProcessEnv;
67
+ /** A directory of the run's own, removed after the run is released. */
68
+ work: string;
69
+ }
70
+ /** What the group methods use, each replaceable in a test. */
71
+ export interface ComposeGroupDeps {
72
+ /** The project's config. Default: `loadChantConfigUpward` from the current directory. */
73
+ project?: () => Promise<ComposeProject>;
74
+ /** The run for a composition. Default: `EstateRun` when `estate` is set, else `ComposedRun`. */
75
+ runner?: (o: GroupRunnerOptions) => ComposedRunner;
76
+ /**
77
+ * Each target's cloud identity for `env` (`resolveTargetCloudIdentity`,
78
+ * #4332), or undefined when there is no workspace to read. Default:
79
+ * `declaredTargetCloudIdentity` from the project root.
80
+ */
81
+ identity?: (projectRoot: string, env: string) => Promise<TargetCloudIdentityLookup | undefined>;
82
+ /** How `choudoufu version` runs. Default `runBinary`. */
83
+ exec?: BinaryExec;
84
+ /** A root's outputs as its state holds them, for a member whose part of the working state could not be shown. Default `terraformOutputs`. */
85
+ outputs?: (root: string) => Promise<Record<string, unknown>>;
86
+ env?: NodeJS.ProcessEnv;
87
+ log?: (line: string) => void;
88
+ }
89
+ /** The group methods `terraform-apply` declares. */
90
+ export interface ComposeGroupMethods {
91
+ groupReads(ctx: GroupContext, members: readonly GroupMember<StepInput>[]): Promise<ReportedGroupRead[]>;
92
+ groupFallback(ctx: GroupContext, members: readonly GroupMember<StepInput>[]): Promise<GroupFallback | undefined>;
93
+ planGroup(ctx: GroupContext, members: readonly GroupMember<StepInput>[]): Promise<CapabilityGroupPlan>;
94
+ runGroup(ctx: GroupContext, run: GroupRun, members: readonly GroupMember<StepInput>[]): Promise<CapabilityGroupOutcome>;
95
+ }
96
+ /** The members as components, each deploying its one step, for `compositionIfEnabled`. */
97
+ export declare function memberComponents(members: readonly GroupMember<StepInput>[]): DriverComponent[];
98
+ /**
99
+ * Each member's identity as `compositionFor` compares it: `identityKey` of
100
+ * `lookup` for the member's root, which a rule's target globs match by the
101
+ * root's key in `terraform.roots` or by its directory relative to the
102
+ * project (the first declared rule that either matches wins). Undefined
103
+ * without a lookup, so no member falls back as `identity`.
104
+ */
105
+ export declare function memberIdentity(project: ComposeProject, members: readonly GroupMember<StepInput>[], lookup: TargetCloudIdentityLookup | undefined): ((member: string) => string | undefined) | undefined;
106
+ /**
107
+ * The composition of exactly the group's members (core has already formed the
108
+ * group), or undefined when `terraform.compose` is not enabled.
109
+ */
110
+ export declare function groupComposition(project: ComposeProject, members: readonly GroupMember<StepInput>[], env: NodeJS.ProcessEnv, identity?: (member: string) => string | undefined): Composition | undefined;
111
+ /**
112
+ * The members' HCL reads of each other (#4447): each `terraform_remote_state`
113
+ * block whose state is another member's backend, and each
114
+ * `terraform_estate_outputs` block naming another member's estate, as the
115
+ * composition finds them. Each output the reader's code takes off the block
116
+ * is one read, `<upstream>.<output>` as a `stackOutput()` read is named; a
117
+ * block whose outputs the code does not name one by one is one read with
118
+ * the block's address as `ref`. Core cannot see these in the step's `vars`.
119
+ */
120
+ export declare function hclGroupReads(project: ComposeProject, members: readonly GroupMember<StepInput>[]): ReportedGroupRead[];
121
+ /**
122
+ * What core is told of the group (#4419 ruling 1): the whole change's
123
+ * fallback when it is not `one-root`; else each member the composition left
124
+ * out as a drop with its own kind and reason, so core groups the rest
125
+ * again without them; else `one-root`. Undefined when every member
126
+ * composes.
127
+ */
128
+ export declare function groupFallbackOf(c: Composition, members: readonly string[]): GroupFallback | undefined;
129
+ /**
130
+ * The outputs of components outside the group that the members' `vars`
131
+ * read, recovered from each member's input, which core resolved against
132
+ * them: `writeComposedRoot` turns those reads into literals.
133
+ */
134
+ export declare function outsideOutputs(members: readonly GroupMember<StepInput>[]): Record<string, Record<string, unknown>>;
135
+ /** A composed plan that no longer matches the one approved. Core's moved-plan mark, so nothing applies and the command exits 3. */
136
+ export declare class ComposedPlanMovedError extends Error {
137
+ readonly planMoved = true;
138
+ constructor(message: string);
139
+ }
140
+ /** The group methods, with `deps` in place of the defaults. */
141
+ export declare function composeGroup(deps?: ComposeGroupDeps): ComposeGroupMethods;
142
+ /** The group methods `terraform-apply` uses. */
143
+ export declare const terraformComposeGroup: ComposeGroupMethods;
144
+ export {};
145
+ //# sourceMappingURL=compose-group.d.ts.map