@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,183 @@
1
+ /**
2
+ * A composed run over roots that keep state (chant#4444, epic #4419): one
3
+ * plan and one apply of the composed root, with each member's state pulled
4
+ * before and written back after, whatever the apply did.
5
+ *
6
+ * `plan()` inits each member against its backend, takes each state's lock
7
+ * (when `lock` is set), writes first any state an earlier run kept unwritten,
8
+ * pulls each state, writes the composed root and the working state, and
9
+ * plans once. `apply()` applies that saved plan; whether it finished or
10
+ * failed, it splits the working state, writes each changed state upstreams
11
+ * first under its lock, and releases each lock once its state is written.
12
+ * `release()` puts every file back, removes `.chant-compose/` and lets go of
13
+ * every lock but one held on purpose for a state it could not write. Call it
14
+ * whatever happened, a thrown error included. A plan the gate makes wait, or
15
+ * refuses, is followed by `release()` alone: nothing is written.
16
+ *
17
+ * Nothing is atomic. A failure part way leaves what applied, applied, and
18
+ * each member's state says so.
19
+ *
20
+ * Lifted from terragucci's `ComposedRun` (`packages/terragucci/src/compose.ts`,
21
+ * lines 1143 to 1651 at c493e0e), composed from the pieces already in the
22
+ * lexicon: `../state` (#4437) for locks, pulls, writes and the unwritten
23
+ * keep; `./root` (#4441) for the composed root; `./state-merge`, `./slice`
24
+ * and `./apply-events` (#4442) for the working state, each member's plan and
25
+ * each member's outcome. Estates (`Composition.estate`) run through
26
+ * `./estate`'s `EstateRun` (#4445); both implement its `ComposedRunner` and
27
+ * return its `ComposedRunPlan` and `ComposedRunApply`, which this run extends.
28
+ *
29
+ * Like `EstateRun`, `plan()` does not throw for a plan that did not come to
30
+ * be: it answers `error`, with `member` when the failure is about one (a
31
+ * hook, an init, a lock held by another run), and releases every lock first.
32
+ */
33
+ import type { OpWaveHookMoment } from "@intentius/chant/op";
34
+ import type { Composition } from "../compose.js";
35
+ import type { TerraformConfig } from "../config.js";
36
+ import { type BinaryExec } from "../state/exec.js";
37
+ import type { ComposedMemberOutcome, ComposedRunApply, ComposedRunner, ComposedRunPlan } from "./estate.js";
38
+ /** The moments a member's hooks run at in a composed run: those of a member applied alone (#4288), less core's own `after-gate`. */
39
+ export type ComposeHookMoment = Exclude<OpWaveHookMoment, "after-gate">;
40
+ /**
41
+ * Runs one moment's hooks for a member. A hook given the plan file gets the
42
+ * composed one. A hook that failed returns its error, or throws.
43
+ */
44
+ export type ComposeHook = (member: string, moment: ComposeHookMoment, planFile?: string) => Promise<string | undefined | void>;
45
+ export interface ComposedRunOptions {
46
+ /** The checkout's root, where `chant.config.*` was loaded from: `.chant-compose/` is written here. */
47
+ projectRoot: string;
48
+ /** What `compositionFor` answered, with two or more members and no fallback. */
49
+ composition: Composition;
50
+ /** The config's `terraform` namespace: each root's `varFiles`, `backendConfigFiles` and `backendConfig`. */
51
+ terraform: TerraformConfig;
52
+ /** The binary every member runs: its path, and its name when the path does not say it (`tofu`, `terraform`). */
53
+ binary: {
54
+ path: string;
55
+ name?: string;
56
+ };
57
+ /** The job's environment, with a shared provider cache when there is one. Every member runs under it. */
58
+ env: NodeJS.ProcessEnv;
59
+ /** A directory of the job's own for the plan file and the state files `state push` reads. */
60
+ work: string;
61
+ /** Take each state's lock. An apply needs them; a plan alone may go without. */
62
+ lock: boolean;
63
+ /** Names the run in the locks it takes. */
64
+ runId?: string;
65
+ exec?: BinaryExec;
66
+ /** How long to wait before each retry of a state's write. Default `DEFAULT_RETRY_DELAYS` (#4437). */
67
+ retryDelays?: number[];
68
+ /** How a retry waits; a test passes one that does not. */
69
+ sleep?: (ms: number) => Promise<void>;
70
+ /** Runs each init, one at a time when the members share a provider cache. */
71
+ initTurn?: <T>(fn: () => Promise<T>) => Promise<T>;
72
+ /** Each member's hooks, at the moments a member applied alone runs them. */
73
+ hook?: ComposeHook;
74
+ /** Each member's step `vars` as written, by component (`memberVars`). */
75
+ vars?: Readonly<Record<string, Record<string, unknown> | undefined>>;
76
+ /** The outputs of components outside the group that a member's `vars` read. */
77
+ componentOutputs?: Record<string, Record<string, unknown>>;
78
+ /** Where a state goes when its backend cannot take the kept copy. Default `<work>/report`. */
79
+ reportDir?: string;
80
+ log?: (line: string) => void;
81
+ }
82
+ /** Where a composed run stopped before it planned. */
83
+ export type ComposedRunStage = "init" | "hook" | "lock" | "kept" | "pull" | "root" | "plan" | "show";
84
+ /** What the state-backed run's `plan()` answers: `ComposedRunPlan` (#4445) with the reads unknown at plan and where a failed plan stopped. */
85
+ export interface StateRunPlan extends ComposedRunPlan {
86
+ /** The members that read a value of another member unknown at plan, sorted. */
87
+ unknownReads: string[];
88
+ /** Where a plan that did not come to be stopped, with {@link ComposedRunPlan.error}. */
89
+ stage?: ComposedRunStage;
90
+ /** The error underneath {@link ComposedRunPlan.error}: a `StateLockedError` names the lock's holder, a `StateRefusedError` each root chant does not lock. */
91
+ underlying?: unknown;
92
+ }
93
+ /** What became of a member's state after the apply: written, left as pulled, or kept unwritten with its lock held. */
94
+ export type ComposedStateWrite = "written" | "unchanged" | "not-written";
95
+ /** One member's outcome of a state-backed composed apply: `ComposedMemberOutcome` (#4445) with what became of its state. */
96
+ export interface StateMemberOutcome extends ComposedMemberOutcome {
97
+ state: ComposedStateWrite;
98
+ /** The serial written, when `state` is `written`. */
99
+ serial?: number;
100
+ /** Where the state was kept, when `state` is `not-written` and there was one to keep. */
101
+ kept?: string;
102
+ /** Its lock stays held after `release()`. */
103
+ lockHeld?: true;
104
+ }
105
+ /** How a state-backed composed apply ended: `ComposedRunApply` (#4445) with each member's state. */
106
+ export interface StateRunApply extends ComposedRunApply {
107
+ /** Every member applied (or had no changes), every state settled, and no hook failed. */
108
+ ok: boolean;
109
+ members: Record<string, StateMemberOutcome>;
110
+ /** The members whose state the run wrote or left as it was, in apply order: their backend holds what applied to them. */
111
+ settled: string[];
112
+ /** `show -json` of each member's part of the working state, as if it had applied alone: where the report reads the values its plan left unknown. Empty when the state could not be shown. */
113
+ shown: Record<string, unknown>;
114
+ }
115
+ /** The group digest: chant's digest of the composed plan joined with the generated files. */
116
+ export declare function composedPlanDigest(plan: unknown, generated: readonly [string, string][]): string;
117
+ /** Whether a plan changes a resource (not a no-op or a read) or an output. */
118
+ export declare function changesSomething(plan: unknown): boolean;
119
+ export declare class ComposedRun implements ComposedRunner {
120
+ private readonly o;
121
+ private readonly exec;
122
+ private readonly composeDir;
123
+ private readonly states;
124
+ private readonly pulled;
125
+ private root?;
126
+ private planned?;
127
+ private released;
128
+ constructor(o: ComposedRunOptions);
129
+ private get c();
130
+ private dirOf;
131
+ private log;
132
+ private get binaryName();
133
+ /** The locks held now, by member. */
134
+ held(): string[];
135
+ /**
136
+ * The composed root's environment: the job's, its `-var-file` flags taken
137
+ * out (the composed root passes those values as module arguments). With a
138
+ * shared provider cache and no lock file to check a cached provider
139
+ * against, the cache may fill the lock file, as a root's own plan lets it.
140
+ */
141
+ private composedEnv;
142
+ private hook;
143
+ private turn;
144
+ private writeComposed;
145
+ /** Put the composed root's files back, when they are written. */
146
+ private restoreRoot;
147
+ /**
148
+ * Init each member, take each lock (with `lock`), write first any state an
149
+ * earlier run kept unwritten, pull each state, write the composed root and
150
+ * the working state, and plan once. A plan that does not come to be
151
+ * answers `error`, with `member` when it is about one, after releasing
152
+ * every lock and putting every file back.
153
+ */
154
+ plan(): Promise<StateRunPlan>;
155
+ private planOnce;
156
+ /** Every member's outcome when nothing applied and no state was touched. */
157
+ private nothingApplied;
158
+ /**
159
+ * Apply the saved plan, then split the working state by module and write
160
+ * each changed state, upstreams first, whether the apply finished or not.
161
+ * Each lock is released once its state is written or found as pulled; one
162
+ * whose state could not be written stays held, and the state is kept
163
+ * beside it.
164
+ */
165
+ apply(): Promise<StateRunApply>;
166
+ /**
167
+ * `show -json` of the working state, cut into each member's part (its
168
+ * module as the root module, addresses and outputs unprefixed). Empty when
169
+ * the state cannot be shown: the report then leaves the values unknown.
170
+ * Never fails the run.
171
+ */
172
+ private showMembers;
173
+ /**
174
+ * Put every root file back, remove `.chant-compose/`, and release every
175
+ * lock but one held on purpose for a state that could not be written.
176
+ * Safe to call more than once, and after a thrown error.
177
+ * {@link heldOnPurpose} names the members whose lock it leaves held.
178
+ */
179
+ release(): Promise<void>;
180
+ /** The members whose lock stays held past {@link release}, for a state that could not be written. */
181
+ heldOnPurpose(): string[];
182
+ }
183
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/compose/run.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,447 @@
1
+ /**
2
+ * A composed run over roots that keep state (chant#4444, epic #4419): one
3
+ * plan and one apply of the composed root, with each member's state pulled
4
+ * before and written back after, whatever the apply did.
5
+ *
6
+ * `plan()` inits each member against its backend, takes each state's lock
7
+ * (when `lock` is set), writes first any state an earlier run kept unwritten,
8
+ * pulls each state, writes the composed root and the working state, and
9
+ * plans once. `apply()` applies that saved plan; whether it finished or
10
+ * failed, it splits the working state, writes each changed state upstreams
11
+ * first under its lock, and releases each lock once its state is written.
12
+ * `release()` puts every file back, removes `.chant-compose/` and lets go of
13
+ * every lock but one held on purpose for a state it could not write. Call it
14
+ * whatever happened, a thrown error included. A plan the gate makes wait, or
15
+ * refuses, is followed by `release()` alone: nothing is written.
16
+ *
17
+ * Nothing is atomic. A failure part way leaves what applied, applied, and
18
+ * each member's state says so.
19
+ *
20
+ * Lifted from terragucci's `ComposedRun` (`packages/terragucci/src/compose.ts`,
21
+ * lines 1143 to 1651 at c493e0e), composed from the pieces already in the
22
+ * lexicon: `../state` (#4437) for locks, pulls, writes and the unwritten
23
+ * keep; `./root` (#4441) for the composed root; `./state-merge`, `./slice`
24
+ * and `./apply-events` (#4442) for the working state, each member's plan and
25
+ * each member's outcome. Estates (`Composition.estate`) run through
26
+ * `./estate`'s `EstateRun` (#4445); both implement its `ComposedRunner` and
27
+ * return its `ComposedRunPlan` and `ComposedRunApply`, which this run extends.
28
+ *
29
+ * Like `EstateRun`, `plan()` does not throw for a plan that did not come to
30
+ * be: it answers `error`, with `member` when the failure is about one (a
31
+ * hook, an init, a lock held by another run), and releases every lock first.
32
+ */
33
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
34
+ import { basename, join, resolve } from "node:path";
35
+ import { stateOutputs, stateResources } from "../applied-state.js";
36
+ import { backendConfigArgs } from "../backend-config.js";
37
+ import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
38
+ import { plannerForBinary } from "../change-set.js";
39
+ import { terraformPlanDigest } from "../plan-digest.js";
40
+ import { lastLines, runBinary } from "../state/exec.js";
41
+ import { StateLockedError } from "../state/locks.js";
42
+ import { RootStates, StateRefusedError } from "../state/root-states.js";
43
+ import { composedApplyOutcomes } from "./apply-events.js";
44
+ import { COMPOSE_DIR, writeComposedRoot, withoutVarFiles } from "./root.js";
45
+ import { composedMemberPlans, moduleOutputs, moduleValues, planCounts, sliceText } from "./slice.js";
46
+ import { mergeStates, splitState } from "./state-merge.js";
47
+ /** A plan that did not come to be, as `plan()` answers it. */
48
+ class PlanStopped extends Error {
49
+ constructor(message, stage, member, underlying) {
50
+ super(member && !message.startsWith(`${member}:`) ? `${member}: ${message}` : message);
51
+ this.stage = stage;
52
+ this.member = member;
53
+ this.underlying = underlying;
54
+ }
55
+ }
56
+ /** The group digest: chant's digest of the composed plan joined with the generated files. */
57
+ export function composedPlanDigest(plan, generated) {
58
+ return computePlanDigest("chant-terraform-composed-plan", { plan: terraformPlanDigest(plan), files: generated.map(([path, sha]) => `${path} ${sha}`) });
59
+ }
60
+ /** Whether a plan changes a resource (not a no-op or a read) or an output. */
61
+ export function changesSomething(plan) {
62
+ const p = plan;
63
+ const real = (actions) => (actions ?? []).some((a) => a !== "no-op" && a !== "read");
64
+ return (p?.resource_changes ?? []).some((r) => real(r.change?.actions)) || Object.values(p?.output_changes ?? {}).some((o) => real(o.actions));
65
+ }
66
+ const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
67
+ export class ComposedRun {
68
+ constructor(o) {
69
+ this.o = o;
70
+ this.pulled = new Map();
71
+ this.released = false;
72
+ const c = o.composition;
73
+ if (c.fallback || c.members.length < 2)
74
+ throw new Error(`nothing composes${c.fallback ? `: ${c.fallback.reason}` : ""}`);
75
+ if (c.estate)
76
+ throw new Error("the composition is one of estates, which has no state to pull or write back (#4445)");
77
+ this.exec = o.exec ?? runBinary;
78
+ this.composeDir = join(resolve(o.projectRoot), COMPOSE_DIR);
79
+ this.states = new RootStates({
80
+ roots: c.order.map((m) => {
81
+ const config = o.terraform.roots[c.roots.get(m) ?? m];
82
+ return {
83
+ root: m,
84
+ dir: this.dirOf(m),
85
+ env: o.env,
86
+ ...(config?.backendConfig ? { backendConfig: config.backendConfig } : {}),
87
+ ...(config?.backendConfigFiles ? { backendConfigFiles: config.backendConfigFiles } : {}),
88
+ };
89
+ }),
90
+ binary: o.binary.path,
91
+ exec: this.exec,
92
+ ...(o.runId ? { runId: o.runId } : {}),
93
+ work: o.work,
94
+ reportDir: o.reportDir ?? join(o.work, "report"),
95
+ ...(o.retryDelays ? { retryDelays: o.retryDelays } : {}),
96
+ ...(o.sleep ? { sleep: o.sleep } : {}),
97
+ ...(o.log ? { log: o.log } : {}),
98
+ });
99
+ }
100
+ get c() {
101
+ return this.o.composition;
102
+ }
103
+ dirOf(member) {
104
+ const d = this.c.dirs.get(member);
105
+ if (!d)
106
+ throw new Error(`${member} has no root directory in the composition`);
107
+ return d;
108
+ }
109
+ log(line) {
110
+ this.o.log?.(line);
111
+ }
112
+ get binaryName() {
113
+ return this.o.binary.name ?? basename(this.o.binary.path);
114
+ }
115
+ /** The locks held now, by member. */
116
+ held() {
117
+ return this.states.held();
118
+ }
119
+ /**
120
+ * The composed root's environment: the job's, its `-var-file` flags taken
121
+ * out (the composed root passes those values as module arguments). With a
122
+ * shared provider cache and no lock file to check a cached provider
123
+ * against, the cache may fill the lock file, as a root's own plan lets it.
124
+ */
125
+ composedEnv() {
126
+ const env = withoutVarFiles(this.o.env);
127
+ return env.TF_PLUGIN_CACHE_DIR && !existsSync(join(this.composeDir, ".terraform.lock.hcl")) ? { ...env, TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE: "true" } : env;
128
+ }
129
+ async hook(member, moment, planFile) {
130
+ if (!this.o.hook)
131
+ return undefined;
132
+ try {
133
+ return (await this.o.hook(member, moment, planFile)) || undefined;
134
+ }
135
+ catch (e) {
136
+ return e.message || String(e);
137
+ }
138
+ }
139
+ turn(fn) {
140
+ return this.o.initTurn ? this.o.initTurn(fn) : fn();
141
+ }
142
+ writeComposed() {
143
+ return writeComposedRoot({
144
+ projectRoot: this.o.projectRoot,
145
+ composition: this.c,
146
+ terraform: this.o.terraform,
147
+ env: this.o.env,
148
+ ...(this.o.vars ? { vars: this.o.vars } : {}),
149
+ ...(this.o.componentOutputs ? { componentOutputs: this.o.componentOutputs } : {}),
150
+ });
151
+ }
152
+ /** Put the composed root's files back, when they are written. */
153
+ restoreRoot() {
154
+ const r = this.root;
155
+ this.root = undefined;
156
+ r?.restore();
157
+ }
158
+ /**
159
+ * Init each member, take each lock (with `lock`), write first any state an
160
+ * earlier run kept unwritten, pull each state, write the composed root and
161
+ * the working state, and plan once. A plan that does not come to be
162
+ * answers `error`, with `member` when it is about one, after releasing
163
+ * every lock and putting every file back.
164
+ */
165
+ async plan() {
166
+ if (this.released)
167
+ throw new Error("this composed run was released");
168
+ try {
169
+ return await this.planOnce();
170
+ }
171
+ catch (e) {
172
+ await this.release();
173
+ const stopped = e instanceof PlanStopped ? e : new PlanStopped(e.message, "plan", undefined, e);
174
+ return {
175
+ members: {},
176
+ digest: "",
177
+ error: stopped.message,
178
+ ...(stopped.member !== undefined ? { member: stopped.member } : {}),
179
+ stage: stopped.stage,
180
+ ...(stopped.underlying !== undefined ? { underlying: stopped.underlying } : {}),
181
+ unknownReads: [],
182
+ planFile: join(this.o.work, "composed.tfplan"),
183
+ text: "",
184
+ texts: {},
185
+ generated: [],
186
+ counts: { add: 0, change: 0, destroy: 0, changes: 0, destroys: 0 },
187
+ summary: "",
188
+ };
189
+ }
190
+ }
191
+ async planOnce() {
192
+ const bin = this.o.binary.path;
193
+ for (const m of this.c.order) {
194
+ const err = await this.hook(m, "before-observe");
195
+ if (err)
196
+ throw new PlanStopped(`a before-observe hook failed: ${err}`, "hook", m);
197
+ // The root's backend-config files, then its pairs, as every init the lexicon runs passes them (chant#4459).
198
+ const args = ["init", "-input=false", "-no-color", ...backendConfigArgs(this.o.terraform.roots[this.c.roots.get(m) ?? m] ?? {})];
199
+ const init = await this.turn(() => this.exec(bin, args, this.dirOf(m), this.o.env));
200
+ if (init.code !== 0)
201
+ throw new PlanStopped(`init failed: ${lastLines(init.out, 8)}`, "init", m);
202
+ }
203
+ if (this.o.lock) {
204
+ try {
205
+ await this.states.take();
206
+ }
207
+ catch (e) {
208
+ if (e instanceof StateLockedError)
209
+ throw new PlanStopped(e.message, "lock", e.root, e);
210
+ if (e instanceof StateRefusedError)
211
+ throw new PlanStopped(e.message, "lock", e.refusals.length === 1 ? e.refusals[0].root : undefined, e);
212
+ throw new PlanStopped(e.message, "kept", undefined, e);
213
+ }
214
+ }
215
+ for (const m of this.c.order) {
216
+ try {
217
+ this.pulled.set(m, await this.states.pull(m));
218
+ }
219
+ catch (e) {
220
+ throw new PlanStopped(e.message, "pull", m, e);
221
+ }
222
+ }
223
+ for (const m of this.c.order) {
224
+ const err = await this.hook(m, "before-plan");
225
+ if (err)
226
+ throw new PlanStopped(`a before-plan hook failed: ${err}`, "hook", m);
227
+ }
228
+ mkdirSync(this.o.work, { recursive: true });
229
+ const planFile = join(this.o.work, "composed.tfplan");
230
+ rmSync(planFile, { force: true });
231
+ try {
232
+ this.root = this.writeComposed();
233
+ }
234
+ catch (e) {
235
+ throw new PlanStopped(`the composed root could not be written: ${e.message}`, "root", undefined, e);
236
+ }
237
+ const generated = this.root.generated;
238
+ let plan;
239
+ let text = "";
240
+ let summary = "";
241
+ try {
242
+ writeFileSync(join(this.composeDir, "terraform.tfstate"), JSON.stringify(mergeStates(this.c, this.pulled), null, 2) + "\n");
243
+ const env = this.composedEnv();
244
+ const init = await this.turn(() => this.exec(bin, ["init", "-input=false", "-no-color"], this.composeDir, env));
245
+ if (init.code !== 0)
246
+ throw new PlanStopped(`the composed root's init failed: ${lastLines(init.out, 8)}`, "init");
247
+ const p = await this.exec(bin, ["plan", "-input=false", "-no-color", "-lock=false", `-out=${planFile}`], this.composeDir, env);
248
+ if (p.code !== 0 || !existsSync(planFile))
249
+ throw new PlanStopped(`the composed plan failed: ${lastLines(p.out, 8)}`, "plan");
250
+ summary = p.stdout.match(/Plan: .*|No changes\..*/)?.[0] ?? "planned";
251
+ const json = await this.exec(bin, ["show", "-json", planFile], this.composeDir, env);
252
+ try {
253
+ plan = JSON.parse(json.stdout);
254
+ }
255
+ catch {
256
+ throw new PlanStopped(`show -json printed no plan: ${lastLines(json.out)}`, "show");
257
+ }
258
+ text = (await this.exec(bin, ["show", "-no-color", planFile], this.composeDir, env)).stdout;
259
+ }
260
+ finally {
261
+ this.restoreRoot();
262
+ }
263
+ for (const m of this.c.order) {
264
+ const err = await this.hook(m, "after-plan", planFile);
265
+ if (err)
266
+ throw new PlanStopped(`an after-plan hook failed: ${err}`, "hook", m);
267
+ }
268
+ const members = composedMemberPlans(this.c, plan, { planner: plannerForBinary(this.binaryName) });
269
+ const texts = Object.fromEntries(this.c.order.map((m) => [m, sliceText(text, this.c.modules.get(m), members[m].counts)]));
270
+ const unknownReads = this.c.order.filter((m) => members[m].reads.some((r) => r.unknown.length > 0)).sort();
271
+ this.planned = { members, texts, unknownReads, digest: composedPlanDigest(plan, generated), generated, planFile, plan, text, counts: planCounts(plan), summary };
272
+ return this.planned;
273
+ }
274
+ /** Every member's outcome when nothing applied and no state was touched. */
275
+ nothingApplied(error, member, memberError) {
276
+ const members = {};
277
+ for (const m of this.c.order) {
278
+ const planned = this.planned?.members[m]?.counts.changes ?? 0;
279
+ members[m] = { status: "not-started", planned, done: 0, state: "unchanged", ...(m === member && memberError ? { error: memberError } : {}) };
280
+ }
281
+ return { ok: false, members, settled: [], shown: {}, error };
282
+ }
283
+ /**
284
+ * Apply the saved plan, then split the working state by module and write
285
+ * each changed state, upstreams first, whether the apply finished or not.
286
+ * Each lock is released once its state is written or found as pulled; one
287
+ * whose state could not be written stays held, and the state is kept
288
+ * beside it.
289
+ */
290
+ async apply() {
291
+ const p = this.planned;
292
+ if (this.released)
293
+ throw new Error("this composed run was released");
294
+ if (!p || p.error)
295
+ return this.nothingApplied("the composed run did not plan");
296
+ if (!this.o.lock)
297
+ return this.nothingApplied("a composed run applies only with its locks held (`lock`)");
298
+ for (const m of this.c.order) {
299
+ const err = await this.hook(m, "before-apply", p.planFile);
300
+ if (err)
301
+ return this.nothingApplied(`a before-apply hook of ${m} failed, so nothing applied`, m, err);
302
+ }
303
+ try {
304
+ this.root = this.writeComposed();
305
+ }
306
+ catch (e) {
307
+ return this.nothingApplied(`the composed root could not be written: ${e.message}`);
308
+ }
309
+ let out = "";
310
+ let code = 1;
311
+ try {
312
+ if (!sameJson(this.root.generated, p.generated))
313
+ return this.nothingApplied("the composed root differs from the one planned, so nothing applied");
314
+ const a = await this.exec(this.o.binary.path, ["apply", "-json", "-input=false", "-no-color", p.planFile], this.composeDir, this.composedEnv());
315
+ out = a.stdout;
316
+ code = a.code;
317
+ for (const line of a.stdout.split("\n")) {
318
+ if (!line.trim())
319
+ continue;
320
+ try {
321
+ const msg = JSON.parse(line)["@message"];
322
+ if (msg && !/^(Plan to|Refreshing|Reading)/.test(msg))
323
+ this.log(`composed: ${msg}`);
324
+ }
325
+ catch {
326
+ this.log(`composed: ${line}`);
327
+ }
328
+ }
329
+ if (code !== 0 && !out.trim())
330
+ this.log(`composed: ${lastLines(a.out, 8)}`);
331
+ }
332
+ finally {
333
+ this.restoreRoot();
334
+ }
335
+ const planned = new Map(this.c.order.map((m) => [m, p.members[m].counts.changes]));
336
+ const outcomes = composedApplyOutcomes(this.c, planned, out);
337
+ const errored = join(this.composeDir, "errored.tfstate");
338
+ const workingFile = existsSync(errored) ? errored : join(this.composeDir, "terraform.tfstate");
339
+ let working;
340
+ try {
341
+ working = JSON.parse(readFileSync(workingFile, "utf-8"));
342
+ }
343
+ catch (e) {
344
+ // With no working state to split nothing is written, and every lock stays held: what applied is only in the binary's output.
345
+ const why = `the working state could not be read after the apply: ${e.message}`;
346
+ const members = {};
347
+ for (const m of this.c.order) {
348
+ this.states.hold(m);
349
+ const o = outcomes.members[m];
350
+ members[m] = { status: o.status, planned: planned.get(m) ?? 0, done: o.done, state: "not-written", lockHeld: true, error: o.error ?? why };
351
+ }
352
+ return { ok: false, members, settled: [], shown: {}, error: why };
353
+ }
354
+ const shown = await this.showMembers(workingFile);
355
+ const members = {};
356
+ const settled = [];
357
+ for (const m of this.c.order) {
358
+ const o = outcomes.members[m];
359
+ const row = { status: o.status, planned: planned.get(m) ?? 0, done: o.done, state: "unchanged", ...(o.error ? { error: o.error } : {}) };
360
+ const split = splitState(working, this.c, m, this.pulled.get(m) ?? null);
361
+ if (!changesSomething(p.members[m].slice) || split.same) {
362
+ settled.push(m);
363
+ await this.states.release(m);
364
+ }
365
+ else {
366
+ const w = await this.states.write(m, split.state);
367
+ if (w.written) {
368
+ row.state = "written";
369
+ row.serial = split.state.serial;
370
+ settled.push(m);
371
+ this.log(`${m}: wrote its state, serial ${split.state.serial}`);
372
+ await this.states.release(m);
373
+ }
374
+ else {
375
+ Object.assign(row, { state: "not-written", kept: w.kept, lockHeld: true, error: row.error ? `${row.error}; ${w.error}` : w.error });
376
+ }
377
+ }
378
+ if (shown[m] !== undefined) {
379
+ row.resources = stateResources(shown[m]);
380
+ if (row.status === "applied" || row.status === "no-changes")
381
+ row.outputs = stateOutputs(shown[m]);
382
+ }
383
+ members[m] = row;
384
+ }
385
+ for (const m of this.c.order) {
386
+ const row = members[m];
387
+ if (row.status !== "applied" && row.status !== "no-changes")
388
+ continue;
389
+ const err = await this.hook(m, "after-apply");
390
+ if (err)
391
+ row.error = row.error ? `${row.error}; an after-apply hook failed: ${err}` : `an after-apply hook failed: ${err}`;
392
+ }
393
+ const rows = Object.values(members);
394
+ const ok = code === 0 && rows.every((r) => (r.status === "applied" || r.status === "no-changes") && r.state !== "not-written" && !r.error);
395
+ const error = ok ? undefined : (outcomes.other[0] ?? (code !== 0 ? "the composed apply failed" : rows.find((r) => r.error)?.error));
396
+ return { ok, members, settled, shown, ...(error ? { error } : {}) };
397
+ }
398
+ /**
399
+ * `show -json` of the working state, cut into each member's part (its
400
+ * module as the root module, addresses and outputs unprefixed). Empty when
401
+ * the state cannot be shown: the report then leaves the values unknown.
402
+ * Never fails the run.
403
+ */
404
+ async showMembers(workingFile) {
405
+ const out = {};
406
+ try {
407
+ // `show` reads the state the root holds; the errored state is shown by naming its file.
408
+ const args = workingFile.endsWith("errored.tfstate") ? ["show", "-json", "-no-color", workingFile] : ["show", "-json", "-no-color"];
409
+ const r = await this.exec(this.o.binary.path, args, this.composeDir, this.composedEnv());
410
+ if (r.code !== 0)
411
+ return out;
412
+ const whole = JSON.parse(r.stdout);
413
+ for (const m of this.c.order) {
414
+ const mod = this.c.modules.get(m);
415
+ out[m] = { ...whole, values: moduleValues(whole.values, mod, moduleOutputs(whole.values?.outputs, mod)) };
416
+ }
417
+ }
418
+ catch (e) {
419
+ this.log(`composed: the applied state could not be shown (${e.message}); the values unknown at plan stay unknown in the report`);
420
+ }
421
+ return out;
422
+ }
423
+ /**
424
+ * Put every root file back, remove `.chant-compose/`, and release every
425
+ * lock but one held on purpose for a state that could not be written.
426
+ * Safe to call more than once, and after a thrown error.
427
+ * {@link heldOnPurpose} names the members whose lock it leaves held.
428
+ */
429
+ async release() {
430
+ this.released = true;
431
+ let restoreError;
432
+ try {
433
+ this.restoreRoot();
434
+ }
435
+ catch (e) {
436
+ restoreError = e;
437
+ }
438
+ await this.states.release();
439
+ rmSync(this.composeDir, { recursive: true, force: true });
440
+ if (restoreError)
441
+ throw restoreError;
442
+ }
443
+ /** The members whose lock stays held past {@link release}, for a state that could not be written. */
444
+ heldOnPurpose() {
445
+ return this.states.heldOnPurposeRoots();
446
+ }
447
+ }