@intentius/chant 0.124.28 → 0.124.30

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 (449) hide show
  1. package/dist/build-params.d.ts +34 -1
  2. package/dist/build-params.d.ts.map +1 -1
  3. package/dist/build-params.js +31 -1
  4. package/dist/change-set-graph.d.ts +8 -1
  5. package/dist/change-set-graph.d.ts.map +1 -1
  6. package/dist/change-set-graph.js +4 -2
  7. package/dist/change-set.d.ts +10 -0
  8. package/dist/change-set.d.ts.map +1 -1
  9. package/dist/change-set.js +4 -3
  10. package/dist/cli/build-params-cli.d.ts +7 -1
  11. package/dist/cli/build-params-cli.d.ts.map +1 -1
  12. package/dist/cli/build-params-cli.js +17 -8
  13. package/dist/cli/commands/build.d.ts.map +1 -1
  14. package/dist/cli/commands/build.js +6 -3
  15. package/dist/cli/handlers/approve.d.ts.map +1 -1
  16. package/dist/cli/handlers/approve.js +45 -0
  17. package/dist/cli/handlers/build.d.ts.map +1 -1
  18. package/dist/cli/handlers/build.js +18 -4
  19. package/dist/cli/handlers/fan-out.d.ts +9 -0
  20. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  21. package/dist/cli/handlers/fan-out.js +67 -8
  22. package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
  23. package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
  24. package/dist/cli/handlers/lifecycle-gc.js +143 -0
  25. package/dist/cli/handlers/pr.d.ts +11 -3
  26. package/dist/cli/handlers/pr.d.ts.map +1 -1
  27. package/dist/cli/handlers/pr.js +134 -26
  28. package/dist/cli/handlers/promote.d.ts +7 -0
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/promote.js +210 -33
  31. package/dist/cli/handlers/release.d.ts +62 -5
  32. package/dist/cli/handlers/release.d.ts.map +1 -1
  33. package/dist/cli/handlers/release.js +414 -28
  34. package/dist/cli/handlers/run.d.ts +11 -1
  35. package/dist/cli/handlers/run.d.ts.map +1 -1
  36. package/dist/cli/handlers/run.js +67 -6
  37. package/dist/cli/main.d.ts.map +1 -1
  38. package/dist/cli/main.js +91 -10
  39. package/dist/cli/parse-args.d.ts.map +1 -1
  40. package/dist/cli/parse-args.js +73 -2
  41. package/dist/cli/registry.d.ts +32 -4
  42. package/dist/cli/registry.d.ts.map +1 -1
  43. package/dist/cli/run-events.d.ts +3 -2
  44. package/dist/cli/run-events.d.ts.map +1 -1
  45. package/dist/cli/run-events.js +5 -3
  46. package/dist/components/auto-release.d.ts +2 -0
  47. package/dist/components/auto-release.d.ts.map +1 -1
  48. package/dist/components/auto-release.js +1 -0
  49. package/dist/components/capability.d.ts +9 -0
  50. package/dist/components/capability.d.ts.map +1 -1
  51. package/dist/components/driver.d.ts +8 -0
  52. package/dist/components/driver.d.ts.map +1 -1
  53. package/dist/components/driver.js +35 -0
  54. package/dist/components/fan-out-inputs.d.ts +69 -0
  55. package/dist/components/fan-out-inputs.d.ts.map +1 -0
  56. package/dist/components/fan-out-inputs.js +169 -0
  57. package/dist/components/fan-out-record.d.ts +13 -0
  58. package/dist/components/fan-out-record.d.ts.map +1 -1
  59. package/dist/components/fan-out-record.js +16 -0
  60. package/dist/components/fan-out-run.d.ts.map +1 -1
  61. package/dist/components/fan-out-run.js +50 -0
  62. package/dist/components/fan-out.d.ts +4 -0
  63. package/dist/components/fan-out.d.ts.map +1 -1
  64. package/dist/components/pr-pipeline.d.ts +49 -0
  65. package/dist/components/pr-pipeline.d.ts.map +1 -1
  66. package/dist/components/pr-pipeline.js +38 -4
  67. package/dist/components/pr-run.d.ts +20 -2
  68. package/dist/components/pr-run.d.ts.map +1 -1
  69. package/dist/components/pr-run.js +26 -13
  70. package/dist/components/promote.d.ts +23 -0
  71. package/dist/components/promote.d.ts.map +1 -1
  72. package/dist/components/promote.js +60 -2
  73. package/dist/components/release-compile.d.ts +9 -0
  74. package/dist/components/release-compile.d.ts.map +1 -1
  75. package/dist/components/release-compile.js +9 -3
  76. package/dist/components/release-gate.d.ts +11 -1
  77. package/dist/components/release-gate.d.ts.map +1 -1
  78. package/dist/components/release-gate.js +10 -4
  79. package/dist/components/wave-plan.d.ts +16 -0
  80. package/dist/components/wave-plan.d.ts.map +1 -1
  81. package/dist/components/wave-plan.js +36 -2
  82. package/dist/config.d.ts +38 -0
  83. package/dist/config.d.ts.map +1 -1
  84. package/dist/config.js +2 -0
  85. package/dist/flag-params.d.ts +54 -0
  86. package/dist/flag-params.d.ts.map +1 -0
  87. package/dist/flag-params.js +130 -0
  88. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
  89. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
  90. package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
  91. package/dist/lifecycle/change-set-store.d.ts +2 -1
  92. package/dist/lifecycle/change-set-store.d.ts.map +1 -1
  93. package/dist/lifecycle/change-set-store.js +2 -1
  94. package/dist/lifecycle/delivery-metrics.d.ts +139 -0
  95. package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
  96. package/dist/lifecycle/delivery-metrics.js +145 -0
  97. package/dist/lifecycle/deploy-outcome.d.ts +81 -0
  98. package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
  99. package/dist/lifecycle/deploy-outcome.js +101 -0
  100. package/dist/lifecycle/gate-change-set.d.ts +10 -1
  101. package/dist/lifecycle/gate-change-set.d.ts.map +1 -1
  102. package/dist/lifecycle/gate-change-set.js +35 -3
  103. package/dist/lifecycle/gate-ledger.d.ts +2 -3
  104. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  105. package/dist/lifecycle/git.d.ts +30 -1
  106. package/dist/lifecycle/git.d.ts.map +1 -1
  107. package/dist/lifecycle/git.js +31 -5
  108. package/dist/lifecycle/index.d.ts +4 -0
  109. package/dist/lifecycle/index.d.ts.map +1 -1
  110. package/dist/lifecycle/index.js +4 -0
  111. package/dist/lifecycle/mixed-environment.d.ts +95 -0
  112. package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
  113. package/dist/lifecycle/mixed-environment.js +154 -0
  114. package/dist/lifecycle/publish-attachments.d.ts +79 -0
  115. package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
  116. package/dist/lifecycle/publish-attachments.js +179 -0
  117. package/dist/lifecycle/publish-credentials.d.ts +37 -0
  118. package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
  119. package/dist/lifecycle/publish-credentials.js +69 -0
  120. package/dist/lifecycle/publish-declaration.d.ts +97 -0
  121. package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
  122. package/dist/lifecycle/publish-declaration.js +83 -0
  123. package/dist/lifecycle/publish-forge.d.ts +53 -0
  124. package/dist/lifecycle/publish-forge.d.ts.map +1 -0
  125. package/dist/lifecycle/publish-forge.js +298 -0
  126. package/dist/lifecycle/publish-npm.d.ts +61 -0
  127. package/dist/lifecycle/publish-npm.d.ts.map +1 -0
  128. package/dist/lifecycle/publish-npm.js +242 -0
  129. package/dist/lifecycle/publish-oci.d.ts +64 -0
  130. package/dist/lifecycle/publish-oci.d.ts.map +1 -0
  131. package/dist/lifecycle/publish-oci.js +383 -0
  132. package/dist/lifecycle/publish-record.d.ts +66 -0
  133. package/dist/lifecycle/publish-record.d.ts.map +1 -0
  134. package/dist/lifecycle/publish-record.js +84 -0
  135. package/dist/lifecycle/publish-targets.d.ts +45 -0
  136. package/dist/lifecycle/publish-targets.d.ts.map +1 -0
  137. package/dist/lifecycle/publish-targets.js +85 -0
  138. package/dist/lifecycle/release-cut.d.ts +48 -1
  139. package/dist/lifecycle/release-cut.d.ts.map +1 -1
  140. package/dist/lifecycle/release-cut.js +116 -23
  141. package/dist/lifecycle/release-join.d.ts +95 -0
  142. package/dist/lifecycle/release-join.d.ts.map +1 -0
  143. package/dist/lifecycle/release-join.js +139 -0
  144. package/dist/lifecycle/release-ledger.d.ts +8 -3
  145. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  146. package/dist/lifecycle/release-ledger.js +2 -0
  147. package/dist/lifecycle/release-publish.d.ts +312 -0
  148. package/dist/lifecycle/release-publish.d.ts.map +1 -0
  149. package/dist/lifecycle/release-publish.js +335 -0
  150. package/dist/lifecycle/release-revocation.d.ts +80 -0
  151. package/dist/lifecycle/release-revocation.d.ts.map +1 -0
  152. package/dist/lifecycle/release-revocation.js +123 -0
  153. package/dist/lifecycle/release-store.d.ts +12 -2
  154. package/dist/lifecycle/release-store.d.ts.map +1 -1
  155. package/dist/lifecycle/release-store.js +4 -2
  156. package/dist/lifecycle/release-verify.d.ts +61 -0
  157. package/dist/lifecycle/release-verify.d.ts.map +1 -0
  158. package/dist/lifecycle/release-verify.js +92 -0
  159. package/dist/lifecycle/release-workstation.d.ts +91 -0
  160. package/dist/lifecycle/release-workstation.d.ts.map +1 -0
  161. package/dist/lifecycle/release-workstation.js +180 -0
  162. package/dist/lifecycle/retention-policy.d.ts +45 -0
  163. package/dist/lifecycle/retention-policy.d.ts.map +1 -0
  164. package/dist/lifecycle/retention-policy.js +43 -0
  165. package/dist/lifecycle/retention.d.ts +185 -0
  166. package/dist/lifecycle/retention.d.ts.map +1 -0
  167. package/dist/lifecycle/retention.js +400 -0
  168. package/dist/lifecycle/run-ledger.d.ts +1 -1
  169. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  170. package/dist/lifecycle/run-ledger.js +9 -0
  171. package/dist/op/activities/activity-contracts.d.ts +1 -0
  172. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  173. package/dist/op/activities/activity-contracts.js +1 -0
  174. package/dist/op/activities/shell.d.ts +6 -0
  175. package/dist/op/activities/shell.d.ts.map +1 -1
  176. package/dist/op/activities/shell.js +1 -1
  177. package/dist/op/local-executor.d.ts +23 -1
  178. package/dist/op/local-executor.d.ts.map +1 -1
  179. package/dist/op/local-executor.js +9 -3
  180. package/dist/op/runtime.d.ts +27 -0
  181. package/dist/op/runtime.d.ts.map +1 -1
  182. package/dist/op/runtime.js +20 -0
  183. package/dist/plan-summary.d.ts.map +1 -1
  184. package/dist/plan-summary.js +8 -2
  185. package/dist/pr-forge.d.ts +23 -0
  186. package/dist/pr-forge.d.ts.map +1 -1
  187. package/dist/pr-forge.js +44 -0
  188. package/dist/pr-loop.d.ts +17 -0
  189. package/dist/pr-loop.d.ts.map +1 -1
  190. package/dist/pr-loop.js +30 -4
  191. package/dist/pr-report-url.d.ts +82 -0
  192. package/dist/pr-report-url.d.ts.map +1 -0
  193. package/dist/pr-report-url.js +174 -0
  194. package/dist/provenance.d.ts +2 -2
  195. package/dist/provenance.d.ts.map +1 -1
  196. package/dist/run-event-store.d.ts +29 -2
  197. package/dist/run-event-store.d.ts.map +1 -1
  198. package/dist/run-event-store.js +50 -3
  199. package/dist/run-event-stream.d.ts +4 -3
  200. package/dist/run-event-stream.d.ts.map +1 -1
  201. package/dist/run-event-stream.js +7 -3
  202. package/dist/run-event.d.ts +34 -1
  203. package/dist/run-event.d.ts.map +1 -1
  204. package/dist/run-event.js +4 -0
  205. package/dist/wave-runtime.mjs +260 -229
  206. package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
  207. package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
  208. package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
  209. package/dist/workspace/change-set.schema.json +5 -1
  210. package/dist/workspace/check.schema.json +4 -2
  211. package/dist/workspace/checks/records.d.ts +9 -1
  212. package/dist/workspace/checks/records.d.ts.map +1 -1
  213. package/dist/workspace/checks/records.js +35 -1
  214. package/dist/workspace/checks.d.ts +1 -0
  215. package/dist/workspace/checks.d.ts.map +1 -1
  216. package/dist/workspace/checks.js +2 -1
  217. package/dist/workspace/ci-green-forge.d.ts +2 -0
  218. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  219. package/dist/workspace/ci-green.d.ts +10 -0
  220. package/dist/workspace/ci-green.d.ts.map +1 -1
  221. package/dist/workspace/ci-green.js +14 -5
  222. package/dist/workspace/ci-run.d.ts +222 -0
  223. package/dist/workspace/ci-run.d.ts.map +1 -0
  224. package/dist/workspace/ci-run.js +464 -0
  225. package/dist/workspace/ci-serve.d.ts +214 -0
  226. package/dist/workspace/ci-serve.d.ts.map +1 -0
  227. package/dist/workspace/ci-serve.js +412 -0
  228. package/dist/workspace/declaration.d.ts +16 -0
  229. package/dist/workspace/declaration.d.ts.map +1 -1
  230. package/dist/workspace/declaration.js +26 -0
  231. package/dist/workspace/declaration.schema.json +64 -0
  232. package/dist/workspace/declared-kinds.d.ts +16 -0
  233. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  234. package/dist/workspace/declared-kinds.js +25 -0
  235. package/dist/workspace/delivery-metrics.d.ts +91 -0
  236. package/dist/workspace/delivery-metrics.d.ts.map +1 -0
  237. package/dist/workspace/delivery-metrics.js +217 -0
  238. package/dist/workspace/delivery-metrics.schema.json +519 -0
  239. package/dist/workspace/flags-read.d.ts +37 -0
  240. package/dist/workspace/flags-read.d.ts.map +1 -0
  241. package/dist/workspace/flags-read.js +103 -0
  242. package/dist/workspace/flags.d.ts +100 -0
  243. package/dist/workspace/flags.d.ts.map +1 -0
  244. package/dist/workspace/flags.js +98 -0
  245. package/dist/workspace/graph-cli.js +1 -1
  246. package/dist/workspace/intent-cli.d.ts +11 -2
  247. package/dist/workspace/intent-cli.d.ts.map +1 -1
  248. package/dist/workspace/intent-cli.js +54 -5
  249. package/dist/workspace/intent-trace.d.ts +219 -0
  250. package/dist/workspace/intent-trace.d.ts.map +1 -0
  251. package/dist/workspace/intent-trace.js +314 -0
  252. package/dist/workspace/intent.d.ts +30 -6
  253. package/dist/workspace/intent.d.ts.map +1 -1
  254. package/dist/workspace/intent.js +141 -21
  255. package/dist/workspace/intent.schema.json +702 -6
  256. package/dist/workspace/overlap.d.ts +146 -0
  257. package/dist/workspace/overlap.d.ts.map +1 -0
  258. package/dist/workspace/overlap.js +276 -0
  259. package/dist/workspace/pr-report.schema.json +5 -0
  260. package/dist/workspace/reason-codes.d.ts +2 -0
  261. package/dist/workspace/reason-codes.d.ts.map +1 -1
  262. package/dist/workspace/reason-codes.js +3 -0
  263. package/dist/workspace/records.d.ts +15 -2
  264. package/dist/workspace/records.d.ts.map +1 -1
  265. package/dist/workspace/records.js +32 -0
  266. package/dist/workspace/records.schema.json +21 -2
  267. package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  268. package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
  269. package/dist/workspace/releases.d.ts +9 -0
  270. package/dist/workspace/releases.d.ts.map +1 -1
  271. package/dist/workspace/releases.js +9 -0
  272. package/dist/workspace/releases.schema.json +123 -1
  273. package/dist/workspace/run-event.schema.json +24 -6
  274. package/dist/workspace/status-gates.d.ts +8 -0
  275. package/dist/workspace/status-gates.d.ts.map +1 -1
  276. package/dist/workspace/status-stewards.d.ts +2 -0
  277. package/dist/workspace/status-stewards.d.ts.map +1 -1
  278. package/dist/workspace/status-stewards.js +1 -0
  279. package/dist/workspace/status.d.ts +78 -0
  280. package/dist/workspace/status.d.ts.map +1 -1
  281. package/dist/workspace/status.js +170 -3
  282. package/dist/workspace/status.schema.json +210 -2
  283. package/dist/workspace/wip.d.ts.map +1 -1
  284. package/dist/workspace/wip.js +11 -3
  285. package/package.json +1 -1
  286. package/src/build-params.test.ts +46 -0
  287. package/src/build-params.ts +55 -1
  288. package/src/change-set-graph.ts +10 -2
  289. package/src/change-set.ts +14 -3
  290. package/src/cli/build-params-cli.ts +22 -9
  291. package/src/cli/commands/build.ts +6 -3
  292. package/src/cli/handlers/approve-release.test.ts +5 -1
  293. package/src/cli/handlers/approve-revoked.test.ts +155 -0
  294. package/src/cli/handlers/approve.ts +47 -0
  295. package/src/cli/handlers/build.test.ts +30 -0
  296. package/src/cli/handlers/build.ts +20 -4
  297. package/src/cli/handlers/fan-out.test.ts +262 -1
  298. package/src/cli/handlers/fan-out.ts +72 -8
  299. package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
  300. package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
  301. package/src/cli/handlers/lifecycle-gc.ts +146 -0
  302. package/src/cli/handlers/pr.test.ts +148 -1
  303. package/src/cli/handlers/pr.ts +144 -28
  304. package/src/cli/handlers/promote.test.ts +280 -2
  305. package/src/cli/handlers/promote.ts +210 -34
  306. package/src/cli/handlers/release-implicit.e2e.test.ts +12 -4
  307. package/src/cli/handlers/release-join.e2e.test.ts +185 -0
  308. package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
  309. package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
  310. package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
  311. package/src/cli/handlers/release.e2e.test.ts +146 -32
  312. package/src/cli/handlers/release.ts +415 -28
  313. package/src/cli/handlers/run-auto-release.test.ts +9 -0
  314. package/src/cli/handlers/run-release.test.ts +55 -0
  315. package/src/cli/handlers/run.ts +69 -5
  316. package/src/cli/main.test.ts +1 -0
  317. package/src/cli/main.ts +91 -10
  318. package/src/cli/parse-args.ts +54 -2
  319. package/src/cli/registry.ts +32 -4
  320. package/src/cli/run-events.ts +5 -2
  321. package/src/components/auto-release.test.ts +9 -0
  322. package/src/components/auto-release.ts +3 -0
  323. package/src/components/capability.ts +9 -0
  324. package/src/components/driver.test.ts +24 -0
  325. package/src/components/driver.ts +36 -0
  326. package/src/components/fan-out-inputs.test.ts +85 -0
  327. package/src/components/fan-out-inputs.ts +192 -0
  328. package/src/components/fan-out-record.test.ts +10 -0
  329. package/src/components/fan-out-record.ts +19 -0
  330. package/src/components/fan-out-run.ts +48 -0
  331. package/src/components/fan-out.ts +4 -0
  332. package/src/components/pr-pipeline.ts +67 -4
  333. package/src/components/pr-run.test.ts +127 -0
  334. package/src/components/pr-run.ts +32 -12
  335. package/src/components/promote.ts +68 -2
  336. package/src/components/release-compile.ts +17 -3
  337. package/src/components/release-gate.ts +18 -4
  338. package/src/components/wave-plan.ts +45 -2
  339. package/src/config.ts +8 -0
  340. package/src/flag-params.test.ts +144 -0
  341. package/src/flag-params.ts +134 -0
  342. package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
  343. package/src/lifecycle/change-set-store.ts +2 -1
  344. package/src/lifecycle/delivery-metrics.ts +263 -0
  345. package/src/lifecycle/deploy-outcome.ts +147 -0
  346. package/src/lifecycle/gate-change-set.ts +37 -6
  347. package/src/lifecycle/gate-ledger.ts +2 -2
  348. package/src/lifecycle/git.ts +61 -8
  349. package/src/lifecycle/index.ts +4 -0
  350. package/src/lifecycle/mixed-environment.test.ts +97 -0
  351. package/src/lifecycle/mixed-environment.ts +204 -0
  352. package/src/lifecycle/publish-attachments.ts +196 -0
  353. package/src/lifecycle/publish-credentials.ts +84 -0
  354. package/src/lifecycle/publish-declaration.ts +95 -0
  355. package/src/lifecycle/publish-forge.ts +325 -0
  356. package/src/lifecycle/publish-npm.ts +259 -0
  357. package/src/lifecycle/publish-oci.ts +396 -0
  358. package/src/lifecycle/publish-record.ts +113 -0
  359. package/src/lifecycle/publish-resume.test.ts +168 -0
  360. package/src/lifecycle/publish-targets.test.ts +494 -0
  361. package/src/lifecycle/publish-targets.ts +100 -0
  362. package/src/lifecycle/release-cut.test.ts +74 -2
  363. package/src/lifecycle/release-cut.ts +158 -22
  364. package/src/lifecycle/release-flag.test.ts +112 -0
  365. package/src/lifecycle/release-join.test.ts +68 -0
  366. package/src/lifecycle/release-join.ts +182 -0
  367. package/src/lifecycle/release-ledger.ts +9 -3
  368. package/src/lifecycle/release-publish.test.ts +225 -0
  369. package/src/lifecycle/release-publish.ts +568 -0
  370. package/src/lifecycle/release-revocation.ts +147 -0
  371. package/src/lifecycle/release-store.ts +15 -4
  372. package/src/lifecycle/release-verify.test.ts +110 -0
  373. package/src/lifecycle/release-verify.ts +120 -0
  374. package/src/lifecycle/release-workstation.test.ts +204 -0
  375. package/src/lifecycle/release-workstation.ts +216 -0
  376. package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
  377. package/src/lifecycle/retention-policy.ts +63 -0
  378. package/src/lifecycle/retention.test.ts +363 -0
  379. package/src/lifecycle/retention.ts +508 -0
  380. package/src/lifecycle/run-ledger.ts +7 -1
  381. package/src/op/activities/activity-contracts.ts +1 -0
  382. package/src/op/activities/shell.ts +7 -1
  383. package/src/op/local-executor.ts +27 -4
  384. package/src/op/runtime.ts +42 -0
  385. package/src/plan-summary.test.ts +20 -0
  386. package/src/plan-summary.ts +8 -1
  387. package/src/pr-forge.test.ts +25 -0
  388. package/src/pr-forge.ts +62 -0
  389. package/src/pr-loop.test.ts +13 -1
  390. package/src/pr-loop.ts +41 -4
  391. package/src/pr-report-url.test.ts +131 -0
  392. package/src/pr-report-url.ts +209 -0
  393. package/src/provenance.ts +2 -2
  394. package/src/run-event-store.ts +63 -3
  395. package/src/run-event-stream.ts +8 -4
  396. package/src/run-event.ts +38 -1
  397. package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
  398. package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
  399. package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
  400. package/src/workspace/change-set.schema.json +5 -1
  401. package/src/workspace/check.schema.json +4 -2
  402. package/src/workspace/checks/records.ts +35 -1
  403. package/src/workspace/checks.ts +2 -1
  404. package/src/workspace/ci-green-forge.ts +2 -0
  405. package/src/workspace/ci-green.ts +20 -5
  406. package/src/workspace/ci-run.test.ts +416 -0
  407. package/src/workspace/ci-run.ts +571 -0
  408. package/src/workspace/ci-serve.test.ts +268 -0
  409. package/src/workspace/ci-serve.ts +521 -0
  410. package/src/workspace/declaration.schema.json +64 -0
  411. package/src/workspace/declaration.test.ts +28 -0
  412. package/src/workspace/declaration.ts +38 -0
  413. package/src/workspace/declared-kinds.ts +40 -1
  414. package/src/workspace/delivery-metrics.schema.json +519 -0
  415. package/src/workspace/delivery-metrics.test.ts +236 -0
  416. package/src/workspace/delivery-metrics.ts +286 -0
  417. package/src/workspace/flags-read.ts +121 -0
  418. package/src/workspace/flags.test.ts +141 -0
  419. package/src/workspace/flags.ts +157 -0
  420. package/src/workspace/graph-cli.ts +1 -1
  421. package/src/workspace/intent-cli.ts +46 -5
  422. package/src/workspace/intent-trace.test.ts +173 -0
  423. package/src/workspace/intent-trace.ts +484 -0
  424. package/src/workspace/intent.schema.json +702 -6
  425. package/src/workspace/intent.ts +148 -25
  426. package/src/workspace/older-records.test.ts +412 -0
  427. package/src/workspace/overlap.test.ts +165 -0
  428. package/src/workspace/overlap.ts +340 -0
  429. package/src/workspace/pr-report.schema.json +5 -0
  430. package/src/workspace/read-contract.e2e.test.ts +2 -1
  431. package/src/workspace/reason-codes.test.ts +5 -0
  432. package/src/workspace/reason-codes.ts +3 -0
  433. package/src/workspace/record-assets.test.ts +2 -1
  434. package/src/workspace/records-contract.e2e.test.ts +2 -1
  435. package/src/workspace/records.schema.json +21 -2
  436. package/src/workspace/records.ts +36 -2
  437. package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  438. package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
  439. package/src/workspace/releases.schema.json +123 -1
  440. package/src/workspace/releases.ts +11 -0
  441. package/src/workspace/run-event.schema.json +24 -6
  442. package/src/workspace/status-contract.test.ts +29 -0
  443. package/src/workspace/status-gates.ts +8 -0
  444. package/src/workspace/status-mixed.test.ts +152 -0
  445. package/src/workspace/status-revoked.test.ts +153 -0
  446. package/src/workspace/status-stewards.ts +3 -0
  447. package/src/workspace/status.schema.json +210 -2
  448. package/src/workspace/status.ts +254 -3
  449. package/src/workspace/wip.ts +11 -2
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Build-time parameters from workspace flags (#4024): what a build for an
3
+ * environment resolves in a member with flags, and the precedence rule that a
4
+ * flag-backed parameter can't be overridden where the environment has a gate.
5
+ * On the fixture in `workspace/__fixtures__/flag-workspace.ts`, where member
6
+ * `shop`'s component declares a gate and member `blog`'s does not.
7
+ */
8
+
9
+ import { writeFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
12
+ import { buildCommand } from "./cli/commands/build";
13
+ import type { Serializer } from "./serializer";
14
+ import type { BuildParamsConfig } from "./build-params";
15
+ import { loadBuildParamFlags, resolveFlaggedBuildParams } from "./flag-params";
16
+ import { cleanScratch, scratchDir } from "./workspace/__fixtures__/contract-repo";
17
+ import { flagWorkspace } from "./workspace/__fixtures__/flag-workspace";
18
+
19
+ const SHOP: BuildParamsConfig = { newCheckout: { type: "boolean", default: false }, tier: { type: "string", default: "small" } };
20
+ const BLOG: BuildParamsConfig = { banner: { type: "boolean", default: false } };
21
+
22
+ let root: string;
23
+ beforeAll(() => {
24
+ root = flagWorkspace();
25
+ });
26
+ afterAll(cleanScratch);
27
+
28
+ const quiet = { echo: false } as const;
29
+
30
+ describe("loadBuildParamFlags", () => {
31
+ test("outside a workspace, and without an environment, there are no flags", async () => {
32
+ const plain = scratchDir("chant-flags-plain-");
33
+ expect(await loadBuildParamFlags(plain, "prod")).toEqual({ flags: undefined, errors: [] });
34
+ expect(await loadBuildParamFlags(join(root, "shop"), undefined)).toEqual({ flags: undefined, errors: [] });
35
+ });
36
+
37
+ test("in a member, each flag-backed parameter with the flag's value for the environment", async () => {
38
+ expect(await loadBuildParamFlags(join(root, "shop"), "dev")).toEqual({
39
+ flags: { env: "dev", byParam: { newCheckout: { flag: "new-checkout", value: true } } },
40
+ errors: [],
41
+ });
42
+ });
43
+
44
+ test("a flag kind that can't be read stops the build rather than building without its flags", async () => {
45
+ const dir = flagWorkspace({ "flags/flag.schema.json": "{ not json" });
46
+ const loaded = await loadBuildParamFlags(join(dir, "shop"), "prod");
47
+ expect(loaded.flags).toBeUndefined();
48
+ expect(loaded.errors[0]).toMatch(/the workspace's flag records can't be read/);
49
+ });
50
+ });
51
+
52
+ describe("resolveFlaggedBuildParams", () => {
53
+ test("records the flag as the parameter's source, per environment", async () => {
54
+ const prod = await resolveFlaggedBuildParams(join(root, "shop"), SHOP, quiet, "prod");
55
+ expect(prod).toEqual({
56
+ success: true,
57
+ errors: [],
58
+ provenance: [
59
+ { name: "newCheckout", value: false, source: "flag:new-checkout" },
60
+ { name: "tier", value: "small", source: "default" },
61
+ ],
62
+ });
63
+ const dev = await resolveFlaggedBuildParams(join(root, "shop"), SHOP, quiet, "dev");
64
+ expect(dev.provenance[0]).toEqual({ name: "newCheckout", value: true, source: "flag:new-checkout" });
65
+ // staging: the record gives no value, so the declared default applies.
66
+ expect((await resolveFlaggedBuildParams(join(root, "shop"), SHOP, quiet, "staging")).provenance[0]).toEqual({ name: "newCheckout", value: false, source: "default" });
67
+ });
68
+
69
+ test("--param can't override a flag in an environment with a gate: the build refuses", async () => {
70
+ const refused = await resolveFlaggedBuildParams(join(root, "shop"), SHOP, { ...quiet, cli: { newCheckout: "true" } }, "prod");
71
+ expect(refused.success).toBe(false);
72
+ expect(refused.provenance).toEqual([]);
73
+ expect(refused.errors).toHaveLength(1);
74
+ expect(refused.errors[0]).toMatch(/build parameter "newCheckout" is backed by the flag "new-checkout", and "prod" has a gate, so --param can't set it/);
75
+ expect(refused.errors[0]).toMatch(/gates: prod-release/);
76
+ });
77
+
78
+ test("--params-file can't either, and a parameter no flag backs still can", async () => {
79
+ const file = join(scratchDir("chant-flags-file-"), "params.json");
80
+ writeFileSync(file, JSON.stringify({ newCheckout: true }));
81
+ const refused = await resolveFlaggedBuildParams(join(root, "shop"), SHOP, { ...quiet, paramsFile: file }, "prod");
82
+ expect(refused.success).toBe(false);
83
+ expect(refused.errors[0]).toMatch(/so --params-file can't set it/);
84
+ const tier = await resolveFlaggedBuildParams(join(root, "shop"), SHOP, { ...quiet, cli: { tier: "large" } }, "prod");
85
+ expect(tier.success).toBe(true);
86
+ expect(tier.provenance).toContainEqual({ name: "tier", value: "large", source: "cli" });
87
+ });
88
+
89
+ test("where no component declares a gate, --param still overrides the flag", async () => {
90
+ const r = await resolveFlaggedBuildParams(join(root, "blog"), BLOG, { ...quiet, cli: { banner: "false" } }, "prod");
91
+ expect(r.success).toBe(true);
92
+ expect(r.provenance).toEqual([{ name: "banner", value: false, source: "cli" }]);
93
+ });
94
+
95
+ test("components that can't be read refuse an override, since the gate can't be ruled out", async () => {
96
+ const dir = flagWorkspace({ "shop/broken.component.ts": "export const = ;\n" });
97
+ const r = await resolveFlaggedBuildParams(join(dir, "shop"), SHOP, { ...quiet, cli: { newCheckout: "true" } }, "prod");
98
+ expect(r.success).toBe(false);
99
+ expect(r.errors[0]).toMatch(/whether "prod" has a gate can't be told/);
100
+ });
101
+ });
102
+
103
+ describe("chant build --env in a member with flags", () => {
104
+ const runtime = join(import.meta.dirname, "runtime");
105
+ const paramsModule = join(import.meta.dirname, "params");
106
+ const serializer: Serializer = {
107
+ name: "aws",
108
+ rulePrefix: "TEST",
109
+ serialize: (entities) => JSON.stringify([...entities.values()].map((e) => (e as unknown as { props: unknown }).props)),
110
+ };
111
+ const shopFiles = {
112
+ "shop/chant.config.ts": `export default { environments: ["dev", "staging", "prod"], buildParams: { newCheckout: { type: "boolean", default: false } } };\n`,
113
+ "shop/main.ts":
114
+ `import { createResource } from ${JSON.stringify(runtime)};\n` +
115
+ `import { params } from ${JSON.stringify(paramsModule)};\n` +
116
+ `const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });\n` +
117
+ `export const bucket = new Bucket({ checkout: params.newCheckout ? "new" : "old" });\n`,
118
+ };
119
+
120
+ test("records the flag on BuildResult.buildParams and builds with its value", async () => {
121
+ const dir = flagWorkspace(shopFiles);
122
+ const spy = vi.spyOn(console, "error").mockImplementation(() => {});
123
+ try {
124
+ const result = await buildCommand({ path: join(dir, "shop"), format: "json", serializers: [serializer], env: "dev" });
125
+ expect(result.errors).toEqual([]);
126
+ expect(result.buildParams).toEqual([{ name: "newCheckout", value: true, source: "flag:new-checkout" }]);
127
+ expect(spy.mock.calls.map((c) => String(c[0])).some((l) => l.includes("1 build parameter resolved (1 from flags)"))).toBe(true);
128
+ } finally {
129
+ spy.mockRestore();
130
+ }
131
+ });
132
+
133
+ test("refuses --param for a flag-backed parameter in an environment with a gate", async () => {
134
+ const dir = flagWorkspace(shopFiles);
135
+ const spy = vi.spyOn(console, "error").mockImplementation(() => {});
136
+ try {
137
+ const result = await buildCommand({ path: join(dir, "shop"), format: "json", serializers: [serializer], env: "prod", params: { newCheckout: "true" } });
138
+ expect(result.success).toBe(false);
139
+ expect(result.errors.join("\n")).toMatch(/"newCheckout" is backed by the flag "new-checkout", and "prod" has a gate, so --param can't set it/);
140
+ } finally {
141
+ spy.mockRestore();
142
+ }
143
+ });
144
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Build-time parameters from workspace flags (#4024, decided in #4016).
3
+ *
4
+ * A member of a workspace may have flag records: records of a kind with a
5
+ * `flag` block, each naming the member, one of its declared build
6
+ * parameters and a value per environment (`workspace/flags.ts`). A build
7
+ * for an environment (`chant build --env <env>`, `chant run --components
8
+ * --env <env>`, and each environment a release cut compiles) reads them, and
9
+ * a flag-backed parameter takes the flag's value for that environment,
10
+ * recorded as `source: "flag:<name>"` on `BuildResult.buildParams` and on the
11
+ * release target's `params`.
12
+ *
13
+ * The precedence rule: where the environment has a gate, `--param`,
14
+ * `--params-file` and the parameter's env var can't set a flag-backed
15
+ * parameter, and the build refuses. An environment has a gate when a
16
+ * component enabled in it declares a gate step, the same test `components
17
+ * promote --release` uses to bind a release gate. The test runs only when one
18
+ * of those sources did set a flag-backed parameter, since it imports the
19
+ * member's components; a project with no components has no gate.
20
+ *
21
+ * Level 0 pays one upward `existsSync` walk here and nothing more. Workspace
22
+ * code is imported only once a declaration is found (#2525 rule 5, pinned by
23
+ * #2526's goldens).
24
+ */
25
+
26
+ import { findWorkspaceRoot } from "./project-root";
27
+ import { flagGateRefusal, type BuildParamFlags, type BuildParamsConfig } from "./build-params";
28
+ import type { BuildParamProvenance } from "./provenance";
29
+ import { echoBuildParams, resolveCliBuildParams, type CliBuildParamsArgs, type CliBuildParamsResolution } from "./cli/build-params-cli";
30
+ import { formatError } from "./cli/format";
31
+
32
+ /** The flags backing parameters of the member at `projectDir` in `env`, or why they can't be read. `flags` is undefined outside a workspace, without an environment, or when no flag names the member. */
33
+ export async function loadBuildParamFlags(projectDir: string, env: string | undefined): Promise<{ flags: BuildParamFlags | undefined; errors: string[] }> {
34
+ if (!env) return { flags: undefined, errors: [] };
35
+ const found = findWorkspaceRoot(projectDir);
36
+ if (!found) return { flags: undefined, errors: [] };
37
+ const { memberFlagBindings } = await import("./workspace/flags-read");
38
+ try {
39
+ const bindings = await memberFlagBindings(found.dir, projectDir, env);
40
+ if (!bindings) return { flags: undefined, errors: [] };
41
+ if (bindings.errors.length > 0) return { flags: undefined, errors: bindings.errors };
42
+ if (Object.keys(bindings.byParam).length === 0) return { flags: undefined, errors: [] };
43
+ return { flags: { env, byParam: bindings.byParam }, errors: [] };
44
+ } catch (err) {
45
+ const { WorkspaceReadError } = await import("./workspace/declaration");
46
+ const { RecordReadError } = await import("./workspace/records");
47
+ if (err instanceof WorkspaceReadError || err instanceof RecordReadError) {
48
+ return { flags: undefined, errors: [`the workspace's flag records can't be read, so this build can't tell which parameters they set: ${err.message}`] };
49
+ }
50
+ throw err;
51
+ }
52
+ }
53
+
54
+ /** The parameters a flag backs that `--param`, `--params-file` or an env var set instead. */
55
+ export function overriddenFlagParams(provenance: readonly BuildParamProvenance[], flags: BuildParamFlags | undefined): BuildParamProvenance[] {
56
+ if (!flags) return [];
57
+ return provenance.filter((p) => flags.byParam[p.name] !== undefined && (p.source === "cli" || p.source === "params-file" || p.source === "env"));
58
+ }
59
+
60
+ /**
61
+ * The gates the member's components declare in the environment the process
62
+ * builds for (`CHANT_ENV`), with the parameters as resolved, or why the
63
+ * components can't be read. Only components enabled under those parameters
64
+ * count.
65
+ */
66
+ export async function environmentGates(projectDir: string, provenance: BuildParamProvenance[]): Promise<{ gates: string[] } | { error: string }> {
67
+ const { discoverComponents } = await import("./components/discover");
68
+ const { withoutDisabled } = await import("./components/cli-support");
69
+ const { gateNamesOf } = await import("./components/release-gate");
70
+ try {
71
+ const discovered = await discoverComponents(projectDir, { buildParams: provenance });
72
+ if (discovered.errors.length > 0) return { error: discovered.errors.map((e) => e.message).join("; ") };
73
+ const all = [...discovered.components.values()].map((d) => d.component);
74
+ const plain = all.map((c) => ({ name: c.name, dependsOn: c.dependsOn, deploy: c.deploy as never }));
75
+ return { gates: gateNamesOf(withoutDisabled(all, plain)) };
76
+ } catch (err) {
77
+ return { error: err instanceof Error ? err.message : String(err) };
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Resolve the build parameters of the project at `projectDir` for `env`,
83
+ * with the workspace's flags, as `resolveCliBuildParams` does without them.
84
+ * Refuses when a flag record can't be read, and when `--param`,
85
+ * `--params-file` or an env var sets a flag-backed parameter in an
86
+ * environment with a gate. Prints the summary line only on success.
87
+ */
88
+ export async function resolveFlaggedBuildParams(
89
+ projectDir: string,
90
+ buildParamsConfig: BuildParamsConfig | undefined,
91
+ args: CliBuildParamsArgs,
92
+ env: string | undefined,
93
+ ): Promise<CliBuildParamsResolution> {
94
+ const loaded = await loadBuildParamFlags(projectDir, env);
95
+ if (loaded.errors.length > 0) {
96
+ return { success: false, provenance: [], errors: loaded.errors.map((message) => formatError({ message })) };
97
+ }
98
+ const { flags } = loaded;
99
+ const resolution = resolveCliBuildParams(buildParamsConfig, { ...args, ...(flags ? { flags } : {}), echo: false });
100
+ if (!resolution.success || !flags) {
101
+ if (resolution.success && args.echo !== false) echoBuildParams(resolution.provenance, args.verbose);
102
+ return resolution;
103
+ }
104
+ const overridden = overriddenFlagParams(resolution.provenance, flags);
105
+ if (overridden.length > 0) {
106
+ const found = await environmentGates(projectDir, resolution.provenance);
107
+ if ("error" in found) {
108
+ return {
109
+ success: false,
110
+ provenance: [],
111
+ errors: [
112
+ formatError({
113
+ message:
114
+ `${overridden.map((p) => `"${p.name}"`).join(", ")} ${overridden.length === 1 ? "is" : "are"} backed by a flag and set by ${[...new Set(overridden.map((p) => p.source))].join(", ")}, ` +
115
+ `and whether "${flags.env}" has a gate can't be told, because its components can't be read: ${found.error}`,
116
+ }),
117
+ ],
118
+ };
119
+ }
120
+ if (found.gates.length > 0) {
121
+ const around = (p: BuildParamProvenance): string =>
122
+ p.source === "cli" ? "--param" : p.source === "params-file" ? "--params-file" : `the env var ${buildParamsConfig?.[p.name]?.env ?? "(unknown)"}`;
123
+ return {
124
+ success: false,
125
+ provenance: [],
126
+ errors: overridden.map((p) =>
127
+ formatError({ message: `${flagGateRefusal(p.name, flags.byParam[p.name].flag, flags.env, around(p))} (gates: ${found.gates.join(", ")})` }),
128
+ ),
129
+ };
130
+ }
131
+ }
132
+ if (args.echo !== false) echoBuildParams(resolution.provenance, args.verbose);
133
+ return resolution;
134
+ }
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Test stand-ins for the publish targets (#4047): an OCI registry fake that
3
+ * listens on 127.0.0.1 inside the test process and answers with a Bearer
4
+ * challenge as GHCR and Docker Hub do, an OCI image layout writer, a fake
5
+ * `npm` that records each call and keeps a registry with dist-tags in a file,
6
+ * and a forge fake with draft releases (#4048). Nothing here reaches a network or a real
7
+ * registry, and every server a test starts is closed by it.
8
+ */
9
+
10
+ import { createHash } from "node:crypto";
11
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
13
+ import type { AddressInfo } from "node:net";
14
+ import { join } from "node:path";
15
+
16
+ const sha = (b: Uint8Array | string) => `sha256:${createHash("sha256").update(b).digest("hex")}`;
17
+
18
+ export interface FakeRegistry {
19
+ /** `127.0.0.1:<port>`, for a repository reference. */
20
+ host: string;
21
+ blobs: Map<string, Buffer>;
22
+ manifests: Map<string, { name: string; type: string; bytes: Buffer }>;
23
+ /** Each request: method, path and the authorization header it carried. */
24
+ requests: Array<{ method: string; path: string; authorization: string | null }>;
25
+ uploads: number;
26
+ close(): Promise<void>;
27
+ }
28
+
29
+ /** A registry fake with a Bearer token service that accepts `username` and `password`. */
30
+ export async function startFakeRegistry(creds: { username: string; password: string; token: string }): Promise<FakeRegistry> {
31
+ const blobs = new Map<string, Buffer>();
32
+ const manifests = new Map<string, { name: string; type: string; bytes: Buffer }>();
33
+ const requests: FakeRegistry["requests"] = [];
34
+ const state = { uploads: 0, port: 0 };
35
+ const read = (req: IncomingMessage) =>
36
+ new Promise<Buffer>((res) => {
37
+ const parts: Buffer[] = [];
38
+ req.on("data", (c: Buffer) => parts.push(c));
39
+ req.on("end", () => res(Buffer.concat(parts)));
40
+ });
41
+ const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
42
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${state.port}`);
43
+ const body = await read(req);
44
+ requests.push({ method: req.method ?? "", path: url.pathname, authorization: req.headers.authorization ?? null });
45
+ const send = (status: number, headers: Record<string, string> = {}, text = "") => {
46
+ res.writeHead(status, headers);
47
+ res.end(text);
48
+ };
49
+ if (url.pathname === "/token") {
50
+ const basic = `Basic ${Buffer.from(`${creds.username}:${creds.password}`).toString("base64")}`;
51
+ if (req.headers.authorization !== basic) return send(401, {}, "bad credentials");
52
+ return send(200, { "content-type": "application/json" }, JSON.stringify({ token: creds.token }));
53
+ }
54
+ if (req.headers.authorization !== `Bearer ${creds.token}`) {
55
+ return send(401, { "www-authenticate": `Bearer realm="http://127.0.0.1:${state.port}/token",service="fake-registry"` }, "unauthorized");
56
+ }
57
+ let m = url.pathname.match(/^\/v2\/(.+)\/blobs\/uploads\/(.*)$/);
58
+ if (m) {
59
+ if (req.method === "POST") return send(202, { location: `/v2/${m[1]}/blobs/uploads/u${requests.length}?_state=s` });
60
+ const digest = url.searchParams.get("digest") ?? "";
61
+ if (req.method === "PUT" && sha(body) === digest && url.searchParams.get("_state") === "s") {
62
+ blobs.set(digest, body);
63
+ state.uploads++;
64
+ return send(201, { "docker-content-digest": digest });
65
+ }
66
+ return send(400, {}, "digest invalid");
67
+ }
68
+ m = url.pathname.match(/^\/v2\/(.+)\/blobs\/(sha256:[0-9a-f]+)$/);
69
+ if (m && req.method === "HEAD") return send(blobs.has(m[2]!) ? 200 : 404);
70
+ m = url.pathname.match(/^\/v2\/(.+)\/manifests\/(.+)$/);
71
+ if (m && (req.method === "HEAD" || req.method === "GET")) {
72
+ const found = manifests.get(m[2]!);
73
+ return found ? send(200, { "content-type": found.type, "docker-content-digest": sha(found.bytes) }, req.method === "GET" ? found.bytes.toString("utf-8") : "") : send(404);
74
+ }
75
+ if (m && req.method === "PUT") {
76
+ const ref = m[2]!;
77
+ if (ref.startsWith("sha256:") && sha(body) !== ref) return send(400, {}, "manifest digest invalid");
78
+ const parsed = JSON.parse(body.toString("utf-8")) as { config?: { digest: string }; layers?: Array<{ digest: string }> };
79
+ for (const d of [parsed.config, ...(parsed.layers ?? [])]) if (d && !blobs.has(d.digest)) return send(400, {}, `blob unknown ${d.digest}`);
80
+ manifests.set(ref, { name: m[1]!, type: String(req.headers["content-type"]), bytes: body });
81
+ return send(201, { "docker-content-digest": sha(body) });
82
+ }
83
+ return send(404, {}, "not found");
84
+ });
85
+ await new Promise<void>((r) => server.listen(0, "127.0.0.1", () => r()));
86
+ state.port = (server.address() as AddressInfo).port;
87
+ return {
88
+ host: `127.0.0.1:${state.port}`,
89
+ blobs,
90
+ manifests,
91
+ requests,
92
+ get uploads() {
93
+ return state.uploads;
94
+ },
95
+ close: () => new Promise<void>((r) => server.close(() => r())),
96
+ };
97
+ }
98
+
99
+ /** Write a one-image OCI layout at `dir`, returning the manifest's and the config's digests. */
100
+ export function writeOciLayout(dir: string, content = "layer bytes"): { digest: string; configDigest: string } {
101
+ const blobsDir = join(dir, "blobs", "sha256");
102
+ mkdirSync(blobsDir, { recursive: true });
103
+ const put = (bytes: string) => {
104
+ const d = sha(bytes);
105
+ writeFileSync(join(blobsDir, d.slice(7)), bytes);
106
+ return { digest: d, size: Buffer.byteLength(bytes) };
107
+ };
108
+ const layer = put(content);
109
+ const config = put(JSON.stringify({ architecture: "amd64", os: "linux", rootfs: { type: "layers", diff_ids: [layer.digest] } }));
110
+ const manifest = put(
111
+ JSON.stringify({
112
+ schemaVersion: 2,
113
+ mediaType: "application/vnd.oci.image.manifest.v1+json",
114
+ config: { mediaType: "application/vnd.oci.image.config.v1+json", ...config },
115
+ layers: [{ mediaType: "application/vnd.oci.image.layer.v1.tar", ...layer }],
116
+ }),
117
+ );
118
+ writeFileSync(join(dir, "oci-layout"), JSON.stringify({ imageLayoutVersion: "1.0.0" }));
119
+ writeFileSync(join(dir, "index.json"), JSON.stringify({ schemaVersion: 2, manifests: [{ mediaType: "application/vnd.oci.image.manifest.v1+json", ...manifest }] }));
120
+ return { digest: manifest.digest, configDigest: config.digest };
121
+ }
122
+
123
+ /** What the fake npm's registry holds: each package's versions and dist-tags. */
124
+ export interface FakeNpmRegistry {
125
+ packages: Record<string, { versions: string[]; distTags: Record<string, string> }>;
126
+ }
127
+
128
+ /** The registry file a fake npm writing calls to `log` keeps its state in. */
129
+ export const fakeNpmRegistryFile = (log: string) => `${log}.registry.json`;
130
+
131
+ /** Read the fake npm's registry. */
132
+ export function readFakeNpmRegistry(log: string): FakeNpmRegistry {
133
+ const file = fakeNpmRegistryFile(log);
134
+ return existsSync(file) ? (JSON.parse(readFileSync(file, "utf-8")) as FakeNpmRegistry) : { packages: {} };
135
+ }
136
+
137
+ /** Seed the fake npm's registry, such as with an earlier version as `latest`. */
138
+ export function seedFakeNpmRegistry(log: string, registry: FakeNpmRegistry): void {
139
+ writeFileSync(fakeNpmRegistryFile(log), JSON.stringify(registry));
140
+ }
141
+
142
+ /**
143
+ * Write a fake `npm` at `<binDir>/npm` that appends `{ args, tokenSet }` to
144
+ * `log` for each call and prints the token it was given (so a test can show
145
+ * it is masked). It keeps a registry in `<log>.registry.json`, as npm's does:
146
+ *
147
+ * - `publish <dir|tgz> [--tag <t>]` adds the version under `<t>` (default
148
+ * `latest`), and refuses a version already there with a 403. As npm's
149
+ * registry does, a package's first version is also `latest`. It exits 1
150
+ * when the package path contains `$FAKE_NPM_FAIL`.
151
+ * - `view <name>@<version> version` prints the version when it is there.
152
+ * - `dist-tag add <name>@<version> <tag>` points the tag at a version that is
153
+ * there. It exits 1 when the name contains `$FAKE_NPM_FAIL_TAG`.
154
+ * - `install` (#4049) checks that every dependency in `./package.json` is in
155
+ * its registry at that version, and installs nothing.
156
+ * - `audit signatures` (#4049) prints what npm prints for verified
157
+ * signatures and attestations, and exits 1 when `$FAKE_NPM_AUDIT_FAIL` is set.
158
+ */
159
+ export function writeFakeNpm(binDir: string, log: string): string {
160
+ mkdirSync(binDir, { recursive: true });
161
+ const file = join(binDir, "npm");
162
+ writeFileSync(
163
+ file,
164
+ `#!${process.execPath}
165
+ const fs = require("fs");
166
+ const path = require("path");
167
+ const { execFileSync } = require("child_process");
168
+ const args = process.argv.slice(2);
169
+ const token = process.env.NODE_AUTH_TOKEN || "";
170
+ const regFile = ${JSON.stringify(log)} + ".registry.json";
171
+ fs.appendFileSync(${JSON.stringify(log)}, JSON.stringify({ args, tokenSet: token !== "" }) + "\\n");
172
+ console.log("npm notice using //registry.example/:_authToken=" + token);
173
+ console.error("npm notice token " + token);
174
+ const reg = fs.existsSync(regFile) ? JSON.parse(fs.readFileSync(regFile, "utf-8")) : { packages: {} };
175
+ const save = () => fs.writeFileSync(regFile, JSON.stringify(reg));
176
+ const flag = (name) => { const i = args.indexOf(name); return i < 0 ? null : args[i + 1]; };
177
+ const split = (spec) => { const at = spec.lastIndexOf("@"); return [spec.slice(0, at), spec.slice(at + 1)]; };
178
+ if (args[0] === "publish") {
179
+ if (process.env.FAKE_NPM_FAIL && args[1] && args[1].includes(process.env.FAKE_NPM_FAIL)) { console.error("npm error 403 Forbidden"); process.exit(1); }
180
+ const text = fs.statSync(args[1]).isDirectory() ? fs.readFileSync(path.join(args[1], "package.json"), "utf-8") : execFileSync("tar", ["-xzOf", args[1], "package/package.json"], { encoding: "utf-8" });
181
+ const { name, version } = JSON.parse(text);
182
+ const p = reg.packages[name] || (reg.packages[name] = { versions: [], distTags: {} });
183
+ if (p.versions.includes(version)) { console.error("npm error 403 You cannot publish over the previously published versions: " + version); process.exit(1); }
184
+ p.versions.push(version);
185
+ p.distTags[flag("--tag") || "latest"] = version;
186
+ if (!p.distTags.latest) p.distTags.latest = version;
187
+ save();
188
+ console.log("+ " + name + "@" + version);
189
+ } else if (args[0] === "view") {
190
+ const [name, version] = split(args[1]);
191
+ const p = reg.packages[name];
192
+ if (!p) { console.error("npm error 404 Not Found - " + name); process.exit(1); }
193
+ if (p.versions.includes(version)) console.log(version);
194
+ } else if (args[0] === "dist-tag" && args[1] === "add") {
195
+ const [name, version] = split(args[2]);
196
+ if (process.env.FAKE_NPM_FAIL_TAG && name.includes(process.env.FAKE_NPM_FAIL_TAG)) { console.error("npm error 403 Forbidden - no permission to change dist-tags"); process.exit(1); }
197
+ const p = reg.packages[name];
198
+ if (!p || !p.versions.includes(version)) { console.error("npm error 404 " + args[2] + " is not in the npm registry"); process.exit(1); }
199
+ p.distTags[args[3]] = version;
200
+ save();
201
+ console.log("+" + args[3] + ": " + args[2]);
202
+ } else if (args[0] === "install") {
203
+ const deps = JSON.parse(fs.readFileSync("package.json", "utf-8")).dependencies || {};
204
+ for (const [name, version] of Object.entries(deps)) {
205
+ const p = reg.packages[name];
206
+ if (!p || !p.versions.includes(version)) { console.error("npm error 404 " + name + "@" + version + " is not in this registry"); process.exit(1); }
207
+ }
208
+ console.log("added " + Object.keys(deps).length + " packages");
209
+ } else if (args[0] === "audit" && args[1] === "signatures") {
210
+ const n = Object.keys(JSON.parse(fs.readFileSync("package.json", "utf-8")).dependencies || {}).length;
211
+ if (process.env.FAKE_NPM_AUDIT_FAIL) { console.error(n + " packages have invalid attestations"); process.exit(1); }
212
+ console.log("audited " + n + " packages");
213
+ console.log(n + " packages have verified registry signatures");
214
+ console.log(n + " packages have verified attestations");
215
+ } else {
216
+ console.error("fake npm: unknown command " + args.join(" ")); process.exit(2);
217
+ }
218
+ `,
219
+ );
220
+ chmodSync(file, 0o755);
221
+ return file;
222
+ }
223
+
224
+ /** A forge release as {@link startFakeForge} keeps it. */
225
+ export interface FakeForgeRelease {
226
+ id: number;
227
+ tag_name: string;
228
+ draft: boolean;
229
+ target: string;
230
+ assets: Array<{ name: string; digest: string | null; bytes: string }>;
231
+ }
232
+
233
+ /** A forge fake answering through `fetch`: GitHub or Forgejo with draft releases, or GitLab with releases, links and a generic package registry. */
234
+ export interface FakeForge {
235
+ fetch: (url: string, init: { method: string; headers: Record<string, string>; body?: Uint8Array | string }) => Promise<{ ok: boolean; status: number; headers: { get(name: string): string | null }; text(): Promise<string> }>;
236
+ calls: Array<{ method: string; url: string; headers: Record<string, string>; body?: Uint8Array | string }>;
237
+ releases: FakeForgeRelease[];
238
+ /** GitLab: the package registry's files, by URL. */
239
+ packages: Map<string, string>;
240
+ /** The releases a consumer sees: published (not draft) ones, with their asset names. */
241
+ visible(): Array<{ tag: string; assets: string[] }>;
242
+ /** Make the next request whose `METHOD url` starts with `prefix` answer 500. */
243
+ failOnce(prefix: string): void;
244
+ }
245
+
246
+ /** A forge fake at `api` for `repo`, which holds `releases` to begin with. */
247
+ export function startFakeForge(forge: "github" | "forgejo" | "gitlab", api: string, repo: string, releases: FakeForgeRelease[] = []): FakeForge {
248
+ const calls: FakeForge["calls"] = [];
249
+ const packages = new Map<string, string>();
250
+ const failures: string[] = [];
251
+ let nextId = 100 + releases.length;
252
+ const root = forge === "gitlab" ? `${api}/projects/${encodeURIComponent(repo)}` : `${api}/repos/${repo}`;
253
+ const uploads = forge === "github" ? "https://uploads.forge.test" : api;
254
+ const json = (status: number, body: unknown) => ({ ok: status < 300, status, headers: { get: () => null }, text: async () => (body === undefined ? "" : JSON.stringify(body)) });
255
+ const view = (r: FakeForgeRelease) =>
256
+ forge === "gitlab"
257
+ ? { tag_name: r.tag_name, assets: { links: r.assets.map((a) => ({ name: a.name })) } }
258
+ : { id: r.id, tag_name: r.tag_name, draft: r.draft, upload_url: `${uploads}/repos/${repo}/releases/${r.id}/assets{?name,label}`, assets: r.assets.map((a) => ({ name: a.name, ...(forge === "github" ? { digest: a.digest } : {}) })) };
259
+ const text = (b: Uint8Array | string | undefined) => (b === undefined ? "" : typeof b === "string" ? b : Buffer.from(b).toString("utf-8"));
260
+ const fetch: FakeForge["fetch"] = async (url, init) => {
261
+ calls.push({ method: init.method, url, headers: init.headers, ...(init.body !== undefined ? { body: init.body } : {}) });
262
+ const key = `${init.method} ${url}`;
263
+ const f = failures.findIndex((p) => key.startsWith(p));
264
+ if (f >= 0) {
265
+ failures.splice(f, 1);
266
+ return json(500, { message: "Internal Server Error" });
267
+ }
268
+ const u = new URL(url);
269
+ const path = url.split("?")[0]!;
270
+ if (forge === "gitlab") {
271
+ if (init.method === "PUT" && path.startsWith(`${root}/packages/generic/`)) {
272
+ packages.set(path, text(init.body));
273
+ return json(201, { message: "201 Created" });
274
+ }
275
+ const byTag = path.startsWith(`${root}/releases/`) ? decodeURIComponent(path.slice(`${root}/releases/`.length)) : null;
276
+ if (init.method === "GET" && byTag !== null) {
277
+ const r = releases.find((x) => x.tag_name === byTag);
278
+ return r ? json(200, view(r)) : json(404, { message: "404 Not Found" });
279
+ }
280
+ if (init.method === "POST" && path === `${root}/releases`) {
281
+ const b = JSON.parse(text(init.body)) as { tag_name: string; ref: string; assets?: { links?: Array<{ name: string; url: string }> } };
282
+ if (releases.some((x) => x.tag_name === b.tag_name)) return json(409, { message: "Release already exists" });
283
+ const r: FakeForgeRelease = { id: nextId++, tag_name: b.tag_name, draft: false, target: b.ref, assets: (b.assets?.links ?? []).map((l) => ({ name: l.name, digest: null, bytes: packages.get(l.url) ?? "" })) };
284
+ releases.push(r);
285
+ return json(201, view(r));
286
+ }
287
+ return json(404, { message: "404 Not Found" });
288
+ }
289
+ if (init.method === "GET" && path.startsWith(`${root}/releases/tags/`)) {
290
+ const tag = decodeURIComponent(path.slice(`${root}/releases/tags/`.length));
291
+ const r = releases.find((x) => x.tag_name === tag && !x.draft);
292
+ return r ? json(200, view(r)) : json(404, { message: "Not Found" });
293
+ }
294
+ if (init.method === "GET" && path === `${root}/releases`) return json(200, releases.map(view));
295
+ if (init.method === "POST" && path === `${root}/releases`) {
296
+ const b = JSON.parse(text(init.body)) as { tag_name: string; target_commitish: string; draft?: boolean };
297
+ const r: FakeForgeRelease = { id: nextId++, tag_name: b.tag_name, draft: b.draft === true, target: b.target_commitish, assets: [] };
298
+ releases.push(r);
299
+ return json(201, view(r));
300
+ }
301
+ const asset = path.match(/\/releases\/(\d+)\/assets$/);
302
+ if (init.method === "POST" && asset && (path.startsWith(uploads) || path.startsWith(root))) {
303
+ const r = releases.find((x) => x.id === Number(asset[1]));
304
+ const name = u.searchParams.get("name") ?? "";
305
+ if (!r) return json(404, { message: "Not Found" });
306
+ if (r.assets.some((a) => a.name === name)) return json(422, { message: "already_exists" });
307
+ let bytes = text(init.body);
308
+ if (forge === "forgejo") bytes = bytes.split("\r\n\r\n")[1]?.split("\r\n--")[0] ?? "";
309
+ r.assets.push({ name, digest: sha(bytes), bytes });
310
+ return json(201, { name, browser_download_url: `https://forge.test/${repo}/releases/download/${r.tag_name}/${name}` });
311
+ }
312
+ const edit = path.match(/\/releases\/(\d+)$/);
313
+ if (init.method === "PATCH" && edit) {
314
+ const r = releases.find((x) => x.id === Number(edit[1]));
315
+ if (!r) return json(404, { message: "Not Found" });
316
+ const b = JSON.parse(text(init.body)) as { draft?: boolean };
317
+ if (b.draft === false) r.draft = false;
318
+ return json(200, view(r));
319
+ }
320
+ return json(404, { message: "Not Found" });
321
+ };
322
+ return {
323
+ fetch,
324
+ calls,
325
+ releases,
326
+ packages,
327
+ visible: () => releases.filter((r) => !r.draft).map((r) => ({ tag: r.tag_name, assets: r.assets.map((a) => a.name) })),
328
+ failOnce: (prefix) => void failures.push(prefix),
329
+ };
330
+ }
331
+
332
+ /** A small SPDX document, as `npm sbom --sbom-format spdx` writes one. */
333
+ export function spdxDocument(name: string): string {
334
+ return `${JSON.stringify({ spdxVersion: "SPDX-2.3", dataLicense: "CC0-1.0", SPDXID: "SPDXRef-DOCUMENT", name, documentNamespace: `https://example.test/${name}`, creationInfo: { created: "2026-10-17T00:00:00Z", creators: ["Tool: npm"] }, packages: [] }, null, 2)}\n`;
335
+ }
336
+
337
+ /** Write an npm package directory, with an `sbom.spdx.json` at its root unless `sbom` is false (#4049). */
338
+ export function writePackage(dir: string, name: string, version: string, sbom = true): void {
339
+ mkdirSync(dir, { recursive: true });
340
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name, version }, null, 2));
341
+ if (sbom) writeFileSync(join(dir, "sbom.spdx.json"), spdxDocument(name));
342
+ }
343
+
344
+ /** Write a `package-lock.json` at `dir`, which the OCI and forge targets write their SBOM from (#4049). */
345
+ export function writeLockfile(dir: string): void {
346
+ mkdirSync(dir, { recursive: true });
347
+ writeFileSync(
348
+ join(dir, "package-lock.json"),
349
+ JSON.stringify({ name: "acme", lockfileVersion: 3, packages: { "": { name: "acme" }, "node_modules/left-pad": { version: "1.3.0" } } }, null, 2),
350
+ );
351
+ }
@@ -21,7 +21,8 @@
21
21
  * component gate in a plain `run --components` binds a lifecycle-plan digest
22
22
  * and stores nothing here.
23
23
  *
24
- * How long a stored change set is kept is #4036's retention policy.
24
+ * `chant lifecycle gc` removes a stored change set from the branch's tree
25
+ * once #4036's retention policy lets it go (./retention.ts); history keeps it.
25
26
  */
26
27
 
27
28
  import { sortedJsonReplacer } from "../utils";