@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,259 @@
1
+ /**
2
+ * The npm publish target (#4047): `npm publish` for each package the
3
+ * declaration lists, in the order it lists them, so a set of packages that
4
+ * depend on each other goes out dependencies first.
5
+ *
6
+ * Each package is a directory with a `package.json` or a `.tgz` that
7
+ * `npm pack` wrote, relative to the member.
8
+ *
9
+ * A publish is two steps (#4048), as chant's own releases are (#3903):
10
+ *
11
+ * - stage: for each package, `npm view <name>@<version> version` asks the
12
+ * registry whether it already holds the version, and when it does not,
13
+ *
14
+ * npm publish <package> --tag next [--registry <url>] [--access <access>] [--provenance]
15
+ *
16
+ * puts it out under the `next` dist-tag, where nothing that resolves
17
+ * `latest` or a range finds it. A version already there is not published
18
+ * again, so a stage run after a failure publishes only the rest.
19
+ * - reveal: `npm dist-tag add <name>@<version> latest` for each package in
20
+ * the declared order. The first package is the set's core: it moves first,
21
+ * so a consumer never resolves a new package whose new core is not `latest`
22
+ * yet. Order the list core first, then the packages that depend on it.
23
+ * `ctx.tag` names another tag to move in place of `latest`.
24
+ *
25
+ * `--provenance` is passed when the run has an OIDC token (`detectProvenance`
26
+ * in ./release-publish.ts), so a CI publish gets npm provenance, and npm's
27
+ * trusted publishing exchanges that token for its own. With that token, the
28
+ * reveal then checks what it published (#4049): in an empty directory it
29
+ * installs every package at its version with `npm install --ignore-scripts`
30
+ * and runs `npm audit signatures`, which fails on a package whose registry
31
+ * signature or provenance attestation does not verify. From a workstation
32
+ * there is no npm provenance, and the release's signature, which
33
+ * `chant release verify` checks, is the only provenance.
34
+ *
35
+ * Each package carries its SBOM (#4049): npm keeps no file beside a package,
36
+ * so the SBOM is a file at the package's root, the one `sbom` names or the
37
+ * first of `sbom.spdx.json`, `sbom.cdx.json`, `sbom.cyclonedx.json` it holds,
38
+ * such as `npm sbom` writes. A package with none is refused
39
+ * (`publish-sbom-missing`) unless the declaration says `sbom: false`. Moving a dist-tag over
40
+ * OIDC needs "Allow npm dist-tag" on the package's trusted-publisher record.
41
+ * npm makes the first version of a new package `latest` whatever tag it is
42
+ * published under, so a package's first release is visible when it stages.
43
+ *
44
+ * Credentials are npm's own. chant reads no npm token and writes no `.npmrc`:
45
+ * npm authenticates with the registry's `_authToken` in the user's or the
46
+ * CI's npm config (`actions/setup-node` writes one that reads
47
+ * `NODE_AUTH_TOKEN`), or by trusted publishing. npm runs with the run's
48
+ * environment, and everything it prints is redacted before chant prints it.
49
+ */
50
+
51
+ import { execFileSync, spawn } from "node:child_process";
52
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
53
+ import { tmpdir } from "node:os";
54
+ import { join } from "node:path";
55
+ import { packageSbom } from "./publish-attachments";
56
+ import type { NpmPublishDeclaration } from "./publish-declaration";
57
+ import { checkReleaseDigest, fileDigest, isDir, memberFile, publishTargetKey } from "./publish-targets";
58
+ import { NPM_STAGE_TAG, PublishTargetError, type PublishContext, type PublishDeps, type PublishItem, type PublishTarget } from "./release-publish";
59
+
60
+ interface NpmPackage {
61
+ path: string;
62
+ abs: string;
63
+ name: string;
64
+ version: string;
65
+ digest?: string;
66
+ /** The SBOM file at its root, or undefined under `sbom: false`. */
67
+ sbom?: string;
68
+ }
69
+
70
+ /** Read a package's name and version, from its directory or its packed `.tgz`, and find the SBOM it carries. */
71
+ function readPackage(ctx: PublishContext, path: string, sbom: NpmPublishDeclaration["sbom"]): NpmPackage {
72
+ const abs = memberFile(ctx.cwd, path);
73
+ let text: string;
74
+ let digest: string | undefined;
75
+ const directory = isDir(abs);
76
+ if (directory) {
77
+ try {
78
+ text = readFileSync(join(abs, "package.json"), "utf-8");
79
+ } catch {
80
+ throw new PublishTargetError("publish-npm-not-a-package", `${path} has no package.json`);
81
+ }
82
+ } else {
83
+ digest = fileDigest(abs);
84
+ checkReleaseDigest(ctx.release, "asset", ctx.cwd, path, [digest]);
85
+ try {
86
+ text = execFileSync("tar", ["-xzOf", abs, "package/package.json"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
87
+ } catch {
88
+ throw new PublishTargetError("publish-npm-not-a-package", `${path} is not a package tarball with package/package.json`);
89
+ }
90
+ }
91
+ let pkg: { name?: unknown; version?: unknown; private?: unknown };
92
+ try {
93
+ pkg = JSON.parse(text);
94
+ } catch {
95
+ throw new PublishTargetError("publish-npm-not-a-package", `${path}'s package.json is not JSON`);
96
+ }
97
+ if (typeof pkg.name !== "string" || typeof pkg.version !== "string") {
98
+ throw new PublishTargetError("publish-npm-not-a-package", `${path}'s package.json has no name and version`);
99
+ }
100
+ if (pkg.private === true) {
101
+ throw new PublishTargetError("publish-npm-private", `${pkg.name} (${path}) is private, and npm refuses to publish it`, "Remove it from release.publish, or remove private: true.");
102
+ }
103
+ const carried = packageSbom(abs, directory, path, pkg.name, sbom);
104
+ return { path, abs, name: pkg.name, version: pkg.version, ...(digest ? { digest } : {}), ...(carried ? { sbom: carried.file } : {}) };
105
+ }
106
+
107
+ /** The arguments of one `npm publish`, which stages under `tag`. */
108
+ export function npmPublishArgs(decl: Pick<NpmPublishDeclaration, "registry" | "access">, abs: string, ctx: Pick<PublishContext, "provenance">, tag: string = NPM_STAGE_TAG): string[] {
109
+ return [
110
+ "publish",
111
+ abs,
112
+ "--tag",
113
+ tag,
114
+ ...(decl.registry ? ["--registry", decl.registry] : []),
115
+ ...(decl.access ? ["--access", decl.access] : []),
116
+ ...(ctx.provenance.oidc ? ["--provenance"] : []),
117
+ ];
118
+ }
119
+
120
+ /** The arguments of the `npm dist-tag add` that reveals a staged version under `tag`. */
121
+ export function npmDistTagArgs(decl: Pick<NpmPublishDeclaration, "registry">, name: string, version: string, tag: string): string[] {
122
+ return ["dist-tag", "add", `${name}@${version}`, tag, ...(decl.registry ? ["--registry", decl.registry] : [])];
123
+ }
124
+
125
+ /** The arguments of the `npm install` that fetches the published packages into an empty directory for `npm audit signatures`. */
126
+ export function npmAuditInstallArgs(decl: Pick<NpmPublishDeclaration, "registry">): string[] {
127
+ return ["install", "--ignore-scripts", "--no-audit", "--no-fund", ...(decl.registry ? ["--registry", decl.registry] : [])];
128
+ }
129
+
130
+ /** The arguments of `npm audit signatures`. */
131
+ export function npmAuditSignaturesArgs(decl: Pick<NpmPublishDeclaration, "registry">): string[] {
132
+ return ["audit", "signatures", ...(decl.registry ? ["--registry", decl.registry] : [])];
133
+ }
134
+
135
+ function run(bin: string, args: string[], ctx: PublishContext, cwd: string = ctx.cwd): Promise<{ code: number; output: string[] }> {
136
+ return new Promise((resolvePromise, reject) => {
137
+ const child = spawn(bin, args, { cwd, env: ctx.env, stdio: ["ignore", "pipe", "pipe"] });
138
+ const output: string[] = [];
139
+ let partial = { out: "", err: "" };
140
+ const take = (key: "out" | "err") => (chunk: Buffer) => {
141
+ const text = partial[key] + chunk.toString("utf-8");
142
+ const lines = text.split("\n");
143
+ partial = { ...partial, [key]: lines.pop() ?? "" };
144
+ for (const l of lines) if (l.trim() !== "") output.push(ctx.redactor.redact(l));
145
+ };
146
+ child.stdout.on("data", take("out"));
147
+ child.stderr.on("data", take("err"));
148
+ child.on("error", reject);
149
+ child.on("close", (code) => {
150
+ for (const l of [partial.out, partial.err]) if (l.trim() !== "") output.push(ctx.redactor.redact(l));
151
+ resolvePromise({ code: code ?? 1, output });
152
+ });
153
+ });
154
+ }
155
+
156
+ export function npmTarget(decl: NpmPublishDeclaration, deps: PublishDeps): PublishTarget {
157
+ const bin = deps.npm ?? "npm";
158
+ const registry = decl.registry ?? "the registry npm's config names";
159
+ const shown = (tag: string) => `${registry}, dist-tag ${tag}`;
160
+ const revealTag = (ctx: PublishContext) => ctx.tag ?? "latest";
161
+ const npm = async (ctx: PublishContext, args: string[], cwd?: string): Promise<{ code: number; output: string[] }> => {
162
+ try {
163
+ return await run(bin, args, ctx, cwd);
164
+ } catch (err) {
165
+ throw new PublishTargetError("publish-npm-failed", ctx.redactor.redact(`npm could not be run: ${err instanceof Error ? err.message : String(err)}`), "Install npm, or put it on PATH.");
166
+ }
167
+ };
168
+ /** Whether the registry already holds the version. A failed lookup reads as no, and the publish that follows says why. */
169
+ const onRegistry = async (ctx: PublishContext, pkg: NpmPackage): Promise<boolean> => {
170
+ const r = await npm(ctx, ["view", `${pkg.name}@${pkg.version}`, "version", ...(decl.registry ? ["--registry", decl.registry] : [])]);
171
+ return r.code === 0 && r.output.some((l) => l.trim() === pkg.version || l.trim() === JSON.stringify(pkg.version));
172
+ };
173
+ const read = (ctx: PublishContext) => decl.packages.map((p) => readPackage(ctx, p, decl.sbom));
174
+ const extra = (pkg: NpmPackage) => ({ ...(pkg.digest ? { digest: pkg.digest } : {}), ...(pkg.sbom ? { sbom: pkg.sbom } : {}) });
175
+ const plan = async (ctx: PublishContext): Promise<PublishItem[]> =>
176
+ read(ctx).map((pkg) => ({ name: `${pkg.name}@${pkg.version}`, destination: `${shown(NPM_STAGE_TAG)}, then ${revealTag(ctx)}`, ...extra(pkg) }));
177
+ /**
178
+ * In CI, install what was published into an empty directory and run
179
+ * `npm audit signatures` there (#4049), which checks each package's
180
+ * registry signature and provenance attestation.
181
+ */
182
+ const audit = async (ctx: PublishContext, pkgs: NpmPackage[]): Promise<void> => {
183
+ const dir = mkdtempSync(join(tmpdir(), "chant-npm-audit-"));
184
+ try {
185
+ const dependencies = Object.fromEntries(pkgs.map((p) => [p.name, p.version]));
186
+ writeFileSync(join(dir, "package.json"), `${JSON.stringify({ name: "chant-publish-audit", version: "0.0.0", private: true, dependencies }, null, 2)}\n`);
187
+ for (const args of [npmAuditInstallArgs(decl), npmAuditSignaturesArgs(decl)]) {
188
+ ctx.log(`npm ${args.join(" ")}`);
189
+ const result = await npm(ctx, args, dir);
190
+ for (const line of result.output) ctx.log(` ${line}`);
191
+ if (result.code !== 0) {
192
+ throw new PublishTargetError(
193
+ "publish-npm-audit-failed",
194
+ `npm ${args.slice(0, 2).join(" ")} exited ${result.code} for ${pkgs.map((p) => `${p.name}@${p.version}`).join(", ")}, after latest moved: ${result.output.at(-1) ?? "npm printed nothing"}`,
195
+ "Each package was published; check its signature and provenance on the registry. Running the publish again moves no tag that moved and audits again.",
196
+ );
197
+ }
198
+ }
199
+ } finally {
200
+ rmSync(dir, { recursive: true, force: true });
201
+ }
202
+ };
203
+ return {
204
+ kind: "npm",
205
+ name: `npm (${decl.packages.length} package${decl.packages.length === 1 ? "" : "s"})`,
206
+ destination: `npm ${registry}`,
207
+ credentialEnv: [],
208
+ key: publishTargetKey(decl),
209
+ plan,
210
+ async stage(ctx) {
211
+ const pkgs = read(ctx);
212
+ const items: PublishItem[] = [];
213
+ for (const pkg of pkgs) {
214
+ const item: PublishItem = { name: `${pkg.name}@${pkg.version}`, destination: shown(NPM_STAGE_TAG), ...extra(pkg) };
215
+ if (await onRegistry(ctx, pkg)) {
216
+ ctx.log(`${item.name} is already on the registry; not published again`);
217
+ items.push({ ...item, present: true });
218
+ continue;
219
+ }
220
+ const args = npmPublishArgs(decl, pkg.abs, ctx);
221
+ ctx.log(`npm publish ${pkg.path} ${args.slice(2).join(" ")}`);
222
+ const result = await npm(ctx, args);
223
+ for (const line of result.output) ctx.log(` ${line}`);
224
+ if (result.code !== 0) {
225
+ const done = items.map((i) => i.name);
226
+ throw new PublishTargetError(
227
+ "publish-npm-failed",
228
+ `npm publish of ${item.name} exited ${result.code}${done.length > 0 ? `, after ${done.join(", ")} were staged under ${NPM_STAGE_TAG}` : ", before any package was staged"}: ${result.output.at(-1) ?? "npm printed nothing"}`,
229
+ );
230
+ }
231
+ items.push(item);
232
+ }
233
+ return { target: this.name, destination: this.destination, items, state: {} };
234
+ },
235
+ async reveal(ctx) {
236
+ const tag = revealTag(ctx);
237
+ const pkgs = read(ctx);
238
+ const items: PublishItem[] = [];
239
+ // The declared order: the first package is the set's core, and its tag moves first.
240
+ for (const pkg of pkgs) {
241
+ const args = npmDistTagArgs(decl, pkg.name, pkg.version, tag);
242
+ ctx.log(`npm ${args.join(" ")}`);
243
+ const result = await npm(ctx, args);
244
+ for (const line of result.output) ctx.log(` ${line}`);
245
+ if (result.code !== 0) {
246
+ const moved = items.map((i) => i.name);
247
+ throw new PublishTargetError(
248
+ "publish-npm-tag-failed",
249
+ `npm dist-tag add ${pkg.name}@${pkg.version} ${tag} exited ${result.code}${moved.length > 0 ? `, after ${tag} moved for ${moved.join(", ")}` : `, before ${tag} moved for any package`}: ${result.output.at(-1) ?? "npm printed nothing"}`,
250
+ `Moving a dist-tag needs npm credentials that may change dist-tags; over OIDC that is "Allow npm dist-tag" on the package's trusted-publisher record.`,
251
+ );
252
+ }
253
+ items.push({ name: `${pkg.name}@${pkg.version}`, destination: shown(tag), ...extra(pkg) });
254
+ }
255
+ if (ctx.provenance.oidc) await audit(ctx, pkgs);
256
+ return { target: this.name, destination: this.destination, items };
257
+ },
258
+ };
259
+ }
@@ -0,0 +1,396 @@
1
+ /**
2
+ * The OCI publish target (#4047): an image pushed to an OCI registry, such
3
+ * as GHCR or Docker Hub, by digest, then tagged.
4
+ *
5
+ * The image is an OCI image layout (`index.json` and `blobs/`), as a
6
+ * directory or a tar file such as `docker save` writes. Its `index.json`
7
+ * names one manifest: an image manifest, or an index of them. The target
8
+ * speaks the registry HTTP API (the OCI distribution spec) through an
9
+ * injectable `fetch`, so it needs no docker, crane or oras, and the tests
10
+ * run it against a registry fake on 127.0.0.1. A publish is two steps
11
+ * (#4048). The stage pushes by digest only: for each image manifest it
12
+ * uploads the config and layers the registry does not hold yet, then puts
13
+ * the manifest by its digest, then the index when there is one. A registry
14
+ * that already holds the top manifest's digest is sent nothing. Every blob
15
+ * is checked against its digest before it is sent. Nothing a consumer pulls
16
+ * by tag changes until the reveal, which points each tag at that digest.
17
+ *
18
+ * The stage also pushes, by digest, two referrers of the image (#4049): its
19
+ * SBOM and the release's provenance statement (./publish-attachments.ts),
20
+ * each an OCI artifact manifest whose `subject` is the image, so a registry
21
+ * with the referrers API lists them under the image's digest and `oras
22
+ * discover` or `cosign tree` finds them. A referrer the registry already
23
+ * holds is not pushed again.
24
+ *
25
+ * Credentials come from `usernameEnv` and `passwordEnv` (default
26
+ * `OCI_USERNAME`, `OCI_PASSWORD`), or else from the `auths` entry for the
27
+ * registry in the docker config file (`$DOCKER_CONFIG/config.json` or
28
+ * `~/.docker/config.json`), which `docker login` writes; a credential helper
29
+ * (`credsStore`) is not read. With none, the push is anonymous. The registry
30
+ * decides how they are used: a `Basic` challenge takes them as they are, and
31
+ * a `Bearer` challenge (GHCR, Docker Hub) exchanges them for a token at the
32
+ * realm it names. The password and the token are masked in all output.
33
+ *
34
+ * A host of `localhost`, `127.0.0.1` or `[::1]` is spoken to over http, as
35
+ * docker does; any other over https. `docker.io` is `registry-1.docker.io`,
36
+ * and a one-part Docker Hub name is under `library/`.
37
+ */
38
+
39
+ import { execFileSync } from "node:child_process";
40
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
41
+ import { tmpdir } from "node:os";
42
+ import { join } from "node:path";
43
+ import { memberSbom, releaseProvenance, type Attachment } from "./publish-attachments";
44
+ import { readCredential, type Credential } from "./publish-credentials";
45
+ import type { OciPublishDeclaration } from "./publish-declaration";
46
+ import { bytesDigest, checkReleaseDigest, fileDigest, isDir, memberFile, publishTargetKey } from "./publish-targets";
47
+ import { PublishTargetError, type PublishContext, type PublishDeps, type PublishFetch, type PublishItem, type PublishTarget } from "./release-publish";
48
+
49
+ const INDEX_TYPES = new Set(["application/vnd.oci.image.index.v1+json", "application/vnd.docker.distribution.manifest.list.v2+json"]);
50
+
51
+ /** A repository reference split into where the registry is and the name in it. */
52
+ export interface OciRepository {
53
+ /** The host as written, used to find docker config credentials. */
54
+ host: string;
55
+ /** `http(s)://<host>` the API is under. */
56
+ base: string;
57
+ /** The repository's name in the registry. */
58
+ name: string;
59
+ }
60
+
61
+ export function parseRepository(repository: string): OciRepository {
62
+ const parts = repository.split("/");
63
+ const first = parts[0]!;
64
+ const isHost = parts.length > 1 && (first.includes(".") || first.includes(":") || first === "localhost");
65
+ let host = isHost ? first : "docker.io";
66
+ let path = isHost ? parts.slice(1) : parts;
67
+ if (host === "docker.io" || host === "index.docker.io") {
68
+ host = "docker.io";
69
+ if (path.length === 1) path = ["library", ...path];
70
+ }
71
+ const apiHost = host === "docker.io" ? "registry-1.docker.io" : host;
72
+ const local = /^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(apiHost);
73
+ return { host, base: `${local ? "http" : "https"}://${apiHost}`, name: path.join("/") };
74
+ }
75
+
76
+ interface Descriptor {
77
+ mediaType: string;
78
+ digest: string;
79
+ size?: number;
80
+ }
81
+
82
+ /** What a layout holds, in the order it is pushed. */
83
+ interface LayoutImage {
84
+ top: Descriptor & { bytes: Uint8Array };
85
+ /** The image manifests: one, or an index's children. */
86
+ manifests: Array<Descriptor & { bytes: Uint8Array; blobs: Descriptor[] }>;
87
+ configDigests: string[];
88
+ readBlob(digest: string): Uint8Array;
89
+ cleanup(): void;
90
+ }
91
+
92
+ function readLayout(abs: string): LayoutImage {
93
+ let dir = abs;
94
+ let tmp: string | null = null;
95
+ if (!isDir(abs)) {
96
+ tmp = mkdtempSync(join(tmpdir(), "chant-oci-"));
97
+ try {
98
+ execFileSync("tar", ["-xf", abs, "-C", tmp], { stdio: "ignore" });
99
+ } catch {
100
+ rmSync(tmp, { recursive: true, force: true });
101
+ throw new PublishTargetError("publish-oci-not-a-layout", `${abs} is neither an OCI image layout directory nor a tar file of one`);
102
+ }
103
+ dir = tmp;
104
+ }
105
+ const cleanup = () => {
106
+ if (tmp) rmSync(tmp, { recursive: true, force: true });
107
+ };
108
+ try {
109
+ const readBlob = (digest: string): Uint8Array => {
110
+ const [alg, hex] = digest.split(":");
111
+ const file = join(dir, "blobs", alg ?? "", hex ?? "");
112
+ if (!alg || !hex || !existsSync(file)) throw new PublishTargetError("publish-oci-not-a-layout", `the layout has no blob ${digest}`);
113
+ const bytes = readFileSync(file);
114
+ if (bytesDigest(bytes) !== digest) throw new PublishTargetError("publish-oci-blob-corrupt", `blob ${digest} in the layout does not match its digest`);
115
+ return bytes;
116
+ };
117
+ const indexFile = join(dir, "index.json");
118
+ if (!existsSync(indexFile)) throw new PublishTargetError("publish-oci-not-a-layout", `${abs} has no index.json, so it is not an OCI image layout`);
119
+ const index = JSON.parse(readFileSync(indexFile, "utf-8")) as { manifests?: Descriptor[] };
120
+ const tops = index.manifests ?? [];
121
+ if (tops.length !== 1) {
122
+ throw new PublishTargetError("publish-oci-not-one-image", `${abs} names ${tops.length} manifests in its index.json, and a target pushes one image`, "Save one image per layout.");
123
+ }
124
+ const topDesc = tops[0]!;
125
+ const topBytes = readBlob(topDesc.digest);
126
+ const manifests: LayoutImage["manifests"] = [];
127
+ const configDigests: string[] = [];
128
+ const image = (d: Descriptor, bytes: Uint8Array) => {
129
+ const m = JSON.parse(Buffer.from(bytes).toString("utf-8")) as { config?: Descriptor; layers?: Descriptor[]; mediaType?: string };
130
+ if (!m.config) throw new PublishTargetError("publish-oci-not-a-layout", `manifest ${d.digest} has no config`);
131
+ configDigests.push(m.config.digest);
132
+ manifests.push({ ...d, mediaType: d.mediaType ?? m.mediaType, bytes, blobs: [m.config, ...(m.layers ?? [])] });
133
+ };
134
+ const topParsed = JSON.parse(Buffer.from(topBytes).toString("utf-8")) as { mediaType?: string; manifests?: Descriptor[] };
135
+ const topType = topDesc.mediaType ?? topParsed.mediaType ?? "application/vnd.oci.image.manifest.v1+json";
136
+ if (INDEX_TYPES.has(topType)) {
137
+ for (const child of topParsed.manifests ?? []) image(child, readBlob(child.digest));
138
+ } else {
139
+ image({ ...topDesc, mediaType: topType }, topBytes);
140
+ }
141
+ return { top: { ...topDesc, mediaType: topType, bytes: topBytes }, manifests, configDigests, readBlob, cleanup };
142
+ } catch (err) {
143
+ cleanup();
144
+ if (err instanceof SyntaxError) throw new PublishTargetError("publish-oci-not-a-layout", `${abs} holds a manifest or index.json that is not JSON`);
145
+ throw err;
146
+ }
147
+ }
148
+
149
+ /** The credential for `repo`: the declared variables, else the docker config's `auths` entry. */
150
+ function ociCredential(decl: OciPublishDeclaration, repo: OciRepository, ctx: PublishContext): { username: string; password: Credential } | null {
151
+ const password = readCredential(ctx.env, [decl.passwordEnv ?? "OCI_PASSWORD"], ctx.redactor);
152
+ if (password) {
153
+ const user = ctx.env[decl.usernameEnv ?? "OCI_USERNAME"];
154
+ return { username: user && user !== "" ? user : "oauth2", password };
155
+ }
156
+ const dir = ctx.env.DOCKER_CONFIG ?? (ctx.env.HOME ? join(ctx.env.HOME, ".docker") : null);
157
+ if (!dir || !existsSync(join(dir, "config.json"))) return null;
158
+ try {
159
+ const conf = JSON.parse(readFileSync(join(dir, "config.json"), "utf-8")) as { auths?: Record<string, { auth?: string }> };
160
+ const keys = repo.host === "docker.io" ? ["https://index.docker.io/v1/", "docker.io", "index.docker.io"] : [repo.host, `https://${repo.host}`];
161
+ for (const k of keys) {
162
+ const auth = conf.auths?.[k]?.auth;
163
+ if (!auth) continue;
164
+ ctx.redactor.add(auth);
165
+ const decoded = Buffer.from(auth, "base64").toString("utf-8");
166
+ const at = decoded.indexOf(":");
167
+ if (at < 0) continue;
168
+ const value = decoded.slice(at + 1);
169
+ ctx.redactor.add(value);
170
+ return { username: decoded.slice(0, at), password: { value, from: "docker config" } };
171
+ }
172
+ } catch {
173
+ // An unreadable docker config gives no credential.
174
+ }
175
+ return null;
176
+ }
177
+
178
+ /** A registry client that answers the registry's auth challenge once and keeps the token. */
179
+ class Registry {
180
+ private auth: string | null = null;
181
+ constructor(
182
+ private readonly repo: OciRepository,
183
+ private readonly fetch: PublishFetch,
184
+ private readonly cred: { username: string; password: Credential } | null,
185
+ private readonly ctx: PublishContext,
186
+ ) {}
187
+
188
+ private fail(what: string, status: number, body: string): never {
189
+ throw new PublishTargetError("publish-oci-failed", this.ctx.redactor.redact(`${what} at ${this.repo.base} answered ${status}${body ? `: ${body.slice(0, 300)}` : ""}`));
190
+ }
191
+
192
+ async request(method: string, path: string, headers: Record<string, string> = {}, body?: Uint8Array): Promise<Awaited<ReturnType<PublishFetch>>> {
193
+ const url = /^https?:\/\//.test(path) ? path : `${this.repo.base}${path}`;
194
+ const send = () => this.fetch(url, { method, headers: { ...headers, ...(this.auth ? { authorization: this.auth } : {}) }, ...(body ? { body } : {}) });
195
+ let res = await send();
196
+ if (res.status === 401 && this.auth === null) {
197
+ await this.answerChallenge(res.headers.get("www-authenticate") ?? "");
198
+ res = await send();
199
+ }
200
+ return res;
201
+ }
202
+
203
+ private async answerChallenge(header: string): Promise<void> {
204
+ const basic = this.cred ? `Basic ${Buffer.from(`${this.cred.username}:${this.cred.password.value}`).toString("base64")}` : null;
205
+ if (basic) this.ctx.redactor.add(basic.slice(6));
206
+ if (/^basic/i.test(header)) {
207
+ if (!basic) throw new PublishTargetError("publish-credentials-missing", `${this.repo.base} asks for credentials, and none are set`, "Set OCI_USERNAME and OCI_PASSWORD, or the variables usernameEnv and passwordEnv name, or docker login.");
208
+ this.auth = basic;
209
+ return;
210
+ }
211
+ if (!/^bearer/i.test(header)) this.fail("the registry", 401, "with no challenge chant can answer");
212
+ const params = Object.fromEntries([...header.matchAll(/(\w+)="([^"]*)"/g)].map((m) => [m[1]!.toLowerCase(), m[2]!]));
213
+ if (!params.realm) this.fail("the registry", 401, "with a Bearer challenge that names no realm");
214
+ const u = new URL(params.realm);
215
+ if (params.service) u.searchParams.set("service", params.service);
216
+ u.searchParams.set("scope", `repository:${this.repo.name}:pull,push`);
217
+ const res = await this.fetch(u.toString(), { method: "GET", headers: basic ? { authorization: basic } : {} });
218
+ const text = await res.text();
219
+ if (!res.ok) this.fail("the token service", res.status, text);
220
+ const json = JSON.parse(text) as { token?: string; access_token?: string };
221
+ const token = json.token ?? json.access_token;
222
+ if (!token) this.fail("the token service", res.status, "with no token");
223
+ this.ctx.redactor.add(token);
224
+ this.auth = `Bearer ${token}`;
225
+ }
226
+
227
+ /** Upload a blob unless the registry holds it. True when it was uploaded. */
228
+ async pushBlob(digest: string, bytes: () => Uint8Array): Promise<boolean> {
229
+ const head = await this.request("HEAD", `/v2/${this.repo.name}/blobs/${digest}`);
230
+ if (head.status === 200) return false;
231
+ const start = await this.request("POST", `/v2/${this.repo.name}/blobs/uploads/`, { "content-length": "0" });
232
+ if (start.status !== 202) this.fail(`starting an upload of ${digest}`, start.status, await start.text());
233
+ const location = start.headers.get("location");
234
+ if (!location) this.fail(`starting an upload of ${digest}`, start.status, "with no Location");
235
+ const abs = /^https?:\/\//.test(location) ? location : `${this.repo.base}${location.startsWith("/") ? "" : "/"}${location}`;
236
+ const put = `${abs}${abs.includes("?") ? "&" : "?"}digest=${encodeURIComponent(digest)}`;
237
+ const data = bytes();
238
+ const done = await this.request("PUT", put, { "content-type": "application/octet-stream", "content-length": String(data.byteLength) }, data);
239
+ if (done.status !== 201) this.fail(`uploading ${digest}`, done.status, await done.text());
240
+ return true;
241
+ }
242
+
243
+ /** Whether the registry holds the manifest `digest`. */
244
+ async hasManifest(digest: string, mediaType: string): Promise<boolean> {
245
+ const res = await this.request("HEAD", `/v2/${this.repo.name}/manifests/${digest}`, { accept: mediaType });
246
+ return res.status === 200;
247
+ }
248
+
249
+ async putManifest(ref: string, mediaType: string, bytes: Uint8Array): Promise<void> {
250
+ const res = await this.request("PUT", `/v2/${this.repo.name}/manifests/${ref}`, { "content-type": mediaType }, bytes);
251
+ if (res.status !== 201) this.fail(`putting manifest ${ref}`, res.status, await res.text());
252
+ }
253
+ }
254
+
255
+ /** The media type of the empty config an artifact manifest names. */
256
+ const EMPTY_CONFIG = { mediaType: "application/vnd.oci.empty.v1+json", bytes: new Uint8Array(Buffer.from("{}")) };
257
+ const IMAGE_MANIFEST = "application/vnd.oci.image.manifest.v1+json";
258
+
259
+ /** One referrer of the image: an attached file and the artifact manifest that names the image as its subject. */
260
+ interface Referrer {
261
+ attachment: Attachment;
262
+ manifest: Uint8Array;
263
+ digest: string;
264
+ }
265
+
266
+ /**
267
+ * The artifact manifest that attaches `a` to the image `subject` (OCI image
268
+ * spec 1.1): `artifactType` is the file's media type, the config is the empty
269
+ * descriptor, the one layer is the file, and `subject` is the image.
270
+ */
271
+ export function referrerManifest(a: Attachment, subject: { mediaType: string; digest: string; size: number }): Uint8Array {
272
+ const manifest = {
273
+ schemaVersion: 2,
274
+ mediaType: IMAGE_MANIFEST,
275
+ artifactType: a.mediaType,
276
+ config: { mediaType: EMPTY_CONFIG.mediaType, digest: bytesDigest(EMPTY_CONFIG.bytes), size: EMPTY_CONFIG.bytes.byteLength },
277
+ layers: [{ mediaType: a.mediaType, digest: a.digest, size: a.bytes.byteLength, annotations: { "org.opencontainers.image.title": a.name } }],
278
+ subject,
279
+ annotations: { "org.opencontainers.image.title": a.name },
280
+ };
281
+ return new Uint8Array(Buffer.from(JSON.stringify(manifest)));
282
+ }
283
+
284
+ /** The tags to point at the digest: the context's tag when set, else the declared ones with `{label}` filled in. */
285
+ export function ociTags(decl: OciPublishDeclaration, ctx: Pick<PublishContext, "tag" | "label">): string[] {
286
+ if (ctx.tag) return [ctx.tag];
287
+ const declared = decl.tags ?? (ctx.label ? ["{label}"] : []);
288
+ return declared.filter((t) => ctx.label !== null || !t.includes("{label}")).map((t) => t.split("{label}").join(ctx.label ?? ""));
289
+ }
290
+
291
+ export function ociTarget(decl: OciPublishDeclaration, deps: PublishDeps): PublishTarget {
292
+ const repo = parseRepository(decl.repository);
293
+ const fetchImpl: PublishFetch = deps.fetch ?? ((url, init) => fetch(url, init as RequestInit));
294
+ const inspect = (ctx: PublishContext) => {
295
+ const abs = memberFile(ctx.cwd, decl.image);
296
+ const layout = readLayout(abs);
297
+ try {
298
+ const identities = [layout.top.digest, ...layout.configDigests, ...(isDir(abs) ? [] : [fileDigest(abs)])];
299
+ checkReleaseDigest(ctx.release, "image", ctx.cwd, decl.image, identities);
300
+ } catch (err) {
301
+ layout.cleanup();
302
+ throw err;
303
+ }
304
+ return layout;
305
+ };
306
+ const item = (ctx: PublishContext, digest: string): PublishItem => {
307
+ const tags = ociTags(decl, ctx);
308
+ return {
309
+ name: `${decl.image}${tags.length > 0 ? ` (tag${tags.length === 1 ? "" : "s"} ${tags.join(", ")})` : ""}`,
310
+ destination: `${decl.repository}@${digest}`,
311
+ digest,
312
+ };
313
+ };
314
+ /** The SBOM and the provenance statement, each with the artifact manifest that refers to the image. */
315
+ const referrers = (ctx: PublishContext, layout: LayoutImage): Referrer[] => {
316
+ const subject = { mediaType: layout.top.mediaType, digest: layout.top.digest, size: layout.top.bytes.byteLength };
317
+ const files = [
318
+ memberSbom(ctx, decl.sbom, `${decl.repository}@${layout.top.digest}`, `oci ${decl.repository}`),
319
+ releaseProvenance(ctx, [{ name: `${decl.repository}@${layout.top.digest}`, digest: layout.top.digest }]),
320
+ ].filter((a): a is Attachment => a !== null);
321
+ return files.map((attachment) => {
322
+ const manifest = referrerManifest(attachment, subject);
323
+ return { attachment, manifest, digest: bytesDigest(manifest) };
324
+ });
325
+ };
326
+ const referrerItem = (r: Referrer): PublishItem => ({ name: r.attachment.name, destination: `${decl.repository}@${r.digest}, a referrer of the image`, digest: r.digest, attachment: r.attachment.kind });
327
+ const connect = (ctx: PublishContext) => {
328
+ const cred = ociCredential(decl, repo, ctx);
329
+ return { cred, registry: new Registry(repo, fetchImpl, cred, ctx) };
330
+ };
331
+ return {
332
+ kind: "oci",
333
+ name: `oci ${decl.repository}`,
334
+ destination: decl.repository,
335
+ credentialEnv: [decl.passwordEnv ?? "OCI_PASSWORD"],
336
+ key: publishTargetKey(decl),
337
+ async plan(ctx) {
338
+ const layout = inspect(ctx);
339
+ try {
340
+ return [item(ctx, layout.top.digest), ...referrers(ctx, layout).map(referrerItem)];
341
+ } finally {
342
+ layout.cleanup();
343
+ }
344
+ },
345
+ async stage(ctx) {
346
+ const layout = inspect(ctx);
347
+ try {
348
+ const attached = referrers(ctx, layout);
349
+ const { cred, registry } = connect(ctx);
350
+ const staged: PublishItem = { ...item(ctx, layout.top.digest), name: decl.image };
351
+ const items: PublishItem[] = [];
352
+ if (await registry.hasManifest(layout.top.digest, layout.top.mediaType)) {
353
+ ctx.log(`${decl.repository} already holds ${layout.top.digest}; the image is not pushed again`);
354
+ items.push({ ...staged, present: true });
355
+ } else {
356
+ ctx.log(`pushing ${decl.image} to ${decl.repository} by digest ${layout.top.digest}, ${cred ? `with credentials from ${cred.password.from}` : "anonymously"}; no tag moves yet`);
357
+ let uploaded = 0;
358
+ for (const m of layout.manifests) {
359
+ for (const b of m.blobs) if (await registry.pushBlob(b.digest, () => layout.readBlob(b.digest))) uploaded++;
360
+ await registry.putManifest(m.digest, m.mediaType, m.bytes);
361
+ }
362
+ if (INDEX_TYPES.has(layout.top.mediaType)) await registry.putManifest(layout.top.digest, layout.top.mediaType, layout.top.bytes);
363
+ ctx.log(` ${uploaded} blob(s) uploaded`);
364
+ items.push(staged);
365
+ }
366
+ // #4049: the SBOM and the provenance statement, as referrers of the image, by digest.
367
+ for (const r of attached) {
368
+ if (await registry.hasManifest(r.digest, IMAGE_MANIFEST)) {
369
+ items.push({ ...referrerItem(r), present: true });
370
+ continue;
371
+ }
372
+ ctx.log(`attaching ${r.attachment.name} to ${layout.top.digest} as referrer ${r.digest}`);
373
+ await registry.pushBlob(bytesDigest(EMPTY_CONFIG.bytes), () => EMPTY_CONFIG.bytes);
374
+ await registry.pushBlob(r.attachment.digest, () => r.attachment.bytes);
375
+ await registry.putManifest(r.digest, IMAGE_MANIFEST, r.manifest);
376
+ items.push(referrerItem(r));
377
+ }
378
+ return { target: this.name, destination: this.destination, items, state: {} };
379
+ } finally {
380
+ layout.cleanup();
381
+ }
382
+ },
383
+ async reveal(ctx) {
384
+ const layout = inspect(ctx);
385
+ try {
386
+ const { registry } = connect(ctx);
387
+ const tags = ociTags(decl, ctx);
388
+ for (const t of tags) await registry.putManifest(t, layout.top.mediaType, layout.top.bytes);
389
+ ctx.log(tags.length > 0 ? `tagged ${layout.top.digest} ${tags.join(", ")}` : `no tag to point at ${layout.top.digest}`);
390
+ return { target: this.name, destination: this.destination, items: [item(ctx, layout.top.digest), ...referrers(ctx, layout).map(referrerItem)] };
391
+ } finally {
392
+ layout.cleanup();
393
+ }
394
+ },
395
+ };
396
+ }