@intentius/chant-lexicon-terraform 0.124.54 → 0.124.56

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 (351) hide show
  1. package/README.md +7 -4
  2. package/dist/ci-setup.d.ts +11 -0
  3. package/dist/ci-setup.d.ts.map +1 -0
  4. package/dist/ci-setup.js +41 -0
  5. package/dist/components/capability-plugin.d.ts +40 -6
  6. package/dist/components/capability-plugin.d.ts.map +1 -1
  7. package/dist/components/capability-plugin.js +174 -30
  8. package/dist/components/compose-group.d.ts +1 -1
  9. package/dist/components/compose-group.d.ts.map +1 -1
  10. package/dist/components/compose-group.js +1 -1
  11. package/dist/components/release-inputs.d.ts +29 -0
  12. package/dist/components/release-inputs.d.ts.map +1 -0
  13. package/dist/components/release-inputs.js +57 -0
  14. package/dist/components/trace-facts.d.ts +16 -0
  15. package/dist/components/trace-facts.d.ts.map +1 -0
  16. package/dist/components/trace-facts.js +19 -0
  17. package/dist/cone.d.ts +53 -0
  18. package/dist/cone.d.ts.map +1 -0
  19. package/dist/cone.js +119 -0
  20. package/dist/cost-hold.d.ts +27 -0
  21. package/dist/cost-hold.d.ts.map +1 -0
  22. package/dist/cost-hold.js +21 -0
  23. package/dist/drift-rows.d.ts +61 -0
  24. package/dist/drift-rows.d.ts.map +1 -0
  25. package/dist/drift-rows.js +108 -0
  26. package/dist/exit-codes.d.ts +55 -0
  27. package/dist/exit-codes.d.ts.map +1 -0
  28. package/dist/exit-codes.js +48 -0
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -0
  32. package/dist/integrity.json +7 -4
  33. package/dist/lint/audit-catalog.d.ts.map +1 -1
  34. package/dist/lint/audit-catalog.js +27 -3
  35. package/dist/lint/audit-lineage.d.ts +14 -2
  36. package/dist/lint/audit-lineage.d.ts.map +1 -1
  37. package/dist/lint/post-synth/index.d.ts.map +1 -1
  38. package/dist/lint/post-synth/index.js +4 -0
  39. package/dist/lint/post-synth/record-store.d.ts +54 -0
  40. package/dist/lint/post-synth/record-store.d.ts.map +1 -0
  41. package/dist/lint/post-synth/record-store.js +104 -0
  42. package/dist/lint/post-synth/tf001.d.ts +2 -2
  43. package/dist/lint/post-synth/tf001.js +2 -2
  44. package/dist/lint/post-synth/tf024.d.ts +35 -11
  45. package/dist/lint/post-synth/tf024.d.ts.map +1 -1
  46. package/dist/lint/post-synth/tf024.js +92 -20
  47. package/dist/lint/post-synth/tf046.d.ts +64 -0
  48. package/dist/lint/post-synth/tf046.d.ts.map +1 -0
  49. package/dist/lint/post-synth/tf046.js +175 -0
  50. package/dist/lint/post-synth/tf047.d.ts +36 -0
  51. package/dist/lint/post-synth/tf047.d.ts.map +1 -0
  52. package/dist/lint/post-synth/tf047.js +94 -0
  53. package/dist/lock-rows.d.ts +36 -0
  54. package/dist/lock-rows.d.ts.map +1 -0
  55. package/dist/lock-rows.js +60 -0
  56. package/dist/lock-timeout.d.ts +22 -0
  57. package/dist/lock-timeout.d.ts.map +1 -0
  58. package/dist/lock-timeout.js +32 -0
  59. package/dist/manifest.json +1 -1
  60. package/dist/module-catalog/index.d.ts +69 -0
  61. package/dist/module-catalog/index.d.ts.map +1 -0
  62. package/dist/module-catalog/index.js +110 -0
  63. package/dist/module-catalog/refresh-cli.d.ts +4 -0
  64. package/dist/module-catalog/refresh-cli.d.ts.map +1 -0
  65. package/dist/module-catalog/refresh-cli.js +64 -0
  66. package/dist/module-catalog/static-tier.d.ts +6 -0
  67. package/dist/module-catalog/static-tier.d.ts.map +1 -0
  68. package/dist/module-catalog/static-tier.js +1650 -0
  69. package/dist/module-catalog/types.d.ts +65 -0
  70. package/dist/module-catalog/types.d.ts.map +1 -0
  71. package/dist/module-catalog/types.js +33 -0
  72. package/dist/okf/index.md +3 -1
  73. package/dist/okf/rules/TF024.md +2 -2
  74. package/dist/okf/rules/TF046.md +11 -0
  75. package/dist/okf/rules/TF047.md +11 -0
  76. package/dist/op/activities/terraform.d.ts +29 -0
  77. package/dist/op/activities/terraform.d.ts.map +1 -1
  78. package/dist/op/activities/terraform.js +19 -4
  79. package/dist/pin/consumers.d.ts +35 -0
  80. package/dist/pin/consumers.d.ts.map +1 -0
  81. package/dist/pin/consumers.js +70 -0
  82. package/dist/plugin.d.ts.map +1 -1
  83. package/dist/plugin.js +5 -0
  84. package/dist/registry/address.d.ts +63 -0
  85. package/dist/registry/address.d.ts.map +1 -0
  86. package/dist/registry/address.js +65 -0
  87. package/dist/registry/catalog-read.d.ts +14 -0
  88. package/dist/registry/catalog-read.d.ts.map +1 -0
  89. package/dist/registry/catalog-read.js +58 -0
  90. package/dist/registry/deprecated.d.ts +37 -0
  91. package/dist/registry/deprecated.d.ts.map +1 -0
  92. package/dist/registry/deprecated.js +43 -0
  93. package/dist/registry/docs.d.ts +96 -0
  94. package/dist/registry/docs.d.ts.map +1 -0
  95. package/dist/registry/docs.js +210 -0
  96. package/dist/registry/edge/edge.d.ts +46 -0
  97. package/dist/registry/edge/edge.d.ts.map +1 -0
  98. package/dist/registry/edge/edge.js +194 -0
  99. package/dist/registry/edge/index.d.ts +11 -0
  100. package/dist/registry/edge/index.d.ts.map +1 -0
  101. package/dist/registry/edge/index.js +10 -0
  102. package/dist/registry/edge/lambda-edge.d.ts +76 -0
  103. package/dist/registry/edge/lambda-edge.d.ts.map +1 -0
  104. package/dist/registry/edge/lambda-edge.js +95 -0
  105. package/dist/registry/edge/sigv4-web.d.ts +33 -0
  106. package/dist/registry/edge/sigv4-web.d.ts.map +1 -0
  107. package/dist/registry/edge/sigv4-web.js +56 -0
  108. package/dist/registry/edge/token-source.d.ts +73 -0
  109. package/dist/registry/edge/token-source.d.ts.map +1 -0
  110. package/dist/registry/edge/token-source.js +115 -0
  111. package/dist/registry/edge/worker.d.ts +45 -0
  112. package/dist/registry/edge/worker.d.ts.map +1 -0
  113. package/dist/registry/edge/worker.js +26 -0
  114. package/dist/registry/handler.d.ts +25 -0
  115. package/dist/registry/handler.d.ts.map +1 -0
  116. package/dist/registry/handler.js +75 -0
  117. package/dist/registry/headers-file.d.ts +21 -0
  118. package/dist/registry/headers-file.d.ts.map +1 -0
  119. package/dist/registry/headers-file.js +57 -0
  120. package/dist/registry/index.d.ts +31 -0
  121. package/dist/registry/index.d.ts.map +1 -0
  122. package/dist/registry/index.js +30 -0
  123. package/dist/registry/mirror-paths.d.ts +9 -0
  124. package/dist/registry/mirror-paths.d.ts.map +1 -0
  125. package/dist/registry/mirror-paths.js +8 -0
  126. package/dist/registry/mirror.d.ts +130 -0
  127. package/dist/registry/mirror.d.ts.map +1 -0
  128. package/dist/registry/mirror.js +319 -0
  129. package/dist/registry/proxies.d.ts +73 -0
  130. package/dist/registry/proxies.d.ts.map +1 -0
  131. package/dist/registry/proxies.js +111 -0
  132. package/dist/registry/reader.d.ts +57 -0
  133. package/dist/registry/reader.d.ts.map +1 -0
  134. package/dist/registry/reader.js +198 -0
  135. package/dist/registry/semver.d.ts +16 -0
  136. package/dist/registry/semver.d.ts.map +1 -0
  137. package/dist/registry/semver.js +24 -0
  138. package/dist/registry/serve.d.ts +24 -0
  139. package/dist/registry/serve.d.ts.map +1 -0
  140. package/dist/registry/serve.js +64 -0
  141. package/dist/registry/source-forms.d.ts +79 -0
  142. package/dist/registry/source-forms.d.ts.map +1 -0
  143. package/dist/registry/source-forms.js +202 -0
  144. package/dist/registry/targets/artifactory.d.ts +28 -0
  145. package/dist/registry/targets/artifactory.d.ts.map +1 -0
  146. package/dist/registry/targets/artifactory.js +206 -0
  147. package/dist/registry/targets/fake-artifactory.d.ts +30 -0
  148. package/dist/registry/targets/fake-artifactory.d.ts.map +1 -0
  149. package/dist/registry/targets/fake-artifactory.js +142 -0
  150. package/dist/registry/targets/fake-gitlab.d.ts +39 -0
  151. package/dist/registry/targets/fake-gitlab.d.ts.map +1 -0
  152. package/dist/registry/targets/fake-gitlab.js +136 -0
  153. package/dist/registry/targets/fake-hcp.d.ts +50 -0
  154. package/dist/registry/targets/fake-hcp.d.ts.map +1 -0
  155. package/dist/registry/targets/fake-hcp.js +202 -0
  156. package/dist/registry/targets/fake-http.d.ts +31 -0
  157. package/dist/registry/targets/fake-http.d.ts.map +1 -0
  158. package/dist/registry/targets/fake-http.js +87 -0
  159. package/dist/registry/targets/fake-nexus.d.ts +27 -0
  160. package/dist/registry/targets/fake-nexus.d.ts.map +1 -0
  161. package/dist/registry/targets/fake-nexus.js +137 -0
  162. package/dist/registry/targets/gitlab.d.ts +53 -0
  163. package/dist/registry/targets/gitlab.d.ts.map +1 -0
  164. package/dist/registry/targets/gitlab.js +187 -0
  165. package/dist/registry/targets/hcp.d.ts +29 -0
  166. package/dist/registry/targets/hcp.d.ts.map +1 -0
  167. package/dist/registry/targets/hcp.js +310 -0
  168. package/dist/registry/targets/hosted.d.ts +62 -0
  169. package/dist/registry/targets/hosted.d.ts.map +1 -0
  170. package/dist/registry/targets/hosted.js +74 -0
  171. package/dist/registry/targets/index.d.ts +18 -0
  172. package/dist/registry/targets/index.d.ts.map +1 -0
  173. package/dist/registry/targets/index.js +17 -0
  174. package/dist/registry/targets/module-target.d.ts +68 -0
  175. package/dist/registry/targets/module-target.d.ts.map +1 -0
  176. package/dist/registry/targets/module-target.js +63 -0
  177. package/dist/registry/targets/nexus.d.ts +43 -0
  178. package/dist/registry/targets/nexus.d.ts.map +1 -0
  179. package/dist/registry/targets/nexus.js +250 -0
  180. package/dist/registry/targets/zip.d.ts +55 -0
  181. package/dist/registry/targets/zip.d.ts.map +1 -0
  182. package/dist/registry/targets/zip.js +278 -0
  183. package/dist/registry/writer.d.ts +101 -0
  184. package/dist/registry/writer.d.ts.map +1 -0
  185. package/dist/registry/writer.js +109 -0
  186. package/dist/rules/record-store.ts +122 -0
  187. package/dist/rules/tf001.ts +2 -2
  188. package/dist/rules/tf024.ts +105 -22
  189. package/dist/rules/tf046.ts +204 -0
  190. package/dist/rules/tf047.ts +114 -0
  191. package/dist/state/azure-blob.d.ts +8 -67
  192. package/dist/state/azure-blob.d.ts.map +1 -1
  193. package/dist/state/azure-blob.js +6 -199
  194. package/dist/state/gcs.d.ts +7 -61
  195. package/dist/state/gcs.d.ts.map +1 -1
  196. package/dist/state/gcs.js +5 -222
  197. package/dist/state/object-store.d.ts +4 -33
  198. package/dist/state/object-store.d.ts.map +1 -1
  199. package/dist/state/object-store.js +4 -17
  200. package/dist/state/s3.d.ts +11 -58
  201. package/dist/state/s3.d.ts.map +1 -1
  202. package/dist/state/s3.js +10 -161
  203. package/dist/state/store.js +2 -2
  204. package/dist/state-ops.d.ts +255 -0
  205. package/dist/state-ops.d.ts.map +1 -0
  206. package/dist/state-ops.js +305 -0
  207. package/dist/state-reads.d.ts +57 -0
  208. package/dist/state-reads.d.ts.map +1 -0
  209. package/dist/state-reads.js +104 -0
  210. package/dist/used-sources.d.ts +54 -0
  211. package/dist/used-sources.d.ts.map +1 -0
  212. package/dist/used-sources.js +157 -0
  213. package/package.json +19 -3
  214. package/src/__fixtures__/drift-record/README.md +5 -0
  215. package/src/__fixtures__/drift-rows/live-root-plan.json +70 -0
  216. package/src/__fixtures__/drift-rows/refresh-plan.json +61 -0
  217. package/src/__fixtures__/used-sources/root/extra.tofu +1 -0
  218. package/src/__fixtures__/used-sources/root/main.tf +31 -0
  219. package/src/__fixtures__/used-sources/root/modules/net/main.tf +14 -0
  220. package/src/__fixtures__/used-sources/root/modules/net/versions.tf.json +7 -0
  221. package/src/__fixtures__/used-sources/unit/terragrunt.hcl +3 -0
  222. package/src/ci-setup.ts +42 -0
  223. package/src/components/capability-plugin.test.ts +26 -14
  224. package/src/components/capability-plugin.ts +185 -31
  225. package/src/components/compose-group.ts +1 -1
  226. package/src/components/pr-loop-compose.tofu.e2e.test.ts +1 -1
  227. package/src/components/pr-loop.tofu.test.ts +1 -1
  228. package/src/components/release-inputs.ts +69 -0
  229. package/src/components/trace-facts.test.ts +21 -0
  230. package/src/components/trace-facts.ts +27 -0
  231. package/src/cone.ts +138 -0
  232. package/src/cost-hold.ts +29 -0
  233. package/src/describe-resources.engine-facts.test.ts +2 -3
  234. package/src/drift-rows.test.ts +135 -0
  235. package/src/drift-rows.ts +137 -0
  236. package/src/exit-codes.test.ts +23 -0
  237. package/src/exit-codes.ts +59 -0
  238. package/src/index.ts +3 -0
  239. package/src/lint/audit-catalog.ts +29 -3
  240. package/src/lint/audit-lineage.ts +14 -2
  241. package/src/lint/post-synth/fixtures/TF024/live-with-record-store/main.tf +18 -0
  242. package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/estate.chdf.hcl +1 -0
  243. package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/main.tf +10 -0
  244. package/src/lint/post-synth/fixtures/TF046/azurerm-no-resource-group/main.tf +15 -0
  245. package/src/lint/post-synth/fixtures/TF046/backend-config-only/main.tf +14 -0
  246. package/src/lint/post-synth/fixtures/TF046/derived-s3/backend.tf +7 -0
  247. package/src/lint/post-synth/fixtures/TF046/derived-s3/main.tf +9 -0
  248. package/src/lint/post-synth/fixtures/TF046/explicit-local/main.tf +16 -0
  249. package/src/lint/post-synth/fixtures/TF046/explicit-s3-beside-local-backend/main.tf +15 -0
  250. package/src/lint/post-synth/fixtures/TF046/implied-local/main.tf +9 -0
  251. package/src/lint/post-synth/fixtures/TF046/sidecar-implied/estate.chdf.hcl +1 -0
  252. package/src/lint/post-synth/fixtures/TF046/sidecar-implied/main.tf +5 -0
  253. package/src/lint/post-synth/fixtures/TF047/clear/main.tf +12 -0
  254. package/src/lint/post-synth/fixtures/TF047/refused/main.tf +12 -0
  255. package/src/lint/post-synth/fixtures/TF047/unknown-module/main.tf +12 -0
  256. package/src/lint/post-synth/fixtures/TF047/unknown-version/main.tf +12 -0
  257. package/src/lint/post-synth/index.ts +4 -0
  258. package/src/lint/post-synth/record-store.ts +122 -0
  259. package/src/lint/post-synth/tf001.ts +2 -2
  260. package/src/lint/post-synth/tf024.test.ts +26 -5
  261. package/src/lint/post-synth/tf024.ts +105 -22
  262. package/src/lint/post-synth/tf046.test.ts +137 -0
  263. package/src/lint/post-synth/tf046.ts +204 -0
  264. package/src/lint/post-synth/tf047.test.ts +56 -0
  265. package/src/lint/post-synth/tf047.ts +114 -0
  266. package/src/lock-rows.test.ts +70 -0
  267. package/src/lock-rows.ts +82 -0
  268. package/src/lock-timeout.ts +34 -0
  269. package/src/module-catalog/index.test.ts +93 -0
  270. package/src/module-catalog/index.ts +159 -0
  271. package/src/module-catalog/refresh-cli.ts +68 -0
  272. package/src/module-catalog/static-tier.ts +1659 -0
  273. package/src/module-catalog/types.ts +88 -0
  274. package/src/op/activities/terraform.ts +46 -5
  275. package/src/op-wave-step.test.ts +109 -0
  276. package/src/pin/__fixtures__/consumers/live/dev/main.tf +4 -0
  277. package/src/pin/__fixtures__/consumers/live/prod/main.tf +21 -0
  278. package/src/pin/__fixtures__/consumers/units/vpc/terragrunt.hcl +7 -0
  279. package/src/pin/consumers.test.ts +57 -0
  280. package/src/pin/consumers.ts +95 -0
  281. package/src/plugin.ts +6 -0
  282. package/src/registry/__fixtures__/docs/documented/.terraform-docs.yml +3 -0
  283. package/src/registry/__fixtures__/docs/documented/README.md +1 -0
  284. package/src/registry/__fixtures__/docs/documented/main.tf +8 -0
  285. package/src/registry/__fixtures__/docs/json/main.tf.json +14 -0
  286. package/src/registry/__fixtures__/docs/vpc/README.md +3 -0
  287. package/src/registry/__fixtures__/docs/vpc/examples/complete/main.tf +5 -0
  288. package/src/registry/__fixtures__/docs/vpc/examples/simple/README.md +1 -0
  289. package/src/registry/__fixtures__/docs/vpc/examples/simple/main.tf +4 -0
  290. package/src/registry/__fixtures__/docs/vpc/main.tf +14 -0
  291. package/src/registry/__fixtures__/docs/vpc/outputs.tf +9 -0
  292. package/src/registry/__fixtures__/docs/vpc/variables.tf +16 -0
  293. package/src/registry/__fixtures__/proxied-root/main.tf +12 -0
  294. package/src/registry/address.ts +100 -0
  295. package/src/registry/catalog-read.ts +56 -0
  296. package/src/registry/deprecated.test.ts +139 -0
  297. package/src/registry/deprecated.ts +76 -0
  298. package/src/registry/docs.test.ts +140 -0
  299. package/src/registry/docs.ts +299 -0
  300. package/src/registry/edge/bundle.test.ts +50 -0
  301. package/src/registry/edge/edge.test.ts +176 -0
  302. package/src/registry/edge/edge.ts +211 -0
  303. package/src/registry/edge/index.ts +10 -0
  304. package/src/registry/edge/lambda-edge.ts +127 -0
  305. package/src/registry/edge/sigv4-web.ts +79 -0
  306. package/src/registry/edge/token-source.test.ts +258 -0
  307. package/src/registry/edge/token-source.ts +169 -0
  308. package/src/registry/edge/worker.ts +61 -0
  309. package/src/registry/handler.ts +89 -0
  310. package/src/registry/headers-file.ts +61 -0
  311. package/src/registry/index.ts +31 -0
  312. package/src/registry/mirror-paths.ts +10 -0
  313. package/src/registry/mirror.test.ts +198 -0
  314. package/src/registry/mirror.ts +395 -0
  315. package/src/registry/proxies.test.ts +153 -0
  316. package/src/registry/proxies.ts +132 -0
  317. package/src/registry/reader.ts +225 -0
  318. package/src/registry/registry.test.ts +262 -0
  319. package/src/registry/semver.ts +34 -0
  320. package/src/registry/serve.ts +82 -0
  321. package/src/registry/source-forms.test.ts +87 -0
  322. package/src/registry/source-forms.ts +216 -0
  323. package/src/registry/targets/artifactory.test.ts +248 -0
  324. package/src/registry/targets/artifactory.ts +257 -0
  325. package/src/registry/targets/fake-artifactory.ts +158 -0
  326. package/src/registry/targets/fake-gitlab.ts +161 -0
  327. package/src/registry/targets/fake-hcp.ts +233 -0
  328. package/src/registry/targets/fake-http.ts +97 -0
  329. package/src/registry/targets/fake-nexus.ts +152 -0
  330. package/src/registry/targets/gitlab.test.ts +208 -0
  331. package/src/registry/targets/gitlab.ts +223 -0
  332. package/src/registry/targets/hcp.test.ts +230 -0
  333. package/src/registry/targets/hcp.ts +360 -0
  334. package/src/registry/targets/hosted.ts +117 -0
  335. package/src/registry/targets/index.ts +17 -0
  336. package/src/registry/targets/module-target.ts +131 -0
  337. package/src/registry/targets/nexus.test.ts +220 -0
  338. package/src/registry/targets/nexus.ts +314 -0
  339. package/src/registry/targets/zip.test.ts +65 -0
  340. package/src/registry/targets/zip.ts +287 -0
  341. package/src/registry/writer.ts +155 -0
  342. package/src/state/azure-blob.ts +6 -221
  343. package/src/state/gcs.ts +5 -252
  344. package/src/state/object-store.ts +4 -32
  345. package/src/state/s3.ts +10 -182
  346. package/src/state/store.ts +2 -2
  347. package/src/state-ops.test.ts +182 -0
  348. package/src/state-ops.ts +462 -0
  349. package/src/state-reads.ts +116 -0
  350. package/src/used-sources.test.ts +75 -0
  351. package/src/used-sources.ts +204 -0
@@ -22,7 +22,7 @@
22
22
  * system and refuses with exit 3 when its fresh plan differs (choudoufu
23
23
  * #878). Either refusal fails the step. choudoufu's refusal of a plan the
24
24
  * wave handed `run` throws {@link TerraformPlanMovedError}, so the command
25
- * that applied it exits 3 (#3997). terraform's and tofu's stale-plan error
25
+ * that applied it exits 4, the table's moved code (#3997, #4553). terraform's and tofu's stale-plan error
26
26
  * is not classified and exits 1.
27
27
  *
28
28
  * Run without a wave plan (`chant run --components`, or a fan-out with no
@@ -100,9 +100,31 @@
100
100
  * read the way `plan` reads a root's (`terraformPlannedOutputs`). The member
101
101
  * is not a `terraform.roots` entry of the reading project; its directory
102
102
  * comes from the workspace declaration.
103
+ *
104
+ * ## Op waves (#4545)
105
+ *
106
+ * The step is what an op wave's `step` names to plan and apply each root
107
+ * through chant (`chant run wave`), so terragucci's waves and plywood's run
108
+ * the engine in chant. Three inputs come from terragucci's wave callbacks:
109
+ *
110
+ * - `lockTimeout` (default `5m`, `false` for none): `plan` and `apply` wait
111
+ * that long for another run's state lock, unless `TF_CLI_ARGS` sets
112
+ * `-lock-timeout` already (`../lock-timeout.ts`).
113
+ * - `cone`: a revision. A live choudoufu root plans the cone of the change
114
+ * since it (`plan -cone`, `../cone.ts`), and the cone's targets join the
115
+ * plan digest, so an approval of one cone counts for no other. Such a root
116
+ * plans with `plan`, never as a one-root set.
117
+ * - `reads`: vars read from another root's outputs, Atmos's
118
+ * `!terraform.state` (`../state-reads.ts`). An upstream with no such output
119
+ * yet makes the member a hole naming it, so the root is never planned on a
120
+ * stand-in.
121
+ *
122
+ * Every plan also writes its `show -json` beside the plan file and names it
123
+ * as `artifact.planJson`, the document a wave's cost hold prices
124
+ * (`../cost-hold.ts`).
103
125
  */
104
126
 
105
- import { rmSync } from "node:fs";
127
+ import { rmSync, writeFileSync } from "node:fs";
106
128
  import { dirname, join, resolve } from "node:path";
107
129
  import type { Capability, DeployContext, CapabilityPlan, PlannedOutputs } from "@intentius/chant/components/capability";
108
130
  import { loadChantConfigUpward } from "@intentius/chant/config";
@@ -127,13 +149,22 @@ import {
127
149
  terraformOutputs,
128
150
  terraformPlan,
129
151
  } from "../op/activities/terraform";
152
+ import { terraformPlanMoved } from "../exit-codes";
130
153
  import { terraformPlanShape } from "../plan-shape";
131
- import { terraformChangeSetPart } from "../change-set";
154
+ import { plannerForBinary, terraformChangeSetPart } from "../change-set";
155
+ import { terraformDefaultLockGrain, terraformLockGrainRefusal, terraformLockRows, terraformLockRowsFromPart } from "../lock-rows";
156
+ import type { LockRow } from "@intentius/chant/lifecycle/lock-rows";
132
157
  import type { TerraformConfig } from "../config";
133
158
  import { rootTexts } from "../compose/hcl";
134
159
  import { declaredVariables } from "../compose/root";
135
160
  import { terraformComposeGroup } from "./compose-group";
136
161
  import { markPlaceholderDriven, placeholderVars, waitingHole } from "./placeholder-preview";
162
+ import { terraformReleaseInputs } from "./release-inputs";
163
+ import { terraformTraceFacts } from "./trace-facts";
164
+ import { DEFAULT_LOCK_TIMEOUT, isLockTimeout } from "../lock-timeout";
165
+ import { conePlanDigest, type PlanCone } from "../cone";
166
+ import { fillStateReads, stateReadsProblems, waitingReadsReason, type TerraformStateRead } from "../state-reads";
167
+ import { terraformStateOpCapabilities } from "../state-ops";
137
168
 
138
169
  /** The `terraform-apply` step's input. */
139
170
  export interface TerraformApplyStepInput {
@@ -143,6 +174,14 @@ export interface TerraformApplyStepInput {
143
174
  vars?: Record<string, unknown>;
144
175
  /** Where the `chant.config.*` search starts. Default: the current directory. */
145
176
  cwd?: string;
177
+ /** The root's files from here instead of its entry's `dir` (#4542): a promote under a release sets it to the recorded copy. */
178
+ dir?: string;
179
+ /** How long `plan` and `apply` wait for a state lock (#4545). Default `"5m"`; `false` passes no flag. */
180
+ lockTimeout?: string | false;
181
+ /** choudoufu live roots (#4545): plan the cone of the change since this revision. */
182
+ cone?: string;
183
+ /** Vars read from other roots' outputs (#4545), Atmos's `!terraform.state`. The step's `vars` win over them. */
184
+ reads?: TerraformStateRead[];
146
185
  }
147
186
 
148
187
  /** What `terraform-apply` returns. `outputs` is what a dependent's `stackOutput()` reads. */
@@ -159,9 +198,53 @@ export interface TerraformApplyStepOutput {
159
198
  interface PlannedRoot {
160
199
  planFile: string;
161
200
  planDigest: string;
201
+ /** The plan's `show -json`, beside the plan file (#4545), for a cost hold to price. */
202
+ planJson?: string;
203
+ /** The cone the plan covered, when asked for one (#4545). */
204
+ cone?: PlanCone;
205
+ /** A choudoufu root's rows on the resource lock grain (#4552), from the plan's `show -json`. */
206
+ lockRows?: LockRow[];
162
207
  }
163
208
 
164
- const rootArgs = (input: TerraformApplyStepInput) => ({ root: input.root, ...(input.cwd ? { cwd: input.cwd } : {}) });
209
+ /** The lock timeout a step's plan and apply pass (#4545). */
210
+ function lockTimeoutOf(input: TerraformApplyStepInput): { lockTimeout?: string } {
211
+ if (input.lockTimeout === false) return {};
212
+ const value = input.lockTimeout ?? DEFAULT_LOCK_TIMEOUT;
213
+ if (!isLockTimeout(value)) throw new Error(`terraform-apply: root "${input.root}": lockTimeout "${String(value)}" is not a duration such as "5m"`);
214
+ return { lockTimeout: value };
215
+ }
216
+
217
+ /**
218
+ * The step's input with its `reads` filled (#4545): each upstream's outputs
219
+ * read once, the values under the step's own `vars`. `waiting` when an
220
+ * upstream has no such output yet. Throws when a state cannot be read.
221
+ */
222
+ async function withReads(input: TerraformApplyStepInput): Promise<{ input: TerraformApplyStepInput } | { waiting: string; upstreams: string[] }> {
223
+ const reads = input.reads ?? [];
224
+ if (reads.length === 0) return { input };
225
+ const problems = stateReadsProblems(reads);
226
+ if (problems.length > 0) throw new Error(`terraform-apply: root "${input.root}": ${problems.join("; ")}`);
227
+ const filled = await fillStateReads(reads, async (root) => {
228
+ const at = { root, ...(input.cwd ? { cwd: input.cwd } : {}) };
229
+ await terraformInit(at);
230
+ return terraformOutputs(at);
231
+ });
232
+ if (filled.errors.length > 0) throw new Error(`terraform-apply: root "${input.root}": ${filled.errors.join("; ")}`);
233
+ if (filled.waiting.length > 0) return { waiting: waitingReadsReason(filled.waiting), upstreams: [...new Set(filled.waiting.map((w) => w.read.root))].sort() };
234
+ return { input: { ...input, vars: { ...filled.vars, ...(input.vars ?? {}) } } };
235
+ }
236
+
237
+ /** A root whose reads wait on an upstream (#4545): a hole naming it, and a digest over the root and those upstreams. */
238
+ function waitingOnReads(ctx: DeployContext, input: TerraformApplyStepInput, waiting: { waiting: string; upstreams: string[] }): CapabilityPlan {
239
+ const planDigest = computePlanDigest("terraform-apply-reads-waiting", { env: ctx.env, root: input.root, upstreams: waiting.upstreams });
240
+ const changeSet: ChangeSetPart = {
241
+ member: { member: input.root, lexicon: "terraform", planner: "chant", status: "planned", planDigest, holes: [{ address: input.root, type: "terraform_root", reason: waiting.waiting }] },
242
+ entries: [],
243
+ };
244
+ return { member: input.root, planDigest, changeSet };
245
+ }
246
+
247
+ const rootArgs = (input: TerraformApplyStepInput) => ({ root: input.root, ...(input.cwd ? { cwd: input.cwd } : {}), ...(input.dir ? { dir: input.dir } : {}) });
165
248
 
166
249
  function requireRoot(input: TerraformApplyStepInput): void {
167
250
  if (typeof input.root !== "string" || input.root === "") {
@@ -179,12 +262,27 @@ async function planRoot(input: TerraformApplyStepInput, targets?: string[]): Pro
179
262
  ...rootArgs(input),
180
263
  ...(input.vars ? { vars: input.vars } : {}),
181
264
  ...(targets && targets.length > 0 ? { targets } : {}),
265
+ ...lockTimeoutOf(input),
266
+ ...(input.cone && !(targets && targets.length > 0) ? { cone: input.cone } : {}),
182
267
  text: false,
183
268
  });
269
+ // #4545: the cone's targets join the digest, so an approval of this cone counts for no other.
270
+ const planDigest = conePlanDigest(planned.planDigest, planned.cone);
271
+ const changeSet = planned.changeSet && planDigest !== planned.planDigest ? { ...planned.changeSet, member: { ...planned.changeSet.member, planDigest } } : planned.changeSet;
272
+ // #4545: the plan document beside the plan file, which a wave's cost hold prices.
273
+ const planJson = join(planned.dir, `${planned.planFile}.json`);
274
+ let wrote = true;
275
+ try {
276
+ writeFileSync(planJson, JSON.stringify(planned.json));
277
+ } catch {
278
+ wrote = false;
279
+ }
184
280
  return {
185
281
  planFile: planned.planFile,
186
- planDigest: planned.planDigest,
187
- ...(planned.changeSet ? { changeSet: planned.changeSet } : {}),
282
+ planDigest,
283
+ ...(wrote ? { planJson } : {}),
284
+ ...(planned.cone ? { cone: planned.cone } : {}),
285
+ ...(changeSet ? { changeSet } : {}),
188
286
  json: planned.json,
189
287
  };
190
288
  }
@@ -290,8 +388,9 @@ async function rootDir(input: TerraformApplyStepInput): Promise<{ dir: string; b
290
388
  const start = resolve(input.cwd ?? process.cwd());
291
389
  const { config, configPath } = await loadChantConfigUpward(start);
292
390
  const terraform = (config as { terraform?: TerraformConfig }).terraform;
293
- const dir = terraform?.roots?.[input.root]?.dir;
294
- if (dir === undefined) return undefined;
391
+ const declared = terraform?.roots?.[input.root]?.dir;
392
+ if (declared === undefined) return undefined;
393
+ const dir = input.dir ?? declared;
295
394
  return { dir: resolve(configPath ? dirname(configPath) : start, dir), ...(terraform?.binary ? { binary: terraform.binary } : {}) };
296
395
  }
297
396
 
@@ -364,11 +463,12 @@ async function previewRoot(ctx: DeployContext, input: TerraformApplyStepInput, u
364
463
  }
365
464
 
366
465
  /**
367
- * choudoufu refused the plan `run` was handed by its wave (its exit 3):
368
- * the live system moved since the plan, or the plan names another estate.
369
- * Nothing was applied. `planMoved` is core's mark for a moved plan
370
- * (`isPlanMovedError`), so the command that applied it exits 3, as for the
371
- * other capabilities that refuse a moved plan (#3997).
466
+ * choudoufu refused the plan `run` was handed by its wave (its exit 3)
467
+ * because the live system moved since the plan. Nothing was applied.
468
+ * `planMoved` is core's mark for a moved plan (`isPlanMovedError`), so the
469
+ * command that applied it exits `CHANT_EXIT.moved`, 4, as for the other
470
+ * capabilities that refuse a moved plan (#3997, #4553). A plan made against
471
+ * another estate is a plain failure (`terraformExitOf` in ../exit-codes).
372
472
  */
373
473
  export class TerraformPlanMovedError extends Error {
374
474
  readonly planMoved = true;
@@ -378,9 +478,6 @@ export class TerraformPlanMovedError extends Error {
378
478
  }
379
479
  }
380
480
 
381
- /** `live-wave-apply`'s exit status for a set whose plan moved (`CHOUDOUFU_WAVE_EXIT.setMoved`). */
382
- const CHOUDOUFU_SET_MOVED_EXIT = 3;
383
-
384
481
  function isPlannedRoot(value: unknown): value is PlannedRoot {
385
482
  const v = value as Partial<PlannedRoot> | undefined;
386
483
  return typeof v?.planFile === "string" && typeof v.planDigest === "string";
@@ -390,6 +487,8 @@ function isPlannedRoot(value: unknown): value is PlannedRoot {
390
487
  interface ChoudoufuWavePlannedRoot {
391
488
  planDigest: string;
392
489
  choudoufu: { planSetFile: string; setDigest: string };
490
+ /** The root's rows on the resource lock grain (#4552), from its embedded plan. */
491
+ lockRows?: LockRow[];
393
492
  }
394
493
 
395
494
  function isChoudoufuWavePlan(value: unknown): value is ChoudoufuWavePlannedRoot {
@@ -416,13 +515,28 @@ async function runChoudoufuWave(
416
515
  if (applied.carry) ctx.carry?.(input.root, applied.carry);
417
516
  if (!applied.landed) {
418
517
  const message = `terraform-apply: root "${input.root}": ${applied.message}`;
419
- if (applied.exitCode === CHOUDOUFU_SET_MOVED_EXIT) throw new TerraformPlanMovedError(input.root, message);
518
+ if (terraformPlanMoved({ command: "live-wave-apply", code: applied.exitCode })) throw new TerraformPlanMovedError(input.root, message);
420
519
  throw new Error(message);
421
520
  }
422
521
  const outputs = await terraformOutputs(rootArgs(input));
423
522
  return { root: input.root, planDigest: planned.planDigest, fromWavePlan: true, outputs };
424
523
  }
425
524
 
525
+ /** The input a `run` that plans for itself plans with (#4545): its reads filled, or an error naming the upstream to apply first. */
526
+ async function readsForRun(input: TerraformApplyStepInput): Promise<TerraformApplyStepInput> {
527
+ const read = await withReads(input);
528
+ if ("waiting" in read) throw new Error(`terraform-apply: root "${input.root}": ${read.waiting}`);
529
+ return read.input;
530
+ }
531
+
532
+ /** The engine a root plans with (#4552): the configured binary's, else the plan's planner. */
533
+ async function plannerOf(input: TerraformApplyStepInput, plan?: CapabilityPlan): Promise<"terraform" | "tofu" | "choudoufu" | undefined> {
534
+ const binary = (await rootDir(input).catch(() => undefined))?.binary;
535
+ if (binary !== undefined) return plannerForBinary(binary) as "terraform" | "tofu" | "choudoufu";
536
+ const planner = plan?.changeSet?.member.planner;
537
+ return planner === "terraform" || planner === "tofu" || planner === "choudoufu" ? planner : "terraform";
538
+ }
539
+
426
540
  export const terraformApplyCapability: Capability<TerraformApplyStepInput, TerraformApplyStepOutput> = {
427
541
  kind: "terraform-apply",
428
542
  // A root's apply has no undo short of applying the previous configuration,
@@ -430,26 +544,40 @@ export const terraformApplyCapability: Capability<TerraformApplyStepInput, Terra
430
544
  rollbackPolicy: "needs-opt-out",
431
545
  // #4300: `--target` passes through as `-target`, and the saved plan holds those addresses alone.
432
546
  targets: "accept",
433
- async plan(ctx: DeployContext, input: TerraformApplyStepInput): Promise<CapabilityPlan> {
434
- requireRoot(input);
547
+ // #4627: `plan` refreshes the root's state before it plans unless told `-refresh=false`, which chant never passes,
548
+ // so `--refresh` asks for what every plan does and the digest still binds it. `-target` plans a resource with what it
549
+ // depends on and never what depends on it, so `--target-dependents` has nothing to map onto.
550
+ planFlags: {
551
+ refresh: "accept",
552
+ targetDependents: { refuse: "terraform and tofu plan a -target with what it depends on, never what depends on it, so there is no flag to widen it to its dependents; name each dependent with --target" },
553
+ },
554
+ async plan(ctx: DeployContext, given: TerraformApplyStepInput): Promise<CapabilityPlan> {
555
+ requireRoot(given);
556
+ // #4545: a root reading an upstream's output that is not there yet is a hole naming the upstream.
557
+ const read = await withReads(given);
558
+ if ("waiting" in read) return waitingOnReads(ctx, given, read);
559
+ const { input } = read;
435
560
  const unresolved = [...(ctx.unresolvedInputs ?? [])].sort();
436
561
  const targets = ctx.targets && ctx.targets.length > 0 ? ctx.targets : undefined;
437
562
  if (unresolved.length > 0) return previewRoot(ctx, input, unresolved, targets);
438
- // A targeted root plans with `plan -target`, never as a choudoufu one-root set.
439
- const wave = targets ? { wave: false } : await choudoufuWaveRoot({ ...rootArgs(input), ...(input.vars ? { vars: input.vars } : {}) });
563
+ // A targeted root plans with `plan -target`, and a cone plan with `plan -cone` (#4545), never as a choudoufu one-root set.
564
+ const wave = targets || input.cone ? { wave: false } : await choudoufuWaveRoot({ ...rootArgs(input), ...(input.vars ? { vars: input.vars } : {}) });
440
565
  if (wave.wave) {
441
566
  const set = await choudoufuPlanSet(rootArgs(input));
442
567
  const artifact: ChoudoufuWavePlannedRoot = {
443
568
  planDigest: set.planDigest,
444
569
  choudoufu: { planSetFile: set.planSetFile, setDigest: set.setDigest },
570
+ lockRows: terraformLockRows(input.root, set.changeSet.member.scope, set.json),
445
571
  };
446
572
  return { member: input.root, planDigest: set.planDigest, artifact, changeSet: set.changeSet, outputs: terraformPlannedOutputs(set.json), shape: terraformPlanShape(set.json, input.root) };
447
573
  }
448
574
  const { changeSet, json, ...planned } = await planRoot(input, targets);
575
+ // #4552: a choudoufu root's rows, keyed from the plan document while it is at hand.
576
+ const lockRows = changeSet?.member.planner === "choudoufu" ? terraformLockRows(input.root, changeSet.member.scope, json) : undefined;
449
577
  return {
450
578
  member: input.root,
451
579
  planDigest: planned.planDigest,
452
- artifact: planned,
580
+ artifact: { ...planned, ...(lockRows ? { lockRows } : {}) },
453
581
  ...(changeSet ? { changeSet } : {}),
454
582
  outputs: terraformPlannedOutputs(json),
455
583
  shape: terraformPlanShape(json, input.root),
@@ -466,7 +594,29 @@ export const terraformApplyCapability: Capability<TerraformApplyStepInput, Terra
466
594
  // #4447 (ws-107): a group core forms of components that read each other plans and applies as one composed run
467
595
  // when `terraform.compose.enabled` is set (./compose-group.ts). Delegates, so the methods are read when called.
468
596
  groupReads: (ctx, members) => terraformComposeGroup.groupReads(ctx, members),
469
- groupFallback: (ctx, members) => terraformComposeGroup.groupFallback(ctx, members),
597
+ // #4542: a member pointed at a release's recorded copy plans alone, since a composition reads the declared roots.
598
+ groupFallback: async (ctx, members) =>
599
+ members.some((m) => typeof m.step.dir === "string")
600
+ ? { kind: "release-copy", reason: "a member deploys a release's recorded copy of its root, which a composition does not read" }
601
+ : terraformComposeGroup.groupFallback(ctx, members),
602
+ // #4542: the root's files, which a release cut records and a promote under it plans.
603
+ releaseInputs: (input, ctx) => terraformReleaseInputs(input, ctx),
604
+ // #4552: resource under choudoufu, the root under Terraform, tofu and Terragrunt, which refuse resource by name.
605
+ async defaultLockGrain(input, plan) {
606
+ return terraformDefaultLockGrain(await plannerOf(input, plan));
607
+ },
608
+ async lockGrainRefusal(input, grain, plan) {
609
+ if (grain !== "resource") return undefined;
610
+ const binary = (await rootDir(input).catch(() => undefined))?.binary;
611
+ return terraformLockGrainRefusal(grain, await plannerOf(input, plan), binary);
612
+ },
613
+ lockRows(input, plan) {
614
+ const kept = (plan.artifact as { lockRows?: LockRow[] } | undefined)?.lockRows;
615
+ if (Array.isArray(kept)) return kept;
616
+ return plan.changeSet ? terraformLockRowsFromPart(input.root, plan.changeSet) : [];
617
+ },
618
+ // #4555: the root and the binary, for the step's span and run record.
619
+ traceFacts: async (input, outcome) => terraformTraceFacts(input, outcome, (await rootDir(input).catch(() => undefined))?.binary),
470
620
  planGroup: (ctx, members) => terraformComposeGroup.planGroup(ctx, members),
471
621
  runGroup: (ctx, run, members) => terraformComposeGroup.runGroup(ctx, run, members),
472
622
  async run(ctx: DeployContext, input: TerraformApplyStepInput): Promise<TerraformApplyStepOutput> {
@@ -475,10 +625,10 @@ export const terraformApplyCapability: Capability<TerraformApplyStepInput, Terra
475
625
  if (isShapeArtifact(fromWave)) {
476
626
  // A root previewed with stand-ins (#4463): plan it now with the real
477
627
  // values, and apply that plan only when it is within the preview's shape.
478
- const fresh = await planRoot(input, ctx.targets);
628
+ const fresh = await planRoot(await readsForRun(input), ctx.targets);
479
629
  const differences = planShapeDifferences(fromWave.shape, terraformPlanShape(fresh.json, input.root));
480
630
  if (differences.length > 0) throw new PlanShapeMovedError(input.root, differences);
481
- const applied = await terraformApply({ ...rootArgs(input), planFile: fresh.planFile });
631
+ const applied = await terraformApply({ ...rootArgs(input), planFile: fresh.planFile, ...lockTimeoutOf(input) });
482
632
  if (!applied.applied) {
483
633
  throw new Error(`terraform-apply: root "${input.root}" refused the plan it was given (${applied.refused ?? "refused"}). ${applied.refusal ?? ""}`);
484
634
  }
@@ -487,15 +637,16 @@ export const terraformApplyCapability: Capability<TerraformApplyStepInput, Terra
487
637
  }
488
638
  const planned: PlannedRoot = isPlannedRoot(fromWave)
489
639
  ? fromWave
490
- : await planRoot(input, ctx.targets).then(({ planFile, planDigest }) => ({ planFile, planDigest }));
491
- const applied = await terraformApply({ ...rootArgs(input), planFile: planned.planFile });
640
+ : await planRoot(await readsForRun(input), ctx.targets).then(({ planFile, planDigest }) => ({ planFile, planDigest }));
641
+ const applied = await terraformApply({ ...rootArgs(input), planFile: planned.planFile, ...lockTimeoutOf(input) });
492
642
  if (!applied.applied) {
493
643
  const message =
494
644
  `terraform-apply: root "${input.root}" refused the plan it was given (${applied.refused ?? "refused"}). ` +
495
645
  (applied.refusal ?? "");
496
- // A plan the wave approved that choudoufu refuses moved since the
497
- // approval. One this run made itself is planned afresh by the next run.
498
- if (applied.refused !== undefined && isPlannedRoot(fromWave)) throw new TerraformPlanMovedError(input.root, message);
646
+ // A plan the wave approved that choudoufu refuses as no longer matching
647
+ // moved since the approval (#4553: one made against another estate is a
648
+ // failure). One this run made itself is planned afresh by the next run.
649
+ if (applied.refused === "approval-mismatch" && isPlannedRoot(fromWave)) throw new TerraformPlanMovedError(input.root, message);
499
650
  throw new Error(message);
500
651
  }
501
652
  const outputs = await terraformOutputs(rootArgs(input));
@@ -505,6 +656,9 @@ export const terraformApplyCapability: Capability<TerraformApplyStepInput, Terra
505
656
 
506
657
  export const TERRAFORM_VERB_FAMILIES = {
507
658
  apply: ["terraform-apply"],
659
+ // #4618: the binary runs outside waves, each a fixed operation; the two that change state on their own run under a gated request.
660
+ state: ["terraform-state-unlock", "terraform-state-push"],
661
+ copy: ["terraform-copy"],
508
662
  } as const;
509
663
 
510
664
  export const terraformCapabilityPlugin: CapabilityPlugin = {
@@ -513,7 +667,7 @@ export const terraformCapabilityPlugin: CapabilityPlugin = {
513
667
  return ownPackageVersion(import.meta.url);
514
668
  },
515
669
  capabilities(): Array<Capability<never, unknown>> {
516
- return [terraformApplyCapability as Capability<never, unknown>];
670
+ return [terraformApplyCapability as Capability<never, unknown>, ...terraformStateOpCapabilities.map((c) => c as unknown as Capability<never, unknown>)];
517
671
  },
518
672
  families(): Record<string, readonly string[]> {
519
673
  return TERRAFORM_VERB_FAMILIES;
@@ -341,7 +341,7 @@ function waitingGroup(ctx: GroupContext, members: readonly GroupMember<StepInput
341
341
  return { members: plans, digest: computePlanDigest("terraform-apply-group-unresolved", { components: ctx.components, unresolved: waits }) };
342
342
  }
343
343
 
344
- /** A composed plan that no longer matches the one approved. Core's moved-plan mark, so nothing applies and the command exits 3. */
344
+ /** A composed plan that no longer matches the one approved. Core's moved-plan mark, so nothing applies and the command exits `CHANT_EXIT.moved`, 4 (#4553). */
345
345
  export class ComposedPlanMovedError extends Error {
346
346
  readonly planMoved = true;
347
347
  constructor(message: string) {
@@ -287,7 +287,7 @@ chainSuite("with terraform.compose, three roots plan and apply as one group", {
287
287
  writeFileSync(join(loop.dir, "roots/a/main.tf"), CHAIN.a!.replace("a-v1", "a-v4"));
288
288
  loop.git("commit", "-qam", "a v4");
289
289
 
290
- expect(await loop.merge(2)).toBe(3);
290
+ expect(await loop.merge(2)).toBe(4);
291
291
  const refused = loop.report("apply");
292
292
  expect(refused.status).toBe("refused");
293
293
  expect(refused.refusal).toBe("plan-changed");
@@ -298,7 +298,7 @@ describe.skipIf(!BINARY)(`the pull-request loop over five ${BINARY ?? "terraform
298
298
  write("roots/a/main.tf", ROOTS.a.replace("a-v1", "a-v4"));
299
299
  git("commit", "-qam", "a v4");
300
300
 
301
- expect(await merge(2)).toBe(3);
301
+ expect(await merge(2)).toBe(4);
302
302
  const refused = report("apply");
303
303
  expect(refused.status).toBe("refused");
304
304
  expect(refused.refusal).toBe("plan-changed");
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The files a release records for a `terraform-apply` step (chant#4542).
3
+ *
4
+ * The step names its root by key, so core can't see which files it deploys.
5
+ * `terraformReleaseInputs` answers core's `releaseInputs` with the set a
6
+ * pull request's plan treats as the root's change (`rootWatchPaths`): the
7
+ * root's directory, each local module it reaches, and its var files, plus
8
+ * its backend-config files. A release cut records them for the component,
9
+ * and a promote or rollback under the release writes them under
10
+ * `.chant/release-templates/<digest>/` and sets the step's `dir` to the copy
11
+ * of the root, so `terraform-apply` plans the release's configuration against
12
+ * the environment's state.
13
+ *
14
+ * A root whose state is a file in its own directory (the `local` backend) is
15
+ * unbound: a plan run in a copy elsewhere would read no state and propose
16
+ * creating everything. Such a root rolls back through `chant lifecycle
17
+ * rollback --to`. A live choudoufu root has no state file and is recorded.
18
+ */
19
+
20
+ import { dirname, relative, resolve } from "node:path";
21
+ import type { ReleaseInputs, ReleaseInputsContext, ReleaseInputsUnbound } from "@intentius/chant/components/capability";
22
+ import { loadChantConfigUpward } from "@intentius/chant/config";
23
+ import { rootWatchPaths } from "../changed-roots";
24
+ import type { TerraformConfig } from "../config";
25
+ import { liveEstate } from "../compose/hcl";
26
+ import { readBackend } from "../state/backend";
27
+
28
+ /** The parts of the `terraform-apply` input this reads. */
29
+ interface StepInput {
30
+ root: string;
31
+ cwd?: string;
32
+ dir?: string;
33
+ }
34
+
35
+ const toPosix = (p: string): string => p.split("\\").join("/") || ".";
36
+
37
+ /** What a release records for one `terraform-apply` step, relative to the member's directory. Null when the root is not declared. */
38
+ export async function terraformReleaseInputs(input: StepInput, ctx: ReleaseInputsContext): Promise<ReleaseInputs | ReleaseInputsUnbound | null> {
39
+ if (typeof input.root !== "string" || input.root === "") return null;
40
+ const start = resolve(ctx.memberDir, input.cwd ?? ".");
41
+ const { config, configPath } = await loadChantConfigUpward(start);
42
+ const projectRoot = configPath ? dirname(configPath) : start;
43
+ const terraform = (config as { terraform?: TerraformConfig }).terraform;
44
+ const root = terraform?.roots?.[input.root];
45
+ if (!root) return null;
46
+ const binary = terraform?.binary;
47
+ const rootDir = resolve(projectRoot, input.dir ?? root.dir);
48
+ const live = binary === "choudoufu" && (root.estate !== undefined || liveEstate(rootDir, binary).live);
49
+ if (!live) {
50
+ const { backend } = readBackend(rootDir, {
51
+ ...(root.backendConfig ? { backendConfig: root.backendConfig } : {}),
52
+ ...(root.backendConfigFiles ? { backendConfigFiles: root.backendConfigFiles } : {}),
53
+ ...(binary ? { binary } : {}),
54
+ });
55
+ if (backend.type === "local") {
56
+ return {
57
+ unbound:
58
+ `root "${input.root}" keeps its state in a local file under ${toPosix(relative(ctx.memberDir, rootDir))}, ` +
59
+ "so a plan of a recorded copy would read no state; roll it back with chant lifecycle rollback --to <ref>",
60
+ };
61
+ }
62
+ }
63
+ const fromMember = (abs: string): string => toPosix(relative(ctx.memberDir, abs));
64
+ const paths = rootWatchPaths(projectRoot, { dir: relative(projectRoot, rootDir) || ".", ...(root.varFiles ? { varFiles: root.varFiles } : {}) }, binary).map((p) =>
65
+ fromMember(resolve(projectRoot, p)),
66
+ );
67
+ for (const file of root.backendConfigFiles ?? []) paths.push(fromMember(resolve(rootDir, file)));
68
+ return { field: "dir", dir: fromMember(rootDir), paths: [...new Set(paths)].sort() };
69
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `terraform-apply`'s engine facts (INTENTIUS/chant#4555): the root and the
3
+ * binary, for the step's span and run record.
4
+ */
5
+
6
+ import { describe, expect, test } from "vitest";
7
+ import { terraformTraceFacts } from "./trace-facts";
8
+
9
+ describe("terraform-apply's trace facts (#4555)", () => {
10
+ test("name the root, the configured binary and whether the wave's plan applied", () => {
11
+ expect(terraformTraceFacts({ root: "network" }, { output: { root: "network", planDigest: "sha256:x", fromWavePlan: true, outputs: { vpc_id: "v" } } }, "/usr/local/bin/tofu")).toEqual({
12
+ attributes: { root: "network", binary: "tofu", "apply.from_wave_plan": true, "apply.outputs": 1 },
13
+ });
14
+ });
15
+
16
+ test("default to terraform, and keep the root and the cone on a failed apply", () => {
17
+ expect(terraformTraceFacts({ root: "network", cone: "HEAD~1" }, { error: new Error("refused") }, undefined)).toEqual({
18
+ attributes: { root: "network", binary: "terraform", "plan.cone": "HEAD~1" },
19
+ });
20
+ });
21
+ });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `terraform-apply`'s engine facts for its span and op-wave run record
3
+ * (INTENTIUS/chant#4555): the root, the binary that applied it, and whether
4
+ * it applied the plan the wave decided. Core adds the plan's resource counts
5
+ * by action, and writes every key under the CI product's prefix
6
+ * (`terragucci.root`, `terragucci.binary`).
7
+ */
8
+
9
+ import type { CapabilityTraceFacts } from "@intentius/chant/otlp";
10
+ import type { TerraformApplyStepInput, TerraformApplyStepOutput } from "./capability-plugin";
11
+
12
+ /** `traceFacts` for `terraform-apply` (#4555). `binary` is the config's `terraform.binary`, `terraform` when it names none. */
13
+ export function terraformTraceFacts(
14
+ input: TerraformApplyStepInput,
15
+ outcome: { output: TerraformApplyStepOutput } | { error: unknown },
16
+ binary: string | undefined,
17
+ ): CapabilityTraceFacts {
18
+ const bin = (binary ?? "terraform").split(/[\\/]/).pop() || "terraform";
19
+ return {
20
+ attributes: {
21
+ root: input.root,
22
+ binary: bin,
23
+ ...("output" in outcome && outcome.output ? { "apply.from_wave_plan": outcome.output.fromWavePlan, "apply.outputs": Object.keys(outcome.output.outputs ?? {}).length } : {}),
24
+ ...(input.cone ? { "plan.cone": input.cone } : {}),
25
+ },
26
+ };
27
+ }