@intentius/chant-lexicon-terraform 0.124.52 → 0.124.55

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 (641) hide show
  1. package/README.md +14 -10
  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/ci-setup.d.ts +11 -0
  27. package/dist/ci-setup.d.ts.map +1 -0
  28. package/dist/ci-setup.js +41 -0
  29. package/dist/commands.d.ts.map +1 -1
  30. package/dist/commands.js +4 -3
  31. package/dist/components/capability-plugin.d.ts +105 -9
  32. package/dist/components/capability-plugin.d.ts.map +1 -1
  33. package/dist/components/capability-plugin.js +357 -36
  34. package/dist/components/compose-group.d.ts +145 -0
  35. package/dist/components/compose-group.d.ts.map +1 -0
  36. package/dist/components/compose-group.js +407 -0
  37. package/dist/components/placeholder-preview.d.ts +78 -0
  38. package/dist/components/placeholder-preview.d.ts.map +1 -0
  39. package/dist/components/placeholder-preview.js +254 -0
  40. package/dist/components/release-inputs.d.ts +29 -0
  41. package/dist/components/release-inputs.d.ts.map +1 -0
  42. package/dist/components/release-inputs.js +57 -0
  43. package/dist/components/trace-facts.d.ts +16 -0
  44. package/dist/components/trace-facts.d.ts.map +1 -0
  45. package/dist/components/trace-facts.js +19 -0
  46. package/dist/compose/apply-events.d.ts +46 -0
  47. package/dist/compose/apply-events.d.ts.map +1 -0
  48. package/dist/compose/apply-events.js +77 -0
  49. package/dist/compose/estate.d.ts +194 -0
  50. package/dist/compose/estate.d.ts.map +1 -0
  51. package/dist/compose/estate.js +363 -0
  52. package/dist/compose/hcl.d.ts +99 -0
  53. package/dist/compose/hcl.d.ts.map +1 -0
  54. package/dist/compose/hcl.js +302 -0
  55. package/dist/compose/module-address.d.ts +24 -0
  56. package/dist/compose/module-address.d.ts.map +1 -0
  57. package/dist/compose/module-address.js +43 -0
  58. package/dist/compose/root.d.ts +127 -0
  59. package/dist/compose/root.d.ts.map +1 -0
  60. package/dist/compose/root.js +586 -0
  61. package/dist/compose/run.d.ts +183 -0
  62. package/dist/compose/run.d.ts.map +1 -0
  63. package/dist/compose/run.js +447 -0
  64. package/dist/compose/slice.d.ts +102 -0
  65. package/dist/compose/slice.d.ts.map +1 -0
  66. package/dist/compose/slice.js +249 -0
  67. package/dist/compose/state-address.d.ts +28 -0
  68. package/dist/compose/state-address.d.ts.map +1 -0
  69. package/dist/compose/state-address.js +154 -0
  70. package/dist/compose/state-merge.d.ts +52 -0
  71. package/dist/compose/state-merge.d.ts.map +1 -0
  72. package/dist/compose/state-merge.js +135 -0
  73. package/dist/compose.d.ts +179 -0
  74. package/dist/compose.d.ts.map +1 -0
  75. package/dist/compose.js +482 -0
  76. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  77. package/dist/composites/terraform-apply-op.js +7 -3
  78. package/dist/cone.d.ts +53 -0
  79. package/dist/cone.d.ts.map +1 -0
  80. package/dist/cone.js +119 -0
  81. package/dist/config.d.ts +59 -13
  82. package/dist/config.d.ts.map +1 -1
  83. package/dist/config.js +43 -9
  84. package/dist/cost-hold.d.ts +27 -0
  85. package/dist/cost-hold.d.ts.map +1 -0
  86. package/dist/cost-hold.js +21 -0
  87. package/dist/describe-resources.d.ts +74 -12
  88. package/dist/describe-resources.d.ts.map +1 -1
  89. package/dist/describe-resources.js +374 -66
  90. package/dist/drift-read.d.ts +33 -0
  91. package/dist/drift-read.d.ts.map +1 -0
  92. package/dist/drift-read.js +243 -0
  93. package/dist/drift-record.d.ts +147 -0
  94. package/dist/drift-record.d.ts.map +1 -0
  95. package/dist/drift-record.js +383 -0
  96. package/dist/drift-rows.d.ts +61 -0
  97. package/dist/drift-rows.d.ts.map +1 -0
  98. package/dist/drift-rows.js +108 -0
  99. package/dist/exit-codes.d.ts +55 -0
  100. package/dist/exit-codes.d.ts.map +1 -0
  101. package/dist/exit-codes.js +48 -0
  102. package/dist/hcl/edges.d.ts.map +1 -1
  103. package/dist/hcl/edges.js +24 -1
  104. package/dist/hcl/files.d.ts +61 -0
  105. package/dist/hcl/files.d.ts.map +1 -0
  106. package/dist/hcl/files.js +221 -0
  107. package/dist/hcl/instances.d.ts +82 -0
  108. package/dist/hcl/instances.d.ts.map +1 -0
  109. package/dist/hcl/instances.js +390 -0
  110. package/dist/hcl/parse.d.ts +81 -9
  111. package/dist/hcl/parse.d.ts.map +1 -1
  112. package/dist/hcl/parse.js +114 -16
  113. package/dist/hcl/roots.d.ts.map +1 -1
  114. package/dist/hcl/roots.js +5 -0
  115. package/dist/hcl/suppressions.d.ts.map +1 -1
  116. package/dist/hcl/suppressions.js +8 -2
  117. package/dist/index.d.ts +13 -2
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +24 -2
  120. package/dist/integrity.json +8 -5
  121. package/dist/lint/audit-catalog.d.ts.map +1 -1
  122. package/dist/lint/audit-catalog.js +27 -3
  123. package/dist/lint/audit-lineage.d.ts +14 -2
  124. package/dist/lint/audit-lineage.d.ts.map +1 -1
  125. package/dist/lint/post-synth/index.d.ts.map +1 -1
  126. package/dist/lint/post-synth/index.js +4 -0
  127. package/dist/lint/post-synth/record-store.d.ts +54 -0
  128. package/dist/lint/post-synth/record-store.d.ts.map +1 -0
  129. package/dist/lint/post-synth/record-store.js +104 -0
  130. package/dist/lint/post-synth/tf001.d.ts +2 -2
  131. package/dist/lint/post-synth/tf001.js +2 -2
  132. package/dist/lint/post-synth/tf024.d.ts +35 -11
  133. package/dist/lint/post-synth/tf024.d.ts.map +1 -1
  134. package/dist/lint/post-synth/tf024.js +92 -20
  135. package/dist/lint/post-synth/tf046.d.ts +64 -0
  136. package/dist/lint/post-synth/tf046.d.ts.map +1 -0
  137. package/dist/lint/post-synth/tf046.js +175 -0
  138. package/dist/lint/post-synth/tf047.d.ts +36 -0
  139. package/dist/lint/post-synth/tf047.d.ts.map +1 -0
  140. package/dist/lint/post-synth/tf047.js +94 -0
  141. package/dist/live-plan-reasons.d.ts +21 -0
  142. package/dist/live-plan-reasons.d.ts.map +1 -0
  143. package/dist/live-plan-reasons.js +47 -0
  144. package/dist/lock-rows.d.ts +36 -0
  145. package/dist/lock-rows.d.ts.map +1 -0
  146. package/dist/lock-rows.js +60 -0
  147. package/dist/lock-timeout.d.ts +22 -0
  148. package/dist/lock-timeout.d.ts.map +1 -0
  149. package/dist/lock-timeout.js +32 -0
  150. package/dist/lsp/option-keys.d.ts.map +1 -1
  151. package/dist/lsp/option-keys.js +2 -0
  152. package/dist/manifest.json +1 -1
  153. package/dist/module-catalog/index.d.ts +69 -0
  154. package/dist/module-catalog/index.d.ts.map +1 -0
  155. package/dist/module-catalog/index.js +110 -0
  156. package/dist/module-catalog/refresh-cli.d.ts +4 -0
  157. package/dist/module-catalog/refresh-cli.d.ts.map +1 -0
  158. package/dist/module-catalog/refresh-cli.js +64 -0
  159. package/dist/module-catalog/static-tier.d.ts +6 -0
  160. package/dist/module-catalog/static-tier.d.ts.map +1 -0
  161. package/dist/module-catalog/static-tier.js +1650 -0
  162. package/dist/module-catalog/types.d.ts +65 -0
  163. package/dist/module-catalog/types.d.ts.map +1 -0
  164. package/dist/module-catalog/types.js +33 -0
  165. package/dist/okf/index.md +3 -1
  166. package/dist/okf/rules/TF024.md +2 -2
  167. package/dist/okf/rules/TF046.md +11 -0
  168. package/dist/okf/rules/TF047.md +11 -0
  169. package/dist/op/activities/index.d.ts +6 -2
  170. package/dist/op/activities/index.d.ts.map +1 -1
  171. package/dist/op/activities/index.js +5 -1
  172. package/dist/op/activities/live-detect.d.ts +4 -3
  173. package/dist/op/activities/live-detect.d.ts.map +1 -1
  174. package/dist/op/activities/live-detect.js +57 -32
  175. package/dist/op/activities/pin-rollout.d.ts +2 -0
  176. package/dist/op/activities/pin-rollout.d.ts.map +1 -1
  177. package/dist/op/activities/pin-rollout.js +1 -0
  178. package/dist/op/activities/terraform.d.ts +182 -5
  179. package/dist/op/activities/terraform.d.ts.map +1 -1
  180. package/dist/op/activities/terraform.js +224 -24
  181. package/dist/op/activity-contracts.d.ts +27 -0
  182. package/dist/op/activity-contracts.d.ts.map +1 -1
  183. package/dist/op/activity-contracts.js +19 -1
  184. package/dist/op/builders.d.ts +7 -1
  185. package/dist/op/builders.d.ts.map +1 -1
  186. package/dist/op/builders.js +9 -0
  187. package/dist/pin/consumers.d.ts +35 -0
  188. package/dist/pin/consumers.d.ts.map +1 -0
  189. package/dist/pin/consumers.js +70 -0
  190. package/dist/pin/edit.d.ts.map +1 -1
  191. package/dist/pin/edit.js +35 -0
  192. package/dist/pin/rollout.d.ts +2 -0
  193. package/dist/pin/rollout.d.ts.map +1 -1
  194. package/dist/pin/rollout.js +14 -11
  195. package/dist/plan-shape.d.ts +30 -0
  196. package/dist/plan-shape.d.ts.map +1 -0
  197. package/dist/plan-shape.js +125 -0
  198. package/dist/plugin.d.ts.map +1 -1
  199. package/dist/plugin.js +37 -18
  200. package/dist/registry/address.d.ts +63 -0
  201. package/dist/registry/address.d.ts.map +1 -0
  202. package/dist/registry/address.js +65 -0
  203. package/dist/registry/catalog-read.d.ts +14 -0
  204. package/dist/registry/catalog-read.d.ts.map +1 -0
  205. package/dist/registry/catalog-read.js +58 -0
  206. package/dist/registry/deprecated.d.ts +37 -0
  207. package/dist/registry/deprecated.d.ts.map +1 -0
  208. package/dist/registry/deprecated.js +43 -0
  209. package/dist/registry/docs.d.ts +96 -0
  210. package/dist/registry/docs.d.ts.map +1 -0
  211. package/dist/registry/docs.js +210 -0
  212. package/dist/registry/edge/edge.d.ts +46 -0
  213. package/dist/registry/edge/edge.d.ts.map +1 -0
  214. package/dist/registry/edge/edge.js +194 -0
  215. package/dist/registry/edge/index.d.ts +11 -0
  216. package/dist/registry/edge/index.d.ts.map +1 -0
  217. package/dist/registry/edge/index.js +10 -0
  218. package/dist/registry/edge/lambda-edge.d.ts +76 -0
  219. package/dist/registry/edge/lambda-edge.d.ts.map +1 -0
  220. package/dist/registry/edge/lambda-edge.js +95 -0
  221. package/dist/registry/edge/sigv4-web.d.ts +33 -0
  222. package/dist/registry/edge/sigv4-web.d.ts.map +1 -0
  223. package/dist/registry/edge/sigv4-web.js +56 -0
  224. package/dist/registry/edge/token-source.d.ts +73 -0
  225. package/dist/registry/edge/token-source.d.ts.map +1 -0
  226. package/dist/registry/edge/token-source.js +115 -0
  227. package/dist/registry/edge/worker.d.ts +45 -0
  228. package/dist/registry/edge/worker.d.ts.map +1 -0
  229. package/dist/registry/edge/worker.js +26 -0
  230. package/dist/registry/handler.d.ts +25 -0
  231. package/dist/registry/handler.d.ts.map +1 -0
  232. package/dist/registry/handler.js +75 -0
  233. package/dist/registry/headers-file.d.ts +21 -0
  234. package/dist/registry/headers-file.d.ts.map +1 -0
  235. package/dist/registry/headers-file.js +57 -0
  236. package/dist/registry/index.d.ts +31 -0
  237. package/dist/registry/index.d.ts.map +1 -0
  238. package/dist/registry/index.js +30 -0
  239. package/dist/registry/mirror-paths.d.ts +9 -0
  240. package/dist/registry/mirror-paths.d.ts.map +1 -0
  241. package/dist/registry/mirror-paths.js +8 -0
  242. package/dist/registry/mirror.d.ts +130 -0
  243. package/dist/registry/mirror.d.ts.map +1 -0
  244. package/dist/registry/mirror.js +319 -0
  245. package/dist/registry/proxies.d.ts +73 -0
  246. package/dist/registry/proxies.d.ts.map +1 -0
  247. package/dist/registry/proxies.js +111 -0
  248. package/dist/registry/reader.d.ts +57 -0
  249. package/dist/registry/reader.d.ts.map +1 -0
  250. package/dist/registry/reader.js +198 -0
  251. package/dist/registry/semver.d.ts +16 -0
  252. package/dist/registry/semver.d.ts.map +1 -0
  253. package/dist/registry/semver.js +24 -0
  254. package/dist/registry/serve.d.ts +24 -0
  255. package/dist/registry/serve.d.ts.map +1 -0
  256. package/dist/registry/serve.js +64 -0
  257. package/dist/registry/source-forms.d.ts +79 -0
  258. package/dist/registry/source-forms.d.ts.map +1 -0
  259. package/dist/registry/source-forms.js +202 -0
  260. package/dist/registry/targets/artifactory.d.ts +28 -0
  261. package/dist/registry/targets/artifactory.d.ts.map +1 -0
  262. package/dist/registry/targets/artifactory.js +206 -0
  263. package/dist/registry/targets/fake-artifactory.d.ts +30 -0
  264. package/dist/registry/targets/fake-artifactory.d.ts.map +1 -0
  265. package/dist/registry/targets/fake-artifactory.js +142 -0
  266. package/dist/registry/targets/fake-gitlab.d.ts +39 -0
  267. package/dist/registry/targets/fake-gitlab.d.ts.map +1 -0
  268. package/dist/registry/targets/fake-gitlab.js +136 -0
  269. package/dist/registry/targets/fake-hcp.d.ts +50 -0
  270. package/dist/registry/targets/fake-hcp.d.ts.map +1 -0
  271. package/dist/registry/targets/fake-hcp.js +202 -0
  272. package/dist/registry/targets/fake-http.d.ts +31 -0
  273. package/dist/registry/targets/fake-http.d.ts.map +1 -0
  274. package/dist/registry/targets/fake-http.js +87 -0
  275. package/dist/registry/targets/fake-nexus.d.ts +27 -0
  276. package/dist/registry/targets/fake-nexus.d.ts.map +1 -0
  277. package/dist/registry/targets/fake-nexus.js +137 -0
  278. package/dist/registry/targets/gitlab.d.ts +53 -0
  279. package/dist/registry/targets/gitlab.d.ts.map +1 -0
  280. package/dist/registry/targets/gitlab.js +187 -0
  281. package/dist/registry/targets/hcp.d.ts +29 -0
  282. package/dist/registry/targets/hcp.d.ts.map +1 -0
  283. package/dist/registry/targets/hcp.js +310 -0
  284. package/dist/registry/targets/hosted.d.ts +62 -0
  285. package/dist/registry/targets/hosted.d.ts.map +1 -0
  286. package/dist/registry/targets/hosted.js +74 -0
  287. package/dist/registry/targets/index.d.ts +18 -0
  288. package/dist/registry/targets/index.d.ts.map +1 -0
  289. package/dist/registry/targets/index.js +17 -0
  290. package/dist/registry/targets/module-target.d.ts +68 -0
  291. package/dist/registry/targets/module-target.d.ts.map +1 -0
  292. package/dist/registry/targets/module-target.js +63 -0
  293. package/dist/registry/targets/nexus.d.ts +43 -0
  294. package/dist/registry/targets/nexus.d.ts.map +1 -0
  295. package/dist/registry/targets/nexus.js +250 -0
  296. package/dist/registry/targets/zip.d.ts +55 -0
  297. package/dist/registry/targets/zip.d.ts.map +1 -0
  298. package/dist/registry/targets/zip.js +278 -0
  299. package/dist/registry/writer.d.ts +101 -0
  300. package/dist/registry/writer.d.ts.map +1 -0
  301. package/dist/registry/writer.js +109 -0
  302. package/dist/report.d.ts +15 -6
  303. package/dist/report.d.ts.map +1 -1
  304. package/dist/report.js +28 -55
  305. package/dist/rules/record-store.ts +122 -0
  306. package/dist/rules/tf001.ts +2 -2
  307. package/dist/rules/tf024.ts +105 -22
  308. package/dist/rules/tf046.ts +204 -0
  309. package/dist/rules/tf047.ts +114 -0
  310. package/dist/skills/chant-terraform.md +1 -1
  311. package/dist/state/azure-blob.d.ts +9 -0
  312. package/dist/state/azure-blob.d.ts.map +1 -0
  313. package/dist/state/azure-blob.js +8 -0
  314. package/dist/state/backend.d.ts +158 -0
  315. package/dist/state/backend.d.ts.map +1 -0
  316. package/dist/state/backend.js +406 -0
  317. package/dist/state/exec.d.ts +39 -0
  318. package/dist/state/exec.d.ts.map +1 -0
  319. package/dist/state/exec.js +80 -0
  320. package/dist/state/gcs.d.ts +8 -0
  321. package/dist/state/gcs.d.ts.map +1 -0
  322. package/dist/state/gcs.js +7 -0
  323. package/dist/state/index.d.ts +15 -0
  324. package/dist/state/index.d.ts.map +1 -0
  325. package/dist/state/index.js +14 -0
  326. package/dist/state/locks.d.ts +105 -0
  327. package/dist/state/locks.d.ts.map +1 -0
  328. package/dist/state/locks.js +130 -0
  329. package/dist/state/object-store.d.ts +8 -0
  330. package/dist/state/object-store.d.ts.map +1 -0
  331. package/dist/state/object-store.js +7 -0
  332. package/dist/state/root-states.d.ts +131 -0
  333. package/dist/state/root-states.d.ts.map +1 -0
  334. package/dist/state/root-states.js +241 -0
  335. package/dist/state/s3.d.ts +21 -0
  336. package/dist/state/s3.d.ts.map +1 -0
  337. package/dist/state/s3.js +19 -0
  338. package/dist/state/store.d.ts +51 -0
  339. package/dist/state/store.d.ts.map +1 -0
  340. package/dist/state/store.js +185 -0
  341. package/dist/state-ops.d.ts +255 -0
  342. package/dist/state-ops.d.ts.map +1 -0
  343. package/dist/state-ops.js +305 -0
  344. package/dist/state-reads.d.ts +57 -0
  345. package/dist/state-reads.d.ts.map +1 -0
  346. package/dist/state-reads.js +104 -0
  347. package/dist/terragrunt/affected.d.ts.map +1 -1
  348. package/dist/terragrunt/affected.js +2 -1
  349. package/dist/used-sources.d.ts +54 -0
  350. package/dist/used-sources.d.ts.map +1 -0
  351. package/dist/used-sources.js +157 -0
  352. package/package.json +24 -4
  353. package/src/__fixtures__/ACCEPTANCE.md +69 -7
  354. package/src/__fixtures__/compose/apply-failed-in-app.jsonl +12 -0
  355. package/src/__fixtures__/compose/state-app.json +66 -0
  356. package/src/__fixtures__/compose/state-edge.json +40 -0
  357. package/src/__fixtures__/compose/state-net.json +58 -0
  358. package/src/__fixtures__/compose/three-app.plan.json +184 -0
  359. package/src/__fixtures__/compose/three-composed.plan.json +509 -0
  360. package/src/__fixtures__/compose/three-edge.plan.json +144 -0
  361. package/src/__fixtures__/compose/three-net.plan.json +174 -0
  362. package/src/__fixtures__/compose/unknown-composed.plan.json +228 -0
  363. package/src/__fixtures__/compose/working-after-apply.json +145 -0
  364. package/src/__fixtures__/config-blocks/app/main.tf +61 -0
  365. package/src/__fixtures__/config-blocks/app/modules/network/main.tf +4 -0
  366. package/src/__fixtures__/config-blocks/app-json/main.tf.json +39 -0
  367. package/src/__fixtures__/config-files/shadow/main.tf +3 -0
  368. package/src/__fixtures__/config-files/shadow/main.tofu +3 -0
  369. package/src/__fixtures__/config-files/shadow/other.tf +3 -0
  370. package/src/__fixtures__/config-files/tf/main.tf +11 -0
  371. package/src/__fixtures__/config-files/tf-json/main.tf.json +18 -0
  372. package/src/__fixtures__/config-files/tofu/main.tofu +11 -0
  373. package/src/__fixtures__/config-files/tofu-json/main.tofu.json +18 -0
  374. package/src/__fixtures__/config-files/tofu-only/main.tofu +3 -0
  375. package/src/__fixtures__/drift-record/README.md +16 -0
  376. package/src/__fixtures__/drift-record/live-plan-sections.json +27 -0
  377. package/src/__fixtures__/drift-rows/live-root-plan.json +70 -0
  378. package/src/__fixtures__/drift-rows/refresh-plan.json +61 -0
  379. package/src/__fixtures__/instance-forms/app/main.tf +110 -0
  380. package/src/__fixtures__/instance-forms/app/modules/svc/main.tf +3 -0
  381. package/src/__fixtures__/instance-vars/app/main.tf +52 -0
  382. package/src/__fixtures__/instance-vars/app/modules/app/main.tf +12 -0
  383. package/src/__fixtures__/instance-vars/app/modules/edge/main.tf +3 -0
  384. package/src/__fixtures__/instance-vars/app/terraform.tfvars +1 -0
  385. package/src/__fixtures__/live-module-keyed/live-graph.json +259 -0
  386. package/src/__fixtures__/live-module-keyed/live-plan.json +87 -0
  387. package/src/__fixtures__/live-module-keyed/root/estate.chdf.hcl +1 -0
  388. package/src/__fixtures__/live-module-keyed/root/main.tf +21 -0
  389. package/src/__fixtures__/live-module-keyed/root/modules/app/main.tf +17 -0
  390. package/src/__fixtures__/live-module-keyed/root/modules/edge/main.tf +7 -0
  391. package/src/__fixtures__/live-module-keyed/root/versions.tf +30 -0
  392. package/src/__fixtures__/live-module-keyed/show-state.json +146 -0
  393. package/src/__fixtures__/pr-loop/README.md +50 -0
  394. package/src/__fixtures__/pr-loop/chant.config.json +13 -0
  395. package/src/__fixtures__/pr-loop/estate.component.ts +10 -0
  396. package/src/__fixtures__/pr-loop/roots/a/main.tf +11 -0
  397. package/src/__fixtures__/pr-loop/roots/app/main.tf +19 -0
  398. package/src/__fixtures__/pr-loop/roots/b/main.tf +7 -0
  399. package/src/__fixtures__/pr-loop/roots/dns/main.tf +3 -0
  400. package/src/__fixtures__/pr-loop/roots/net/main.tf +7 -0
  401. package/src/__fixtures__/report/applied.show.json +51 -0
  402. package/src/__fixtures__/report/generate.ts +63 -0
  403. package/src/__fixtures__/report/plan.show.json +200 -0
  404. package/src/__fixtures__/report/tf-apply/change-set.json +170 -0
  405. package/src/__fixtures__/report/tf-apply/report.json +338 -0
  406. package/src/__fixtures__/report/tf-plan/change-set.json +170 -0
  407. package/src/__fixtures__/report/tf-plan/report.json +320 -0
  408. package/src/__fixtures__/three-cloud-azure.graph.json +290 -0
  409. package/src/__fixtures__/used-sources/root/extra.tofu +1 -0
  410. package/src/__fixtures__/used-sources/root/main.tf +31 -0
  411. package/src/__fixtures__/used-sources/root/modules/net/main.tf +14 -0
  412. package/src/__fixtures__/used-sources/root/modules/net/versions.tf.json +7 -0
  413. package/src/__fixtures__/used-sources/unit/terragrunt.hcl +3 -0
  414. package/src/applied-state.ts +52 -0
  415. package/src/backend-config.test.ts +105 -0
  416. package/src/backend-config.ts +133 -0
  417. package/src/behaviour/kinds.ts +4 -0
  418. package/src/behaviour/predict.ts +5 -2
  419. package/src/behaviour/reference-catalog.plugin.test.ts +65 -0
  420. package/src/behaviour/reference-catalog.ts +16 -5
  421. package/src/behaviour/request.ts +3 -2
  422. package/src/change-set-instances.test.ts +98 -0
  423. package/src/changed-roots.ts +14 -16
  424. package/src/ci-setup.ts +42 -0
  425. package/src/commands.ts +4 -3
  426. package/src/components/capability-plugin.test.ts +198 -19
  427. package/src/components/capability-plugin.ts +383 -35
  428. package/src/components/change-set-graph.e2e.test.ts +11 -5
  429. package/src/components/compose-group.test.ts +486 -0
  430. package/src/components/compose-group.ts +489 -0
  431. package/src/components/demo-composed-roots.e2e.test.ts +440 -0
  432. package/src/components/member-outputs.test.ts +76 -0
  433. package/src/components/placeholder-preview.test.ts +147 -0
  434. package/src/components/placeholder-preview.ts +257 -0
  435. package/src/components/pr-loop-compose.tofu.e2e.test.ts +363 -0
  436. package/src/components/pr-loop.tofu.test.ts +113 -57
  437. package/src/components/release-inputs.ts +69 -0
  438. package/src/components/trace-facts.test.ts +21 -0
  439. package/src/components/trace-facts.ts +27 -0
  440. package/src/compose/apply-events.test.ts +62 -0
  441. package/src/compose/apply-events.ts +100 -0
  442. package/src/compose/estate.choudoufu.e2e.test.ts +112 -0
  443. package/src/compose/estate.test.ts +421 -0
  444. package/src/compose/estate.ts +443 -0
  445. package/src/compose/hcl.ts +311 -0
  446. package/src/compose/module-address.ts +49 -0
  447. package/src/compose/root.test.ts +416 -0
  448. package/src/compose/root.ts +592 -0
  449. package/src/compose/run.test.ts +440 -0
  450. package/src/compose/run.tofu.test.ts +176 -0
  451. package/src/compose/run.ts +527 -0
  452. package/src/compose/slice.test.ts +184 -0
  453. package/src/compose/slice.ts +290 -0
  454. package/src/compose/state-address.ts +157 -0
  455. package/src/compose/state-merge.test.ts +131 -0
  456. package/src/compose/state-merge.ts +148 -0
  457. package/src/compose.test.ts +248 -0
  458. package/src/compose.ts +627 -0
  459. package/src/composites/terraform-apply-op.test.ts +9 -0
  460. package/src/composites/terraform-apply-op.ts +7 -3
  461. package/src/cone.ts +138 -0
  462. package/src/config-file-listers.test.ts +265 -0
  463. package/src/config.ts +44 -9
  464. package/src/cost-hold.ts +29 -0
  465. package/src/describe-resources.engine-facts.test.ts +411 -0
  466. package/src/describe-resources.live-module-keyed.test.ts +290 -0
  467. package/src/describe-resources.ts +416 -70
  468. package/src/drift-read.ts +284 -0
  469. package/src/drift-record.test.ts +223 -0
  470. package/src/drift-record.ts +455 -0
  471. package/src/drift-rows.test.ts +135 -0
  472. package/src/drift-rows.ts +137 -0
  473. package/src/exit-codes.test.ts +23 -0
  474. package/src/exit-codes.ts +59 -0
  475. package/src/hcl/config-blocks.test.ts +174 -0
  476. package/src/hcl/edges.ts +21 -1
  477. package/src/hcl/files.ts +234 -0
  478. package/src/hcl/instance-forms.test.ts +94 -0
  479. package/src/hcl/instances.test.ts +137 -0
  480. package/src/hcl/instances.ts +422 -0
  481. package/src/hcl/parse.test.ts +139 -1
  482. package/src/hcl/parse.ts +149 -16
  483. package/src/hcl/roots.ts +6 -0
  484. package/src/hcl/suppressions.ts +8 -2
  485. package/src/index.ts +79 -0
  486. package/src/lint/audit-catalog.ts +29 -3
  487. package/src/lint/audit-lineage.ts +14 -2
  488. package/src/lint/post-synth/fixtures/TF024/live-with-record-store/main.tf +18 -0
  489. package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/estate.chdf.hcl +1 -0
  490. package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/main.tf +10 -0
  491. package/src/lint/post-synth/fixtures/TF046/azurerm-no-resource-group/main.tf +15 -0
  492. package/src/lint/post-synth/fixtures/TF046/backend-config-only/main.tf +14 -0
  493. package/src/lint/post-synth/fixtures/TF046/derived-s3/backend.tf +7 -0
  494. package/src/lint/post-synth/fixtures/TF046/derived-s3/main.tf +9 -0
  495. package/src/lint/post-synth/fixtures/TF046/explicit-local/main.tf +16 -0
  496. package/src/lint/post-synth/fixtures/TF046/explicit-s3-beside-local-backend/main.tf +15 -0
  497. package/src/lint/post-synth/fixtures/TF046/implied-local/main.tf +9 -0
  498. package/src/lint/post-synth/fixtures/TF046/sidecar-implied/estate.chdf.hcl +1 -0
  499. package/src/lint/post-synth/fixtures/TF046/sidecar-implied/main.tf +5 -0
  500. package/src/lint/post-synth/fixtures/TF047/clear/main.tf +12 -0
  501. package/src/lint/post-synth/fixtures/TF047/refused/main.tf +12 -0
  502. package/src/lint/post-synth/fixtures/TF047/unknown-module/main.tf +12 -0
  503. package/src/lint/post-synth/fixtures/TF047/unknown-version/main.tf +12 -0
  504. package/src/lint/post-synth/index.ts +4 -0
  505. package/src/lint/post-synth/record-store.ts +122 -0
  506. package/src/lint/post-synth/tf001.ts +2 -2
  507. package/src/lint/post-synth/tf024.test.ts +26 -5
  508. package/src/lint/post-synth/tf024.ts +105 -22
  509. package/src/lint/post-synth/tf046.test.ts +137 -0
  510. package/src/lint/post-synth/tf046.ts +204 -0
  511. package/src/lint/post-synth/tf047.test.ts +56 -0
  512. package/src/lint/post-synth/tf047.ts +114 -0
  513. package/src/live-plan-reasons.ts +54 -0
  514. package/src/lock-rows.test.ts +70 -0
  515. package/src/lock-rows.ts +82 -0
  516. package/src/lock-timeout.ts +34 -0
  517. package/src/lsp/completions.test.ts +1 -1
  518. package/src/lsp/option-keys.ts +2 -0
  519. package/src/module-catalog/index.test.ts +93 -0
  520. package/src/module-catalog/index.ts +159 -0
  521. package/src/module-catalog/refresh-cli.ts +68 -0
  522. package/src/module-catalog/static-tier.ts +1659 -0
  523. package/src/module-catalog/types.ts +88 -0
  524. package/src/op/activities/choudoufu-plan-set-op.test.ts +111 -0
  525. package/src/op/activities/index.ts +9 -0
  526. package/src/op/activities/live-detect.ts +55 -26
  527. package/src/op/activities/pin-rollout.ts +3 -0
  528. package/src/op/activities/registry.test.ts +2 -1
  529. package/src/op/activities/terraform.test.ts +55 -0
  530. package/src/op/activities/terraform.ts +312 -27
  531. package/src/op/activity-contracts.test.ts +1 -0
  532. package/src/op/activity-contracts.ts +24 -1
  533. package/src/op/builders.ts +14 -0
  534. package/src/op-wave-step.test.ts +109 -0
  535. package/src/pin/__fixtures__/consumers/live/dev/main.tf +4 -0
  536. package/src/pin/__fixtures__/consumers/live/prod/main.tf +21 -0
  537. package/src/pin/__fixtures__/consumers/units/vpc/terragrunt.hcl +7 -0
  538. package/src/pin/bundle.test.ts +2 -0
  539. package/src/pin/consumers.test.ts +57 -0
  540. package/src/pin/consumers.ts +95 -0
  541. package/src/pin/edit.ts +30 -0
  542. package/src/pin/rollout.ts +16 -11
  543. package/src/plan-shape.test.ts +148 -0
  544. package/src/plan-shape.ts +128 -0
  545. package/src/plugin.ts +39 -19
  546. package/src/registry/__fixtures__/docs/documented/.terraform-docs.yml +3 -0
  547. package/src/registry/__fixtures__/docs/documented/README.md +1 -0
  548. package/src/registry/__fixtures__/docs/documented/main.tf +8 -0
  549. package/src/registry/__fixtures__/docs/json/main.tf.json +14 -0
  550. package/src/registry/__fixtures__/docs/vpc/README.md +3 -0
  551. package/src/registry/__fixtures__/docs/vpc/examples/complete/main.tf +5 -0
  552. package/src/registry/__fixtures__/docs/vpc/examples/simple/README.md +1 -0
  553. package/src/registry/__fixtures__/docs/vpc/examples/simple/main.tf +4 -0
  554. package/src/registry/__fixtures__/docs/vpc/main.tf +14 -0
  555. package/src/registry/__fixtures__/docs/vpc/outputs.tf +9 -0
  556. package/src/registry/__fixtures__/docs/vpc/variables.tf +16 -0
  557. package/src/registry/__fixtures__/proxied-root/main.tf +12 -0
  558. package/src/registry/address.ts +100 -0
  559. package/src/registry/catalog-read.ts +56 -0
  560. package/src/registry/deprecated.test.ts +139 -0
  561. package/src/registry/deprecated.ts +76 -0
  562. package/src/registry/docs.test.ts +140 -0
  563. package/src/registry/docs.ts +299 -0
  564. package/src/registry/edge/bundle.test.ts +50 -0
  565. package/src/registry/edge/edge.test.ts +176 -0
  566. package/src/registry/edge/edge.ts +211 -0
  567. package/src/registry/edge/index.ts +10 -0
  568. package/src/registry/edge/lambda-edge.ts +127 -0
  569. package/src/registry/edge/sigv4-web.ts +79 -0
  570. package/src/registry/edge/token-source.test.ts +258 -0
  571. package/src/registry/edge/token-source.ts +169 -0
  572. package/src/registry/edge/worker.ts +61 -0
  573. package/src/registry/handler.ts +89 -0
  574. package/src/registry/headers-file.ts +61 -0
  575. package/src/registry/index.ts +31 -0
  576. package/src/registry/mirror-paths.ts +10 -0
  577. package/src/registry/mirror.test.ts +198 -0
  578. package/src/registry/mirror.ts +395 -0
  579. package/src/registry/proxies.test.ts +153 -0
  580. package/src/registry/proxies.ts +132 -0
  581. package/src/registry/reader.ts +225 -0
  582. package/src/registry/registry.test.ts +262 -0
  583. package/src/registry/semver.ts +34 -0
  584. package/src/registry/serve.ts +82 -0
  585. package/src/registry/source-forms.test.ts +87 -0
  586. package/src/registry/source-forms.ts +216 -0
  587. package/src/registry/targets/artifactory.test.ts +248 -0
  588. package/src/registry/targets/artifactory.ts +257 -0
  589. package/src/registry/targets/fake-artifactory.ts +158 -0
  590. package/src/registry/targets/fake-gitlab.ts +161 -0
  591. package/src/registry/targets/fake-hcp.ts +233 -0
  592. package/src/registry/targets/fake-http.ts +97 -0
  593. package/src/registry/targets/fake-nexus.ts +152 -0
  594. package/src/registry/targets/gitlab.test.ts +208 -0
  595. package/src/registry/targets/gitlab.ts +223 -0
  596. package/src/registry/targets/hcp.test.ts +230 -0
  597. package/src/registry/targets/hcp.ts +360 -0
  598. package/src/registry/targets/hosted.ts +117 -0
  599. package/src/registry/targets/index.ts +17 -0
  600. package/src/registry/targets/module-target.ts +131 -0
  601. package/src/registry/targets/nexus.test.ts +220 -0
  602. package/src/registry/targets/nexus.ts +314 -0
  603. package/src/registry/targets/zip.test.ts +65 -0
  604. package/src/registry/targets/zip.ts +287 -0
  605. package/src/registry/writer.ts +155 -0
  606. package/src/report.test.ts +82 -1
  607. package/src/report.ts +31 -54
  608. package/src/signal-only-root.test.ts +72 -0
  609. package/src/skills/chant-terraform.md +1 -1
  610. package/src/state/azure-blob.ts +8 -0
  611. package/src/state/backend-verdict.test.ts +187 -0
  612. package/src/state/backend.test.ts +127 -0
  613. package/src/state/backend.ts +488 -0
  614. package/src/state/exec.ts +111 -0
  615. package/src/state/gcs.ts +7 -0
  616. package/src/state/index.ts +15 -0
  617. package/src/state/locks.test.ts +84 -0
  618. package/src/state/locks.ts +201 -0
  619. package/src/state/object-store.ts +7 -0
  620. package/src/state/root-states-floci.e2e.test.ts +66 -0
  621. package/src/state/root-states.test.ts +462 -0
  622. package/src/state/root-states.ts +309 -0
  623. package/src/state/s3.ts +21 -0
  624. package/src/state/store.ts +225 -0
  625. package/src/state-ops.test.ts +182 -0
  626. package/src/state-ops.ts +462 -0
  627. package/src/state-reads.ts +116 -0
  628. package/src/terragrunt/affected.ts +2 -1
  629. package/src/used-sources.test.ts +75 -0
  630. package/src/used-sources.ts +204 -0
  631. package/src/workspace-kinds.test.ts +4 -17
  632. package/workspace-kinds.json +1 -24
  633. package/dist/workspace-member.d.ts +0 -30
  634. package/dist/workspace-member.d.ts.map +0 -1
  635. package/dist/workspace-member.js +0 -91
  636. package/schemas/pulumi.schema.json +0 -34
  637. package/src/testdata/pulumi-member/Pulumi.yaml +0 -2
  638. package/src/testdata/pulumi-member/stack-export.json +0 -53
  639. package/src/testdata/pulumi-unexported/Pulumi.yaml +0 -2
  640. package/src/workspace-member.test.ts +0 -89
  641. package/src/workspace-member.ts +0 -126
@@ -0,0 +1,208 @@
1
+ // chant#4603: the GitLab module target against the in-memory GitLab fake. No request leaves the
2
+ // process, and no token value appears in a log line, an error or a staged record.
3
+ import { gzipSync } from "node:zlib";
4
+ import { describe, expect, it } from "vitest";
5
+ import { PublishTargetError } from "@intentius/chant/lifecycle/release-publish";
6
+ import { createRedactor, REDACTED } from "@intentius/chant/lifecycle/publish-credentials";
7
+ import { archiveContent, gitlabModuleTarget, gitlabUploadUrl, type ModuleVersionContext } from "../index";
8
+ import { createFakeGitlab } from "./fake-gitlab";
9
+
10
+ const HOST = "gitlab.example.com";
11
+ const API = `https://${HOST}/api/v4`;
12
+ const WRITE_TOKEN = "glpat-write-0123456789";
13
+ const READ_TOKEN = "glpat-read-9876543210";
14
+ const JOB_TOKEN = "job-token-abcdefghij";
15
+ const OTHER_TOKEN = "glpat-other-project-0000";
16
+
17
+ function fake() {
18
+ return createFakeGitlab({
19
+ host: HOST,
20
+ projects: { "42": "acme/modules", "7": "elsewhere/other" },
21
+ tokens: { [WRITE_TOKEN]: ["42"], [READ_TOKEN]: [], [JOB_TOKEN]: ["42"], [OTHER_TOKEN]: ["7"] },
22
+ });
23
+ }
24
+
25
+ const archiveOf = (text: string) => gzipSync(Buffer.from(`module tar: ${text}`));
26
+
27
+ function context(env: NodeJS.ProcessEnv, over: Partial<ModuleVersionContext> = {}): ModuleVersionContext & { lines: string[] } {
28
+ const archive = over.archive ?? archiveOf("vpc 1.2.0");
29
+ const redactor = createRedactor(env);
30
+ const lines: string[] = [];
31
+ return {
32
+ cwd: "/nowhere",
33
+ env,
34
+ redactor,
35
+ log: (line) => lines.push(redactor.redact(line)),
36
+ provenance: { oidc: false, ci: null, reason: "not in CI" },
37
+ tag: null,
38
+ address: { namespace: "acme", name: "vpc", system: "aws" },
39
+ version: "1.2.0",
40
+ revision: "3f2a9c1d0b7e3f2a9c1d0b7e3f2a9c1d0b7e3f2a",
41
+ content: archiveContent(archive),
42
+ archive,
43
+ ...over,
44
+ lines,
45
+ };
46
+ }
47
+
48
+ const target = (gl: ReturnType<typeof fake>, opts = {}) => gitlabModuleTarget({ api: API, project: "42", ...opts }, { fetch: gl.fetch, registryFetch: gl.fetch });
49
+
50
+ const errorOf = async (p: Promise<unknown>): Promise<PublishTargetError> => {
51
+ const e = await p.then(
52
+ () => undefined,
53
+ (err: unknown) => err,
54
+ );
55
+ expect(e).toBeInstanceOf(PublishTargetError);
56
+ return e as PublishTargetError;
57
+ };
58
+
59
+ describe("GitLab module target (chant#4603)", () => {
60
+ it("uploads with one PUT to the project's module file path, with the token header and the archive as the body", async () => {
61
+ const gl = fake();
62
+ const ctx = context({ GITLAB_TOKEN: WRITE_TOKEN });
63
+ const t = target(gl);
64
+ expect(await t.plan(ctx)).toEqual([{ name: "acme/vpc/aws 1.2.0", destination: `${HOST}/acme/vpc/aws`, digest: ctx.content }]);
65
+ const staged = await t.stage(ctx);
66
+ const puts = gl.requests.filter((r) => r.method === "PUT");
67
+ expect(puts).toHaveLength(1);
68
+ expect(puts[0]!.url).toBe(`${API}/projects/42/packages/terraform/modules/vpc/aws/1.2.0/file`);
69
+ expect(puts[0]!.headers["private-token"]).toBe(WRITE_TOKEN);
70
+ expect(puts[0]!.headers["job-token"]).toBeUndefined();
71
+ expect(puts[0]!.headers["content-type"]).toBe("application/octet-stream");
72
+ expect(Buffer.from(puts[0]!.body!)).toEqual(Buffer.from(ctx.archive));
73
+ expect(staged.items[0]!.present).toBeUndefined();
74
+ expect(JSON.stringify(staged)).not.toContain(WRITE_TOKEN);
75
+
76
+ const published = await t.reveal(ctx, staged);
77
+ expect(published.destination).toBe(`${HOST}/acme/vpc/aws`);
78
+ expect(await t.versions(ctx.address, ctx)).toEqual(["1.2.0"]);
79
+ });
80
+
81
+ it("sends a CI job token as JOB-TOKEN, with the project and API root from GitLab CI's variables", async () => {
82
+ const gl = fake();
83
+ const ctx = context({ CI_JOB_TOKEN: JOB_TOKEN, CI_PROJECT_ID: "42", CI_API_V4_URL: API });
84
+ const t = gitlabModuleTarget({}, { fetch: gl.fetch, registryFetch: gl.fetch });
85
+ await t.stage(ctx);
86
+ const put = gl.requests.find((r) => r.method === "PUT")!;
87
+ expect(put.headers["job-token"]).toBe(JOB_TOKEN);
88
+ expect(put.headers["private-token"]).toBeUndefined();
89
+ expect(put.url).toBe(gitlabUploadUrl(API, "42", ctx.address, "1.2.0"));
90
+ });
91
+
92
+ it("encodes a project given by its full path", () => {
93
+ expect(gitlabUploadUrl(API, "acme/modules", { name: "vpc", system: "aws" }, "1.2.0")).toBe(`${API}/projects/acme%2Fmodules/packages/terraform/modules/vpc/aws/1.2.0/file`);
94
+ });
95
+
96
+ it("comes back present and sends nothing when the version is held with the same content", async () => {
97
+ const gl = fake();
98
+ const ctx = context({ GITLAB_TOKEN: WRITE_TOKEN });
99
+ gl.seed("42", "vpc", "aws", "1.2.0", ctx.archive);
100
+ const t = target(gl);
101
+ expect(await t.held(ctx.address, "1.2.0", ctx)).toEqual({ content: ctx.content });
102
+ const staged = await t.stage(ctx);
103
+ expect(staged.items[0]!.present).toBe(true);
104
+ expect(gl.requests.filter((r) => r.method === "PUT")).toHaveLength(0);
105
+ expect(ctx.lines.some((l) => l.includes("nothing sent"))).toBe(true);
106
+ });
107
+
108
+ it("refuses a version held with other content, and sends nothing", async () => {
109
+ const gl = fake();
110
+ const ctx = context({ GITLAB_TOKEN: WRITE_TOKEN });
111
+ gl.seed("42", "vpc", "aws", "1.2.0", archiveOf("something else"));
112
+ const err = await errorOf(target(gl).stage(ctx));
113
+ expect(err.code).toBe("publish-version-held");
114
+ expect(err.message).toContain(archiveContent(archiveOf("something else")));
115
+ expect(gl.requests.filter((r) => r.method === "PUT")).toHaveLength(0);
116
+ });
117
+
118
+ it("answers held with undefined for a version the host does not list", async () => {
119
+ const gl = fake();
120
+ const ctx = context({ GITLAB_TOKEN: WRITE_TOKEN });
121
+ expect(await target(gl).held(ctx.address, "9.9.9", ctx)).toBeUndefined();
122
+ expect(await target(gl).versions(ctx.address, ctx)).toEqual([]);
123
+ });
124
+
125
+ it("names the variable on a 401, never its value", async () => {
126
+ const gl = fake();
127
+ const ctx = context({ GITLAB_TOKEN: "glpat-unknown-to-the-fake" }, {});
128
+ gl.seed("42", "other", "aws", "0.1.0", archiveOf("x"));
129
+ // Reads take TF_TOKEN_<host>, so the upload is the call that meets the unknown token.
130
+ const env = { ...ctx.env, TF_TOKEN_gitlab_example_com: READ_TOKEN };
131
+ const c = context(env);
132
+ const err = await errorOf(target(gl).stage(c));
133
+ expect(err.code).toBe("publish-credentials-refused");
134
+ expect(err.message).toContain("401");
135
+ expect(err.message).toContain("GITLAB_TOKEN");
136
+ expect(`${err.message} ${err.fix}`).not.toContain("glpat-unknown-to-the-fake");
137
+ });
138
+
139
+ it("names the variable on a 403, never its value", async () => {
140
+ const gl = fake();
141
+ const ctx = context({ DEPLOY_TOKEN: OTHER_TOKEN, TF_TOKEN_gitlab_example_com: READ_TOKEN });
142
+ const err = await errorOf(target(gl, { tokenEnv: "DEPLOY_TOKEN" }).stage(ctx));
143
+ expect(err.code).toBe("publish-credentials-refused");
144
+ expect(err.message).toContain("403");
145
+ expect(err.message).toContain("DEPLOY_TOKEN");
146
+ expect(`${err.message} ${err.fix}`).not.toContain(OTHER_TOKEN);
147
+ });
148
+
149
+ it("refuses to plan without a token or a project, naming what to set", async () => {
150
+ const gl = fake();
151
+ const none = await errorOf(target(gl).plan(context({})));
152
+ expect(none.code).toBe("publish-credentials-missing");
153
+ expect(none.message).toContain("GITLAB_TOKEN or CI_JOB_TOKEN");
154
+ const noProject = await errorOf(gitlabModuleTarget({ api: API }, { fetch: gl.fetch, registryFetch: gl.fetch }).plan(context({ GITLAB_TOKEN: WRITE_TOKEN })));
155
+ expect(noProject.code).toBe("publish-config-invalid");
156
+ expect(noProject.fix).toContain("CI_PROJECT_ID");
157
+ });
158
+
159
+ it("refuses an archive whose content is not the one the context names", async () => {
160
+ const gl = fake();
161
+ const ctx = context({ GITLAB_TOKEN: WRITE_TOKEN }, { content: "sha256:0000" });
162
+ expect((await errorOf(target(gl).plan(ctx))).code).toBe("publish-artifact-digest-mismatch");
163
+ });
164
+
165
+ it("reads back with TF_TOKEN_<host> as a bearer token, apart from the upload token", async () => {
166
+ const gl = fake();
167
+ const ctx = context({ GITLAB_TOKEN: WRITE_TOKEN, TF_TOKEN_gitlab_example_com: READ_TOKEN });
168
+ const t = target(gl);
169
+ await t.reveal(ctx, await t.stage(ctx));
170
+ const reads = gl.requests.filter((r) => r.method === "GET" && r.url.includes("/packages/terraform/modules/v1/") && !r.url.includes("/file?"));
171
+ expect(reads.length).toBeGreaterThan(0);
172
+ for (const r of reads) expect(r.headers.authorization).toBe(`Bearer ${READ_TOKEN}`);
173
+ const tarball = gl.requests.find((r) => r.url.includes("/file?"));
174
+ // The download link carries its own grant, so no Authorization header goes with it.
175
+ if (tarball) expect(tarball.headers.authorization).toBeUndefined();
176
+ });
177
+
178
+ it("a refused read names TF_TOKEN_<host>, never a value", async () => {
179
+ const gl = fake();
180
+ const ctx = context({ TF_TOKEN_gitlab_example_com: "not-a-known-token-value" });
181
+ const err = await errorOf(target(gl).versions(ctx.address, ctx));
182
+ expect(err.code).toBe("publish-read-failed");
183
+ expect(err.message).toContain("refused the token");
184
+ expect(err.message).not.toContain("not-a-known-token-value");
185
+ });
186
+
187
+ it("masks every token in every log line and error", async () => {
188
+ const gl = fake();
189
+ const ctx = context({ GITLAB_TOKEN: WRITE_TOKEN, TF_TOKEN_gitlab_example_com: READ_TOKEN });
190
+ const t = target(gl);
191
+ await t.reveal(ctx, await t.stage(ctx));
192
+ // A rerun with other content fails; its error must be clean too.
193
+ const other = context(ctx.env, { archive: archiveOf("changed"), content: archiveContent(archiveOf("changed")) });
194
+ const err = await errorOf(t.stage(other));
195
+ const all = [...ctx.lines, ...other.lines, err.message, err.fix ?? ""].join("\n");
196
+ for (const secret of [WRITE_TOKEN, READ_TOKEN]) expect(all).not.toContain(secret);
197
+ expect(ctx.redactor.redact(`PRIVATE-TOKEN: ${WRITE_TOKEN} and ${READ_TOKEN}`)).toBe(`PRIVATE-TOKEN: ${REDACTED} and ${REDACTED}`);
198
+ });
199
+
200
+ it("keeps the download link's grant out of error text", async () => {
201
+ const gl = fake();
202
+ const ctx = context({ GITLAB_TOKEN: WRITE_TOKEN, TF_TOKEN_gitlab_example_com: READ_TOKEN });
203
+ gl.seed("42", "vpc", "aws", "1.2.0", new Uint8Array([1, 2, 3]));
204
+ const err = await errorOf(target(gl).held(ctx.address, "1.2.0", ctx));
205
+ expect(err.message).toContain("?[query]");
206
+ expect(err.message).not.toContain("fake-download-grant");
207
+ });
208
+ });
@@ -0,0 +1,223 @@
1
+ /**
2
+ * GitLab's Terraform module registry as a module target (chant#4603).
3
+ *
4
+ * The one write is GitLab's documented upload
5
+ * (docs.gitlab.com/api/packages/terraform-modules/):
6
+ *
7
+ * PUT /projects/:id/packages/terraform/modules/:name/:system/:version/file
8
+ *
9
+ * with the archive as the body. The token follows chant's forge target
10
+ * (core lifecycle/publish-forge.ts `forgeTokenEnv`): the variable the
11
+ * declaration names, else `GITLAB_TOKEN`, then `CI_JOB_TOKEN`. A job token
12
+ * goes in `JOB-TOKEN` and any other in `PRIVATE-TOKEN`. The API root is the
13
+ * declaration's, else `CI_API_V4_URL`, else gitlab.com's; the project is the
14
+ * declaration's (an id or a full path), else `CI_PROJECT_ID`.
15
+ *
16
+ * GitLab lists a version as soon as it is uploaded, so there is no staging
17
+ * step to hide it: `stage` uploads, and `reveal` reads the version back the
18
+ * way Terraform and OpenTofu do (discovery, versions, download) with the
19
+ * lexicon's registry reader, sending `TF_TOKEN_<host>` (chant#4563), else the
20
+ * upload token, which GitLab also takes as a bearer token on these reads.
21
+ * `held` downloads the version's archive and compares its content digest, so
22
+ * a version holding other content is refused and the same content comes back
23
+ * `present` with nothing sent.
24
+ *
25
+ * GitLab's module API has no call to deprecate a version, so a deprecation
26
+ * stays in the bucket catalog and GitLab shows none. The namespace a GitLab
27
+ * source names is the project's top-level group (`CI_PROJECT_ROOT_NAMESPACE`
28
+ * in CI): the declaration's `namespace`, else the context's.
29
+ */
30
+ import { readCredential, type Credential } from "@intentius/chant/lifecycle/publish-credentials";
31
+ import { PublishTargetError, type PublishContextBase, type PublishItem, type StagedTarget } from "@intentius/chant/lifecycle/release-publish";
32
+ import { terraformCredential } from "@intentius/chant/terraform/credentials";
33
+ import { createHash } from "node:crypto";
34
+ import { RegistryError, fetchTarball, registryLocation, registryVersions, type RegistryReadOptions } from "../reader";
35
+ import {
36
+ answerError,
37
+ checkModuleVersion,
38
+ contentDigest,
39
+ defaultPublishFetch,
40
+ versionHeldError,
41
+ type HeldVersion,
42
+ type ModuleAddress,
43
+ type ModulePublishTarget,
44
+ type ModuleTargetDeps,
45
+ type ModuleVersionContext,
46
+ } from "./module-target";
47
+
48
+ /** How a GitLab module target is declared. Every field is optional in GitLab CI. */
49
+ export interface GitlabModuleTargetOptions {
50
+ /** The project the module is uploaded to: its id or its full path. Default `CI_PROJECT_ID`. */
51
+ project?: string;
52
+ /** The API root, such as `https://gitlab.example.com/api/v4`. Default `CI_API_V4_URL`, else gitlab.com's. */
53
+ api?: string;
54
+ /** The variable the token is read from, in place of `GITLAB_TOKEN` then `CI_JOB_TOKEN`. */
55
+ tokenEnv?: string;
56
+ /** The namespace sources name: the project's top-level group. Default: the context's. */
57
+ namespace?: string;
58
+ }
59
+
60
+ export const GITLAB_DEFAULT_API = "https://gitlab.com/api/v4";
61
+
62
+ /** The variables the upload token is read from, in order. */
63
+ export const gitlabTokenEnv = (o: Pick<GitlabModuleTargetOptions, "tokenEnv">): string[] => (o.tokenEnv ? [o.tokenEnv] : ["GITLAB_TOKEN", "CI_JOB_TOKEN"]);
64
+
65
+ /** The API root a target uploads to, without a trailing slash. */
66
+ export const gitlabApi = (o: Pick<GitlabModuleTargetOptions, "api">, env: NodeJS.ProcessEnv): string => (o.api ?? env.CI_API_V4_URL ?? GITLAB_DEFAULT_API).replace(/\/+$/, "");
67
+
68
+ /** The headers a GitLab token goes in: `JOB-TOKEN` for a CI job token, `PRIVATE-TOKEN` for any other. */
69
+ export const gitlabAuthHeader = (token: Credential): Record<string, string> => (token.from === "CI_JOB_TOKEN" ? { "job-token": token.value } : { "private-token": token.value });
70
+
71
+ /** The upload URL for one version. */
72
+ export function gitlabUploadUrl(api: string, project: string, address: Pick<ModuleAddress, "name" | "system">, version: string): string {
73
+ const id = /^\d+$/.test(project) ? project : encodeURIComponent(project);
74
+ return `${api}/projects/${id}/packages/terraform/modules/${encodeURIComponent(address.name)}/${encodeURIComponent(address.system)}/${encodeURIComponent(version)}/file`;
75
+ }
76
+
77
+ /** A signed URL's query (GitLab's download link carries a token in it) cut from a message. */
78
+ const withoutQueries = (text: string): string => text.replace(/(https?:\/\/[^\s?]+)\?[^\s)"']*/g, "$1?[query]");
79
+
80
+ export function gitlabModuleTarget(options: GitlabModuleTargetOptions = {}, deps: ModuleTargetDeps = {}): ModulePublishTarget<"gitlab"> {
81
+ const fetch = deps.fetch ?? defaultPublishFetch;
82
+ const tokenEnv = gitlabTokenEnv(options);
83
+ const key = `gitlab:${createHash("sha256")
84
+ .update(JSON.stringify({ project: options.project ?? null, api: options.api ?? null, tokenEnv: options.tokenEnv ?? null, namespace: options.namespace ?? null }))
85
+ .digest("hex")
86
+ .slice(0, 16)}`;
87
+
88
+ const where = (env: NodeJS.ProcessEnv) => {
89
+ const api = gitlabApi(options, env);
90
+ let u: URL;
91
+ try {
92
+ u = new URL(api);
93
+ } catch {
94
+ throw new PublishTargetError("publish-config-invalid", `${api} is not a URL`, "Set the declaration's api, or CI_API_V4_URL, to the API root, such as https://gitlab.example.com/api/v4.");
95
+ }
96
+ return { api, host: u.host.toLowerCase(), scheme: (u.protocol === "http:" ? "http" : "https") as "http" | "https" };
97
+ };
98
+ const addressOf = (address: ModuleAddress): ModuleAddress => ({ ...address, namespace: options.namespace ?? address.namespace });
99
+ const sourceOf = (host: string, a: ModuleAddress) => `${host}/${a.namespace}/${a.name}/${a.system}`;
100
+
101
+ const uploadToken = (ctx: Pick<PublishContextBase, "env" | "redactor">): Credential | null => readCredential(ctx.env, tokenEnv, ctx.redactor);
102
+ const needToken = (ctx: Pick<PublishContextBase, "env" | "redactor">): Credential => {
103
+ const t = uploadToken(ctx);
104
+ if (!t) throw answerError(ctx, "publish-credentials-missing", `no GitLab token: set ${tokenEnv.join(" or ")}`, "In GitLab CI the job's CI_JOB_TOKEN can upload to its own project; elsewhere use a token with the api scope.");
105
+ return t;
106
+ };
107
+ const projectOf = (env: NodeJS.ProcessEnv): string => {
108
+ const p = options.project ?? env.CI_PROJECT_ID;
109
+ if (!p) throw new PublishTargetError("publish-config-invalid", "no GitLab project to upload to", "Name the project (its id or full path) in the declaration, or run in GitLab CI, which sets CI_PROJECT_ID.");
110
+ return p;
111
+ };
112
+
113
+ /** The reader's options: the CLIs' token for the host, else the upload token. */
114
+ const readOptions = (ctx: Pick<PublishContextBase, "env" | "redactor">): RegistryReadOptions & { host: string } => {
115
+ const { host, scheme } = where(ctx.env);
116
+ const cli = terraformCredential(host, { env: ctx.env });
117
+ const token = cli?.token ?? uploadToken(ctx)?.value;
118
+ ctx.redactor.add(token);
119
+ return { host, scheme, token: token ?? false, ...(deps.registryFetch ? { fetch: deps.registryFetch } : {}) };
120
+ };
121
+ const read = async <T>(ctx: Pick<PublishContextBase, "redactor">, run: () => Promise<T>): Promise<T> => {
122
+ try {
123
+ return await run();
124
+ } catch (e) {
125
+ if (e instanceof RegistryError) throw answerError(ctx, "publish-read-failed", withoutQueries(e.message));
126
+ throw e;
127
+ }
128
+ };
129
+
130
+ const versions = async (address: ModuleAddress, ctx: Pick<PublishContextBase, "env" | "redactor">): Promise<string[]> => {
131
+ const o = readOptions(ctx);
132
+ return read(ctx, () => registryVersions(sourceOf(o.host, addressOf(address)), o));
133
+ };
134
+ const held = async (address: ModuleAddress, version: string, ctx: Pick<PublishContextBase, "env" | "redactor">): Promise<HeldVersion | undefined> => {
135
+ if (!(await versions(address, ctx)).includes(version)) return undefined;
136
+ const o = readOptions(ctx);
137
+ const source = sourceOf(o.host, addressOf(address));
138
+ return read(ctx, async () => {
139
+ const location = await registryLocation(source, version, o);
140
+ return { content: contentDigest(await fetchTarball(location, o)) };
141
+ });
142
+ };
143
+
144
+ const item = (ctx: ModuleVersionContext, present?: boolean): PublishItem => {
145
+ const { host } = where(ctx.env);
146
+ const a = addressOf(ctx.address);
147
+ return { name: `${a.namespace}/${a.name}/${a.system} ${ctx.version}`, destination: sourceOf(host, a), digest: ctx.content, ...(present ? { present: true } : {}) };
148
+ };
149
+
150
+ return {
151
+ kind: "gitlab",
152
+ name: "GitLab module registry",
153
+ destination: gitlabApi(options, process.env),
154
+ credentialEnv: tokenEnv,
155
+ key,
156
+ versions,
157
+ held,
158
+
159
+ async plan(ctx) {
160
+ checkModuleVersion(ctx);
161
+ needToken(ctx);
162
+ projectOf(ctx.env);
163
+ return [item(ctx)];
164
+ },
165
+
166
+ async stage(ctx): Promise<StagedTarget> {
167
+ checkModuleVersion(ctx);
168
+ const token = needToken(ctx);
169
+ const project = projectOf(ctx.env);
170
+ const { api, host } = where(ctx.env);
171
+ const a = addressOf(ctx.address);
172
+ const source = sourceOf(host, a);
173
+ const staged = (present: boolean): StagedTarget => ({ target: "gitlab", destination: source, items: [item(ctx, present)], state: { project, host, version: ctx.version } });
174
+
175
+ const before = await held(ctx.address, ctx.version, ctx);
176
+ if (before) {
177
+ if (before.content !== ctx.content) throw versionHeldError(source, ctx, before);
178
+ ctx.log(`${source} ${ctx.version} is already on GitLab with this content; nothing sent`);
179
+ return staged(true);
180
+ }
181
+
182
+ const url = gitlabUploadUrl(api, project, a, ctx.version);
183
+ let res: Awaited<ReturnType<typeof fetch>>;
184
+ try {
185
+ res = await fetch(url, { method: "PUT", headers: { ...gitlabAuthHeader(token), "content-type": "application/octet-stream" }, body: ctx.archive });
186
+ } catch (e) {
187
+ throw answerError(ctx, "publish-upload-failed", `PUT ${url}: ${(e as Error).message}`);
188
+ }
189
+ const text = await res.text().catch(() => "");
190
+ if (res.ok) {
191
+ ctx.log(`uploaded ${source} ${ctx.version} to project ${project}`);
192
+ return staged(false);
193
+ }
194
+ if (/already exists/i.test(text)) {
195
+ // Another run uploaded it between the read and the upload: judge what it holds.
196
+ const now = await held(ctx.address, ctx.version, ctx);
197
+ if (now && now.content === ctx.content) return staged(true);
198
+ if (now) throw versionHeldError(source, ctx, now);
199
+ }
200
+ if (res.status === 401 || res.status === 403) {
201
+ throw answerError(
202
+ ctx,
203
+ "publish-credentials-refused",
204
+ `GitLab answered ${res.status} to the upload of ${source} ${ctx.version}: it refused the token in ${token.from}`,
205
+ token.from === "CI_JOB_TOKEN"
206
+ ? "A job token uploads only to its own project; name another project's token in the declaration's tokenEnv."
207
+ : `Give the token in ${token.from} the api scope and at least the Developer role on project ${project}.`,
208
+ );
209
+ }
210
+ throw answerError(ctx, "publish-upload-failed", `GitLab answered ${res.status} to the upload of ${source} ${ctx.version}: ${withoutQueries(text.slice(0, 300))}`);
211
+ },
212
+
213
+ async reveal(ctx, staged) {
214
+ const listed = await versions(ctx.address, ctx);
215
+ if (!listed.includes(ctx.version)) {
216
+ throw answerError(ctx, "publish-reveal-missing", `${staged.destination} lists no ${ctx.version} after the upload`, "Check the project's package registry; run the publish again to upload it.");
217
+ }
218
+ const o = readOptions(ctx);
219
+ await read(ctx, () => registryLocation(staged.destination, ctx.version, o));
220
+ return { target: staged.target, destination: staged.destination, items: staged.items };
221
+ },
222
+ };
223
+ }
@@ -0,0 +1,230 @@
1
+ // chant#4604: the HCP Terraform module target against the in-memory HCP fake. No HCP account and no
2
+ // HCP call: every request goes to the fake, the clock is the test's, and no token value appears in a
3
+ // log line, an error or a staged record.
4
+ import { gzipSync } from "node:zlib";
5
+ import { describe, expect, it } from "vitest";
6
+ import { PublishTargetError } from "@intentius/chant/lifecycle/release-publish";
7
+ import { createRedactor } from "@intentius/chant/lifecycle/publish-credentials";
8
+ import { HCP_RATE_RETRIES, archiveContent, hcpModuleTarget, hcpTokenEnv, retryDelay, type HcpModuleTargetOptions, type ModuleVersionContext } from "../index";
9
+ import { ARCHIVIST, createFakeHcp, type FakeHcpOptions } from "./fake-hcp";
10
+
11
+ const ORG = "acme";
12
+ const TOKEN = "team-token.atlasv1.0123456789abcdef";
13
+ const NO_MANAGE = "team-token.atlasv1.readonly-000000";
14
+
15
+ const fake = (over: Partial<FakeHcpOptions> = {}) => createFakeHcp({ organization: ORG, tokens: { [TOKEN]: { manageModules: true }, [NO_MANAGE]: { manageModules: false } }, ...over });
16
+ const archiveOf = (text: string) => gzipSync(Buffer.from(`module tar: ${text}`));
17
+
18
+ function clock() {
19
+ let t = 1_000_000;
20
+ const sleeps: number[] = [];
21
+ return { now: () => t, sleep: async (ms: number) => void (sleeps.push(ms), (t += ms)), sleeps };
22
+ }
23
+
24
+ function context(env: NodeJS.ProcessEnv, over: Partial<ModuleVersionContext> = {}): ModuleVersionContext & { lines: string[] } {
25
+ const archive = over.archive ?? archiveOf("vpc 1.2.0");
26
+ const redactor = createRedactor(env);
27
+ const lines: string[] = [];
28
+ return {
29
+ cwd: "/nowhere",
30
+ env,
31
+ redactor,
32
+ log: (line) => lines.push(redactor.redact(line)),
33
+ provenance: { oidc: false, ci: null, reason: "not in CI" },
34
+ tag: null,
35
+ address: { namespace: "ignored", name: "vpc", system: "aws" },
36
+ version: "1.2.0",
37
+ revision: "3f2a9c1d0b7e3f2a9c1d0b7e3f2a9c1d0b7e3f2a",
38
+ content: archiveContent(archive),
39
+ archive,
40
+ ...over,
41
+ lines,
42
+ };
43
+ }
44
+
45
+ function target(hcp: ReturnType<typeof fake>, c = clock(), opts: Partial<HcpModuleTargetOptions> = {}) {
46
+ return hcpModuleTarget({ organization: ORG, waitMs: 10_000, pollMs: 1000, ...opts }, { fetch: hcp.fetch, registryFetch: hcp.fetch, sleep: c.sleep, now: c.now });
47
+ }
48
+
49
+ const writes = (hcp: ReturnType<typeof fake>) => hcp.requests.filter((r) => r.method !== "GET").map((r) => `${r.method} ${r.url}`);
50
+
51
+ const errorOf = async (p: Promise<unknown>): Promise<PublishTargetError> => {
52
+ const e = await p.then(
53
+ () => undefined,
54
+ (err: unknown) => err,
55
+ );
56
+ expect(e).toBeInstanceOf(PublishTargetError);
57
+ return e as PublishTargetError;
58
+ };
59
+
60
+ const API = "https://app.terraform.io/api/v2/organizations/acme/registry-modules";
61
+
62
+ describe("HCP Terraform module target (chant#4604)", () => {
63
+ it("makes the three calls in order: create the module, create the version, upload the tarball", async () => {
64
+ const hcp = fake();
65
+ const ctx = context({ TFE_TOKEN: TOKEN });
66
+ const t = target(hcp);
67
+ expect(await t.plan(ctx)).toEqual([{ name: "acme/vpc/aws 1.2.0", destination: "app.terraform.io/acme/vpc/aws", digest: ctx.content }]);
68
+ const staged = await t.stage(ctx);
69
+ expect(writes(hcp)).toEqual([`POST ${API}`, `POST ${API}/private/acme/vpc/aws/versions`, `PUT https://${ARCHIVIST}/v1/object/obj1`]);
70
+
71
+ const [createModule, createVersion, upload] = hcp.requests.filter((r) => r.method !== "GET");
72
+ expect(JSON.parse(new TextDecoder().decode(createModule!.body))).toEqual({ data: { type: "registry-modules", attributes: { name: "vpc", provider: "aws", "registry-name": "private" } } });
73
+ expect(JSON.parse(new TextDecoder().decode(createVersion!.body))).toEqual({ data: { type: "registry-module-versions", attributes: { version: "1.2.0", "commit-sha": ctx.revision } } });
74
+ expect(createModule!.headers.authorization).toBe(`Bearer ${TOKEN}`);
75
+ expect(createModule!.headers["content-type"]).toBe("application/vnd.api+json");
76
+ expect(upload!.headers["content-type"]).toBe("application/octet-stream");
77
+ expect(upload!.headers.authorization).toBeUndefined();
78
+ expect(Buffer.from(upload!.body!)).toEqual(Buffer.from(ctx.archive));
79
+ // The module was read first, and created only because it was absent.
80
+ expect(hcp.requests[0]!.method).toBe("GET");
81
+ expect(hcp.requests[0]!.url).toBe(`${API}/private/acme/vpc/aws`);
82
+ expect(staged.items[0]!.present).toBeUndefined();
83
+ expect(JSON.stringify(staged)).not.toContain(TOKEN);
84
+
85
+ await t.reveal(ctx, staged);
86
+ expect(hcp.modules.get("vpc/aws")!.get("1.2.0")!.commitSha).toBe(ctx.revision);
87
+ });
88
+
89
+ it("does not create a module that is already there", async () => {
90
+ const hcp = fake();
91
+ hcp.seed("vpc", "aws");
92
+ await target(hcp).stage(context({ TFE_TOKEN: TOKEN }));
93
+ expect(writes(hcp)).toEqual([`POST ${API}/private/acme/vpc/aws/versions`, `PUT https://${ARCHIVIST}/v1/object/obj1`]);
94
+ });
95
+
96
+ it("comes back present and sends nothing for a version held with the same content", async () => {
97
+ const hcp = fake();
98
+ const ctx = context({ TFE_TOKEN: TOKEN });
99
+ hcp.seed("vpc", "aws", { version: "1.2.0", archive: ctx.archive });
100
+ const t = target(hcp);
101
+ expect(await t.held(ctx.address, "1.2.0", ctx)).toEqual({ content: ctx.content });
102
+ const staged = await t.stage(ctx);
103
+ expect(staged.items[0]!.present).toBe(true);
104
+ expect(writes(hcp)).toEqual([]);
105
+ expect(await t.versions(ctx.address, ctx)).toEqual(["1.2.0"]);
106
+ });
107
+
108
+ it("refuses a version held with other content, and sends nothing", async () => {
109
+ const hcp = fake();
110
+ hcp.seed("vpc", "aws", { version: "1.2.0", archive: archiveOf("other") });
111
+ const err = await errorOf(target(hcp).stage(context({ TFE_TOKEN: TOKEN })));
112
+ expect(err.code).toBe("publish-version-held");
113
+ expect(writes(hcp)).toEqual([]);
114
+ });
115
+
116
+ it("refuses a version an earlier run left pending, naming the fix", async () => {
117
+ const hcp = fake();
118
+ hcp.seed("vpc", "aws", { version: "1.2.0", archive: archiveOf("x"), status: "pending" });
119
+ hcp.modules.get("vpc/aws")!.get("1.2.0")!.archive = undefined;
120
+ const err = await errorOf(target(hcp).stage(context({ TFE_TOKEN: TOKEN })));
121
+ expect(err.code).toBe("publish-version-unfinished");
122
+ expect(err.fix).toContain("delete the version");
123
+ });
124
+
125
+ it("retries a 429 after Retry-After, then succeeds", async () => {
126
+ const hcp = fake();
127
+ const c = clock();
128
+ hcp.throttle(2, "3");
129
+ const ctx = context({ TFE_TOKEN: TOKEN });
130
+ await target(hcp, c).stage(ctx);
131
+ expect(c.sleeps).toEqual([3000, 3000]);
132
+ expect(writes(hcp)).toContain(`PUT https://${ARCHIVIST}/v1/object/obj1`);
133
+ expect(ctx.lines.filter((l) => l.includes("429"))).toHaveLength(2);
134
+ });
135
+
136
+ it("backs off, doubling, when a 429 has no Retry-After", async () => {
137
+ const hcp = fake();
138
+ const c = clock();
139
+ hcp.throttle(3);
140
+ await target(hcp, c).stage(context({ TFE_TOKEN: TOKEN }));
141
+ expect(c.sleeps).toEqual([1000, 2000, 4000]);
142
+ expect(retryDelay(null, 0, 0)).toBe(1000);
143
+ expect(retryDelay(new Date(5000).toUTCString(), 0, 2000)).toBe(3000);
144
+ });
145
+
146
+ it("stops with publish-rate-limited after the retry limit", async () => {
147
+ const hcp = fake();
148
+ const c = clock();
149
+ hcp.throttle(HCP_RATE_RETRIES + 1, "1");
150
+ const err = await errorOf(target(hcp, c).stage(context({ TFE_TOKEN: TOKEN })));
151
+ expect(err.code).toBe("publish-rate-limited");
152
+ expect(c.sleeps).toHaveLength(HCP_RATE_RETRIES);
153
+ expect(writes(hcp)).toEqual([]);
154
+ });
155
+
156
+ it("names both causes on a 404: the Manage modules permission and the entitlement", async () => {
157
+ for (const [hcp, env] of [
158
+ [fake(), { TFE_TOKEN: NO_MANAGE }],
159
+ [fake({ entitled: false }), { TFE_TOKEN: TOKEN }],
160
+ ] as const) {
161
+ const err = await errorOf(target(hcp).stage(context({ ...env })));
162
+ expect(err.code).toBe("publish-not-found");
163
+ expect(err.message).toContain("Manage modules");
164
+ expect(err.message).toContain("entitlement");
165
+ expect(err.message).not.toContain(env.TFE_TOKEN);
166
+ }
167
+ });
168
+
169
+ it("names the variable on a refused token, never its value", async () => {
170
+ const hcp = fake();
171
+ const err = await errorOf(target(hcp).stage(context({ TFE_TOKEN: "unknown-token-value-1234" })));
172
+ expect(err.code).toBe("publish-credentials-refused");
173
+ expect(err.message).toContain("TFE_TOKEN");
174
+ expect(`${err.message} ${err.fix}`).not.toContain("unknown-token-value-1234");
175
+ });
176
+
177
+ it("takes TF_TOKEN_<host> when TFE_TOKEN is not set, and refuses with neither", async () => {
178
+ const hcp = fake();
179
+ await target(hcp).stage(context({ TF_TOKEN_app_terraform_io: TOKEN }));
180
+ expect(hcp.requests[0]!.headers.authorization).toBe(`Bearer ${TOKEN}`);
181
+ const none = await errorOf(target(fake()).plan(context({})));
182
+ expect(none.code).toBe("publish-credentials-missing");
183
+ expect(none.message).toContain("TFE_TOKEN or TF_TOKEN_app_terraform_io");
184
+ expect(hcpTokenEnv({ host: "tfe.example.com" })).toEqual(["TFE_TOKEN", "TF_TOKEN_tfe_example_com"]);
185
+ });
186
+
187
+ it("talks to a Terraform Enterprise host when one is given", async () => {
188
+ const hcp = fake({ host: "tfe.example.com" });
189
+ await target(hcp, clock(), { host: "tfe.example.com" }).stage(context({ TFE_TOKEN: TOKEN }));
190
+ expect(hcp.requests[0]!.url).toBe("https://tfe.example.com/api/v2/organizations/acme/registry-modules/private/acme/vpc/aws");
191
+ });
192
+
193
+ it("reveals once polling sees the version's status turn ok", async () => {
194
+ const hcp = fake({ parseAfterReads: 3 });
195
+ const c = clock();
196
+ const ctx = context({ TFE_TOKEN: TOKEN });
197
+ const t = target(hcp, c);
198
+ const published = await t.reveal(ctx, await t.stage(ctx));
199
+ expect(published.destination).toBe("app.terraform.io/acme/vpc/aws");
200
+ expect(hcp.modules.get("vpc/aws")!.get("1.2.0")!.status).toBe("ok");
201
+ expect(c.sleeps.every((ms) => ms === 1000)).toBe(true);
202
+ expect(c.sleeps.length).toBeGreaterThan(0);
203
+ expect(ctx.lines.some((l) => l.includes("still"))).toBe(false);
204
+ });
205
+
206
+ it("reports a version still parsing past the wait, without failing", async () => {
207
+ const hcp = fake({ parseAfterReads: Infinity });
208
+ const c = clock();
209
+ const ctx = context({ TFE_TOKEN: TOKEN });
210
+ const t = target(hcp, c);
211
+ const published = await t.reveal(ctx, await t.stage(ctx));
212
+ expect(published.items).toHaveLength(1);
213
+ expect(ctx.lines.some((l) => l.includes("is still pending"))).toBe(true);
214
+ expect(c.sleeps.reduce((a, b) => a + b, 0)).toBeGreaterThanOrEqual(10_000);
215
+ });
216
+
217
+ it("masks the token in every log line and error", async () => {
218
+ const hcp = fake({ parseAfterReads: Infinity });
219
+ const c = clock();
220
+ hcp.throttle(1, "1");
221
+ const ctx = context({ TFE_TOKEN: TOKEN });
222
+ const t = target(hcp, c);
223
+ await t.reveal(ctx, await t.stage(ctx));
224
+ const other = context(ctx.env, { version: "1.2.0", archive: archiveOf("changed"), content: archiveContent(archiveOf("changed")) });
225
+ const err = await errorOf(t.stage(other));
226
+ const all = [...ctx.lines, ...other.lines, err.message, err.fix ?? ""].join("\n");
227
+ expect(all).not.toContain(TOKEN);
228
+ expect(ctx.redactor.redact(`Authorization: Bearer ${TOKEN}`)).not.toContain(TOKEN);
229
+ });
230
+ });