@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
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose-group.d.ts","sourceRoot":"","sources":["../../src/components/compose-group.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,407 @@
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 { mkdtempSync, rmSync } from "node:fs";
39
+ import { tmpdir } from "node:os";
40
+ import { dirname, isAbsolute, join, resolve } from "node:path";
41
+ import { loadChantConfigUpward } from "@intentius/chant/config";
42
+ import { wiringReferencesAt } from "@intentius/chant/components/driver";
43
+ import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
44
+ import { declaredTargetCloudIdentity } from "@intentius/chant/workspace/ci-cloud-identity";
45
+ import { COMPOSE_STEP_KIND, composeOff, compositionIfEnabled, identityKey, rootHclReads } from "../compose.js";
46
+ import { terraformBinary, terraformOutputs } from "../op/activities/terraform.js";
47
+ import { runBinary } from "../state/exec.js";
48
+ import { estateVersionFallback, EstateRun } from "../compose/estate.js";
49
+ import { ComposedRun } from "../compose/run.js";
50
+ import { relPath, rootTexts } from "../compose/hcl.js";
51
+ async function loadProject() {
52
+ const start = resolve(process.cwd());
53
+ const { config, configPath } = await loadChantConfigUpward(start);
54
+ const projectRoot = configPath ? dirname(configPath) : start;
55
+ const terraform = config.terraform;
56
+ return { projectRoot, terraform: { ...(terraform ?? {}), roots: terraform?.roots ?? {} } };
57
+ }
58
+ function defaultRunner(o, log) {
59
+ if (o.composition.estate) {
60
+ return new EstateRun({ projectRoot: o.projectRoot, composition: o.composition, terraform: o.terraform, env: o.env, work: o.work, ...(log ? { log } : {}) });
61
+ }
62
+ return new ComposedRun({
63
+ projectRoot: o.projectRoot,
64
+ composition: o.composition,
65
+ terraform: o.terraform,
66
+ binary: { path: terraformBinary(o.terraform) },
67
+ env: o.env,
68
+ work: o.work,
69
+ lock: o.lock,
70
+ ...(o.env.GITHUB_RUN_ID ? { runId: `github-run-${o.env.GITHUB_RUN_ID}` } : {}),
71
+ vars: o.vars,
72
+ componentOutputs: o.componentOutputs,
73
+ // A state that could not be written is kept here when its backend cannot take it; it outlives the run's own directory.
74
+ reportDir: join(o.projectRoot, ".chant", "compose", "unwritten"),
75
+ ...(log ? { log } : {}),
76
+ });
77
+ }
78
+ /** The members as components, each deploying its one step, for `compositionIfEnabled`. */
79
+ export function memberComponents(members) {
80
+ return members.map((m) => ({
81
+ name: m.component,
82
+ ...(m.dependsOn ? { dependsOn: [...m.dependsOn] } : {}),
83
+ deploy: [{ phase: "Apply", steps: [{ ...m.step, kind: COMPOSE_STEP_KIND }] }],
84
+ }));
85
+ }
86
+ /**
87
+ * Each member's identity as `compositionFor` compares it: `identityKey` of
88
+ * `lookup` for the member's root, which a rule's target globs match by the
89
+ * root's key in `terraform.roots` or by its directory relative to the
90
+ * project (the first declared rule that either matches wins). Undefined
91
+ * without a lookup, so no member falls back as `identity`.
92
+ */
93
+ export function memberIdentity(project, members, lookup) {
94
+ if (!lookup)
95
+ return undefined;
96
+ const names = (root) => {
97
+ const dir = project.terraform.roots[root]?.dir;
98
+ if (dir === undefined)
99
+ return [root];
100
+ return [root, relPath(project.projectRoot, isAbsolute(dir) ? dir : resolve(project.projectRoot, dir))];
101
+ };
102
+ const keys = new Map(members.map((m) => [m.component, identityKey(lookup(names(m.step.root)))]));
103
+ return (member) => keys.get(member);
104
+ }
105
+ /**
106
+ * The composition of exactly the group's members (core has already formed the
107
+ * group), or undefined when `terraform.compose` is not enabled.
108
+ */
109
+ export function groupComposition(project, members, env, identity) {
110
+ const targets = Object.fromEntries(members.flatMap((m) => (m.ctx.targets?.length ? [[m.component, m.ctx.targets]] : [])));
111
+ return compositionIfEnabled({
112
+ projectRoot: project.projectRoot,
113
+ terraform: project.terraform,
114
+ components: memberComponents(members),
115
+ selected: new Set(members.map((m) => m.component)),
116
+ env,
117
+ targets,
118
+ ...(identity ? { identity } : {}),
119
+ });
120
+ }
121
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
122
+ /** The data source and the attribute that holds its outputs, for each kind of HCL read. */
123
+ const HCL_READ = { "remote-state": ["terraform_remote_state", "outputs"], "estate-outputs": ["terraform_estate_outputs", "values"] };
124
+ /** The outputs the texts read off `data.<type>.<name>.<field>`, by attribute or by index, sorted. */
125
+ function outputsRead(texts, type, name, field) {
126
+ const re = new RegExp(`\\bdata\\.${type}\\.${escapeRe(name)}\\.${field}(?:\\.([A-Za-z_][\\w-]*)|\\[\\s*\\\\?"([^"\\\\]+)\\\\?"\\s*\\])`, "g");
127
+ const out = new Set();
128
+ for (const text of texts)
129
+ for (const m of text.matchAll(re))
130
+ out.add((m[1] ?? m[2]));
131
+ return [...out].sort();
132
+ }
133
+ /**
134
+ * The members' HCL reads of each other (#4447): each `terraform_remote_state`
135
+ * block whose state is another member's backend, and each
136
+ * `terraform_estate_outputs` block naming another member's estate, as the
137
+ * composition finds them. Each output the reader's code takes off the block
138
+ * is one read, `<upstream>.<output>` as a `stackOutput()` read is named; a
139
+ * block whose outputs the code does not name one by one is one read with
140
+ * the block's address as `ref`. Core cannot see these in the step's `vars`.
141
+ */
142
+ export function hclGroupReads(project, members) {
143
+ const reads = rootHclReads({ projectRoot: project.projectRoot, terraform: project.terraform, components: memberComponents(members) });
144
+ const out = [];
145
+ for (const [reader, rs] of reads) {
146
+ const m = members.find((x) => x.component === reader);
147
+ const dir = m ? project.terraform.roots[m.step.root]?.dir : undefined;
148
+ const texts = dir === undefined ? [] : rootTexts(isAbsolute(dir) ? dir : resolve(project.projectRoot, dir), project.terraform.binary).map((t) => t.text);
149
+ for (const r of rs) {
150
+ if (r.via === "vars")
151
+ continue;
152
+ const [type, field] = HCL_READ[r.via];
153
+ const named = outputsRead(texts, type, r.name, field);
154
+ if (named.length === 0)
155
+ out.push({ reader, upstream: r.upstream, ref: `data.${type}.${r.name}` });
156
+ for (const output of named)
157
+ out.push({ reader, upstream: r.upstream, ref: `${r.upstream}.${output}`, output });
158
+ }
159
+ }
160
+ return out;
161
+ }
162
+ /**
163
+ * What core is told of the group (#4419 ruling 1): the whole change's
164
+ * fallback when it is not `one-root`; else each member the composition left
165
+ * out as a drop with its own kind and reason, so core groups the rest
166
+ * again without them; else `one-root`. Undefined when every member
167
+ * composes.
168
+ */
169
+ export function groupFallbackOf(c, members) {
170
+ if (c.fallback && c.fallback.kind !== "one-root")
171
+ return { kind: c.fallback.kind, reason: c.fallback.reason };
172
+ const drop = c.fallbacks.flatMap((f) => (f.member !== undefined && members.includes(f.member) ? [{ component: f.member, kind: f.kind, reason: f.reason }] : []));
173
+ if (!c.fallback) {
174
+ const named = new Set(drop.map((d) => d.component));
175
+ for (const m of members) {
176
+ if (!named.has(m) && !c.members.includes(m))
177
+ drop.push({ component: m, kind: "path", reason: `\`${m}\` deploys no root the composition could read` });
178
+ }
179
+ }
180
+ if (drop.length > 0)
181
+ return { kind: drop[0].kind, reason: drop[0].reason, drop };
182
+ if (c.fallback)
183
+ return { kind: c.fallback.kind, reason: c.fallback.reason };
184
+ return undefined;
185
+ }
186
+ /** `value` at the JSON Pointer `at`. */
187
+ function atPointer(value, at) {
188
+ if (at === "")
189
+ return value;
190
+ return at
191
+ .slice(1)
192
+ .split("/")
193
+ .map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"))
194
+ .reduce((acc, key) => (acc == null ? acc : acc[key]), value);
195
+ }
196
+ /** Set `value` at the dotted `path` of `target`. */
197
+ function setPath(target, path, value) {
198
+ const keys = path.split(".");
199
+ let at = target;
200
+ for (const k of keys.slice(0, -1)) {
201
+ const next = at[k];
202
+ at = (next && typeof next === "object" ? next : (at[k] = {}));
203
+ }
204
+ at[keys[keys.length - 1]] = value;
205
+ }
206
+ /**
207
+ * The outputs of components outside the group that the members' `vars`
208
+ * read, recovered from each member's input, which core resolved against
209
+ * them: `writeComposedRoot` turns those reads into literals.
210
+ */
211
+ export function outsideOutputs(members) {
212
+ var _a;
213
+ const inGroup = new Set(members.map((m) => m.component));
214
+ const out = {};
215
+ for (const m of members) {
216
+ for (const r of wiringReferencesAt(m.step)) {
217
+ if (inGroup.has(r.source))
218
+ continue;
219
+ const value = atPointer(m.input, r.at);
220
+ if (value !== undefined)
221
+ setPath((out[_a = r.source] ?? (out[_a] = {})), r.output, value);
222
+ }
223
+ }
224
+ return out;
225
+ }
226
+ /** The members' unresolved reads of components outside the group, by member. */
227
+ function unresolvedOf(members) {
228
+ return new Map(members.flatMap((m) => (m.ctx.unresolvedInputs?.length ? [[m.component, [...m.ctx.unresolvedInputs].sort()]] : [])));
229
+ }
230
+ /**
231
+ * The group while a member reads an output of a component outside it that
232
+ * is not known yet: no member is planned, each gets a hole naming what the
233
+ * group waits on, and core plans the group again once those apply.
234
+ */
235
+ function waitingGroup(ctx, members, unresolved) {
236
+ const waits = [...new Set([...unresolved.values()].flat())].sort();
237
+ const by = [...unresolved.keys()].sort();
238
+ const plans = {};
239
+ for (const m of members) {
240
+ const root = m.step.root;
241
+ const vars = Object.fromEntries(Object.entries((m.input?.vars ?? {})).map(([k, v]) => [k, v === undefined ? null : v]));
242
+ const planDigest = computePlanDigest("terraform-apply-group-unresolved", { env: ctx.env, root, vars, unresolved: waits });
243
+ const reason = by.length === 1 && by[0] === m.component
244
+ ? `inputs from ${waits.join(", ")}; planned again once they apply`
245
+ : `inputs from ${waits.join(", ")}, read by ${by.join(", ")} in its group; planned again once they apply`;
246
+ const changeSet = {
247
+ member: { member: root, lexicon: "terraform", planner: "chant", status: "planned", planDigest, holes: [{ address: root, type: "terraform_root", reason }] },
248
+ entries: [],
249
+ };
250
+ plans[m.component] = { member: root, planDigest, changeSet };
251
+ }
252
+ return { members: plans, digest: computePlanDigest("terraform-apply-group-unresolved", { components: ctx.components, unresolved: waits }) };
253
+ }
254
+ /** A composed plan that no longer matches the one approved. Core's moved-plan mark, so nothing applies and the command exits 3. */
255
+ export class ComposedPlanMovedError extends Error {
256
+ constructor(message) {
257
+ super(message);
258
+ this.planMoved = true;
259
+ this.name = "ComposedPlanMovedError";
260
+ }
261
+ }
262
+ /** The group methods, with `deps` in place of the defaults. */
263
+ export function composeGroup(deps = {}) {
264
+ const project = deps.project ?? loadProject;
265
+ const exec = deps.exec ?? runBinary;
266
+ const envOf = () => deps.env ?? process.env;
267
+ const log = deps.log ?? ((line) => console.error(line));
268
+ const readOutputs = deps.outputs ?? ((root) => terraformOutputs({ root }));
269
+ const identities = deps.identity ?? declaredTargetCloudIdentity;
270
+ /** The composition of `members` as `groupFallback` judges it, with each member's identity. */
271
+ async function compositionOf(ctx, p, members) {
272
+ const lookup = await identities(p.projectRoot, ctx.env);
273
+ const c = groupComposition(p, members, envOf(), memberIdentity(p, members, lookup));
274
+ if (!c)
275
+ throw new Error(composeOff(p.terraform)?.reason ?? "`terraform.compose` is not enabled");
276
+ return c;
277
+ }
278
+ /** The composition the group plans as, or throws when it does not compose. */
279
+ async function composed(ctx, members) {
280
+ const p = await project();
281
+ const off = composeOff(p.terraform);
282
+ if (off)
283
+ throw new Error(off.reason);
284
+ const c = await compositionOf(ctx, p, members);
285
+ const refused = groupFallbackOf(c, members.map((m) => m.component));
286
+ if (refused)
287
+ throw new Error(`the group no longer composes: ${refused.reason}`);
288
+ return { p, c };
289
+ }
290
+ /** Build a runner, run `fn` on it, and release it and its directory whatever happened. */
291
+ async function withRunner(p, c, members, lock, fn) {
292
+ const work = mkdtempSync(join(tmpdir(), "chant-compose-"));
293
+ const o = {
294
+ ...p,
295
+ composition: c,
296
+ lock,
297
+ vars: Object.fromEntries(members.map((m) => [m.component, m.step.vars])),
298
+ componentOutputs: outsideOutputs(members),
299
+ env: envOf(),
300
+ work,
301
+ };
302
+ const runner = deps.runner ? deps.runner(o) : defaultRunner(o, log);
303
+ try {
304
+ return await fn(runner);
305
+ }
306
+ finally {
307
+ try {
308
+ await runner.release();
309
+ }
310
+ finally {
311
+ rmSync(work, { recursive: true, force: true });
312
+ }
313
+ }
314
+ }
315
+ return {
316
+ async groupReads(_ctx, members) {
317
+ const p = await project();
318
+ if (composeOff(p.terraform))
319
+ return [];
320
+ return hclGroupReads(p, members);
321
+ },
322
+ async groupFallback(ctx, members) {
323
+ const p = await project();
324
+ const off = composeOff(p.terraform);
325
+ if (off)
326
+ return { kind: off.kind, reason: off.reason };
327
+ const c = await compositionOf(ctx, p, members);
328
+ // A member left out is dropped alone; core asks again with the rest, and the estate check runs then.
329
+ const refused = groupFallbackOf(c, members.map((m) => m.component));
330
+ if (refused)
331
+ return refused;
332
+ if (c.estate) {
333
+ const binary = p.terraform.binary ?? "choudoufu";
334
+ const v = await exec(binary, ["version"], p.projectRoot, envOf());
335
+ // A binary that cannot say its version is left to the plan, which says why it fails.
336
+ const old = v.code === 0 ? estateVersionFallback(v.stdout) : undefined;
337
+ if (old)
338
+ return { kind: old.kind, reason: old.reason };
339
+ }
340
+ return undefined;
341
+ },
342
+ async planGroup(ctx, members) {
343
+ const unresolved = unresolvedOf(members);
344
+ if (unresolved.size > 0)
345
+ return waitingGroup(ctx, members, unresolved);
346
+ const { p, c } = await composed(ctx, members);
347
+ // Planned without locks and released at once: nothing stays held or changed if core refuses the plan or makes it wait.
348
+ const planned = await withRunner(p, c, members, false, (r) => r.plan());
349
+ if (planned.fallback)
350
+ throw new Error(`the group no longer composes: ${planned.fallback.reason}`);
351
+ if (planned.error)
352
+ throw new Error(planned.error);
353
+ const plans = {};
354
+ for (const m of members) {
355
+ const part = planned.members[m.component];
356
+ if (!part)
357
+ throw new Error(`the composed plan has no part for ${m.component}`);
358
+ plans[m.component] = { member: part.member, planDigest: part.planDigest, changeSet: part.changeSet, outputs: part.outputs, shape: part.shape };
359
+ }
360
+ const artifact = { digest: planned.digest, estate: c.estate === true };
361
+ return { members: plans, digest: planned.digest, artifact };
362
+ },
363
+ async runGroup(ctx, run, members) {
364
+ if (unresolvedOf(members).size > 0)
365
+ throw new Error("the group reads outputs of components outside it that are not known yet, so it cannot apply");
366
+ const approved = run.artifact;
367
+ if (typeof approved?.digest !== "string" || approved.digest === "")
368
+ throw new Error("no composed plan of the group to apply");
369
+ const { p, c } = await composed(ctx, members);
370
+ const notStarted = (error) => ({
371
+ members: Object.fromEntries(members.map((m) => [m.component, { status: "not-started", error }])),
372
+ error,
373
+ });
374
+ const applied = await withRunner(p, c, members, true, async (r) => {
375
+ // The approved plan was made without locks; plan again under them, and apply only that same plan.
376
+ const planned = await r.plan();
377
+ if (planned.fallback)
378
+ return notStarted(`the group no longer composes: ${planned.fallback.reason}`);
379
+ if (planned.error)
380
+ return notStarted(planned.error);
381
+ if (planned.digest !== approved.digest) {
382
+ throw new ComposedPlanMovedError(`the composed plan moved since it was approved (approved ${approved.digest}, planned now ${planned.digest}), so nothing applied`);
383
+ }
384
+ return r.apply();
385
+ });
386
+ const out = {};
387
+ for (const m of members) {
388
+ const o = applied.members[m.component];
389
+ if (!o) {
390
+ out[m.component] = { status: "failed", error: applied.error ?? `the composed apply said nothing of ${m.component}` };
391
+ continue;
392
+ }
393
+ let outputs = o.outputs;
394
+ // A root's state is written by now; read its outputs from there when its part of the working state could not be shown.
395
+ if (!outputs && !c.estate && (o.status === "applied" || o.status === "no-changes")) {
396
+ outputs = await readOutputs(m.step.root).catch(() => undefined);
397
+ }
398
+ const held = o.lockHeld ? `its state was kept unwritten${o.kept ? ` at ${o.kept}` : ""}, and its lock stays held until a later run writes it` : undefined;
399
+ const error = [o.error, held].filter(Boolean).join("; ");
400
+ out[m.component] = { status: o.status, ...(outputs ? { outputs } : {}), ...(error ? { error } : {}) };
401
+ }
402
+ return { members: out, ...(applied.error ? { error: applied.error } : {}) };
403
+ },
404
+ };
405
+ }
406
+ /** The group methods `terraform-apply` uses. */
407
+ export const terraformComposeGroup = composeGroup();
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Previewing a root whose `vars` read outputs not known yet (#4463).
3
+ *
4
+ * terraform takes no unknown variable, so such a root used to get no plan at
5
+ * pull-request time, only a hole. Now it is planned with a stand-in for each
6
+ * variable that holds no value yet, typed from the root's own `variable`
7
+ * block ({@link terraformPlaceholder}). Every attribute and output the
8
+ * stand-ins drive is then marked unknown after apply in the plan JSON
9
+ * ({@link markPlaceholderDriven}) before the change set, the shape and the
10
+ * planned outputs are read from it, so no stand-in value reaches the note or
11
+ * the digest. The member's change-set part lists the variables under
12
+ * `placeholders`.
13
+ *
14
+ * What a stand-in drives is read from the plan's own `configuration`: an
15
+ * attribute whose expression references `var.<name>`, through module inputs
16
+ * too, plus any string attribute or output whose planned value carries
17
+ * {@link TERRAFORM_UNKNOWN_MARK}. A value that reaches an attribute another
18
+ * way (a number through a `local`) is not marked; the fresh plan at apply
19
+ * then differs from the preview and asks for an approval of its own, which
20
+ * costs a round and never applies a change nobody approved.
21
+ *
22
+ * A preview is not possible when a variable that holds the unknown read is
23
+ * not declared, when the unknown sits inside a larger value, when a `count`
24
+ * or `for_each` reads a stand-in (the preview would show a count the real
25
+ * value does not have), or when the placeholder plan fails, say on a
26
+ * `validation` block. The member then stays a hole, as before, and the
27
+ * hole's reason says why (the 2026-10-25 ruling on #4463).
28
+ */
29
+ import type { ChangeSetHole } from "@intentius/chant/change-set";
30
+ /** The marker every string stand-in carries, so a reader can tell it from a real value. */
31
+ export declare const TERRAFORM_UNKNOWN_MARK = "known-after-apply";
32
+ /**
33
+ * A stand-in for a variable of `type`, a Terraform type expression as the
34
+ * `variable` block writes it (`string`, `list(string)`, `object({ id = string })`).
35
+ *
36
+ * - `string`, `any` or no type: {@link TERRAFORM_UNKNOWN_MARK}.
37
+ * - `number`: 0. `bool`: false.
38
+ * - `list(...)`, `set(...)`: an empty list. `map(...)`: an empty map.
39
+ * - `tuple([a, b])`: one stand-in per element type.
40
+ * - `object({ a = t })`: one stand-in per attribute, `optional(t)` read as `t`.
41
+ *
42
+ * An empty collection plans no element, so a resource that iterates one
43
+ * plans none; the fresh plan at apply then asks for its own approval.
44
+ */
45
+ export declare function terraformPlaceholder(type: string | undefined): unknown;
46
+ /** The variables a preview gives stand-ins, and the `vars` it plans with. */
47
+ export interface PlaceholderVars {
48
+ /** The variable names given a stand-in, sorted. */
49
+ placeholders: string[];
50
+ /** The step's `vars` with each of them replaced by its stand-in. */
51
+ vars: Record<string, unknown>;
52
+ }
53
+ /**
54
+ * The stand-ins for a step's `vars` (#4463): each variable whose value is
55
+ * `undefined` (a `stackOutput()` read with nothing to read yet) gets
56
+ * {@link terraformPlaceholder} of its declared type. Returns why instead when
57
+ * no preview can be made: no variable holds the unknown read, one that does
58
+ * is not declared in the root, or the unknown sits inside a larger value.
59
+ */
60
+ export declare function placeholderVars(vars: Record<string, unknown> | undefined, declared: ReadonlyMap<string, {
61
+ type?: string;
62
+ }>): PlaceholderVars | string;
63
+ /** What {@link markPlaceholderDriven} returns. */
64
+ export type MarkedPlan = {
65
+ plan: unknown;
66
+ } | {
67
+ refused: string;
68
+ };
69
+ /**
70
+ * `planJson` (`show -json` of the placeholder plan) with every attribute and
71
+ * root output the stand-ins for `placeholders` drive marked unknown after
72
+ * apply, their planned values dropped. Returns why instead when a `count` or
73
+ * `for_each` reads a stand-in. The input is not changed.
74
+ */
75
+ export declare function markPlaceholderDriven(planJson: unknown, placeholders: readonly string[]): MarkedPlan;
76
+ /** The hole a root keeps when its preview is not possible: the old reason, and why there is no preview. */
77
+ export declare function waitingHole(root: string, unresolved: readonly string[], why?: string): ChangeSetHole;
78
+ //# sourceMappingURL=placeholder-preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"placeholder-preview.d.ts","sourceRoot":"","sources":["../../src/components/placeholder-preview.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEjE,2FAA2F;AAC3F,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAgC1D;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CA2BtE;AAKD,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,mDAAmD;IACnD,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,eAAe,GAAG,MAAM,CAgBrJ;AAiED,kDAAkD;AAClD,MAAM,MAAM,UAAU,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjE;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CAiCpG;AAED,2GAA2G;AAC3G,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,aAAa,CAMpG"}