@intentius/chant 0.124.31 → 0.124.33

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 (509) hide show
  1. package/bin/chant +7 -2
  2. package/dist/ci-jobs.d.ts +4 -0
  3. package/dist/ci-jobs.d.ts.map +1 -1
  4. package/dist/ci-jobs.js +9 -2
  5. package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
  6. package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
  7. package/dist/cli/commands/check-lexicon-plugin.js +10 -7
  8. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  9. package/dist/cli/commands/check-lexicon.js +18 -12
  10. package/dist/cli/commands/import.d.ts +10 -0
  11. package/dist/cli/commands/import.d.ts.map +1 -1
  12. package/dist/cli/commands/import.js +37 -0
  13. package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
  14. package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
  15. package/dist/cli/commands/onboard.d.ts.map +1 -1
  16. package/dist/cli/commands/onboard.js +8 -4
  17. package/dist/cli/commands/update.d.ts.map +1 -1
  18. package/dist/cli/commands/update.js +2 -1
  19. package/dist/cli/dependencies.d.ts +18 -0
  20. package/dist/cli/dependencies.d.ts.map +1 -0
  21. package/dist/cli/dependencies.js +120 -0
  22. package/dist/cli/handlers/build.d.ts.map +1 -1
  23. package/dist/cli/handlers/build.js +38 -4
  24. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  25. package/dist/cli/handlers/fan-out.js +12 -4
  26. package/dist/cli/handlers/graph.d.ts.map +1 -1
  27. package/dist/cli/handlers/graph.js +16 -6
  28. package/dist/cli/handlers/lifecycle-health.d.ts +16 -0
  29. package/dist/cli/handlers/lifecycle-health.d.ts.map +1 -0
  30. package/dist/cli/handlers/lifecycle-health.js +150 -0
  31. package/dist/cli/handlers/lifecycle.d.ts +26 -0
  32. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  33. package/dist/cli/handlers/lifecycle.js +1 -1
  34. package/dist/cli/handlers/lint.d.ts.map +1 -1
  35. package/dist/cli/handlers/lint.js +6 -0
  36. package/dist/cli/handlers/operator.d.ts +2 -2
  37. package/dist/cli/handlers/operator.js +3 -3
  38. package/dist/cli/handlers/pr.d.ts +16 -0
  39. package/dist/cli/handlers/pr.d.ts.map +1 -1
  40. package/dist/cli/handlers/pr.js +610 -32
  41. package/dist/cli/handlers/promote.d.ts.map +1 -1
  42. package/dist/cli/handlers/promote.js +19 -2
  43. package/dist/cli/handlers/release.d.ts +16 -1
  44. package/dist/cli/handlers/release.d.ts.map +1 -1
  45. package/dist/cli/handlers/release.js +83 -11
  46. package/dist/cli/handlers/run.d.ts +13 -1
  47. package/dist/cli/handlers/run.d.ts.map +1 -1
  48. package/dist/cli/handlers/run.js +38 -15
  49. package/dist/cli/main.d.ts.map +1 -1
  50. package/dist/cli/main.js +121 -33
  51. package/dist/cli/parse-args.d.ts.map +1 -1
  52. package/dist/cli/parse-args.js +37 -0
  53. package/dist/cli/registry.d.ts +29 -6
  54. package/dist/cli/registry.d.ts.map +1 -1
  55. package/dist/cli/registry.js +6 -2
  56. package/dist/codegen/generate-post-synth-barrel.js +2 -2
  57. package/dist/codegen/lexicon-regen.d.ts +4 -0
  58. package/dist/codegen/lexicon-regen.d.ts.map +1 -1
  59. package/dist/codegen/lexicon-regen.js +1 -1
  60. package/dist/codegen/pinned-upgrade.js +2 -2
  61. package/dist/components/capability.d.ts +44 -3
  62. package/dist/components/capability.d.ts.map +1 -1
  63. package/dist/components/capability.js +21 -0
  64. package/dist/components/changed-files.d.ts +7 -1
  65. package/dist/components/changed-files.d.ts.map +1 -1
  66. package/dist/components/changed-files.js +9 -1
  67. package/dist/components/deploy-inputs.d.ts +91 -0
  68. package/dist/components/deploy-inputs.d.ts.map +1 -0
  69. package/dist/components/deploy-inputs.js +184 -0
  70. package/dist/components/driver.d.ts +38 -0
  71. package/dist/components/driver.d.ts.map +1 -1
  72. package/dist/components/driver.js +56 -6
  73. package/dist/components/fan-out-record.d.ts +6 -1
  74. package/dist/components/fan-out-record.d.ts.map +1 -1
  75. package/dist/components/fan-out-record.js +24 -0
  76. package/dist/components/fan-out-run.d.ts +20 -0
  77. package/dist/components/fan-out-run.d.ts.map +1 -1
  78. package/dist/components/fan-out-run.js +21 -1
  79. package/dist/components/gate-plan.d.ts +8 -0
  80. package/dist/components/gate-plan.d.ts.map +1 -1
  81. package/dist/components/gate-plan.js +6 -0
  82. package/dist/components/pr-pipeline.d.ts +98 -12
  83. package/dist/components/pr-pipeline.d.ts.map +1 -1
  84. package/dist/components/pr-pipeline.js +146 -23
  85. package/dist/components/pr-run.d.ts +128 -30
  86. package/dist/components/pr-run.d.ts.map +1 -1
  87. package/dist/components/pr-run.js +290 -70
  88. package/dist/components/release-compile.d.ts +37 -2
  89. package/dist/components/release-compile.d.ts.map +1 -1
  90. package/dist/components/release-compile.js +64 -8
  91. package/dist/components/release-promote.d.ts +17 -3
  92. package/dist/components/release-promote.d.ts.map +1 -1
  93. package/dist/components/release-promote.js +79 -18
  94. package/dist/components/verbs/bom-writer.d.ts +1 -1
  95. package/dist/components/verbs/bom-writer.d.ts.map +1 -1
  96. package/dist/components/verbs/lockfile-sbom-generator.d.ts +20 -0
  97. package/dist/components/verbs/lockfile-sbom-generator.d.ts.map +1 -1
  98. package/dist/components/verbs/lockfile-sbom-generator.js +41 -4
  99. package/dist/components/wave-plan.d.ts +15 -2
  100. package/dist/components/wave-plan.d.ts.map +1 -1
  101. package/dist/components/wave-plan.js +44 -5
  102. package/dist/discovery/fold-import.d.ts.map +1 -1
  103. package/dist/discovery/fold-import.js +16 -6
  104. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  105. package/dist/discovery/sandbox/bundle.js +6 -0
  106. package/dist/discovery/specifier-resolve.d.ts.map +1 -1
  107. package/dist/discovery/specifier-resolve.js +8 -2
  108. package/dist/fold/fold.d.ts.map +1 -1
  109. package/dist/fold/fold.js +68 -1
  110. package/dist/fold/subset.d.ts +37 -0
  111. package/dist/fold/subset.d.ts.map +1 -1
  112. package/dist/fold/subset.js +88 -0
  113. package/dist/graph-detail.d.ts.map +1 -1
  114. package/dist/graph-detail.js +32 -7
  115. package/dist/health.d.ts +110 -0
  116. package/dist/health.d.ts.map +1 -0
  117. package/dist/health.js +105 -0
  118. package/dist/health.schema.json +191 -0
  119. package/dist/lexicon-module.d.ts +10 -0
  120. package/dist/lexicon-module.d.ts.map +1 -1
  121. package/dist/lexicon-module.js +53 -8
  122. package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
  123. package/dist/lexicon-plugin-helpers.js +6 -4
  124. package/dist/lexicon.d.ts +29 -0
  125. package/dist/lexicon.d.ts.map +1 -1
  126. package/dist/lifecycle/affected.d.ts.map +1 -1
  127. package/dist/lifecycle/affected.js +7 -1
  128. package/dist/lifecycle/merge-record.d.ts +104 -0
  129. package/dist/lifecycle/merge-record.d.ts.map +1 -0
  130. package/dist/lifecycle/merge-record.js +102 -0
  131. package/dist/lifecycle/publish-attachments.d.ts +8 -5
  132. package/dist/lifecycle/publish-attachments.d.ts.map +1 -1
  133. package/dist/lifecycle/publish-attachments.js +27 -13
  134. package/dist/lifecycle/publish-npm.d.ts +3 -2
  135. package/dist/lifecycle/publish-npm.d.ts.map +1 -1
  136. package/dist/lifecycle/publish-npm.js +4 -3
  137. package/dist/lifecycle/release-at.d.ts +40 -0
  138. package/dist/lifecycle/release-at.d.ts.map +1 -0
  139. package/dist/lifecycle/release-at.js +83 -0
  140. package/dist/lifecycle/release-cut.d.ts.map +1 -1
  141. package/dist/lifecycle/release-cut.js +23 -12
  142. package/dist/lifecycle/release-ledger.d.ts +21 -0
  143. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  144. package/dist/lifecycle/release-ledger.js +24 -0
  145. package/dist/lifecycle/release-publish.d.ts +28 -12
  146. package/dist/lifecycle/release-publish.d.ts.map +1 -1
  147. package/dist/lifecycle/release-publish.js +29 -17
  148. package/dist/lifecycle/release-store.d.ts +25 -2
  149. package/dist/lifecycle/release-store.d.ts.map +1 -1
  150. package/dist/lifecycle/release-store.js +40 -13
  151. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  152. package/dist/lifecycle/run-ledger.js +1 -0
  153. package/dist/lifecycle/work-dir.d.ts +45 -0
  154. package/dist/lifecycle/work-dir.d.ts.map +1 -0
  155. package/dist/lifecycle/work-dir.js +78 -0
  156. package/dist/lint/discover.d.ts +17 -9
  157. package/dist/lint/discover.d.ts.map +1 -1
  158. package/dist/lint/discover.js +62 -54
  159. package/dist/lint/rules/op/index.d.ts +2 -1
  160. package/dist/lint/rules/op/index.d.ts.map +1 -1
  161. package/dist/lint/rules/op/index.js +4 -2
  162. package/dist/lint/rules/op/ops018-shell-image-digest.d.ts +10 -0
  163. package/dist/lint/rules/op/ops018-shell-image-digest.d.ts.map +1 -0
  164. package/dist/lint/rules/op/ops018-shell-image-digest.js +38 -0
  165. package/dist/module-source.d.ts +57 -0
  166. package/dist/module-source.d.ts.map +1 -0
  167. package/dist/module-source.js +104 -0
  168. package/dist/op/activities/activity-contracts.d.ts +5 -0
  169. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  170. package/dist/op/activities/activity-contracts.js +3 -0
  171. package/dist/op/activities/factory.js +1 -1
  172. package/dist/op/activities/shell.d.ts +38 -0
  173. package/dist/op/activities/shell.d.ts.map +1 -1
  174. package/dist/op/activities/shell.js +89 -3
  175. package/dist/op/builders.d.ts +7 -0
  176. package/dist/op/builders.d.ts.map +1 -1
  177. package/dist/op/builders.js +7 -0
  178. package/dist/op/discover.d.ts.map +1 -1
  179. package/dist/op/discover.js +13 -3
  180. package/dist/op/gate.d.ts.map +1 -1
  181. package/dist/op/gate.js +3 -0
  182. package/dist/op/op-waves-run.d.ts.map +1 -1
  183. package/dist/op/op-waves-run.js +20 -5
  184. package/dist/op/operator.d.ts +2 -2
  185. package/dist/op/operator.js +2 -2
  186. package/dist/op/runtime.d.ts +2 -0
  187. package/dist/op/runtime.d.ts.map +1 -1
  188. package/dist/op/steward.d.ts +6 -4
  189. package/dist/op/steward.d.ts.map +1 -1
  190. package/dist/op/steward.js +5 -3
  191. package/dist/op/work-lease-run.d.ts +20 -7
  192. package/dist/op/work-lease-run.d.ts.map +1 -1
  193. package/dist/op/work-lease-run.js +56 -12
  194. package/dist/pr-forge.d.ts +10 -0
  195. package/dist/pr-forge.d.ts.map +1 -1
  196. package/dist/pr-forge.js +39 -0
  197. package/dist/pr-loop.d.ts +155 -7
  198. package/dist/pr-loop.d.ts.map +1 -1
  199. package/dist/pr-loop.js +137 -11
  200. package/dist/pr-report-url.d.ts +4 -4
  201. package/dist/pr-report-url.d.ts.map +1 -1
  202. package/dist/pr-report-url.js +5 -5
  203. package/dist/runner-start.d.ts +179 -0
  204. package/dist/runner-start.d.ts.map +1 -0
  205. package/dist/runner-start.js +277 -0
  206. package/dist/ts-loader.d.ts +38 -4
  207. package/dist/ts-loader.d.ts.map +1 -1
  208. package/dist/ts-loader.js +190 -16
  209. package/dist/wave-runtime.mjs +238 -231
  210. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
  211. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
  212. package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
  213. package/dist/workspace/chant-migrations.d.ts.map +1 -1
  214. package/dist/workspace/chant-migrations.js +2 -0
  215. package/dist/workspace/check.schema.json +70 -2
  216. package/dist/workspace/checks/boxes.d.ts +6 -6
  217. package/dist/workspace/checks/boxes.js +9 -9
  218. package/dist/workspace/checks/security.d.ts +35 -0
  219. package/dist/workspace/checks/security.d.ts.map +1 -0
  220. package/dist/workspace/checks/security.js +305 -0
  221. package/dist/workspace/checks.d.ts +2 -0
  222. package/dist/workspace/checks.d.ts.map +1 -1
  223. package/dist/workspace/checks.js +5 -0
  224. package/dist/workspace/ci-builds.d.ts +163 -0
  225. package/dist/workspace/ci-builds.d.ts.map +1 -0
  226. package/dist/workspace/ci-builds.js +315 -0
  227. package/dist/workspace/ci-green-cli.d.ts +2 -1
  228. package/dist/workspace/ci-green-cli.d.ts.map +1 -1
  229. package/dist/workspace/ci-green-cli.js +25 -5
  230. package/dist/workspace/ci-green-forge.d.ts +18 -4
  231. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  232. package/dist/workspace/ci-green-forge.js +12 -0
  233. package/dist/workspace/ci-green-workflow.d.ts +55 -1
  234. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  235. package/dist/workspace/ci-green-workflow.js +128 -1
  236. package/dist/workspace/ci-green.d.ts +117 -16
  237. package/dist/workspace/ci-green.d.ts.map +1 -1
  238. package/dist/workspace/ci-green.js +358 -43
  239. package/dist/workspace/ci-last-green.schema.json +49 -0
  240. package/dist/workspace/ci-list.schema.json +47 -7
  241. package/dist/workspace/ci-report-url.d.ts +18 -0
  242. package/dist/workspace/ci-report-url.d.ts.map +1 -0
  243. package/dist/workspace/ci-report-url.js +33 -0
  244. package/dist/workspace/ci-run-index.d.ts +13 -1
  245. package/dist/workspace/ci-run-index.d.ts.map +1 -1
  246. package/dist/workspace/ci-run-index.js +6 -4
  247. package/dist/workspace/ci-run-index.schema.json +6 -4
  248. package/dist/workspace/ci-run.d.ts +85 -6
  249. package/dist/workspace/ci-run.d.ts.map +1 -1
  250. package/dist/workspace/ci-run.js +262 -64
  251. package/dist/workspace/ci-runner-key.d.ts +16 -3
  252. package/dist/workspace/ci-runner-key.d.ts.map +1 -1
  253. package/dist/workspace/ci-runner-key.js +30 -10
  254. package/dist/workspace/ci-runner-start.d.ts +77 -0
  255. package/dist/workspace/ci-runner-start.d.ts.map +1 -0
  256. package/dist/workspace/ci-runner-start.js +185 -0
  257. package/dist/workspace/ci-serve.d.ts +130 -11
  258. package/dist/workspace/ci-serve.d.ts.map +1 -1
  259. package/dist/workspace/ci-serve.js +323 -40
  260. package/dist/workspace/declaration.d.ts +25 -0
  261. package/dist/workspace/declaration.d.ts.map +1 -1
  262. package/dist/workspace/declaration.js +14 -2
  263. package/dist/workspace/declaration.schema.json +30 -2
  264. package/dist/workspace/export.d.ts +1 -1
  265. package/dist/workspace/export.d.ts.map +1 -1
  266. package/dist/workspace/export.js +7 -3
  267. package/dist/workspace/ls.d.ts +8 -1
  268. package/dist/workspace/ls.d.ts.map +1 -1
  269. package/dist/workspace/ls.js +2 -1
  270. package/dist/workspace/ls.schema.json +9 -2
  271. package/dist/workspace/member-pipeline.d.ts +1 -0
  272. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  273. package/dist/workspace/member-pipeline.js +2 -0
  274. package/dist/workspace/merge-record.schema.json +49 -0
  275. package/dist/workspace/pin-integrity.d.ts.map +1 -1
  276. package/dist/workspace/pin-integrity.js +3 -2
  277. package/dist/workspace/pr-report.schema.json +75 -8
  278. package/dist/workspace/reason-codes.d.ts +18 -1
  279. package/dist/workspace/reason-codes.d.ts.map +1 -1
  280. package/dist/workspace/reason-codes.js +20 -2
  281. package/dist/workspace/release-candidates.d.ts +129 -0
  282. package/dist/workspace/release-candidates.d.ts.map +1 -0
  283. package/dist/workspace/release-candidates.js +233 -0
  284. package/dist/workspace/release-candidates.schema.json +353 -0
  285. package/dist/workspace/run-event.schema.json +3 -1
  286. package/dist/workspace/security.d.ts +40 -0
  287. package/dist/workspace/security.d.ts.map +1 -0
  288. package/dist/workspace/security.js +99 -0
  289. package/dist/workspace/status.schema.json +3 -1
  290. package/dist/workspace/write-scope.d.ts +9 -0
  291. package/dist/workspace/write-scope.d.ts.map +1 -1
  292. package/dist/workspace/write-scope.js +11 -1
  293. package/package.json +8 -1
  294. package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
  295. package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +1 -1
  296. package/src/ci-jobs.ts +11 -2
  297. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
  298. package/src/cli/commands/audit.test.ts +2 -2
  299. package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
  300. package/src/cli/commands/check-lexicon-plugin.ts +11 -8
  301. package/src/cli/commands/check-lexicon.test.ts +1 -1
  302. package/src/cli/commands/check-lexicon.ts +16 -11
  303. package/src/cli/commands/import.ts +40 -0
  304. package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
  305. package/src/cli/commands/init-lexicon.test.ts +8 -5
  306. package/src/cli/commands/onboard.test.ts +45 -0
  307. package/src/cli/commands/onboard.ts +8 -4
  308. package/src/cli/commands/update.test.ts +6 -0
  309. package/src/cli/commands/update.ts +4 -1
  310. package/src/cli/compiled-cli.e2e.test.ts +76 -11
  311. package/src/cli/dependencies.e2e.test.ts +101 -0
  312. package/src/cli/dependencies.test.ts +93 -0
  313. package/src/cli/dependencies.ts +122 -0
  314. package/src/cli/handlers/build.test.ts +105 -0
  315. package/src/cli/handlers/build.ts +35 -4
  316. package/src/cli/handlers/fan-out.test.ts +73 -0
  317. package/src/cli/handlers/fan-out.ts +11 -4
  318. package/src/cli/handlers/graph.test.ts +76 -0
  319. package/src/cli/handlers/graph.ts +19 -12
  320. package/src/cli/handlers/lifecycle-health.test.ts +123 -0
  321. package/src/cli/handlers/lifecycle-health.ts +157 -0
  322. package/src/cli/handlers/lifecycle.ts +1 -1
  323. package/src/cli/handlers/lint.test.ts +42 -0
  324. package/src/cli/handlers/lint.ts +6 -0
  325. package/src/cli/handlers/operator.ts +3 -3
  326. package/src/cli/handlers/pr-deploy-inputs.test.ts +209 -0
  327. package/src/cli/handlers/pr-record.test.ts +201 -0
  328. package/src/cli/handlers/pr-waves.test.ts +449 -0
  329. package/src/cli/handlers/pr.test.ts +156 -0
  330. package/src/cli/handlers/pr.ts +648 -33
  331. package/src/cli/handlers/promote.test.ts +43 -1
  332. package/src/cli/handlers/promote.ts +19 -3
  333. package/src/cli/handlers/release-cut-at.e2e.test.ts +115 -0
  334. package/src/cli/handlers/release-publish-targets.e2e.test.ts +2 -2
  335. package/src/cli/handlers/release-verify.e2e.test.ts +12 -10
  336. package/src/cli/handlers/release.e2e.test.ts +19 -28
  337. package/src/cli/handlers/release.ts +81 -11
  338. package/src/cli/handlers/run-components-exit.test.ts +99 -0
  339. package/src/cli/handlers/run-signer.test.ts +79 -0
  340. package/src/cli/handlers/run.ts +34 -16
  341. package/src/cli/main.test.ts +18 -0
  342. package/src/cli/main.ts +120 -33
  343. package/src/cli/parse-args.ts +28 -0
  344. package/src/cli/registry.ts +35 -8
  345. package/src/codegen/generate-post-synth-barrel.ts +2 -2
  346. package/src/codegen/lexicon-regen.ts +1 -1
  347. package/src/codegen/pinned-upgrade.ts +2 -2
  348. package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
  349. package/src/components/capability.ts +51 -3
  350. package/src/components/changed-files.test.ts +16 -1
  351. package/src/components/changed-files.ts +9 -1
  352. package/src/components/deploy-inputs.test.ts +108 -0
  353. package/src/components/deploy-inputs.ts +225 -0
  354. package/src/components/driver.test.ts +36 -1
  355. package/src/components/driver.ts +87 -4
  356. package/src/components/fan-out-record.ts +29 -1
  357. package/src/components/fan-out-run.test.ts +35 -0
  358. package/src/components/fan-out-run.ts +40 -1
  359. package/src/components/gate-plan.ts +9 -0
  360. package/src/components/pr-pipeline.test.ts +100 -0
  361. package/src/components/pr-pipeline.ts +184 -28
  362. package/src/components/pr-run.test.ts +323 -18
  363. package/src/components/pr-run.ts +355 -76
  364. package/src/components/release-compile.test.ts +34 -0
  365. package/src/components/release-compile.ts +89 -8
  366. package/src/components/release-promote.test.ts +55 -2
  367. package/src/components/release-promote.ts +93 -23
  368. package/src/components/verbs/bom-writer.ts +1 -1
  369. package/src/components/verbs/lockfile-sbom-generator.test.ts +36 -0
  370. package/src/components/verbs/lockfile-sbom-generator.ts +43 -5
  371. package/src/components/wave-plan.ts +48 -8
  372. package/src/discovery/fold-host-method.test.ts +87 -0
  373. package/src/discovery/fold-import.ts +15 -5
  374. package/src/discovery/fold-package-composite.test.ts +27 -0
  375. package/src/discovery/fold-specifiers.test.ts +8 -0
  376. package/src/discovery/sandbox/bundle.ts +6 -0
  377. package/src/discovery/specifier-resolve.ts +8 -2
  378. package/src/fold/fold.test.ts +72 -0
  379. package/src/fold/fold.ts +71 -0
  380. package/src/fold/subset.test.ts +31 -0
  381. package/src/fold/subset.ts +99 -0
  382. package/src/graph-detail.test.ts +57 -0
  383. package/src/graph-detail.ts +33 -7
  384. package/src/health.schema.json +191 -0
  385. package/src/health.test.ts +62 -0
  386. package/src/health.ts +178 -0
  387. package/src/lexicon-dist.e2e.test.ts +254 -0
  388. package/src/lexicon-module.test.ts +47 -0
  389. package/src/lexicon-module.ts +49 -7
  390. package/src/lexicon-plugin-helpers.ts +6 -4
  391. package/src/lexicon.ts +43 -1
  392. package/src/lifecycle/affected.test.ts +7 -0
  393. package/src/lifecycle/affected.ts +6 -1
  394. package/src/lifecycle/merge-record.test.ts +111 -0
  395. package/src/lifecycle/merge-record.ts +154 -0
  396. package/src/lifecycle/publish-attachments.ts +25 -13
  397. package/src/lifecycle/publish-npm.ts +4 -3
  398. package/src/lifecycle/publish-targets.test.ts +34 -4
  399. package/src/lifecycle/release-at.test.ts +48 -0
  400. package/src/lifecycle/release-at.ts +100 -0
  401. package/src/lifecycle/release-cut.test.ts +41 -0
  402. package/src/lifecycle/release-cut.ts +22 -11
  403. package/src/lifecycle/release-ledger.ts +34 -0
  404. package/src/lifecycle/release-publish.test.ts +43 -13
  405. package/src/lifecycle/release-publish.ts +38 -18
  406. package/src/lifecycle/release-store.ts +57 -13
  407. package/src/lifecycle/run-ledger.test.ts +13 -0
  408. package/src/lifecycle/run-ledger.ts +1 -0
  409. package/src/lifecycle/work-dir.test.ts +77 -0
  410. package/src/lifecycle/work-dir.ts +82 -0
  411. package/src/lint/discover.test.ts +46 -2
  412. package/src/lint/discover.ts +55 -49
  413. package/src/lint/rules/op/index.ts +4 -2
  414. package/src/lint/rules/op/ops018-shell-image-digest.test.ts +60 -0
  415. package/src/lint/rules/op/ops018-shell-image-digest.ts +48 -0
  416. package/src/module-source.test.ts +85 -0
  417. package/src/module-source.ts +112 -0
  418. package/src/op/activities/activity-contracts.ts +3 -0
  419. package/src/op/activities/factory.ts +1 -1
  420. package/src/op/activities/shell-container.e2e.test.ts +67 -0
  421. package/src/op/activities/shell.test.ts +44 -1
  422. package/src/op/activities/shell.ts +110 -3
  423. package/src/op/builders.ts +7 -0
  424. package/src/op/discover-root.test.ts +31 -2
  425. package/src/op/discover.ts +14 -4
  426. package/src/op/gate.test.ts +30 -0
  427. package/src/op/gate.ts +3 -0
  428. package/src/op/op-waves-run.ts +20 -5
  429. package/src/op/op-waves.test.ts +49 -0
  430. package/src/op/operator.ts +2 -2
  431. package/src/op/runtime.ts +2 -0
  432. package/src/op/step-console.test.ts +4 -2
  433. package/src/op/steward.ts +6 -4
  434. package/src/op/work-lease-run.test.ts +88 -2
  435. package/src/op/work-lease-run.ts +52 -12
  436. package/src/pr-forge-bundle.test.ts +2 -2
  437. package/src/pr-forge.test.ts +42 -0
  438. package/src/pr-forge.ts +40 -0
  439. package/src/pr-loop-bundle.test.ts +2 -1
  440. package/src/pr-loop.test.ts +146 -1
  441. package/src/pr-loop.ts +278 -17
  442. package/src/pr-report-url.ts +7 -7
  443. package/src/runner-start-bundle.test.ts +29 -0
  444. package/src/runner-start.test.ts +179 -0
  445. package/src/runner-start.ts +329 -0
  446. package/src/terraform/__fixtures__/advise.test.ts +21 -2
  447. package/src/ts-loader.test.ts +89 -1
  448. package/src/ts-loader.ts +180 -13
  449. package/src/workspace/changes.test.ts +1 -1
  450. package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
  451. package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
  452. package/src/workspace/chant-migrations.e2e.test.ts +5 -0
  453. package/src/workspace/chant-migrations.ts +2 -0
  454. package/src/workspace/check-contract.test.ts +3 -2
  455. package/src/workspace/check.schema.json +70 -2
  456. package/src/workspace/checks/boxes.ts +9 -9
  457. package/src/workspace/checks/security.test.ts +138 -0
  458. package/src/workspace/checks/security.ts +305 -0
  459. package/src/workspace/checks.ts +5 -0
  460. package/src/workspace/ci-builds.e2e.test.ts +208 -0
  461. package/src/workspace/ci-builds.test.ts +174 -0
  462. package/src/workspace/ci-builds.ts +385 -0
  463. package/src/workspace/ci-green-builds.test.ts +180 -0
  464. package/src/workspace/ci-green-cli.test.ts +54 -0
  465. package/src/workspace/ci-green-cli.ts +26 -5
  466. package/src/workspace/ci-green-forge.ts +30 -4
  467. package/src/workspace/ci-green-runs.test.ts +53 -11
  468. package/src/workspace/ci-green-workflow.test.ts +84 -1
  469. package/src/workspace/ci-green-workflow.ts +139 -2
  470. package/src/workspace/ci-green.test.ts +6 -3
  471. package/src/workspace/ci-green.ts +408 -48
  472. package/src/workspace/ci-last-green.schema.json +49 -0
  473. package/src/workspace/ci-list.schema.json +47 -7
  474. package/src/workspace/ci-report-url.test.ts +220 -0
  475. package/src/workspace/ci-report-url.ts +33 -0
  476. package/src/workspace/ci-run-index.schema.json +6 -4
  477. package/src/workspace/ci-run-index.test.ts +42 -3
  478. package/src/workspace/ci-run-index.ts +19 -5
  479. package/src/workspace/ci-run.test.ts +203 -8
  480. package/src/workspace/ci-run.ts +295 -63
  481. package/src/workspace/ci-runner-key.test.ts +55 -17
  482. package/src/workspace/ci-runner-key.ts +36 -11
  483. package/src/workspace/ci-runner-start.test.ts +151 -0
  484. package/src/workspace/ci-runner-start.ts +193 -0
  485. package/src/workspace/ci-serve-builds.test.ts +86 -0
  486. package/src/workspace/ci-serve.test.ts +209 -6
  487. package/src/workspace/ci-serve.ts +341 -42
  488. package/src/workspace/declaration.schema.json +30 -2
  489. package/src/workspace/declaration.test.ts +40 -0
  490. package/src/workspace/declaration.ts +40 -3
  491. package/src/workspace/export.ts +6 -3
  492. package/src/workspace/ls.schema.json +9 -2
  493. package/src/workspace/ls.ts +10 -4
  494. package/src/workspace/member-pipeline.ts +2 -1
  495. package/src/workspace/merge-record.schema.json +49 -0
  496. package/src/workspace/pin-integrity.test.ts +5 -0
  497. package/src/workspace/pin-integrity.ts +3 -2
  498. package/src/workspace/pr-report.schema.json +75 -8
  499. package/src/workspace/reason-codes.test.ts +2 -0
  500. package/src/workspace/reason-codes.ts +20 -2
  501. package/src/workspace/release-candidates.schema.json +353 -0
  502. package/src/workspace/release-candidates.test.ts +106 -0
  503. package/src/workspace/release-candidates.ts +315 -0
  504. package/src/workspace/run-event.schema.json +3 -1
  505. package/src/workspace/security.test.ts +70 -0
  506. package/src/workspace/security.ts +118 -0
  507. package/src/workspace/status.schema.json +3 -1
  508. package/src/workspace/write-scope.test.ts +29 -1
  509. package/src/workspace/write-scope.ts +12 -1
@@ -15,7 +15,14 @@
15
15
  * ./promote.ts: a literal path in a deploy step's `template`, `manifest` or
16
16
  * `values` field ({@link TEMPLATE_FIELDS}), read after the environment's build
17
17
  * command has run. A path with the `archive:` prefix names the file after it.
18
- * A path that names a directory, such as a chart, is not a template.
18
+ *
19
+ * Since #4155 a cut also records the other local paths a deploy step names
20
+ * ({@link recordedInputRefs}: a Helm chart, a kustomize directory, a sync's
21
+ * source tree, the fields `./deploy-inputs.ts` lists). A directory is
22
+ * recorded file by file and named in the target's `trees`; a file that is not
23
+ * UTF-8 is recorded as base64. Such a path that does not exist after the
24
+ * build, as a chart named from a repository, is not recorded, and the
25
+ * promote leaves it as written.
19
26
  *
20
27
  * A project with no components (#4046) is compiled the way `chant build --env
21
28
  * <env> -o <output>` builds it ({@link compileBuildOutput}): every file that
@@ -25,6 +32,7 @@
25
32
  */
26
33
 
27
34
  import { spawn, spawnSync } from "node:child_process";
35
+ import { isUtf8 } from "node:buffer";
28
36
  import { existsSync, readFileSync, statSync } from "node:fs";
29
37
  import { isAbsolute, relative, resolve } from "node:path";
30
38
  import { loadChantConfig, type ChantConfig } from "../config";
@@ -40,7 +48,8 @@ import { memoryGateLedgerPort } from "../op/gate";
40
48
  import type { ReleaseArtifact, ReleaseParam } from "../lifecycle/release-store";
41
49
 
42
50
  /** The deploy step fields whose literal file paths are a component's templates. */
43
- export const TEMPLATE_FIELDS: readonly string[] = ["template", "manifest", "values"];
51
+ export { TEMPLATE_FIELDS } from "./deploy-inputs";
52
+ import { DEPLOY_INPUT_FIELDS, directoryFiles, ignoredByGit, TEMPLATE_FIELDS, walkFiles } from "./deploy-inputs";
44
53
 
45
54
  /** The prefix a step may write before an archive-relative template path. */
46
55
  const ARCHIVE_PREFIX = "archive:";
@@ -61,7 +70,9 @@ export interface TemplateRef {
61
70
  export interface CompiledTemplate {
62
71
  component: string | null;
63
72
  path: string;
73
+ /** The text, or the base64 of the bytes under `encoding: "base64"`. */
64
74
  content: string;
75
+ encoding?: "base64";
65
76
  }
66
77
 
67
78
  /** What one environment compiles to. */
@@ -84,6 +95,8 @@ export interface CompiledTarget {
84
95
  /** For each component, the file that declares it, relative to the member's directory. */
85
96
  componentFiles: Record<string, string>;
86
97
  templates: CompiledTemplate[];
98
+ /** The directories recorded file by file in `templates` (#4155). Absent from a compiler written before #4155. */
99
+ trees?: Array<{ component: string | null; path: string }>;
87
100
  }
88
101
 
89
102
  /** Compiles a member for one environment, and builds its artifacts. The cut talks to a compiler only, so a test can hand it fixtures. */
@@ -134,6 +147,40 @@ export function templateRefs(component: DriverComponent): TemplateRef[] {
134
147
  return out;
135
148
  }
136
149
 
150
+ /** A path a deploy step names that a cut records (#4155): a {@link TemplateRef}, and whether its field is a template field, which must exist. */
151
+ export interface RecordedInputRef extends TemplateRef {
152
+ /** True for `template`, `manifest` and `values`, whose path must exist after the build. */
153
+ required: boolean;
154
+ }
155
+
156
+ /** Every literal path a component's deploy names that a cut records: {@link templateRefs}, plus each kind's own fields in `DEPLOY_INPUT_FIELDS`. */
157
+ export function recordedInputRefs(component: DriverComponent): RecordedInputRef[] {
158
+ const out: RecordedInputRef[] = [];
159
+ for (const { phase, step } of stepsOf(component.deploy)) {
160
+ if (BUILD_STEP_KINDS.includes(step.kind) || PUBLISH_STEP_KINDS.includes(step.kind)) continue;
161
+ for (const field of recordedFields(step.kind)) {
162
+ const raw = step[field];
163
+ for (const value of Array.isArray(raw) ? raw : [raw]) {
164
+ if (!isLiteralPath(value)) continue;
165
+ const path = value.startsWith(ARCHIVE_PREFIX) ? value.slice(ARCHIVE_PREFIX.length) : value;
166
+ out.push({ component: component.name, phase, kind: step.kind, field, value, path, required: TEMPLATE_FIELDS.includes(field) });
167
+ }
168
+ }
169
+ }
170
+ return out;
171
+ }
172
+
173
+ /** The fields of a step of `kind` whose paths a cut records and a promote points at the recorded copy. */
174
+ export function recordedFields(kind: string): string[] {
175
+ return [...TEMPLATE_FIELDS, ...(DEPLOY_INPUT_FIELDS[kind] ?? [])];
176
+ }
177
+
178
+ /** A file's bytes as a template: its text when it is UTF-8, else base64. */
179
+ export function templateOf(component: string | null, path: string, file: string): CompiledTemplate {
180
+ const bytes = readFileSync(file);
181
+ return isUtf8(bytes) ? { component, path, content: bytes.toString("utf-8") } : { component, path, content: bytes.toString("base64"), encoding: "base64" };
182
+ }
183
+
137
184
  /** A component's build-time steps ({@link BUILD_STEP_KINDS}), each as declared, in composition order. */
138
185
  export function buildStepsOf(component: DriverComponent): DriverStep[] {
139
186
  return stepsOf(component.deploy).map((s) => s.step).filter((s) => BUILD_STEP_KINDS.includes(s.kind));
@@ -191,8 +238,19 @@ export interface CompileInputs {
191
238
  output?: string;
192
239
  /** For a project with no components: build only this lexicon, as `chant build --lexicon` does. */
193
240
  lexicon?: string;
241
+ /**
242
+ * The release's label (#4168, decision 5), passed to every compile and
243
+ * artifact build as `CHANT_RELEASE_LABEL`, so a version reaches the build
244
+ * without a version-bump commit. A build parameter reads it by declaring
245
+ * `env: "CHANT_RELEASE_LABEL"`, and a build step's own tool (cargo, npm)
246
+ * reads the variable. Unset for a cut with no label.
247
+ */
248
+ releaseLabel?: string;
194
249
  }
195
250
 
251
+ /** The variable a cut's label reaches the build through (#4168). */
252
+ export const RELEASE_LABEL_ENV = "CHANT_RELEASE_LABEL";
253
+
196
254
  /**
197
255
  * Where a project with no components writes its build output when the cut
198
256
  * names no `-o`. YAML, since the k8s and helm serializers write YAML whatever
@@ -258,21 +316,33 @@ export async function compileEnvironment(inputs: CompileInputs, env: string): Pr
258
316
  if (declared === 0) return compileBuildOutput(inputs, env, config);
259
317
  const rel = (p: string): string => (isAbsolute(p) ? relative(inputs.projectDir, p) : p);
260
318
  const templates: CompiledTemplate[] = [];
319
+ const trees: Array<{ component: string; path: string }> = [];
261
320
  const seen = new Set<string>();
262
321
  for (const { component } of components) {
263
- for (const ref of templateRefs(component)) {
322
+ for (const ref of recordedInputRefs(component)) {
264
323
  const key = `${ref.component}\u0000${ref.path}`;
265
324
  if (seen.has(key)) continue;
266
325
  seen.add(key);
267
326
  const file = resolve(inputs.projectDir, ref.path);
268
327
  if (!existsSync(file)) {
328
+ if (!ref.required) continue;
269
329
  throw new ReleaseCompileError(
270
330
  `component "${ref.component}": its ${ref.kind} step in phase "${ref.phase}" reads ${ref.field} "${ref.value}", ` +
271
331
  `and no such file exists after the build for "${env}"`,
272
332
  );
273
333
  }
274
- if (!statSync(file).isFile()) continue;
275
- templates.push({ component: ref.component, path: ref.path, content: readFileSync(file, "utf-8") });
334
+ if (statSync(file).isFile()) {
335
+ templates.push(templateOf(ref.component, ref.path, file));
336
+ continue;
337
+ }
338
+ // #4155: a directory, file by file. One git ignores is the build's output, recorded whole.
339
+ const files = ignoredByGit(inputs.projectDir, ref.path) ? walkFiles(inputs.projectDir, ref.path) : directoryFiles(inputs.projectDir, ref.path);
340
+ const dir = ref.path.replace(/\/+$/, "");
341
+ for (const f of files) {
342
+ const under = relative(resolve(inputs.projectDir, dir), resolve(inputs.projectDir, f)).split("\\").join("/");
343
+ templates.push(templateOf(ref.component, `${dir}/${under}`, resolve(inputs.projectDir, f)));
344
+ }
345
+ trees.push({ component: ref.component, path: dir });
276
346
  }
277
347
  }
278
348
  return {
@@ -284,6 +354,7 @@ export async function compileEnvironment(inputs: CompileInputs, env: string): Pr
284
354
  buildInputs: Object.fromEntries(components.map(({ component }) => [component.name, buildInputPaths(component)])),
285
355
  componentFiles: Object.fromEntries(components.map(({ component, file }) => [component.name, rel(file)])),
286
356
  templates,
357
+ trees,
287
358
  };
288
359
  }
289
360
 
@@ -408,10 +479,20 @@ export function ownChantCommand(): string[] {
408
479
  return [process.execPath, ...process.execArgv, main];
409
480
  }
410
481
 
411
- function runChild(command: readonly string[], args: readonly string[], cwd: string, env: string): Promise<{ code: number; stdout: string; stderr: string }> {
482
+ function runChild(
483
+ command: readonly string[],
484
+ args: readonly string[],
485
+ cwd: string,
486
+ env: string,
487
+ releaseLabel?: string,
488
+ ): Promise<{ code: number; stdout: string; stderr: string }> {
412
489
  return new Promise((resolvePromise, reject) => {
413
490
  const [bin, ...lead] = command;
414
- const child = spawn(bin, [...lead, ...args], { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, CHANT_ENV: env } });
491
+ const childEnv: NodeJS.ProcessEnv = { ...process.env, CHANT_ENV: env };
492
+ // A label never leaks in from the caller's shell: only the cut sets it.
493
+ delete childEnv[RELEASE_LABEL_ENV];
494
+ if (releaseLabel !== undefined) childEnv[RELEASE_LABEL_ENV] = releaseLabel;
495
+ const child = spawn(bin, [...lead, ...args], { cwd, stdio: ["ignore", "pipe", "pipe"], env: childEnv });
415
496
  const out: Buffer[] = [];
416
497
  const err: Buffer[] = [];
417
498
  child.stdout.on("data", (c: Buffer) => out.push(c));
@@ -440,7 +521,7 @@ export function spawnTargetCompiler(inputs: CompileInputs, command: readonly str
440
521
  return args;
441
522
  };
442
523
  const call = async (env: string, extra: string[]): Promise<unknown> => {
443
- const run = await runChild(command, ["release", "compile", "--env", env, ...common(), ...extra, "--json"], inputs.projectDir, env);
524
+ const run = await runChild(command, ["release", "compile", "--env", env, ...common(), ...extra, "--json"], inputs.projectDir, env, inputs.releaseLabel);
444
525
  if (run.code !== 0) throw new ReleaseCompileError(`compiling "${env}" failed (exit ${run.code})`);
445
526
  try {
446
527
  return JSON.parse(run.stdout);
@@ -5,13 +5,13 @@
5
5
 
6
6
  import { describe, test, expect } from "vitest";
7
7
  import { contentDigest } from "../content-digest";
8
- import { makeRelease } from "../lifecycle/release-store";
8
+ import { makeRelease, templateBytesDigest } from "../lifecycle/release-store";
9
9
  import type { DriverComponent } from "./driver";
10
10
  import { isImplicitRelease, pinReleaseTemplates, releaseArtifactMismatches, removePinnedTemplates, targetFiles, targetFor, writePinnedTemplates, RELEASE_TEMPLATES_DIR } from "./release-promote";
11
11
  import type { ReleaseRecord } from "../lifecycle/release-ledger";
12
12
  import type { BuildArchiveManifest } from "./verbs/build-archive";
13
13
  import { withTestDir } from "@intentius/chant-test-utils";
14
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
15
  import { join } from "node:path";
16
16
 
17
17
  const prodTemplate = `{"env":"prod"}\n`;
@@ -63,6 +63,59 @@ describe("pinReleaseTemplates", () => {
63
63
  expect("errors" in absent && absent.errors[0]).toMatch(/are not on the chant\/lifecycle branch/);
64
64
  });
65
65
 
66
+ test("a target cut since #4155 points a chart at its recorded tree, which a reader of the release can trust", async () => {
67
+ const chartFile = "name: web\n";
68
+ const tgz = Buffer.from([0x1f, 0x8b, 0xff]).toString("base64");
69
+ const tgzDigest = templateBytesDigest(tgz, "base64");
70
+ const cut = makeRelease({
71
+ member: null,
72
+ commit: "c".repeat(40),
73
+ artifactDigests: [],
74
+ targets: [{
75
+ env: "prod",
76
+ params: [],
77
+ templates: [
78
+ { component: "web", path: "chart/Chart.yaml", digest: contentDigest(chartFile) },
79
+ { component: "web", path: "chart/charts/dep.tgz", digest: tgzDigest, encoding: "base64" },
80
+ ],
81
+ trees: [{ component: "web", path: "chart" }],
82
+ }],
83
+ });
84
+ const web: DriverComponent = {
85
+ name: "web",
86
+ deploy: [{ phase: "Deploy", steps: [
87
+ { kind: "helm-upgrade", release: "web", chart: "chart" },
88
+ { kind: "helm-upgrade", release: "remote", chart: "bitnami/nginx" },
89
+ ] }],
90
+ };
91
+ const bytes: Record<string, string> = { [contentDigest(chartFile)]: chartFile, [tgzDigest]: tgz };
92
+ const pinned = await pinReleaseTemplates([web], cut, cut.targets[0]!, async (d, encoding) => (encoding === "base64" && d !== tgzDigest ? null : bytes[d] ?? null));
93
+ if ("errors" in pinned) throw new Error(pinned.errors.join("\n"));
94
+ const step = pinned.components[0]!.deploy[0]!.steps[0] as unknown as { chart: string };
95
+ expect(step.chart).toMatch(/^\.chant\/release-templates\/[^/]+\/chart$/);
96
+ expect(pinned.files.map((f) => f.path.slice(step.chart.length + 1)).sort()).toEqual(["Chart.yaml", "charts/dep.tgz"]);
97
+ expect(pinned.files.find((f) => f.path.endsWith("dep.tgz"))).toMatchObject({ encoding: "base64", content: tgz });
98
+ // A chart named from a repository is not a local path the release records, and is left as written.
99
+ expect(pinned.components[0]!.deploy[0]!.steps[1]).toEqual(web.deploy[0]!.steps[1]);
100
+
101
+ await withTestDir(async (dir) => {
102
+ writePinnedTemplates(dir, pinned.files);
103
+ expect(readFileSync(join(dir, step.chart, "charts", "dep.tgz"))).toEqual(Buffer.from([0x1f, 0x8b, 0xff]));
104
+ removePinnedTemplates(dir, pinned.files);
105
+ expect(existsSync(join(dir, RELEASE_TEMPLATES_DIR))).toBe(false);
106
+ });
107
+
108
+ const edited = await pinReleaseTemplates([web], cut, cut.targets[0]!, async (d) => (d === contentDigest(chartFile) ? "name: edited\n" : bytes[d] ?? null));
109
+ expect("errors" in edited && edited.errors.join("\n")).toMatch(/template "chart\/Chart.yaml" hashes to .* refusing to deploy it/);
110
+ });
111
+
112
+ test("a target cut before #4155 (no trees) is pinned as then: the chart is read from the checkout", async () => {
113
+ const web: DriverComponent = { name: "api", deploy: [{ phase: "Deploy", steps: [{ kind: "helm-upgrade", release: "web", chart: "chart" }, ...api.deploy[1]!.steps] }] };
114
+ const pinned = await pinReleaseTemplates([web], release, prod, store(prodTemplate));
115
+ if ("errors" in pinned) throw new Error(pinned.errors.join("\n"));
116
+ expect(pinned.components[0]!.deploy[0]!.steps[0]).toEqual({ kind: "helm-upgrade", release: "web", chart: "chart" });
117
+ });
118
+
66
119
  test("targetFor names the environments a release was compiled for", () => {
67
120
  expect(targetFor(release, "prod")).toEqual({ target: prod });
68
121
  expect(targetFor(release, "qa")).toEqual({ error: expect.stringMatching(/has no target for "qa"; it was compiled for prod, staging/) });
@@ -11,14 +11,24 @@
11
11
  * under `.chant/release-templates/<digest>/` in the member's directory and the
12
12
  * step reads them from there. The path is relative and fixed by the digest,
13
13
  * so a gate approval bound to the promote's plan still matches on a re-run.
14
+ *
15
+ * A release cut since #4155 (a target with `trees`) records every local path
16
+ * a deploy step names (`recordedInputRefs`), directories file by file. A
17
+ * promote points each of them at the recorded copy: a directory is written
18
+ * under `.chant/release-templates/<digest>/<name>/`, its digest taken over its
19
+ * files the way a gate's directory digest is (`./deploy-inputs.ts`). A path
20
+ * outside the template fields that the release does not record, such as a
21
+ * chart named from a repository, is left as written. A target cut before
22
+ * #4155 is pinned as it was then: template fields only, other paths read from
23
+ * the checkout.
14
24
  */
15
25
 
16
26
  import { mkdirSync, readdirSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
17
27
  import { basename, dirname, join } from "node:path";
18
- import { contentDigest } from "../content-digest";
19
28
  import type { DriverComponent, DriverGate, DriverPhase, DriverStep } from "./driver";
20
- import { TEMPLATE_FIELDS, templateRefs } from "./release-compile";
21
- import type { Release, ReleaseTarget } from "../lifecycle/release-store";
29
+ import { recordedFields, recordedInputRefs, TEMPLATE_FIELDS, templateRefs, type RecordedInputRef } from "./release-compile";
30
+ import { templateBytesDigest, type Release, type ReleaseTarget } from "../lifecycle/release-store";
31
+ import { computePlanDigest } from "../lifecycle/plan-digest";
22
32
  import type { ReleaseRecord } from "../lifecycle/release-ledger";
23
33
  import type { BuildArchiveManifest } from "./verbs/build-archive";
24
34
 
@@ -29,9 +39,14 @@ export const RELEASE_TEMPLATES_DIR = ".chant/release-templates";
29
39
  export interface PinnedTemplate {
30
40
  /** Relative to the member's directory. */
31
41
  path: string;
42
+ /** The text, or the base64 of the bytes under `encoding: "base64"`. */
32
43
  content: string;
44
+ encoding?: "base64";
33
45
  }
34
46
 
47
+ /** Reads a recorded template's stored copy by digest: its text, or its base64 under `base64`. */
48
+ export type TemplateReader = (digest: string, encoding?: "base64") => Promise<string | null>;
49
+
35
50
  /** The release's target for `env`, or an error naming the environments it has. */
36
51
  export function targetFor(release: Release, env: string): { target: ReleaseTarget } | { error: string } {
37
52
  const target = release.targets.find((t) => t.env === env);
@@ -65,37 +80,45 @@ export async function pinReleaseTemplates(
65
80
  components: readonly DriverComponent[],
66
81
  release: Release,
67
82
  target: ReleaseTarget,
68
- readTemplate: (digest: string) => Promise<string | null>,
83
+ readTemplate: TemplateReader,
69
84
  ): Promise<{ components: DriverComponent[]; files: PinnedTemplate[] } | { errors: string[] }> {
70
85
  const errors: string[] = [];
71
86
  const files: PinnedTemplate[] = [];
72
87
  const out: DriverComponent[] = [];
88
+ // #4155: a target with `trees` records every deploy input; one without was cut before and records template fields only.
89
+ const full = target.trees !== undefined;
73
90
  for (const component of components) {
74
91
  const replace = new Map<string, string>();
75
- for (const ref of templateRefs(component)) {
92
+ const refs: RecordedInputRef[] = full ? recordedInputRefs(component) : templateRefs(component).map((r) => ({ ...r, required: true }));
93
+ for (const ref of refs) {
76
94
  if (replace.has(ref.value)) continue;
95
+ const dir = ref.path.replace(/\/+$/, "");
96
+ const tree = full ? target.trees!.find((t) => t.component === component.name && t.path === dir) : undefined;
97
+ if (tree) {
98
+ const pinned = await pinTree(component.name, tree.path, target, readTemplate);
99
+ if ("errors" in pinned) errors.push(...pinned.errors);
100
+ else {
101
+ files.push(...pinned.files);
102
+ replace.set(ref.value, pinned.path);
103
+ }
104
+ continue;
105
+ }
77
106
  const recorded = target.templates.find((t) => t.component === component.name && t.path === ref.path);
78
107
  if (!recorded) {
108
+ if (!ref.required) continue;
79
109
  errors.push(
80
110
  `component "${component.name}" reads ${ref.field} "${ref.value}", which release ${release.releaseDigest} does not record for "${target.env}"; ` +
81
111
  `this checkout's deploy is not the one the release was compiled from (commit ${release.commit.slice(0, 12)})`,
82
112
  );
83
113
  continue;
84
114
  }
85
- const content = await readTemplate(recorded.digest);
86
- if (content === null) {
87
- errors.push(`component "${component.name}": the bytes of template "${ref.path}" (${recorded.digest}) are not on the chant/lifecycle branch; fetch it and try again`);
88
- continue;
89
- }
90
- const got = contentDigest(content);
91
- if (got !== recorded.digest) {
92
- errors.push(
93
- `component "${component.name}": template "${ref.path}" hashes to ${got}, but the release's target for "${target.env}" records ${recorded.digest}; refusing to deploy it`,
94
- );
115
+ const read = await readChecked(component.name, ref.path, recorded, target, readTemplate);
116
+ if ("error" in read) {
117
+ errors.push(read.error);
95
118
  continue;
96
119
  }
97
120
  const path = join(RELEASE_TEMPLATES_DIR, recorded.digest.replace(/:/g, "_"), basename(ref.path));
98
- files.push({ path, content });
121
+ files.push({ path, content: read.content, ...(recorded.encoding ? { encoding: recorded.encoding } : {}) });
99
122
  replace.set(ref.value, path);
100
123
  }
101
124
  const rewrite = (phase: DriverPhase): DriverPhase => ({
@@ -104,7 +127,7 @@ export async function pinReleaseTemplates(
104
127
  if (isPhase(step)) return rewrite(step);
105
128
  if (isGate(step)) return step;
106
129
  const next: DriverStep = { ...step };
107
- for (const field of TEMPLATE_FIELDS) {
130
+ for (const field of full ? recordedFields(step.kind) : TEMPLATE_FIELDS) {
108
131
  const v = next[field];
109
132
  if (typeof v === "string" && replace.has(v)) next[field] = replace.get(v);
110
133
  else if (Array.isArray(v)) next[field] = v.map((x) => (typeof x === "string" && replace.has(x) ? replace.get(x) : x));
@@ -117,6 +140,51 @@ export async function pinReleaseTemplates(
117
140
  return errors.length > 0 ? { errors } : { components: out, files };
118
141
  }
119
142
 
143
+ /** One recorded template's stored copy, checked against its digest. */
144
+ async function readChecked(
145
+ component: string | null,
146
+ path: string,
147
+ recorded: ReleaseTarget["templates"][number],
148
+ target: ReleaseTarget,
149
+ readTemplate: TemplateReader,
150
+ ): Promise<{ content: string } | { error: string }> {
151
+ const who = component === null ? "" : `component "${component}": `;
152
+ const content = await readTemplate(recorded.digest, recorded.encoding);
153
+ if (content === null) return { error: `${who}the bytes of template "${path}" (${recorded.digest}) are not on the chant/lifecycle branch; fetch it and try again` };
154
+ const got = templateBytesDigest(content, recorded.encoding);
155
+ if (got !== recorded.digest) {
156
+ return { error: `${who}template "${path}" hashes to ${got}, but the release's target for "${target.env}" records ${recorded.digest}; refusing to deploy it` };
157
+ }
158
+ return { content };
159
+ }
160
+
161
+ /**
162
+ * A recorded directory's files, under `.chant/release-templates/<digest>/<name>/`
163
+ * (#4155). The digest is taken over the files' paths from the directory and
164
+ * their bytes, as a gate's directory digest is, so the path names the tree.
165
+ */
166
+ async function pinTree(
167
+ component: string,
168
+ dir: string,
169
+ target: ReleaseTarget,
170
+ readTemplate: TemplateReader,
171
+ ): Promise<{ path: string; files: PinnedTemplate[] } | { errors: string[] }> {
172
+ const prefix = `${dir}/`;
173
+ const entries = target.templates.filter((t) => t.component === component && t.path.startsWith(prefix));
174
+ const errors: string[] = [];
175
+ const read: Array<{ rel: string; content: string; encoding?: "base64"; digest: string }> = [];
176
+ for (const e of entries) {
177
+ const got = await readChecked(component, e.path, e, target, readTemplate);
178
+ if ("error" in got) errors.push(got.error);
179
+ else read.push({ rel: e.path.slice(prefix.length), content: got.content, digest: e.digest, ...(e.encoding ? { encoding: e.encoding } : {}) });
180
+ }
181
+ if (errors.length > 0) return { errors };
182
+ read.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
183
+ const digest = computePlanDigest("directory", read.map(({ rel, digest }) => ({ path: rel, digest })));
184
+ const path = join(RELEASE_TEMPLATES_DIR, digest.replace(/:/g, "_"), basename(dir));
185
+ return { path, files: read.map((r) => ({ path: join(path, r.rel), content: r.content, ...(r.encoding ? { encoding: r.encoding } : {}) })) };
186
+ }
187
+
120
188
  /**
121
189
  * The files a release's target records, read back by digest and checked, for
122
190
  * `chant release export` (#4046). A project with no components has no deploy
@@ -129,7 +197,7 @@ export async function pinReleaseTemplates(
129
197
  export async function targetFiles(
130
198
  release: Release,
131
199
  target: ReleaseTarget,
132
- readTemplate: (digest: string) => Promise<string | null>,
200
+ readTemplate: TemplateReader,
133
201
  ): Promise<{ files: PinnedTemplate[] } | { errors: string[] }> {
134
202
  const errors: string[] = [];
135
203
  const files: PinnedTemplate[] = [];
@@ -139,12 +207,12 @@ export async function targetFiles(
139
207
  errors.push(`${who}template "${t.path}" is not a path inside the member's directory; refusing to write it`);
140
208
  continue;
141
209
  }
142
- const content = await readTemplate(t.digest);
210
+ const content = await readTemplate(t.digest, t.encoding);
143
211
  if (content === null) {
144
212
  errors.push(`${who}the bytes of template "${t.path}" (${t.digest}) are not on the chant/lifecycle branch; fetch it and try again`);
145
213
  continue;
146
214
  }
147
- const got = contentDigest(content);
215
+ const got = templateBytesDigest(content, t.encoding);
148
216
  if (got !== t.digest) {
149
217
  errors.push(`${who}template "${t.path}" hashes to ${got}, but release ${release.releaseDigest} records ${t.digest} for "${target.env}"; refusing to write it`);
150
218
  continue;
@@ -154,7 +222,7 @@ export async function targetFiles(
154
222
  if (same.content !== content) errors.push(`two components record different bytes at "${t.path}" for "${target.env}", so they can't both be written there`);
155
223
  continue;
156
224
  }
157
- files.push({ path: t.path, content });
225
+ files.push({ path: t.path, content, ...(t.encoding ? { encoding: t.encoding } : {}) });
158
226
  }
159
227
  return errors.length > 0 ? { errors } : { files };
160
228
  }
@@ -164,7 +232,7 @@ export function writePinnedTemplates(dir: string, files: readonly PinnedTemplate
164
232
  for (const f of files) {
165
233
  const full = join(dir, f.path);
166
234
  mkdirSync(dirname(full), { recursive: true });
167
- writeFileSync(full, f.content);
235
+ writeFileSync(full, f.encoding === "base64" ? Buffer.from(f.content, "base64") : f.content);
168
236
  }
169
237
  }
170
238
 
@@ -175,7 +243,9 @@ export function writePinnedTemplates(dir: string, files: readonly PinnedTemplate
175
243
  * again from the release.
176
244
  */
177
245
  export function removePinnedTemplates(dir: string, files: readonly PinnedTemplate[]): void {
178
- for (const f of files) rmSync(dirname(join(dir, f.path)), { recursive: true, force: true });
246
+ // Each file sits under `.chant/release-templates/<digest>/`, a recorded tree's deeper; that digest directory goes.
247
+ const prefix = RELEASE_TEMPLATES_DIR.split("/").length;
248
+ for (const f of files) rmSync(join(dir, ...f.path.split(/[\\/]/).slice(0, prefix + 1)), { recursive: true, force: true });
179
249
  const root = join(dir, RELEASE_TEMPLATES_DIR);
180
250
  try {
181
251
  if (readdirSync(root).length === 0) rmdirSync(root);
@@ -72,7 +72,7 @@ export interface BomPackage {
72
72
  /** Version string, when known. Omitted for resources with no version concept (e.g. a CFN resource). */
73
73
  version?: string;
74
74
  /** Package manager / ecosystem this entry came from (e.g. "npm", "maven"), surfaced in SPDX's `externalRefs`/CycloneDX's `purl` when derivable. */
75
- type?: "npm" | "maven" | "config" | "external-reference";
75
+ type?: "npm" | "maven" | "cargo" | "config" | "external-reference";
76
76
  /** Package URL (purl), when derivable — e.g. `pkg:npm/left-pad@1.3.0`. */
77
77
  purl?: string;
78
78
  /** SPDX download location, when known. Defaults to `"NOASSERTION"` per the SPDX spec's requirement that every package declare one. */
@@ -11,6 +11,7 @@ import { describe, expect, it, afterEach } from "vitest";
11
11
  import {
12
12
  parseNpmPackageLock,
13
13
  parsePomXml,
14
+ parseCargoLock,
14
15
  createLockfileSbomGenerator,
15
16
  sbomOutputPath,
16
17
  } from "./lockfile-sbom-generator";
@@ -193,3 +194,38 @@ describe("createLockfileSbomGenerator (#613) — hermetic SbomGenerator backend"
193
194
  expect(docA.bytes).toBe(docB.bytes);
194
195
  });
195
196
  });
197
+
198
+ describe("parseCargoLock (#4168)", () => {
199
+ const LOCK = [
200
+ "version = 4",
201
+ "",
202
+ "[[package]]",
203
+ 'name = "app"',
204
+ 'version = "0.1.0"',
205
+ "dependencies = [",
206
+ ' "serde",',
207
+ ' "tokio 1.40.0",',
208
+ "]",
209
+ "",
210
+ "[[package]]",
211
+ 'name = "serde"',
212
+ 'version = "1.0.210"',
213
+ 'source = "registry+https://github.com/rust-lang/crates.io-index"',
214
+ "",
215
+ "[[package]]",
216
+ 'name = "tokio"',
217
+ 'version = "1.40.0"',
218
+ 'source = "registry+https://github.com/rust-lang/crates.io-index"',
219
+ "dependencies = [",
220
+ ' "pin-project-lite",',
221
+ "]",
222
+ "",
223
+ ].join("\n");
224
+
225
+ it("lists every package with a source, with its purl and dependencies, and leaves out the workspace's own crates", () => {
226
+ expect(parseCargoLock(LOCK)).toEqual([
227
+ { name: "serde", version: "1.0.210", type: "cargo", purl: "pkg:cargo/serde@1.0.210", dependsOn: undefined },
228
+ { name: "tokio", version: "1.40.0", type: "cargo", purl: "pkg:cargo/tokio@1.40.0", dependsOn: ["pin-project-lite"] },
229
+ ]);
230
+ });
231
+ });
@@ -148,6 +148,44 @@ export function parsePomXml(content: string): BomPackage[] {
148
148
  return packages.sort((a, b) => a.name.localeCompare(b.name));
149
149
  }
150
150
 
151
+ // ── Cargo.lock (Rust, #4168 decision 12) ────────────────────────────────────
152
+
153
+ /**
154
+ * Parse a `Cargo.lock` (version 3 or 4) into a flat `BomPackage[]`: one
155
+ * entry per `[[package]]` with a `source`, so the workspace's own crates,
156
+ * which have none, are left out as the npm parser leaves out the root.
157
+ * A dependency is named `name` or `name version` in the lock; only the name
158
+ * is kept. A small line reader, not a TOML parser: the lock is written by
159
+ * cargo in one fixed shape.
160
+ */
161
+ export function parseCargoLock(content: string): BomPackage[] {
162
+ const seen = new Map<string, BomPackage>();
163
+ const blocks = content.split(/^\[\[package\]\]\s*$/m).slice(1);
164
+ for (const block of blocks) {
165
+ const body = block.split(/^\[/m)[0] ?? "";
166
+ const field = (key: string) => new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m").exec(body)?.[1];
167
+ const name = field("name");
168
+ const version = field("version");
169
+ if (!name || field("source") === undefined) continue;
170
+ const deps = /^dependencies\s*=\s*\[([\s\S]*?)\]/m.exec(body)?.[1];
171
+ const dependsOn = deps ? [...deps.matchAll(/"([^"\s]+)[^"]*"/g)].map((m) => m[1]!) : [];
172
+ const key = `${name}@${version ?? ""}`;
173
+ if (seen.has(key)) continue;
174
+ seen.set(key, {
175
+ name,
176
+ version,
177
+ type: "cargo",
178
+ purl: version ? `pkg:cargo/${encodeURIComponent(name)}@${version}` : undefined,
179
+ dependsOn: dependsOn.length ? [...new Set(dependsOn)] : undefined,
180
+ });
181
+ }
182
+ return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name) || (a.version ?? "").localeCompare(b.version ?? ""));
183
+ }
184
+
185
+ /** Each lockfile kind's parser and file name. */
186
+ export const LOCKFILE_PARSERS = { npm: parseNpmPackageLock, maven: parsePomXml, cargo: parseCargoLock } as const;
187
+ export const LOCKFILE_NAMES = { npm: "package-lock.json", maven: "pom.xml", cargo: "Cargo.lock" } as const;
188
+
151
189
  // ── hermetic SbomGenerator backend ───────────────────────────────────────────
152
190
 
153
191
  export interface LockfileSbomGeneratorOptions {
@@ -158,8 +196,8 @@ export interface LockfileSbomGeneratorOptions {
158
196
  }
159
197
 
160
198
  /** Locate the first readable lockfile/manifest under `dir` this generator knows how to parse, in preference order (npm before Maven — most chant components are Node-first). Returns `undefined` if none is found. */
161
- function findManifest(dir: string): { kind: "npm" | "maven"; path: string } | undefined {
162
- for (const [kind, filename] of [["npm", "package-lock.json"], ["maven", "pom.xml"]] as const) {
199
+ function findManifest(dir: string): { kind: "npm" | "maven" | "cargo"; path: string } | undefined {
200
+ for (const [kind, filename] of [["npm", "package-lock.json"], ["maven", "pom.xml"], ["cargo", "Cargo.lock"]] as const) {
163
201
  const candidate = join(dir, filename);
164
202
  try {
165
203
  readFileSync(candidate, "utf-8");
@@ -195,13 +233,13 @@ export function createLockfileSbomGenerator(options: LockfileSbomGeneratorOption
195
233
  const manifest = findManifest(dir);
196
234
  if (!manifest) {
197
235
  throw new Error(
198
- `no supported lockfile/manifest found under "${dir}" (looked for package-lock.json, pom.xml) — ` +
236
+ `no supported lockfile/manifest found under "${dir}" (looked for package-lock.json, pom.xml, Cargo.lock) — ` +
199
237
  `the hermetic lockfile SBOM backend requires a lockfile on disk; use the deep-scan path (#610) for images with no lockfile`,
200
238
  );
201
239
  }
202
240
  const content = readFileSync(manifest.path, "utf-8");
203
- const packages = manifest.kind === "npm" ? parseNpmPackageLock(content) : parsePomXml(content);
204
- const generatorName = manifest.kind === "npm" ? "chant-lockfile-sbom/package-lock.json" : "chant-lockfile-sbom/pom.xml";
241
+ const packages = LOCKFILE_PARSERS[manifest.kind](content);
242
+ const generatorName = `chant-lockfile-sbom/${LOCKFILE_NAMES[manifest.kind]}`;
205
243
 
206
244
  const doc = writeBom(
207
245
  format,