@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,494 @@
1
+ /**
2
+ * #4047, #4048: the publish targets, each against a stand-in: npm against a
3
+ * fake `npm` binary that keeps a registry with dist-tags in a file, OCI
4
+ * against a registry fake on 127.0.0.1 started and closed here, and the forge
5
+ * targets against a forge fake with draft releases. Each target stages, and
6
+ * nothing is visible under `latest`, a moved tag or a published release until
7
+ * its reveal runs. Nothing reaches a network or a real registry, and no token
8
+ * is ever printed.
9
+ */
10
+
11
+ import { afterAll, describe, expect, test } from "vitest";
12
+ import { execFileSync } from "node:child_process";
13
+ import { createHash } from "node:crypto";
14
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
17
+ import { createRedactor, REDACTED } from "./publish-credentials";
18
+ import { PublishDeclarationSchema } from "./publish-declaration";
19
+ import { createPublishTargets } from "./publish-targets";
20
+ import { parseRepository } from "./publish-oci";
21
+ import { PublishTargetError, type PublishContext, type PublishTarget, type PublishedTarget } from "./release-publish";
22
+ import { makeRelease, type ReleaseArtifact } from "./release-store";
23
+ import { readFakeNpmRegistry, seedFakeNpmRegistry, startFakeForge, startFakeRegistry, writeFakeNpm, writeLockfile, writeOciLayout, writePackage } from "./__fixtures__/publish-fakes";
24
+
25
+ const dirs: string[] = [];
26
+ afterAll(() => {
27
+ for (const d of dirs) rmSync(d, { recursive: true, force: true });
28
+ });
29
+ const temp = () => {
30
+ const d = mkdtempSync(join(tmpdir(), "chant-publish-targets-"));
31
+ dirs.push(d);
32
+ return d;
33
+ };
34
+
35
+ const OIDC = { oidc: true, ci: "github" as const, reason: "GitHub Actions hands this job an OIDC token" };
36
+ const NO_OIDC = { oidc: false, ci: null, reason: "this run is not in CI, and a workstation has no OIDC token" };
37
+
38
+ function context(cwd: string, over: Partial<PublishContext> & { artifacts?: ReleaseArtifact[] } = {}): PublishContext & { lines: string[] } {
39
+ const env = over.env ?? {};
40
+ const redactor = over.redactor ?? createRedactor(env);
41
+ const lines: string[] = [];
42
+ const release = makeRelease({ member: null, commit: "c0ffee".padEnd(40, "0"), artifactDigests: over.artifacts ?? [], targets: [{ env: "dev", params: [], templates: [] }] });
43
+ return {
44
+ release,
45
+ label: "v1.4.0",
46
+ releaseTag: "release/v1.4.0",
47
+ provenance: NO_OIDC,
48
+ cwd,
49
+ tag: null,
50
+ env,
51
+ redactor,
52
+ log: (l) => lines.push(redactor.redact(l)),
53
+ ...over,
54
+ lines,
55
+ };
56
+ }
57
+
58
+ const decl = (d: unknown) => PublishDeclarationSchema.parse(d);
59
+ /** Both steps of one target, as a publish of that target alone runs them. */
60
+ const publish = async (t: PublishTarget, ctx: PublishContext): Promise<PublishedTarget> => t.reveal(ctx, await t.stage(ctx));
61
+ async function refused(p: Promise<unknown>): Promise<PublishTargetError> {
62
+ try {
63
+ await p;
64
+ } catch (err) {
65
+ if (err instanceof PublishTargetError) return err;
66
+ throw err;
67
+ }
68
+ throw new Error("expected a PublishTargetError");
69
+ }
70
+
71
+ describe("credentials", () => {
72
+ test("the redactor masks token variables, values it is given, and npm and header token forms", () => {
73
+ const r = createRedactor({ GITHUB_TOKEN: "ghp_abcdef123456", PATH: "/usr/bin" }, ["MY_SECRET"]);
74
+ r.add("s3cret-value");
75
+ r.add("ab");
76
+ expect(r.redact("token ghp_abcdef123456 and s3cret-value, ab, /usr/bin")).toBe(`token ${REDACTED} and ${REDACTED}, ab, /usr/bin`);
77
+ expect(r.redact("//registry.npmjs.org/:_authToken=npm_xyz789")).toBe(`//registry.npmjs.org/:_authToken=${REDACTED}`);
78
+ expect(r.redact("Authorization: Bearer eyJhbGciOi.x.y")).toBe(`Authorization: Bearer ${REDACTED}`);
79
+ expect(r.redact("PRIVATE-TOKEN: glpat-123")).toBe(`PRIVATE-TOKEN: ${REDACTED}`);
80
+ });
81
+
82
+ test("a declaration names variables, never values, and an unknown key is refused", () => {
83
+ expect(() => decl({ type: "oci", image: "x", repository: "ghcr.io/a/b", password: "hunter2" })).toThrow();
84
+ expect(() => decl({ type: "forge", forge: "github", repo: "a/b", assets: ["x"], tokenEnv: "not a name" })).toThrow();
85
+ expect(decl({ type: "npm", packages: ["a"] })).toEqual({ type: "npm", packages: ["a"] });
86
+ });
87
+ });
88
+
89
+ describe("npm", () => {
90
+ function setup(): { dir: string; npm: string; log: string } {
91
+ const dir = temp();
92
+ writePackage(join(dir, "packages", "core"), "@acme/core", "1.4.0");
93
+ writePackage(join(dir, "packages", "cli"), "@acme/cli", "1.4.0");
94
+ writePackage(join(dir, "packages", "kit"), "@acme/kit", "1.4.0");
95
+ // The third is packed, as npm pack writes it.
96
+ writePackage(join(dir, "stage", "package"), "@acme/kit", "1.4.0");
97
+ execFileSync("tar", ["-czf", join(dir, "kit.tgz"), "-C", join(dir, "stage"), "package"], { stdio: "ignore" });
98
+ rmSync(join(dir, "stage"), { recursive: true });
99
+ const log = join(dir, "npm.log");
100
+ // The registry holds the previous release, 1.3.0, as latest.
101
+ seedFakeNpmRegistry(log, {
102
+ packages: Object.fromEntries(["@acme/core", "@acme/cli", "@acme/kit"].map((n) => [n, { versions: ["1.3.0"], distTags: { latest: "1.3.0" } }])),
103
+ });
104
+ return { dir, npm: writeFakeNpm(join(dir, "bin"), log), log };
105
+ }
106
+ const calls = (log: string) => (existsSync(log) ? readFileSync(log, "utf-8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l) as { args: string[]; tokenSet: boolean }) : []);
107
+ const latest = (log: string) => Object.fromEntries(Object.entries(readFakeNpmRegistry(log).packages).map(([n, p]) => [n, p.distTags.latest]));
108
+
109
+ test("stages a set under next in the order declared, with nothing under latest until the reveal moves core's latest first", async () => {
110
+ const { dir, npm, log } = setup();
111
+ const [target] = await createPublishTargets([decl({ type: "npm", packages: ["packages/core", "packages/cli", "kit.tgz"], access: "public" })], { npm });
112
+ const env = { PATH: process.env.PATH, NODE_AUTH_TOKEN: "npm_secretToken42" };
113
+ const ctx = context(dir, { env, provenance: OIDC });
114
+
115
+ const planned = await target!.plan(ctx);
116
+ expect(planned.map((i) => i.name)).toEqual(["@acme/core@1.4.0", "@acme/cli@1.4.0", "@acme/kit@1.4.0"]);
117
+ expect(planned[0]!.destination).toBe("the registry npm's config names, dist-tag next, then latest");
118
+ expect(calls(log)).toEqual([]);
119
+
120
+ const staged = await target!.stage(ctx);
121
+ expect(staged.items.map((i) => i.name)).toEqual(["@acme/core@1.4.0", "@acme/cli@1.4.0", "@acme/kit@1.4.0"]);
122
+ expect(calls(log).filter((c) => c.args[0] === "publish").map((c) => c.args)).toEqual([
123
+ ["publish", join(dir, "packages/core"), "--tag", "next", "--access", "public", "--provenance"],
124
+ ["publish", join(dir, "packages/cli"), "--tag", "next", "--access", "public", "--provenance"],
125
+ ["publish", join(dir, "kit.tgz"), "--tag", "next", "--access", "public", "--provenance"],
126
+ ]);
127
+ // Staged: every version is on the registry under next, and latest has not moved.
128
+ expect(latest(log)).toEqual({ "@acme/core": "1.3.0", "@acme/cli": "1.3.0", "@acme/kit": "1.3.0" });
129
+ expect(readFakeNpmRegistry(log).packages["@acme/cli"]!.distTags.next).toBe("1.4.0");
130
+
131
+ const before = calls(log).length;
132
+ const done = await target!.reveal(ctx, staged);
133
+ const moves = calls(log).slice(before).map((c) => c.args);
134
+ expect(moves).toEqual([
135
+ ["dist-tag", "add", "@acme/core@1.4.0", "latest"],
136
+ ["dist-tag", "add", "@acme/cli@1.4.0", "latest"],
137
+ ["dist-tag", "add", "@acme/kit@1.4.0", "latest"],
138
+ // #4049: with an OIDC token, what was published is installed in an empty directory and its signatures audited.
139
+ ["install", "--ignore-scripts", "--no-audit", "--no-fund"],
140
+ ["audit", "signatures"],
141
+ ]);
142
+ expect(done.items.map((i) => i.sbom)).toEqual(Array(3).fill("sbom.spdx.json"));
143
+ expect(latest(log)).toEqual({ "@acme/core": "1.4.0", "@acme/cli": "1.4.0", "@acme/kit": "1.4.0" });
144
+ expect(done.items.map((i) => i.destination)).toEqual(Array(3).fill("the registry npm's config names, dist-tag latest"));
145
+
146
+ // npm got the token from the run's environment; chant printed none of it.
147
+ expect(calls(log).every((x) => x.tokenSet)).toBe(true);
148
+ const out = ctx.lines.join("\n");
149
+ expect(out).toContain(`npm notice token ${REDACTED}`);
150
+ expect(out).toContain(`_authToken=${REDACTED}`);
151
+ expect(out).not.toContain("npm_secretToken42");
152
+ });
153
+
154
+ test("passes no --provenance without an OIDC token, passes the registry to each command, and reveals under the context's tag", async () => {
155
+ const { dir, npm, log } = setup();
156
+ const [target] = await createPublishTargets([decl({ type: "npm", packages: ["kit.tgz"], registry: "http://127.0.0.1:4873/" })], { npm });
157
+ const ctx = context(dir, { env: { PATH: process.env.PATH }, tag: "beta" });
158
+ const done = await publish(target!, ctx);
159
+ expect(done.items).toEqual([{ name: "@acme/kit@1.4.0", destination: "http://127.0.0.1:4873/, dist-tag beta", digest: expect.stringMatching(/^sha256:/), sbom: "sbom.spdx.json" }]);
160
+ expect(calls(log).map((c) => c.args)).toEqual([
161
+ ["view", "@acme/kit@1.4.0", "version", "--registry", "http://127.0.0.1:4873/"],
162
+ ["publish", join(dir, "kit.tgz"), "--tag", "next", "--registry", "http://127.0.0.1:4873/"],
163
+ ["dist-tag", "add", "@acme/kit@1.4.0", "beta", "--registry", "http://127.0.0.1:4873/"],
164
+ ]);
165
+ expect(readFakeNpmRegistry(log).packages["@acme/kit"]!.distTags).toEqual({ latest: "1.3.0", next: "1.4.0", beta: "1.4.0" });
166
+ });
167
+
168
+ test("a stage that failed part way names what it staged and, run again, publishes only the rest; a tag move that fails names what moved", async () => {
169
+ const { dir, npm, log } = setup();
170
+ const [target] = await createPublishTargets([decl({ type: "npm", packages: ["packages/core", "packages/cli", "packages/kit"] })], { npm });
171
+ const err = await refused(target!.stage(context(dir, { env: { PATH: process.env.PATH, FAKE_NPM_FAIL: "packages/cli" } })));
172
+ expect(err.code).toBe("publish-npm-failed");
173
+ expect(err.message).toMatch(/npm publish of @acme\/cli@1\.4\.0 exited 1, after @acme\/core@1\.4\.0 were staged under next: npm error 403 Forbidden/);
174
+ expect(latest(log)["@acme/core"]).toBe("1.3.0");
175
+
176
+ const before = calls(log).length;
177
+ const ctx = context(dir, { env: { PATH: process.env.PATH, FAKE_NPM_FAIL_TAG: "@acme/cli" } });
178
+ const staged = await target!.stage(ctx);
179
+ const publishes = calls(log).slice(before).filter((c) => c.args[0] === "publish").map((c) => c.args[1]);
180
+ expect(publishes).toEqual([join(dir, "packages/cli"), join(dir, "packages/kit")]);
181
+ expect(staged.items.map((i) => [i.name, i.present ?? false])).toEqual([["@acme/core@1.4.0", true], ["@acme/cli@1.4.0", false], ["@acme/kit@1.4.0", false]]);
182
+ expect(ctx.lines.join("\n")).toContain("@acme/core@1.4.0 is already on the registry; not published again");
183
+
184
+ const tagErr = await refused(target!.reveal(ctx, staged));
185
+ expect(tagErr.code).toBe("publish-npm-tag-failed");
186
+ expect(tagErr.message).toMatch(/npm dist-tag add @acme\/cli@1\.4\.0 latest exited 1, after latest moved for @acme\/core@1\.4\.0/);
187
+ expect(latest(log)).toEqual({ "@acme/core": "1.4.0", "@acme/cli": "1.3.0", "@acme/kit": "1.3.0" });
188
+
189
+ const [missing] = await createPublishTargets([decl({ type: "npm", packages: ["packages/nope"] })], { npm });
190
+ expect((await refused(missing!.plan(context(dir)))).code).toBe("publish-file-missing");
191
+
192
+ const [packed] = await createPublishTargets([decl({ type: "npm", packages: ["kit.tgz"] })], { npm });
193
+ const other: ReleaseArtifact = { component: "kit", kind: "asset", path: "kit.tgz", digest: "sha256:" + "0".repeat(64) };
194
+ expect((await refused(packed!.plan(context(dir, { artifacts: [other] })))).code).toBe("publish-artifact-digest-mismatch");
195
+ });
196
+ });
197
+
198
+ describe("oci", () => {
199
+ test("parses GHCR, Docker Hub and local registries", () => {
200
+ expect(parseRepository("ghcr.io/acme/api")).toEqual({ host: "ghcr.io", base: "https://ghcr.io", name: "acme/api" });
201
+ expect(parseRepository("docker.io/nginx")).toEqual({ host: "docker.io", base: "https://registry-1.docker.io", name: "library/nginx" });
202
+ expect(parseRepository("acme/api")).toEqual({ host: "docker.io", base: "https://registry-1.docker.io", name: "acme/api" });
203
+ expect(parseRepository("127.0.0.1:5000/acme/api")).toEqual({ host: "127.0.0.1:5000", base: "http://127.0.0.1:5000", name: "acme/api" });
204
+ });
205
+
206
+ test("stages by digest with no tag moved until the reveal, through a Bearer challenge; a stage run again pushes nothing; no credential is printed", async () => {
207
+ const reg = await startFakeRegistry({ username: "robot", password: "oci-pass-9876", token: "registry-token-abcdef" });
208
+ try {
209
+ const dir = temp();
210
+ writeLockfile(dir);
211
+ const { digest, configDigest } = writeOciLayout(join(dir, "dist", "api.oci"));
212
+ const repo = `${reg.host}/acme/api`;
213
+ const [target] = await createPublishTargets([decl({ type: "oci", image: "dist/api.oci", repository: repo, tags: ["{label}", "latest"] })], {});
214
+ const env = { OCI_USERNAME: "robot", OCI_PASSWORD: "oci-pass-9876" };
215
+ const ctx = context(dir, { env, artifacts: [{ component: "api", kind: "image", path: "dist/api.oci", digest: configDigest }] });
216
+
217
+ const planned = await target!.plan(ctx);
218
+ expect(planned[0]).toEqual({ name: "dist/api.oci (tags v1.4.0, latest)", destination: `${repo}@${digest}`, digest });
219
+ // #4049: the SBOM and the provenance statement go as referrers of the image.
220
+ expect(planned.slice(1).map((i) => [i.name, i.attachment])).toEqual([["sbom.spdx.json", "sbom"], ["release.intoto.json", "provenance"]]);
221
+ expect(reg.requests).toEqual([]);
222
+
223
+ const staged = await target!.stage(ctx);
224
+ expect(staged.items[0]).toEqual({ name: "dist/api.oci", destination: `${repo}@${digest}`, digest });
225
+ const referrers = staged.items.slice(1).map((i) => i.digest!);
226
+ expect(referrers).toEqual(planned.slice(1).map((i) => i.digest));
227
+ // Two image blobs, then the empty config and each attached file.
228
+ expect(reg.uploads).toBe(5);
229
+ // Staged: the manifest and its referrers are there by digest, and no tag names them yet.
230
+ expect([...reg.manifests.keys()]).toEqual([digest, ...referrers]);
231
+ expect(reg.requests.some((r) => r.authorization === "Bearer registry-token-abcdef")).toBe(true);
232
+
233
+ const again = await target!.stage(ctx);
234
+ expect(again.items.map((i) => i.present)).toEqual([true, true, true]);
235
+ expect(reg.uploads).toBe(5);
236
+ expect([...reg.manifests.keys()]).toEqual([digest, ...referrers]);
237
+
238
+ const done = await target!.reveal(ctx, staged);
239
+ expect(done.items[0]!.destination).toBe(`${repo}@${digest}`);
240
+ expect([...reg.manifests.keys()].sort()).toEqual([digest, ...referrers, "latest", "v1.4.0"].sort());
241
+ expect(reg.manifests.get("v1.4.0")!.type).toBe("application/vnd.oci.image.manifest.v1+json");
242
+ const out = ctx.lines.join("\n");
243
+ expect(out).toContain("with credentials from OCI_PASSWORD");
244
+ expect(out).not.toMatch(/oci-pass-9876|registry-token-abcdef/);
245
+ } finally {
246
+ await reg.close();
247
+ }
248
+ });
249
+
250
+ test("pushes a docker save style tar with docker config credentials; refuses bad credentials, a layout the release did not build, and a missing one", async () => {
251
+ const reg = await startFakeRegistry({ username: "dh", password: "dockerhub-pat-555", token: "tok-777777" });
252
+ try {
253
+ const dir = temp();
254
+ writeLockfile(dir);
255
+ const { digest } = writeOciLayout(join(dir, "layout"), "other layer");
256
+ execFileSync("tar", ["-cf", join(dir, "api.tar"), "-C", join(dir, "layout"), "."], { stdio: "ignore" });
257
+ const dockerDir = join(dir, "docker");
258
+ mkdirSync(dockerDir, { recursive: true });
259
+ writeFileSync(join(dockerDir, "config.json"), JSON.stringify({ auths: { [reg.host]: { auth: Buffer.from("dh:dockerhub-pat-555").toString("base64") } } }));
260
+ const [target] = await createPublishTargets([decl({ type: "oci", image: "api.tar", repository: `${reg.host}/acme/api` })], {});
261
+ const ctx = context(dir, { env: { DOCKER_CONFIG: dockerDir } });
262
+ await publish(target!, ctx);
263
+ expect(reg.manifests.has(digest)).toBe(true);
264
+ expect(reg.manifests.has("v1.4.0")).toBe(true);
265
+ expect(ctx.lines.join("\n")).toContain("with credentials from docker config");
266
+ expect(ctx.lines.join("\n")).not.toContain("dockerhub-pat-555");
267
+
268
+ const wrong = context(dir, { env: { OCI_PASSWORD: "wrong-password" } });
269
+ const err = await refused(target!.stage(wrong));
270
+ expect(err.code).toBe("publish-oci-failed");
271
+ expect(err.message).not.toContain("wrong-password");
272
+
273
+ const mismatch = context(dir, { artifacts: [{ component: "api", kind: "image", path: "api.tar", digest: "sha256:" + "1".repeat(64) }] });
274
+ expect((await refused(target!.plan(mismatch))).code).toBe("publish-artifact-digest-mismatch");
275
+ const [missing] = await createPublishTargets([decl({ type: "oci", image: "nope.tar", repository: `${reg.host}/a` })], {});
276
+ expect((await refused(missing!.plan(context(dir)))).code).toBe("publish-file-missing");
277
+ } finally {
278
+ await reg.close();
279
+ }
280
+ });
281
+ });
282
+
283
+ describe("forge release assets", () => {
284
+ function assetDir(): string {
285
+ const dir = temp();
286
+ writeFileSync(join(dir, "api-linux.tar.gz"), "binary bytes");
287
+ writeFileSync(join(dir, "api-darwin.tar.gz"), "other bytes");
288
+ writeLockfile(dir);
289
+ return dir;
290
+ }
291
+ const DIGEST = `sha256:${createHash("sha256").update("binary bytes").digest("hex")}`;
292
+ const ASSETS = ["api-linux.tar.gz", "api-darwin.tar.gz"];
293
+ /** The declared assets, then the SBOM and the provenance statement chant attaches (#4049). */
294
+ const ATTACHED = [...ASSETS, "sbom.spdx.json", "release.intoto.json"];
295
+
296
+ test("GitHub: uploads to a draft release at the commit, which no consumer sees until the reveal publishes it; a stage run again uploads only what is missing", async () => {
297
+ const dir = assetDir();
298
+ const api = "https://api.github.test";
299
+ const forge = startFakeForge("github", api, "acme/api");
300
+ const [target] = await createPublishTargets([decl({ type: "forge", forge: "github", repo: "acme/api", api, assets: ASSETS })], { fetch: forge.fetch });
301
+ const ctx = context(dir, { env: { GITHUB_TOKEN: "ghs_tokentoken" } });
302
+ expect((await target!.plan(ctx))[0]).toEqual({ name: "api-linux.tar.gz", destination: "github acme/api release release/v1.4.0, asset api-linux.tar.gz", digest: DIGEST });
303
+ expect(forge.calls).toEqual([]);
304
+
305
+ // The second upload fails: the draft keeps the first, and nothing is visible.
306
+ forge.failOnce("POST https://uploads.forge.test/repos/acme/api/releases/100/assets?name=api-darwin");
307
+ expect((await refused(target!.stage(ctx))).code).toBe("publish-forge-failed");
308
+ expect(forge.releases).toMatchObject([{ tag_name: "release/v1.4.0", draft: true, target: ctx.release.commit, assets: [{ name: "api-linux.tar.gz" }] }]);
309
+ expect(forge.visible()).toEqual([]);
310
+
311
+ const n = forge.calls.length;
312
+ const staged = await target!.stage(ctx);
313
+ const uploads = forge.calls.slice(n).filter((c) => c.url.startsWith("https://uploads.forge.test"));
314
+ expect(uploads.map((c) => c.url)).toEqual(ATTACHED.slice(1).map((a) => `https://uploads.forge.test/repos/acme/api/releases/100/assets?name=${a}`));
315
+ expect(uploads[0]!.headers).toMatchObject({ authorization: "Bearer ghs_tokentoken", "content-type": "application/octet-stream" });
316
+ expect(staged.items.map((i) => i.present ?? false)).toEqual([true, false, false, false]);
317
+ expect(staged.items.map((i) => i.attachment)).toEqual([undefined, undefined, "sbom", "provenance"]);
318
+ expect(staged.state).toEqual({ releaseId: "100" });
319
+ expect(forge.releases).toHaveLength(1);
320
+ expect(forge.visible()).toEqual([]);
321
+
322
+ await target!.reveal(ctx, staged);
323
+ expect(forge.visible()).toEqual([{ tag: "release/v1.4.0", assets: ATTACHED }]);
324
+ expect(forge.calls.at(-1)).toMatchObject({ method: "PATCH", url: `${api}/repos/acme/api/releases/100`, body: JSON.stringify({ draft: false }) });
325
+ expect(ctx.lines.join("\n")).not.toContain("ghs_tokentoken");
326
+
327
+ // Published with every asset: a stage finds it done and uploads nothing.
328
+ const m = forge.calls.length;
329
+ const again = await target!.stage(ctx);
330
+ expect(again.state).toEqual({ published: "true" });
331
+ expect(forge.calls.slice(m).map((c) => c.method)).toEqual(["GET"]);
332
+ });
333
+
334
+ test("GitHub: a release already published without the assets, and an asset on the draft that differs, are refused", async () => {
335
+ const dir = assetDir();
336
+ const api = "https://api.github.test";
337
+ const published = startFakeForge("github", api, "acme/api", [{ id: 7, tag_name: "release/v1.4.0", draft: false, target: "x", assets: [] }]);
338
+ const [t1] = await createPublishTargets([decl({ type: "forge", forge: "github", repo: "acme/api", api, assets: ASSETS })], { fetch: published.fetch });
339
+ const ctx = context(dir, { env: { GITHUB_TOKEN: "ghs_tokentoken" } });
340
+ expect((await refused(t1!.stage(ctx))).code).toBe("publish-forge-release-published");
341
+ expect(published.calls.every((c) => c.method === "GET")).toBe(true);
342
+
343
+ const differs = startFakeForge("github", api, "acme/api", [{ id: 7, tag_name: "release/v1.4.0", draft: true, target: "x", assets: [{ name: "api-linux.tar.gz", digest: "sha256:" + "2".repeat(64), bytes: "x" }] }]);
344
+ const [t2] = await createPublishTargets([decl({ type: "forge", forge: "github", repo: "acme/api", api, assets: ASSETS })], { fetch: differs.fetch });
345
+ expect((await refused(t2!.stage(ctx))).code).toBe("publish-forge-asset-exists");
346
+ });
347
+
348
+ test("GitLab: assets go to the generic package registry with a CI job token, and the release is made with its links only at the reveal", async () => {
349
+ const dir = assetDir();
350
+ const api = "https://gitlab.test/api/v4";
351
+ const project = `${api}/projects/acme%2Fapi`;
352
+ const forge = startFakeForge("gitlab", api, "acme/api");
353
+ const [target] = await createPublishTargets([decl({ type: "forge", forge: "gitlab", repo: "acme/api", assets: ASSETS })], { fetch: forge.fetch });
354
+ const ctx = context(dir, { env: { CI_JOB_TOKEN: "glcbt-jobtoken", CI_API_V4_URL: api } });
355
+ const staged = await target!.stage(ctx);
356
+ expect(staged.items.map((i) => i.destination)).toEqual(ATTACHED.map((a) => `${project}/packages/generic/release/v1.4.0/${a}`));
357
+ expect(forge.calls.map((c) => c.method)).toEqual(["GET", "PUT", "PUT", "PUT", "PUT"]);
358
+ expect(forge.calls[1]!.headers).toMatchObject({ "job-token": "glcbt-jobtoken" });
359
+ expect(forge.visible()).toEqual([]);
360
+
361
+ await target!.reveal(ctx, staged);
362
+ expect(forge.visible()).toEqual([{ tag: "release/v1.4.0", assets: ATTACHED }]);
363
+ expect(JSON.parse(forge.calls.at(-1)!.body as string)).toEqual({
364
+ tag_name: "release/v1.4.0",
365
+ ref: ctx.release.commit,
366
+ name: "v1.4.0",
367
+ assets: { links: ATTACHED.map((a) => ({ name: a, url: `${project}/packages/generic/release/v1.4.0/${a}`, link_type: "package" })) },
368
+ });
369
+ });
370
+
371
+ test("Forgejo: a multipart upload to a draft, published at the reveal; it needs an API root, a token and a tag", async () => {
372
+ const dir = assetDir();
373
+ const api = "https://forgejo.test/api/v1";
374
+ const forge = startFakeForge("forgejo", api, "acme/api");
375
+ const make = async (over: Record<string, unknown> = {}) => (await createPublishTargets([decl({ type: "forge", forge: "forgejo", repo: "acme/api", assets: ["api-linux.tar.gz"], ...over })], { fetch: forge.fetch }))[0]!;
376
+ expect((await refused((await make()).plan(context(dir, { env: { FORGEJO_TOKEN: "fj-token-1" } })))).code).toBe("publish-forge-no-api");
377
+ expect((await refused((await make({ api })).plan(context(dir, { env: {} })))).code).toBe("publish-credentials-missing");
378
+ expect((await refused((await make({ api })).plan(context(dir, { env: { FORGEJO_TOKEN: "fj-token-1" }, releaseTag: null, label: null })))).code).toBe("publish-forge-no-tag");
379
+
380
+ const ctx = context(dir, { env: { FORGEJO_TOKEN: "fj-token-1" } });
381
+ const target = await make({ api });
382
+ const staged = await target.stage(ctx);
383
+ const upload = forge.calls.find((c) => c.url.endsWith("?name=api-linux.tar.gz"))!;
384
+ expect(upload.headers["content-type"]).toMatch(/^multipart\/form-data; boundary=chant-/);
385
+ expect(upload.headers.authorization).toBe("token fj-token-1");
386
+ expect(Buffer.from(upload.body as Uint8Array).toString()).toContain('name="attachment"; filename="api-linux.tar.gz"');
387
+ expect(forge.releases[0]).toMatchObject({ draft: true });
388
+ expect(forge.releases[0]!.assets[0]).toMatchObject({ name: "api-linux.tar.gz", bytes: "binary bytes" });
389
+ expect(forge.visible()).toEqual([]);
390
+ await target.reveal(ctx, staged);
391
+ expect(forge.visible()).toEqual([{ tag: "release/v1.4.0", assets: ["api-linux.tar.gz", "sbom.spdx.json", "release.intoto.json"] }]);
392
+ expect(ctx.lines.join("\n")).not.toContain("fj-token-1");
393
+ });
394
+ });
395
+
396
+ describe("SBOMs and provenance (#4049)", () => {
397
+ const CUT = { actor: "alice", label: "v1.4.0", timestamp: "2026-10-17T10:00:00.000Z", runId: "local-1", signature: "-----BEGIN SSH SIGNATURE-----\nU1NIU0lH\n-----END SSH SIGNATURE-----\n" };
398
+
399
+ test("npm: a package carries its SBOM, one with none or with one that is not SPDX or CycloneDX is refused, and sbom: false publishes without", async () => {
400
+ const dir = temp();
401
+ writePackage(join(dir, "with"), "@acme/with", "1.0.0");
402
+ writePackage(join(dir, "without"), "@acme/without", "1.0.0", false);
403
+ writePackage(join(dir, "bad"), "@acme/bad", "1.0.0", false);
404
+ writeFileSync(join(dir, "bad", "bom.json"), JSON.stringify({ not: "an sbom" }));
405
+ writePackage(join(dir, "cdx", "package"), "@acme/cdx", "1.0.0", false);
406
+ writeFileSync(join(dir, "cdx", "package", "bom.json"), JSON.stringify({ bomFormat: "CycloneDX", specVersion: "1.5" }));
407
+ execFileSync("tar", ["-czf", join(dir, "cdx.tgz"), "-C", join(dir, "cdx"), "package"], { stdio: "ignore" });
408
+ const npm = writeFakeNpm(join(dir, "bin"), join(dir, "npm.log"));
409
+ const plan = async (d: Record<string, unknown>) => (await createPublishTargets([decl({ type: "npm", ...d })], { npm }))[0]!.plan(context(dir));
410
+
411
+ expect((await plan({ packages: ["with"] }))[0]!.sbom).toBe("sbom.spdx.json");
412
+ const missing = await refused(plan({ packages: ["without"] }));
413
+ expect(missing.code).toBe("publish-sbom-missing");
414
+ expect(missing.message).toBe("@acme/without (without) carries no SBOM: npm keeps no file beside a package, so its SBOM goes in it, and none of sbom.spdx.json, sbom.cdx.json, sbom.cyclonedx.json is at its root");
415
+ expect(missing.fix).toContain("npm sbom --sbom-format spdx > without/sbom.spdx.json");
416
+ expect((await refused(plan({ packages: ["bad"], sbom: "bom.json" }))).code).toBe("publish-sbom-invalid");
417
+ expect((await plan({ packages: ["without"], sbom: false }))[0]!.sbom).toBeUndefined();
418
+ // In a packed .tgz, the file the declaration names under package/.
419
+ expect((await plan({ packages: ["cdx.tgz"], sbom: "bom.json" }))[0]!.sbom).toBe("bom.json");
420
+ });
421
+
422
+ test("npm: in CI the reveal audits what it published with npm audit signatures, and a failed audit names the packages", async () => {
423
+ const dir = temp();
424
+ writePackage(join(dir, "core"), "@acme/core", "1.0.0");
425
+ const log = join(dir, "npm.log");
426
+ const npm = writeFakeNpm(join(dir, "bin"), log);
427
+ const [target] = await createPublishTargets([decl({ type: "npm", packages: ["core"] })], { npm });
428
+ const ctx = context(dir, { env: { PATH: process.env.PATH, FAKE_NPM_AUDIT_FAIL: "1" }, provenance: OIDC });
429
+ const staged = await target!.stage(ctx);
430
+ const err = await refused(target!.reveal(ctx, staged));
431
+ expect(err.code).toBe("publish-npm-audit-failed");
432
+ expect(err.message).toBe("npm audit signatures exited 1 for @acme/core@1.0.0, after latest moved: 1 packages have invalid attestations");
433
+ expect(readFileSync(log, "utf-8")).toContain(`"args":["audit","signatures"]`);
434
+ });
435
+
436
+ test("OCI: the provenance statement carries the cut's signature, says a workstation's provenance is the signature, and is the same bytes every run; a declared SBOM is pushed as it is", async () => {
437
+ const reg = await startFakeRegistry({ username: "robot", password: "pw-attach", token: "tok-attach" });
438
+ try {
439
+ const dir = temp();
440
+ const { digest } = writeOciLayout(join(dir, "image"));
441
+ writeFileSync(join(dir, "image.spdx.json"), JSON.stringify({ spdxVersion: "SPDX-2.3", name: "image", packages: [] }));
442
+ const repo = `${reg.host}/acme/api`;
443
+ const [target] = await createPublishTargets([decl({ type: "oci", image: "image", repository: repo, sbom: "image.spdx.json" })], {});
444
+ const ctx = context(dir, { env: { OCI_USERNAME: "robot", OCI_PASSWORD: "pw-attach" }, cut: CUT });
445
+ const planned = await target!.plan(ctx);
446
+ expect(planned.slice(1).map((i) => i.name)).toEqual(["image.spdx.json", "release.intoto.json"]);
447
+ expect(await target!.plan(ctx)).toEqual(planned);
448
+
449
+ await target!.stage(ctx);
450
+ const manifests = planned.slice(1).map((i) => JSON.parse(reg.manifests.get(i.digest!)!.bytes.toString("utf-8")) as { artifactType: string; subject: { digest: string }; layers: Array<{ digest: string }> });
451
+ expect(manifests.map((m) => [m.artifactType, m.subject.digest])).toEqual([
452
+ ["application/spdx+json", digest],
453
+ ["application/vnd.in-toto+json", digest],
454
+ ]);
455
+ expect(reg.blobs.get(manifests[0]!.layers[0]!.digest)!.toString("utf-8")).toBe(readFileSync(join(dir, "image.spdx.json"), "utf-8"));
456
+ const statement = JSON.parse(reg.blobs.get(manifests[1]!.layers[0]!.digest)!.toString("utf-8"));
457
+ expect(statement.predicate).toEqual({
458
+ releaseDigest: ctx.release.releaseDigest,
459
+ label: "v1.4.0",
460
+ member: null,
461
+ commit: ctx.release.commit,
462
+ cut: { ...CUT, namespace: "chant-release" },
463
+ provenance: { kind: "signature", ci: null },
464
+ });
465
+
466
+ // sbom: false pushes the provenance statement alone.
467
+ const [bare] = await createPublishTargets([decl({ type: "oci", image: "image", repository: repo, sbom: false })], {});
468
+ expect((await bare!.plan(ctx)).slice(1).map((i) => i.attachment)).toEqual(["provenance"]);
469
+ } finally {
470
+ await reg.close();
471
+ }
472
+ });
473
+
474
+ test("forge: with no declared SBOM and no lockfile the target is refused; a declared SBOM goes on the release by its own name", async () => {
475
+ const dir = temp();
476
+ writeFileSync(join(dir, "api.tar.gz"), "bytes");
477
+ const api = "https://api.github.test";
478
+ const forge = startFakeForge("github", api, "acme/api");
479
+ const make = async (over: Record<string, unknown> = {}) => (await createPublishTargets([decl({ type: "forge", forge: "github", repo: "acme/api", api, assets: ["api.tar.gz"], ...over })], { fetch: forge.fetch }))[0]!;
480
+ const ctx = context(dir, { env: { GITHUB_TOKEN: "ghs_x" }, cut: CUT });
481
+ const err = await refused((await make()).plan(ctx));
482
+ expect(err.code).toBe("publish-sbom-missing");
483
+ expect(err.fix).toContain("sbom: false");
484
+
485
+ mkdirSync(join(dir, "dist"));
486
+ writeFileSync(join(dir, "dist", "api.cdx.json"), JSON.stringify({ bomFormat: "CycloneDX", specVersion: "1.5" }));
487
+ const target = await make({ sbom: "dist/api.cdx.json" });
488
+ await target.reveal(ctx, await target.stage(ctx));
489
+ expect(forge.visible()).toEqual([{ tag: "release/v1.4.0", assets: ["api.tar.gz", "api.cdx.json", "release.intoto.json"] }]);
490
+ const statement = JSON.parse(forge.releases[0]!.assets.find((a) => a.name === "release.intoto.json")!.bytes);
491
+ expect(statement.subject).toEqual([{ name: "api.tar.gz", digest: { sha256: createHash("sha256").update("bytes").digest("hex") } }]);
492
+ expect(statement.predicate.cut.signature).toBe(CUT.signature);
493
+ });
494
+ });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The publish targets (#4047): a member's `release.publish` declarations
3
+ * (./publish-declaration.ts) made into {@link PublishTarget}s, and what the
4
+ * targets share.
5
+ *
6
+ * - `npm` (./publish-npm.ts): `npm publish` per package, in order, with
7
+ * `--provenance` when the run has an OIDC token.
8
+ * - `oci` (./publish-oci.ts): an OCI image layout pushed to a registry such
9
+ * as GHCR or Docker Hub over the registry HTTP API, by digest, then tagged.
10
+ * - `forge` (./publish-forge.ts): files attached to the GitHub, GitLab or
11
+ * Forgejo release named by the release's tag.
12
+ *
13
+ * A file a target reads that the release records as an artifact at the same
14
+ * path is checked against the release's digest, and a file that differs is
15
+ * refused (`publish-artifact-digest-mismatch`). Other files are the commit's
16
+ * by the checkout check (`checkCheckoutIsRelease` in ./release-publish.ts).
17
+ */
18
+
19
+ import { createHash } from "node:crypto";
20
+ import { existsSync, readFileSync, statSync } from "node:fs";
21
+ import { isAbsolute, normalize, relative, resolve } from "node:path";
22
+ import type { PublishDeclaration } from "./publish-declaration";
23
+ import { PublishTargetError, type PublishDeps, type PublishTarget } from "./release-publish";
24
+ import type { Release, ReleaseArtifact } from "./release-store";
25
+ import { sortedJsonReplacer } from "../utils";
26
+
27
+ /** Make each declaration a target, in the order declared. */
28
+ export async function createPublishTargets(decls: readonly PublishDeclaration[], deps: PublishDeps = {}): Promise<PublishTarget[]> {
29
+ const out: PublishTarget[] = [];
30
+ for (const d of decls) {
31
+ switch (d.type) {
32
+ case "npm":
33
+ out.push((await import("./publish-npm")).npmTarget(d, deps));
34
+ break;
35
+ case "oci":
36
+ out.push((await import("./publish-oci")).ociTarget(d, deps));
37
+ break;
38
+ case "forge":
39
+ out.push((await import("./publish-forge")).forgeTarget(d, deps));
40
+ break;
41
+ }
42
+ }
43
+ return out;
44
+ }
45
+
46
+ /**
47
+ * The key of a target made from `decl` (#4048): its kind and the first 16 hex
48
+ * digits of the SHA-256 of its sorted JSON. A declaration holds variable
49
+ * names, never a credential, so the key holds none either.
50
+ */
51
+ export function publishTargetKey(decl: PublishDeclaration): string {
52
+ return `${decl.type}:${createHash("sha256").update(JSON.stringify(decl, sortedJsonReplacer)).digest("hex").slice(0, 16)}`;
53
+ }
54
+
55
+ /** `path` resolved in the member at `cwd`, refused when it is not there. */
56
+ export function memberFile(cwd: string, path: string): string {
57
+ const abs = isAbsolute(path) ? path : resolve(cwd, path);
58
+ if (!existsSync(abs)) {
59
+ throw new PublishTargetError("publish-file-missing", `${path} does not exist in ${cwd}`, "Build it before publishing: a publish uploads what is there and never builds.");
60
+ }
61
+ return abs;
62
+ }
63
+
64
+ /** `sha256:<hex>` of a file's bytes. */
65
+ export function fileDigest(abs: string): string {
66
+ return `sha256:${createHash("sha256").update(readFileSync(abs)).digest("hex")}`;
67
+ }
68
+
69
+ /** `sha256:<hex>` of bytes. */
70
+ export function bytesDigest(bytes: Uint8Array): string {
71
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
72
+ }
73
+
74
+ /** True when `abs` is a directory. */
75
+ export function isDir(abs: string): boolean {
76
+ return statSync(abs).isDirectory();
77
+ }
78
+
79
+ const normal = (p: string) => normalize(p).replace(/\\/g, "/").replace(/^\.\//, "");
80
+
81
+ /** The release's artifact of `kind` at `path` (relative to the member at `cwd`), if it records one. */
82
+ export function releaseArtifactAt(release: Release, kind: ReleaseArtifact["kind"], cwd: string, path: string): ReleaseArtifact | undefined {
83
+ const rel = normal(isAbsolute(path) ? relative(cwd, path) : path);
84
+ return release.artifactDigests.find((a) => a.kind === kind && normal(a.path) === rel);
85
+ }
86
+
87
+ /**
88
+ * Refuse a file the release records at `path` whose digest is none of
89
+ * `digests`, the identities the target can compute for it (the file's bytes,
90
+ * and for an image its manifest and config digests).
91
+ */
92
+ export function checkReleaseDigest(release: Release, kind: ReleaseArtifact["kind"], cwd: string, path: string, digests: readonly string[]): void {
93
+ const recorded = releaseArtifactAt(release, kind, cwd, path);
94
+ if (!recorded || digests.includes(recorded.digest)) return;
95
+ throw new PublishTargetError(
96
+ "publish-artifact-digest-mismatch",
97
+ `${path} is not the ${kind} release ${release.releaseDigest} built: the release records ${recorded.digest}, and the file here is ${digests[0]}`,
98
+ "Publish the files the cut built. A publish never builds, so restore them from the cut's build, or cut a new release.",
99
+ );
100
+ }