@intentius/chant 0.124.32 → 0.124.34

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 (320) hide show
  1. package/dist/ci-jobs.d.ts +4 -0
  2. package/dist/ci-jobs.d.ts.map +1 -1
  3. package/dist/ci-jobs.js +9 -2
  4. package/dist/cli/handlers/build.d.ts.map +1 -1
  5. package/dist/cli/handlers/build.js +18 -3
  6. package/dist/cli/handlers/lifecycle-health.d.ts +16 -0
  7. package/dist/cli/handlers/lifecycle-health.d.ts.map +1 -0
  8. package/dist/cli/handlers/lifecycle-health.js +150 -0
  9. package/dist/cli/handlers/lifecycle.d.ts +26 -0
  10. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  11. package/dist/cli/handlers/lifecycle.js +1 -1
  12. package/dist/cli/handlers/pr.d.ts +2 -0
  13. package/dist/cli/handlers/pr.d.ts.map +1 -1
  14. package/dist/cli/handlers/pr.js +124 -2
  15. package/dist/cli/handlers/promote.d.ts.map +1 -1
  16. package/dist/cli/handlers/promote.js +277 -74
  17. package/dist/cli/handlers/release.d.ts +16 -1
  18. package/dist/cli/handlers/release.d.ts.map +1 -1
  19. package/dist/cli/handlers/release.js +83 -11
  20. package/dist/cli/main.d.ts.map +1 -1
  21. package/dist/cli/main.js +99 -22
  22. package/dist/cli/parse-args.d.ts.map +1 -1
  23. package/dist/cli/parse-args.js +30 -1
  24. package/dist/cli/registry.d.ts +21 -5
  25. package/dist/cli/registry.d.ts.map +1 -1
  26. package/dist/components/deploy-inputs.d.ts +91 -0
  27. package/dist/components/deploy-inputs.d.ts.map +1 -0
  28. package/dist/components/deploy-inputs.js +184 -0
  29. package/dist/components/driver.d.ts.map +1 -1
  30. package/dist/components/driver.js +9 -0
  31. package/dist/components/gate-plan.d.ts +8 -0
  32. package/dist/components/gate-plan.d.ts.map +1 -1
  33. package/dist/components/gate-plan.js +6 -0
  34. package/dist/components/pr-pipeline.d.ts +33 -10
  35. package/dist/components/pr-pipeline.d.ts.map +1 -1
  36. package/dist/components/pr-pipeline.js +42 -11
  37. package/dist/components/pr-run.d.ts +1 -1
  38. package/dist/components/pr-run.d.ts.map +1 -1
  39. package/dist/components/promote-rounds.d.ts +102 -0
  40. package/dist/components/promote-rounds.d.ts.map +1 -0
  41. package/dist/components/promote-rounds.js +174 -0
  42. package/dist/components/release-compile.d.ts +37 -2
  43. package/dist/components/release-compile.d.ts.map +1 -1
  44. package/dist/components/release-compile.js +64 -8
  45. package/dist/components/release-gate.d.ts +30 -0
  46. package/dist/components/release-gate.d.ts.map +1 -1
  47. package/dist/components/release-gate.js +92 -42
  48. package/dist/components/release-promote.d.ts +17 -3
  49. package/dist/components/release-promote.d.ts.map +1 -1
  50. package/dist/components/release-promote.js +79 -18
  51. package/dist/components/verbs/bom-writer.d.ts +1 -1
  52. package/dist/components/verbs/bom-writer.d.ts.map +1 -1
  53. package/dist/components/verbs/lockfile-sbom-generator.d.ts +20 -0
  54. package/dist/components/verbs/lockfile-sbom-generator.d.ts.map +1 -1
  55. package/dist/components/verbs/lockfile-sbom-generator.js +41 -4
  56. package/dist/components/wave-plan.d.ts +10 -0
  57. package/dist/components/wave-plan.d.ts.map +1 -1
  58. package/dist/components/wave-plan.js +32 -4
  59. package/dist/graph-declared.d.ts +4 -0
  60. package/dist/graph-declared.d.ts.map +1 -1
  61. package/dist/graph-declared.js +11 -1
  62. package/dist/graph-detail.d.ts.map +1 -1
  63. package/dist/graph-detail.js +32 -7
  64. package/dist/health.d.ts +110 -0
  65. package/dist/health.d.ts.map +1 -0
  66. package/dist/health.js +105 -0
  67. package/dist/health.schema.json +191 -0
  68. package/dist/lexicon.d.ts +27 -0
  69. package/dist/lexicon.d.ts.map +1 -1
  70. package/dist/lifecycle/merge-record.d.ts +104 -0
  71. package/dist/lifecycle/merge-record.d.ts.map +1 -0
  72. package/dist/lifecycle/merge-record.js +102 -0
  73. package/dist/lifecycle/publish-attachments.d.ts +8 -5
  74. package/dist/lifecycle/publish-attachments.d.ts.map +1 -1
  75. package/dist/lifecycle/publish-attachments.js +27 -13
  76. package/dist/lifecycle/publish-npm.d.ts +3 -2
  77. package/dist/lifecycle/publish-npm.d.ts.map +1 -1
  78. package/dist/lifecycle/publish-npm.js +4 -3
  79. package/dist/lifecycle/release-at.d.ts +40 -0
  80. package/dist/lifecycle/release-at.d.ts.map +1 -0
  81. package/dist/lifecycle/release-at.js +83 -0
  82. package/dist/lifecycle/release-cut.d.ts.map +1 -1
  83. package/dist/lifecycle/release-cut.js +23 -12
  84. package/dist/lifecycle/release-publish.d.ts +28 -12
  85. package/dist/lifecycle/release-publish.d.ts.map +1 -1
  86. package/dist/lifecycle/release-publish.js +29 -17
  87. package/dist/lifecycle/release-store.d.ts +25 -2
  88. package/dist/lifecycle/release-store.d.ts.map +1 -1
  89. package/dist/lifecycle/release-store.js +40 -13
  90. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  91. package/dist/lifecycle/run-ledger.js +1 -0
  92. package/dist/lint/rules/op/index.d.ts +2 -1
  93. package/dist/lint/rules/op/index.d.ts.map +1 -1
  94. package/dist/lint/rules/op/index.js +4 -2
  95. package/dist/lint/rules/op/ops018-shell-image-digest.d.ts +10 -0
  96. package/dist/lint/rules/op/ops018-shell-image-digest.d.ts.map +1 -0
  97. package/dist/lint/rules/op/ops018-shell-image-digest.js +38 -0
  98. package/dist/op/activities/activity-contracts.d.ts +5 -0
  99. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  100. package/dist/op/activities/activity-contracts.js +3 -0
  101. package/dist/op/activities/shell.d.ts +38 -0
  102. package/dist/op/activities/shell.d.ts.map +1 -1
  103. package/dist/op/activities/shell.js +89 -3
  104. package/dist/op/builders.d.ts +7 -0
  105. package/dist/op/builders.d.ts.map +1 -1
  106. package/dist/op/builders.js +7 -0
  107. package/dist/op/runtime.d.ts +2 -0
  108. package/dist/op/runtime.d.ts.map +1 -1
  109. package/dist/pr-loop.d.ts +14 -3
  110. package/dist/pr-loop.d.ts.map +1 -1
  111. package/dist/pr-loop.js +15 -5
  112. package/dist/pr-report-url.d.ts +2 -2
  113. package/dist/pr-report-url.d.ts.map +1 -1
  114. package/dist/runner-start.d.ts +179 -0
  115. package/dist/runner-start.d.ts.map +1 -0
  116. package/dist/runner-start.js +277 -0
  117. package/dist/wave-runtime.mjs +236 -229
  118. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
  119. package/dist/workspace/__fixtures__/contract-repo.js +13 -2
  120. package/dist/workspace/check.schema.json +88 -2
  121. package/dist/workspace/checks/ci-workflow.d.ts +17 -0
  122. package/dist/workspace/checks/ci-workflow.d.ts.map +1 -0
  123. package/dist/workspace/checks/ci-workflow.js +123 -0
  124. package/dist/workspace/checks/security.d.ts +50 -0
  125. package/dist/workspace/checks/security.d.ts.map +1 -0
  126. package/dist/workspace/checks/security.js +305 -0
  127. package/dist/workspace/checks.d.ts +2 -0
  128. package/dist/workspace/checks.d.ts.map +1 -1
  129. package/dist/workspace/checks.js +8 -0
  130. package/dist/workspace/ci-builds.d.ts +163 -0
  131. package/dist/workspace/ci-builds.d.ts.map +1 -0
  132. package/dist/workspace/ci-builds.js +315 -0
  133. package/dist/workspace/ci-green-cli.d.ts +2 -1
  134. package/dist/workspace/ci-green-cli.d.ts.map +1 -1
  135. package/dist/workspace/ci-green-cli.js +25 -5
  136. package/dist/workspace/ci-green-forge.d.ts +12 -0
  137. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  138. package/dist/workspace/ci-green-forge.js +12 -0
  139. package/dist/workspace/ci-green-workflow.d.ts +55 -1
  140. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  141. package/dist/workspace/ci-green-workflow.js +128 -1
  142. package/dist/workspace/ci-green.d.ts +89 -4
  143. package/dist/workspace/ci-green.d.ts.map +1 -1
  144. package/dist/workspace/ci-green.js +310 -23
  145. package/dist/workspace/ci-init.d.ts +100 -0
  146. package/dist/workspace/ci-init.d.ts.map +1 -0
  147. package/dist/workspace/ci-init.js +434 -0
  148. package/dist/workspace/ci-last-green.schema.json +49 -0
  149. package/dist/workspace/ci-list.schema.json +35 -1
  150. package/dist/workspace/ci-run-index.d.ts +1 -1
  151. package/dist/workspace/ci-run-index.d.ts.map +1 -1
  152. package/dist/workspace/ci-run-index.schema.json +1 -1
  153. package/dist/workspace/ci-run.d.ts +12 -6
  154. package/dist/workspace/ci-run.d.ts.map +1 -1
  155. package/dist/workspace/ci-run.js +72 -16
  156. package/dist/workspace/ci-runner-start.d.ts +77 -0
  157. package/dist/workspace/ci-runner-start.d.ts.map +1 -0
  158. package/dist/workspace/ci-runner-start.js +185 -0
  159. package/dist/workspace/ci-serve.d.ts +76 -7
  160. package/dist/workspace/ci-serve.d.ts.map +1 -1
  161. package/dist/workspace/ci-serve.js +153 -19
  162. package/dist/workspace/ci-workflow.d.ts +91 -0
  163. package/dist/workspace/ci-workflow.d.ts.map +1 -0
  164. package/dist/workspace/ci-workflow.js +33 -0
  165. package/dist/workspace/declaration.d.ts +35 -0
  166. package/dist/workspace/declaration.d.ts.map +1 -1
  167. package/dist/workspace/declaration.js +12 -2
  168. package/dist/workspace/declaration.schema.json +51 -0
  169. package/dist/workspace/init.js +2 -2
  170. package/dist/workspace/ls.d.ts +2 -0
  171. package/dist/workspace/ls.d.ts.map +1 -1
  172. package/dist/workspace/ls.js +1 -1
  173. package/dist/workspace/ls.schema.json +2 -1
  174. package/dist/workspace/member-pipeline.d.ts +1 -0
  175. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  176. package/dist/workspace/member-pipeline.js +2 -0
  177. package/dist/workspace/merge-record.schema.json +49 -0
  178. package/dist/workspace/pr-report.schema.json +3 -3
  179. package/dist/workspace/reason-codes.d.ts +26 -0
  180. package/dist/workspace/reason-codes.d.ts.map +1 -1
  181. package/dist/workspace/reason-codes.js +28 -0
  182. package/dist/workspace/release-candidates.d.ts +129 -0
  183. package/dist/workspace/release-candidates.d.ts.map +1 -0
  184. package/dist/workspace/release-candidates.js +233 -0
  185. package/dist/workspace/release-candidates.schema.json +353 -0
  186. package/dist/workspace/release-workflow.d.ts +31 -0
  187. package/dist/workspace/release-workflow.d.ts.map +1 -0
  188. package/dist/workspace/release-workflow.js +150 -0
  189. package/dist/workspace/security.d.ts +40 -0
  190. package/dist/workspace/security.d.ts.map +1 -0
  191. package/dist/workspace/security.js +99 -0
  192. package/package.json +8 -1
  193. package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +1 -1
  194. package/src/ci-jobs.ts +11 -2
  195. package/src/cli/handlers/build.test.ts +36 -0
  196. package/src/cli/handlers/build.ts +18 -3
  197. package/src/cli/handlers/lifecycle-health.test.ts +123 -0
  198. package/src/cli/handlers/lifecycle-health.ts +157 -0
  199. package/src/cli/handlers/lifecycle.ts +1 -1
  200. package/src/cli/handlers/pr-deploy-inputs.test.ts +209 -0
  201. package/src/cli/handlers/pr-record.test.ts +201 -0
  202. package/src/cli/handlers/pr.test.ts +22 -0
  203. package/src/cli/handlers/pr.ts +127 -4
  204. package/src/cli/handlers/promote.test.ts +17 -0
  205. package/src/cli/handlers/promote.ts +296 -84
  206. package/src/cli/handlers/release-cut-at.e2e.test.ts +115 -0
  207. package/src/cli/handlers/release-publish-targets.e2e.test.ts +2 -2
  208. package/src/cli/handlers/release-verify.e2e.test.ts +12 -10
  209. package/src/cli/handlers/release.e2e.test.ts +19 -28
  210. package/src/cli/handlers/release.ts +81 -11
  211. package/src/cli/main.ts +99 -22
  212. package/src/cli/parse-args.ts +22 -1
  213. package/src/cli/registry.ts +21 -5
  214. package/src/components/deploy-inputs.test.ts +108 -0
  215. package/src/components/deploy-inputs.ts +225 -0
  216. package/src/components/driver.ts +11 -0
  217. package/src/components/gate-plan.ts +9 -0
  218. package/src/components/pr-pipeline.ts +56 -15
  219. package/src/components/pr-run.ts +1 -1
  220. package/src/components/promote-rounds.test.ts +220 -0
  221. package/src/components/promote-rounds.ts +232 -0
  222. package/src/components/release-compile.test.ts +34 -0
  223. package/src/components/release-compile.ts +89 -8
  224. package/src/components/release-gate.test.ts +47 -0
  225. package/src/components/release-gate.ts +111 -46
  226. package/src/components/release-promote.test.ts +55 -2
  227. package/src/components/release-promote.ts +93 -23
  228. package/src/components/verbs/bom-writer.ts +1 -1
  229. package/src/components/verbs/lockfile-sbom-generator.test.ts +36 -0
  230. package/src/components/verbs/lockfile-sbom-generator.ts +43 -5
  231. package/src/components/wave-plan.ts +32 -4
  232. package/src/graph-declared-exports.test.ts +40 -0
  233. package/src/graph-declared.ts +9 -1
  234. package/src/graph-detail.test.ts +57 -0
  235. package/src/graph-detail.ts +33 -7
  236. package/src/health.schema.json +191 -0
  237. package/src/health.test.ts +62 -0
  238. package/src/health.ts +178 -0
  239. package/src/lexicon.ts +41 -1
  240. package/src/lifecycle/merge-record.test.ts +111 -0
  241. package/src/lifecycle/merge-record.ts +154 -0
  242. package/src/lifecycle/publish-attachments.ts +25 -13
  243. package/src/lifecycle/publish-npm.ts +4 -3
  244. package/src/lifecycle/publish-targets.test.ts +34 -4
  245. package/src/lifecycle/release-at.test.ts +48 -0
  246. package/src/lifecycle/release-at.ts +100 -0
  247. package/src/lifecycle/release-cut.test.ts +41 -0
  248. package/src/lifecycle/release-cut.ts +22 -11
  249. package/src/lifecycle/release-publish.test.ts +43 -13
  250. package/src/lifecycle/release-publish.ts +38 -18
  251. package/src/lifecycle/release-store.ts +57 -13
  252. package/src/lifecycle/run-ledger.test.ts +13 -0
  253. package/src/lifecycle/run-ledger.ts +1 -0
  254. package/src/lint/rules/op/index.ts +4 -2
  255. package/src/lint/rules/op/ops018-shell-image-digest.test.ts +60 -0
  256. package/src/lint/rules/op/ops018-shell-image-digest.ts +48 -0
  257. package/src/op/activities/activity-contracts.ts +3 -0
  258. package/src/op/activities/shell-container.e2e.test.ts +67 -0
  259. package/src/op/activities/shell.test.ts +44 -1
  260. package/src/op/activities/shell.ts +110 -3
  261. package/src/op/builders.ts +7 -0
  262. package/src/op/runtime.ts +2 -0
  263. package/src/op/step-console.test.ts +4 -2
  264. package/src/pr-loop.test.ts +29 -0
  265. package/src/pr-loop.ts +26 -7
  266. package/src/pr-report-url.ts +2 -2
  267. package/src/runner-start-bundle.test.ts +29 -0
  268. package/src/runner-start.test.ts +179 -0
  269. package/src/runner-start.ts +329 -0
  270. package/src/workspace/__fixtures__/contract-repo.ts +11 -2
  271. package/src/workspace/check-contract.test.ts +4 -2
  272. package/src/workspace/check.schema.json +88 -2
  273. package/src/workspace/checks/ci-workflow.test.ts +150 -0
  274. package/src/workspace/checks/ci-workflow.ts +129 -0
  275. package/src/workspace/checks/security.test.ts +138 -0
  276. package/src/workspace/checks/security.ts +305 -0
  277. package/src/workspace/checks.ts +8 -0
  278. package/src/workspace/ci-builds.e2e.test.ts +208 -0
  279. package/src/workspace/ci-builds.test.ts +174 -0
  280. package/src/workspace/ci-builds.ts +385 -0
  281. package/src/workspace/ci-green-builds.test.ts +180 -0
  282. package/src/workspace/ci-green-cli.test.ts +54 -0
  283. package/src/workspace/ci-green-cli.ts +26 -5
  284. package/src/workspace/ci-green-forge.ts +24 -0
  285. package/src/workspace/ci-green-workflow.test.ts +84 -1
  286. package/src/workspace/ci-green-workflow.ts +139 -2
  287. package/src/workspace/ci-green.test.ts +5 -2
  288. package/src/workspace/ci-green.ts +359 -27
  289. package/src/workspace/ci-init.test.ts +159 -0
  290. package/src/workspace/ci-init.ts +445 -0
  291. package/src/workspace/ci-last-green.schema.json +49 -0
  292. package/src/workspace/ci-list.schema.json +35 -1
  293. package/src/workspace/ci-report-url.test.ts +6 -1
  294. package/src/workspace/ci-run-index.schema.json +1 -1
  295. package/src/workspace/ci-run-index.ts +1 -1
  296. package/src/workspace/ci-run.test.ts +24 -7
  297. package/src/workspace/ci-run.ts +78 -16
  298. package/src/workspace/ci-runner-start.test.ts +151 -0
  299. package/src/workspace/ci-runner-start.ts +193 -0
  300. package/src/workspace/ci-serve-builds.test.ts +86 -0
  301. package/src/workspace/ci-serve.test.ts +59 -0
  302. package/src/workspace/ci-serve.ts +177 -22
  303. package/src/workspace/ci-workflow.ts +71 -0
  304. package/src/workspace/declaration.schema.json +51 -0
  305. package/src/workspace/declaration.test.ts +17 -0
  306. package/src/workspace/declaration.ts +53 -3
  307. package/src/workspace/init.ts +2 -2
  308. package/src/workspace/ls.schema.json +2 -1
  309. package/src/workspace/ls.ts +3 -2
  310. package/src/workspace/member-pipeline.ts +2 -1
  311. package/src/workspace/merge-record.schema.json +49 -0
  312. package/src/workspace/pr-report.schema.json +3 -3
  313. package/src/workspace/reason-codes.test.ts +4 -0
  314. package/src/workspace/reason-codes.ts +28 -0
  315. package/src/workspace/release-candidates.schema.json +353 -0
  316. package/src/workspace/release-candidates.test.ts +106 -0
  317. package/src/workspace/release-candidates.ts +315 -0
  318. package/src/workspace/release-workflow.ts +167 -0
  319. package/src/workspace/security.test.ts +70 -0
  320. package/src/workspace/security.ts +118 -0
@@ -311,8 +311,10 @@ export interface ParsedArgs {
311
311
  /**
312
312
  * `chant components pr-apply --require-review` (#3183) — count an approval
313
313
  * only when its approver has a standing approving review on the pull
314
- * request. Needs `--forge`. `chant ci run apply --no-require-review`
315
- * (#4156) sets it false, for a pipeline generated with `requireReview: false`.
314
+ * request. Needs `--forge`. `chant ci run apply --require-review`
315
+ * passes it on, and `chant build --pr-loop --require-review` generates a
316
+ * pipeline that does (#4168); `--no-require-review` (#4156) sets it false,
317
+ * which is the default.
316
318
  */
317
319
  requireReview?: boolean;
318
320
  /**
@@ -323,12 +325,20 @@ export interface ParsedArgs {
323
325
  reportUrl?: string;
324
326
  /**
325
327
  * `chant build --components --generate <lexicon> --pr-loop` (#3183) —
326
- * generate the pull-request pipeline: plan on the pull request, apply on
327
- * merge bound to the approved digest.
328
+ * generate the pull-request pipeline: plan on the pull request, and on
329
+ * merge record the plan (#4168), or apply it with `--apply-on-merge`.
328
330
  */
329
331
  prLoop?: boolean;
332
+ /**
333
+ * `chant build --pr-loop --apply-on-merge` and `chant ci serve
334
+ * --apply-on-merge` (#4168): on merge apply the approved plan, as before
335
+ * #4168, in place of recording it. Opt-in: a merge deploys nothing without it.
336
+ */
337
+ applyOnMerge?: boolean;
330
338
  /** `chant build --pr-loop --pr-build` (#4088) — build and lint before pr-plan and pr-apply. */
331
339
  prBuild?: boolean;
340
+ /** `chant ci serve --build-command <line>` (#4169) — the line that builds a commit a `ci/build/<sha>` tag asks for, in a worktree of it. */
341
+ ciBuildCommand?: string;
332
342
  /** `chant build --pr-loop --pr-build-command <line>` (#4088) — the build line to run in place of `chant build --env <env>`. */
333
343
  prBuildCommand?: string;
334
344
  /**
@@ -457,8 +467,10 @@ export interface ParsedArgs {
457
467
  label?: string;
458
468
  /** `chant workspace wip --branch <branch>` (#3172): list one branch's snapshots. */
459
469
  branch?: string;
460
- /** `chant ci workflow --workflow <name>`, repeatable (#3573): a workflow whose completion triggers a tick, beside those the scan finds. */
470
+ /** `chant ci workflow --workflow <name>`, repeatable (#3573): a workflow whose completion triggers a tick, beside those the scan finds. Also `chant ci init --workflow <name>` (#4168 step 6), the prescribed workflow to set up. */
461
471
  ciWorkflows?: string[];
472
+ /** `chant ci init --security <group>`, repeatable (#4168 step 6): a security group to turn on. */
473
+ securityGroups?: string[];
462
474
  /** `chant ci run plan|apply --commit <sha>` (#4094): the commit the job runs at. Default `HEAD`. */
463
475
  commit?: string;
464
476
  /** `chant ci run --pass-env <NAME>`, repeatable (#4094): a variable of the runner's environment the job gets, beside the few it always gets. */
@@ -511,6 +523,10 @@ export interface ParsedArgs {
511
523
  unsigned?: boolean;
512
524
  /** `chant ci run --in-place` (#4156): run the job in this checkout at HEAD, as a generated forge job does. */
513
525
  ciInPlace?: boolean;
526
+ /** `chant ci run --require-runner-start` (#4147): refuse the job unless a runner-start statement names it. */
527
+ requireRunnerStart?: boolean;
528
+ /** `chant ci runner-start sign --job <json|@file>` (#4147): the CI job a runner was started for. */
529
+ ciJob?: string;
514
530
  /** `chant ci run --no-push` (#4156): write the run record on the local chant/lifecycle and push nothing. */
515
531
  noPush?: boolean;
516
532
  /** `chant ci serve|run --setup-key --trust --principal <name>` (#4099): the principal the runner key is listed for. */
@@ -0,0 +1,108 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
6
+ import { componentDeployInputs, directoryDigest, directoryFiles, stepInputRefs } from "./deploy-inputs";
7
+ import { componentPlanDigest } from "./gate-plan";
8
+ import type { DriverComponent, DriverPhase } from "./driver";
9
+
10
+ let root: string;
11
+
12
+ const write = (path: string, content: string): void => {
13
+ mkdirSync(dirname(join(root, path)), { recursive: true });
14
+ writeFileSync(join(root, path), content);
15
+ };
16
+ const deploy = (...steps: Array<Record<string, unknown>>): DriverPhase[] => [{ phase: "Deploy", steps: steps as never }];
17
+
18
+ beforeEach(() => {
19
+ root = mkdtempSync(join(tmpdir(), "deploy-inputs-"));
20
+ });
21
+
22
+ afterEach(() => {
23
+ rmSync(root, { recursive: true, force: true });
24
+ });
25
+
26
+ describe("stepInputRefs", () => {
27
+ test("a kind's own fields and the template fields, literal paths only", () => {
28
+ const refs = stepInputRefs("Deploy", { kind: "helm-upgrade", release: "web", chart: "chart", values: ["a.yaml", "@Cfg.values"] } as never);
29
+ expect(refs.map((r) => [r.field, r.path])).toEqual([["values", "a.yaml"], ["chart", "chart"]]);
30
+ });
31
+
32
+ test("an archive path or a wired value is a build output and is not named", () => {
33
+ expect(stepInputRefs("Deploy", { kind: "cfn-deploy", template: "archive:stack.json" } as never)).toEqual([]);
34
+ expect(stepInputRefs("Deploy", { kind: "s3-sync", from: "@Build.dir", to: "s3://b" } as never)).toEqual([]);
35
+ });
36
+
37
+ test("a Docker build's Dockerfile is named under its context", () => {
38
+ const refs = stepInputRefs("Build", { kind: "docker-build", context: "app", dockerfile: "Dockerfile.prod" } as never);
39
+ expect(refs.map((r) => r.path)).toEqual(["app", "app/Dockerfile.prod"]);
40
+ });
41
+ });
42
+
43
+ describe("componentDeployInputs", () => {
44
+ test("a file by its bytes, a directory by its files; a missing path is left out", () => {
45
+ write("values.yaml", "a: 1\n");
46
+ write("chart/Chart.yaml", "name: c\n");
47
+ const read = componentDeployInputs(deploy({ kind: "helm-upgrade", chart: "chart", values: ["values.yaml", "gone.yaml"] }), root);
48
+ expect(read.inputs.map((i) => i.path)).toEqual(["chart", "values.yaml"]);
49
+ expect(read.inputs.find((i) => i.path === "values.yaml")?.digest).toMatch(/^sha256:[0-9a-f]{64}$/);
50
+ expect(read.unbound).toEqual([]);
51
+ });
52
+
53
+ test("a directory's digest moves with a file's bytes, a new file and a renamed one", () => {
54
+ write("site/index.html", "<h1>a</h1>");
55
+ const before = directoryDigest(root, "site");
56
+ write("site/index.html", "<h1>b</h1>");
57
+ const edited = directoryDigest(root, "site");
58
+ expect(edited).not.toBe(before);
59
+ write("site/about.html", "x");
60
+ expect(directoryDigest(root, "site")).not.toBe(edited);
61
+ });
62
+
63
+ test("the same files under another parent hash the same: the digest reads paths from the directory", () => {
64
+ write("a/chart/Chart.yaml", "name: c\n");
65
+ write("b/chart/Chart.yaml", "name: c\n");
66
+ expect(directoryDigest(root, "a/chart")).toBe(directoryDigest(root, "b/chart"));
67
+ });
68
+
69
+ test("in a git repository, a directory leaves out what git ignores, and a directory git ignores is a build output", () => {
70
+ execFileSync("git", ["init", "-q"], { cwd: root });
71
+ write(".gitignore", "node_modules/\ndist/\n");
72
+ write("app/server.js", "1");
73
+ write("app/node_modules/dep/index.js", "1");
74
+ write("dist/index.html", "1");
75
+ expect(directoryFiles(root, "app")).toEqual(["app/server.js"]);
76
+ const read = componentDeployInputs(deploy({ kind: "docker-build", context: "app" }, { kind: "s3-sync", from: "dist", to: "s3://b" }), root);
77
+ expect(read.inputs.map((i) => i.path)).toEqual(["app"]);
78
+ expect(read.unbound).toEqual([{ phase: "Deploy", kind: "s3-sync", reason: expect.stringMatching(/"dist" is a directory git ignores, so a build output/) }]);
79
+ });
80
+
81
+ test("a file git ignores is still hashed when a step names it, as a template chant build writes", () => {
82
+ execFileSync("git", ["init", "-q"], { cwd: root });
83
+ write(".gitignore", "dist/\n");
84
+ write("dist/stack.json", "{}");
85
+ expect(componentDeployInputs(deploy({ kind: "cfn-deploy", template: "dist/stack.json" }), root).inputs.map((i) => i.path)).toEqual(["dist/stack.json"]);
86
+ });
87
+
88
+ test("a shell step is unbound; `include` limits the steps read", () => {
89
+ write("chart/Chart.yaml", "x");
90
+ const phases = deploy({ kind: "shell", cmd: "./go.sh", reason: "t" }, { kind: "helm-upgrade", chart: "chart" });
91
+ expect(componentDeployInputs(phases, root).unbound.map((u) => u.kind)).toEqual(["shell"]);
92
+ expect(componentDeployInputs(phases, root, (s) => s.kind !== "helm-upgrade").inputs).toEqual([]);
93
+ });
94
+ });
95
+
96
+ describe("componentPlanDigest with inputs", () => {
97
+ const component: DriverComponent = { name: "web", dependsOn: [], deploy: deploy({ kind: "helm-upgrade", chart: "chart" }) };
98
+
99
+ test("no inputs keeps the digest a composition had before #4155", () => {
100
+ expect(componentPlanDigest({ environment: "prod", component, inputs: [] })).toBe(componentPlanDigest({ environment: "prod", component }));
101
+ });
102
+
103
+ test("an input's digest moves the plan digest", () => {
104
+ const a = componentPlanDigest({ environment: "prod", component, inputs: [{ path: "chart", digest: "sha256:aa" }] });
105
+ const b = componentPlanDigest({ environment: "prod", component, inputs: [{ path: "chart", digest: "sha256:bb" }] });
106
+ expect(a).not.toBe(b);
107
+ });
108
+ });
@@ -0,0 +1,225 @@
1
+ /**
2
+ * The local files a component's steps deploy from, and their content digests
3
+ * (#4155).
4
+ *
5
+ * A component gate approves `componentPlanDigest` (./gate-plan.ts). A step
6
+ * whose capability has a `plan` is bound by what it plans. Every other step
7
+ * was bound only by its input as written, so a Helm chart, a kustomize
8
+ * directory or a static site's tree could change after review without moving
9
+ * the digest. This module names those files and hashes them, so the digest
10
+ * moves when they do.
11
+ *
12
+ * Which fields name a path is data, as the template fields are in
13
+ * ./release-compile.ts and the unit fields in ./deploy-units.ts: a kind's
14
+ * fields in {@link DEPLOY_INPUT_FIELDS}, plus `template`, `manifest` and
15
+ * `values` on any step ({@link TEMPLATE_FIELDS}). Only a literal path counts:
16
+ * a value wired from another step (`@Build.uri`) or an `archive:` path is a
17
+ * build output, and binding it would make an approval unusable by a build
18
+ * that is not reproducible. The build steps' own sources are bound instead.
19
+ *
20
+ * - A file is hashed by its bytes.
21
+ * - A directory is hashed over the files under it that git does not ignore
22
+ * (`git ls-files --cached --others --exclude-standard`), each by its path
23
+ * and bytes, so `node_modules` and a build's `dist` inside a Docker context
24
+ * are left out. Outside a git repository every file is hashed except under
25
+ * `.git` and `node_modules`.
26
+ * - A directory git ignores as a whole is a build output. It is not hashed;
27
+ * the step is listed as unbound, with that reason.
28
+ * - A path that does not exist is left out. One that appears later moves the
29
+ * digest.
30
+ *
31
+ * A step whose kind runs content chant can't name, such as `shell`, is listed
32
+ * as unbound ({@link UNBOUND_KINDS}), so the plan note can show a reviewer
33
+ * what the approval does not cover.
34
+ */
35
+
36
+ import { spawnSync } from "node:child_process";
37
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
38
+ import { join, relative, resolve, sep } from "node:path";
39
+ import { sha256Hex } from "../content-digest";
40
+ import { computePlanDigest } from "../lifecycle/plan-digest";
41
+ import type { DriverGate, DriverPhase, DriverStep } from "./driver";
42
+
43
+ /** The fields that name every step's deployed file, whatever its kind (as ./release-compile.ts reads them). */
44
+ export const TEMPLATE_FIELDS: readonly string[] = ["template", "manifest", "values"];
45
+
46
+ /** Per kind, the fields beyond {@link TEMPLATE_FIELDS} that name a local file or directory the step deploys or builds from. */
47
+ export const DEPLOY_INPUT_FIELDS: Readonly<Record<string, readonly string[]>> = {
48
+ "helm-upgrade": ["chart"],
49
+ "kustomize-apply": ["dir"],
50
+ "fly-release": ["plan"],
51
+ "fly-rollback": ["plan"],
52
+ "s3-sync": ["from"],
53
+ "r2-sync": ["from"],
54
+ "copy-to-host": ["from"],
55
+ "wrangler-deploy": ["config"],
56
+ "wrangler-versions-promote": ["config"],
57
+ "docker-build": ["context"],
58
+ "zip-package": ["source"],
59
+ "jvm-build": ["path"],
60
+ };
61
+
62
+ /** Kinds that deploy content chant can't name from their input: a command, an agent's work. */
63
+ export const UNBOUND_KINDS: readonly string[] = ["shell", "run-agent"];
64
+
65
+ /** One path a step names. */
66
+ export interface DeployInputRef {
67
+ phase: string;
68
+ kind: string;
69
+ field: string;
70
+ /** Relative to the member's directory, as written. */
71
+ path: string;
72
+ }
73
+
74
+ /** A named path and the digest of what is there. */
75
+ export interface DeployInputDigest {
76
+ path: string;
77
+ /** `sha256:<hex>` of a file's bytes, or a plan digest over a directory's files. */
78
+ digest: string;
79
+ }
80
+
81
+ /** A step the digest does not cover, and why. */
82
+ export interface UnboundStep {
83
+ phase: string;
84
+ kind: string;
85
+ reason: string;
86
+ }
87
+
88
+ /** What a component's steps deploy from: the digests, and the steps they can't cover. */
89
+ export interface ComponentDeployInputs {
90
+ inputs: DeployInputDigest[];
91
+ unbound: UnboundStep[];
92
+ }
93
+
94
+ function isGate(step: DriverStep | DriverGate | DriverPhase): step is DriverGate {
95
+ return (step as { kind?: unknown }).kind === "gate";
96
+ }
97
+
98
+ function isPhase(step: DriverStep | DriverGate | DriverPhase): step is DriverPhase {
99
+ return typeof (step as { phase?: unknown }).phase === "string" && Array.isArray((step as DriverPhase).steps);
100
+ }
101
+
102
+ const isLiteralPath = (v: unknown): v is string =>
103
+ typeof v === "string" && v !== "" && !v.startsWith("@") && !v.startsWith("$") && !v.startsWith("archive:");
104
+
105
+ /** Every capability step of a composition with its phase, nested phases included, gates left out. */
106
+ export function stepsWithPhase(phases: readonly DriverPhase[]): Array<{ phase: string; step: DriverStep }> {
107
+ const out: Array<{ phase: string; step: DriverStep }> = [];
108
+ const walk = (phase: DriverPhase): void => {
109
+ for (const step of phase.steps) {
110
+ if (isPhase(step)) walk(step);
111
+ else if (!isGate(step)) out.push({ phase: phase.phase, step });
112
+ }
113
+ };
114
+ phases.forEach(walk);
115
+ return out;
116
+ }
117
+
118
+ /** The literal paths one step names, in field order. A Docker build's Dockerfile is named under its context. */
119
+ export function stepInputRefs(phase: string, step: DriverStep): DeployInputRef[] {
120
+ const out: DeployInputRef[] = [];
121
+ for (const field of [...TEMPLATE_FIELDS, ...(DEPLOY_INPUT_FIELDS[step.kind] ?? [])]) {
122
+ const raw = step[field];
123
+ for (const value of Array.isArray(raw) ? raw : [raw]) {
124
+ if (isLiteralPath(value)) out.push({ phase, kind: step.kind, field, path: value });
125
+ }
126
+ }
127
+ if (step.kind === "docker-build" && isLiteralPath(step.context) && isLiteralPath(step.dockerfile)) {
128
+ out.push({ phase, kind: step.kind, field: "dockerfile", path: join(step.context, step.dockerfile) });
129
+ }
130
+ return out;
131
+ }
132
+
133
+ function git(root: string, args: string[]): { ok: boolean; out: string } {
134
+ const r = spawnSync("git", ["-C", root, ...args], { encoding: "utf-8", maxBuffer: 256 * 1024 * 1024 });
135
+ return { ok: r.status === 0, out: r.stdout ?? "" };
136
+ }
137
+
138
+ /** The files under `dir` (relative to `root`) a directory digest covers, relative to `root`, sorted. */
139
+ export function directoryFiles(root: string, dir: string): string[] {
140
+ const listed = git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", dir]);
141
+ if (listed.ok) {
142
+ return [...new Set(listed.out.split("\0").filter((p) => p !== ""))]
143
+ .filter((p) => {
144
+ const full = resolve(root, p);
145
+ return existsSync(full) && statSync(full).isFile();
146
+ })
147
+ .sort();
148
+ }
149
+ return walkFiles(root, dir);
150
+ }
151
+
152
+ /** Every file under `dir`, relative to `root`, sorted, leaving out `.git` and `node_modules`. What a cut records of a directory git ignores. */
153
+ export function walkFiles(root: string, dir: string): string[] {
154
+ const out: string[] = [];
155
+ const walk = (abs: string): void => {
156
+ for (const entry of readdirSync(abs, { withFileTypes: true })) {
157
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
158
+ const full = join(abs, entry.name);
159
+ if (entry.isDirectory()) walk(full);
160
+ else if (entry.isFile()) out.push(relative(root, full).split(sep).join("/"));
161
+ }
162
+ };
163
+ walk(resolve(root, dir));
164
+ return out.sort();
165
+ }
166
+
167
+ /** Whether git ignores `path` (relative to `root`). False outside a git repository. */
168
+ export function ignoredByGit(root: string, path: string): boolean {
169
+ return git(root, ["check-ignore", "-q", "--", path]).ok;
170
+ }
171
+
172
+ /** `sha256:<hex>` of a file's bytes. */
173
+ export function fileDigest(file: string): string {
174
+ return `sha256:${sha256Hex(readFileSync(file))}`;
175
+ }
176
+
177
+ /** The digest of the files under `dir`: a plan digest over each file's path (from `dir`) and bytes. */
178
+ export function directoryDigest(root: string, dir: string): string {
179
+ const base = resolve(root, dir);
180
+ const files = directoryFiles(root, dir).map((p) => ({
181
+ path: relative(base, resolve(root, p)).split(sep).join("/"),
182
+ digest: fileDigest(resolve(root, p)),
183
+ }));
184
+ return computePlanDigest("directory", files);
185
+ }
186
+
187
+ /**
188
+ * What a component's steps deploy from, read under `root` (the member's
189
+ * directory). `include` picks the steps to read, all of them by default; the
190
+ * wave planner passes the steps without a `plan`, which their plan does not
191
+ * already cover.
192
+ */
193
+ export function componentDeployInputs(
194
+ phases: readonly DriverPhase[],
195
+ root: string,
196
+ include: (step: DriverStep) => boolean = () => true,
197
+ ): ComponentDeployInputs {
198
+ const inputs = new Map<string, string>();
199
+ const unbound: UnboundStep[] = [];
200
+ for (const { phase, step } of stepsWithPhase(phases)) {
201
+ if (!include(step)) continue;
202
+ if (UNBOUND_KINDS.includes(step.kind)) {
203
+ unbound.push({ phase, kind: step.kind, reason: `a ${step.kind} step runs content chant can't name from its input` });
204
+ continue;
205
+ }
206
+ for (const ref of stepInputRefs(phase, step)) {
207
+ if (inputs.has(ref.path)) continue;
208
+ const full = resolve(root, ref.path);
209
+ if (!existsSync(full)) continue;
210
+ if (statSync(full).isDirectory()) {
211
+ if (ignoredByGit(root, ref.path)) {
212
+ unbound.push({ phase, kind: step.kind, reason: `its ${ref.field} "${ref.path}" is a directory git ignores, so a build output; bind its sources through the step that builds it` });
213
+ continue;
214
+ }
215
+ inputs.set(ref.path, directoryDigest(root, ref.path));
216
+ } else {
217
+ inputs.set(ref.path, fileDigest(full));
218
+ }
219
+ }
220
+ }
221
+ return {
222
+ inputs: [...inputs].map(([path, digest]) => ({ path, digest })).sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
223
+ unbound,
224
+ };
225
+ }
@@ -52,6 +52,7 @@ import { evaluateGate, gitGateLedgerPort, type GateDigestMismatch, type GateLedg
52
52
  import { gateName } from "../op/gate-name";
53
53
  import { GateNotRecordableError } from "../lifecycle/no-record";
54
54
  import { componentPlanDigest } from "./gate-plan";
55
+ import { componentDeployInputs } from "./deploy-inputs";
55
56
  import type { PendingGateRecord } from "../lifecycle/gate-ledger";
56
57
  import type { CapabilityRegistry, DeployContext } from "./capability";
57
58
  import { isPlanMovedError, PLAN_MOVED_EXIT_CODE } from "./capability";
@@ -853,6 +854,13 @@ async function rollbackExecuted(
853
854
 
854
855
  // ── Per-component deploy ─────────────────────────────────────────────────
855
856
 
857
+ /** Whether a component's composition has a gate step anywhere, nested phases and `rollback` included. */
858
+ function hasGateStep(component: DriverComponent): boolean {
859
+ const walk = (steps: DriverPhase["steps"]): boolean =>
860
+ steps.some((step) => isGateStep(step) || (Array.isArray((step as DriverPhase).steps) && walk((step as DriverPhase).steps)));
861
+ return [...component.deploy, ...(component.rollback ?? [])].some((p) => walk(p.steps));
862
+ }
863
+
856
864
  /**
857
865
  * Run one component's `deploy` composition. On terminal failure: unwind every
858
866
  * executed step in reverse via its capability's `rollback` (saga
@@ -900,6 +908,9 @@ export async function runComponentDeploy(
900
908
  component,
901
909
  ...(ctx.vars ? { vars: ctx.vars } : {}),
902
910
  ...(release !== undefined ? { release } : {}),
911
+ // #4155: the files the steps deploy from, read where the steps read
912
+ // them. Only a gate reads the digest, so a run with none skips the hashing.
913
+ ...(hasGateStep(component) ? { inputs: componentDeployInputs(component.deploy, process.cwd()).inputs } : {}),
903
914
  }),
904
915
  };
905
916
  const records: DriverStepRecord[] = [];
@@ -17,6 +17,11 @@
17
17
  * its publish output against a recorded digest that is not in the
18
18
  * composition, so it passes that digest here.
19
19
  * - `vars`, the environment config the caller resolved ahead of the run.
20
+ * - The content digests of the local files the steps deploy from (#4155,
21
+ * ./deploy-inputs.ts): a chart, a manifest, a site's tree, a build step's
22
+ * sources. A file changed after review moves the digest. The key is left
23
+ * out when no step names an existing file, so a composition that deploys
24
+ * none keeps the digest it had before #4155.
20
25
  *
21
26
  * ## What it leaves out
22
27
  *
@@ -28,6 +33,7 @@
28
33
  */
29
34
 
30
35
  import { computePlanDigest } from "../lifecycle/plan-digest";
36
+ import type { DeployInputDigest } from "./deploy-inputs";
31
37
  import type { DriverComponent } from "./driver";
32
38
 
33
39
  /** What a component gate's plan digest is taken over. */
@@ -37,6 +43,8 @@ export interface ComponentGatePlan {
37
43
  vars?: Record<string, unknown>;
38
44
  /** The recorded digest a promote or rollback deploys, when the caller has one. */
39
45
  release?: string;
46
+ /** What the steps deploy from (`componentDeployInputs`), when the caller read it. */
47
+ inputs?: readonly DeployInputDigest[];
40
48
  }
41
49
 
42
50
  /** The plan digest a component gate records and approves. */
@@ -49,5 +57,6 @@ export function componentPlanDigest(plan: ComponentGatePlan): string {
49
57
  rollback: component.rollback ?? null,
50
58
  vars: plan.vars ?? null,
51
59
  release: plan.release ?? null,
60
+ ...(plan.inputs && plan.inputs.length > 0 ? { inputs: plan.inputs } : {}),
52
61
  });
53
62
  }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * The jobs of a generated pull-request pipeline (#3183): a plan job on every
3
- * pull request and an apply job on every push to the target branch.
3
+ * pull request and, on every push to the target branch, a record job, or an
4
+ * apply job when the pipeline opts into apply on merge (#4168).
4
5
  *
5
6
  * Each forge's generator (the github, gitlab and forgejo lexicons) adds its
6
7
  * own triggers, permissions and artifact handling; the commands and the
@@ -9,11 +10,19 @@
9
10
  * - The plan job runs `chant components pr-plan` against the pull request's
10
11
  * target commit. It posts the note and the `chant/plan` status and writes
11
12
  * nothing to `chant/lifecycle`, since it runs the pull request's code.
12
- * - The apply job runs `chant components pr-apply` against the commit the
13
+ * - The record job (#4168 step 1, the default) runs `chant components
14
+ * pr-record` against the commit the push replaced. It plans the same
15
+ * members on the merged commit, stores the change set and writes the merge
16
+ * record, `_merges/<commit>.json` on `chant/lifecycle`. It applies nothing:
17
+ * a release that carries the commit deploys it. It needs write access to
18
+ * `chant/lifecycle`. One record runs at a time per environment.
19
+ * - With `applyOnMerge` the apply job takes the record job's place. It runs
20
+ * `chant components pr-apply` against the commit the
13
21
  * push replaced, finds the pull request that merged the pushed commit
14
22
  * through the forge, and applies only when an approval stands for the
15
- * digest it plans. With `requireReview` (the default) the approver must
16
- * also have approved the pull request on the forge. It needs write access
23
+ * digest it plans. With `requireReview` (off by default since #4168, on
24
+ * under the `review-rules` security group) the approver must also have
25
+ * approved the pull request on the forge. It needs write access
17
26
  * to `chant/lifecycle`, to record the pending fact when the gate stops it.
18
27
  * One apply runs at a time per environment.
19
28
  *
@@ -35,6 +44,7 @@
35
44
  * it too. With `reportUrl` both pass `--report-url`, which copies the report,
36
45
  * the change set and the events where a reader outside CI can fetch them;
37
46
  * the job's own environment brings the credentials an `s3://` URL needs.
47
+ * The record job passes the same flags as the apply.
38
48
  *
39
49
  * With `build` (#4088) both jobs run the project's build and `chant lint`
40
50
  * before their command, and fail on an error. A `cfn-deploy` member's plan
@@ -123,8 +133,16 @@ export interface PrLoopPipelineOptions {
123
133
  * GitHub and Forgejo; on GitLab, the project's default branch.
124
134
  */
125
135
  branch?: string;
126
- /** Count an approval only from someone who approved the pull request on the forge. Default true. */
136
+ /** Count an approval only from someone who approved the pull request on the forge. Default false (#4168): review rules are opt-in. */
127
137
  requireReview?: boolean;
138
+ /** The job on merge (apply, or record since #4168) has `chant ci run` refuse to start without a runner-start statement for it (`--require-runner-start`, #4147), as under the `isolated-runners` security group (#4168). Default false. */
139
+ requireRunnerStart?: boolean;
140
+ /**
141
+ * Apply the approved plan on merge (#4168), as every pull-request pipeline
142
+ * did before #4168: the apply job in place of the record job. Default
143
+ * false, so a merge deploys nothing. `waveGate` needs it.
144
+ */
145
+ applyOnMerge?: boolean;
128
146
  /**
129
147
  * Stream each job's run events (#4007): `--events-json` on both commands,
130
148
  * and `CHANT_RUN_EVENTS_DIR` set to {@link PR_LOOP_EVENTS_DIR}. Default false.
@@ -166,8 +184,8 @@ export interface PrLoopPipelineOptions {
166
184
 
167
185
  /** One job's command and the forge values it reads, as environment variables for its step. */
168
186
  export interface PrLoopJob {
169
- /** `plan` or `apply`. */
170
- jobName: "plan" | "apply";
187
+ /** `plan`, `apply` or `record` (#4168). */
188
+ jobName: "plan" | "apply" | "record";
171
189
  /** The command line. Reads `$BASE_SHA` and, for the plan, `$PR_NUMBER`. */
172
190
  command: string;
173
191
  /**
@@ -200,9 +218,10 @@ export function prLoopBuildLines(env: string, options: PrLoopPipelineOptions = {
200
218
  }
201
219
 
202
220
  /**
203
- * The two jobs for `forge`. `member` is the workspace member the pipeline is
204
- * generated for; the commands then run in its directory, so `--output` is
205
- * relative to it.
221
+ * The jobs for `forge`: plan, apply and record, in that order. A generator
222
+ * writes the plan and, on merge, {@link prLoopMergeJob}'s choice of the
223
+ * other two. `member` is the workspace member the pipeline is generated for;
224
+ * the commands then run in its directory, so `--output` is relative to it.
206
225
  */
207
226
  export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelineOptions = {}, member?: string): PrLoopJob[] {
208
227
  const gate = options.gate ?? PR_APPLY_GATE;
@@ -220,13 +239,14 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
220
239
  ...(options.events ? { CHANT_RUN_EVENTS_DIR: PR_LOOP_EVENTS_DIR } : {}),
221
240
  };
222
241
  // #4156: the flags `chant ci run` rebuilds `command` from. It finds the member from the directory it runs in.
223
- const ciRun = (job: "plan" | "apply"): string[] => [
242
+ const ciRun = (job: PrLoopJob["jobName"]): string[] => [
224
243
  "chant", "ci", "run", job, "--in-place", ...(job === "plan" ? ["--no-push"] : []),
225
244
  "--base", '"$BASE_SHA"', ...(job === "plan" ? ["--pr", '"$PR_NUMBER"'] : []),
226
245
  "--env", env, "--gate", gate, "--forge", forge,
227
- ...(options.waveGate ? ["--wave-gate", options.waveGate] : []),
228
- ...(job === "apply" && options.branch ? ["--branch", JSON.stringify(options.branch)] : []),
229
- ...(job === "apply" && options.requireReview === false ? ["--no-require-review"] : []),
246
+ ...(job !== "record" && options.waveGate ? ["--wave-gate", options.waveGate] : []),
247
+ ...(job !== "plan" && options.branch ? ["--branch", JSON.stringify(options.branch)] : []),
248
+ ...(job === "apply" && options.requireReview === true ? ["--require-review"] : []),
249
+ ...(job !== "plan" && options.requireRunnerStart === true ? ["--require-runner-start"] : []),
230
250
  ...(options.events ? ["--events-json"] : []),
231
251
  ...(options.reportUrl ? ["--report-url", JSON.stringify(options.reportUrl)] : []),
232
252
  ];
@@ -248,7 +268,7 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
248
268
  jobName: "apply",
249
269
  command: [
250
270
  "chant", "components", "pr-apply", "--base", '"$BASE_SHA"', ...common,
251
- ...(options.requireReview === false ? [] : ["--require-review"]),
271
+ ...(options.requireReview === true ? ["--require-review"] : []),
252
272
  ...(options.waveGate ? ["--wave-gate", options.waveGate] : []),
253
273
  "--resume", PR_APPLY_RECORD,
254
274
  ].join(" "),
@@ -257,9 +277,22 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
257
277
  ...build,
258
278
  run: { command: ciRun("apply").join(" "), env: runEnv(applyEnv) },
259
279
  },
280
+ {
281
+ jobName: "record",
282
+ command: ["chant", "components", "pr-record", "--base", '"$BASE_SHA"', ...common].join(" "),
283
+ env: applyEnv,
284
+ setup: [github ? githubBaseFallback(options.branch) : gitlabBaseFallback(options.branch)],
285
+ ...build,
286
+ run: { command: ciRun("record").join(" "), env: runEnv(applyEnv) },
287
+ },
260
288
  ];
261
289
  }
262
290
 
291
+ /** The job a push to the target branch runs (#4168): `record`, or `apply` when the pipeline opts into apply on merge. */
292
+ export function prLoopMergeJob(jobs: readonly PrLoopJob[], options: PrLoopPipelineOptions = {}): PrLoopJob {
293
+ return jobs.find((j) => j.jobName === (options.applyOnMerge ? "apply" : "record"))!;
294
+ }
295
+
263
296
  /**
264
297
  * The pull-request options with `--wave-gate` beside `--pr-loop` (#4003):
265
298
  * the apply gets a gate per wave. A canary or a fixed base is refused, since
@@ -268,6 +301,9 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
268
301
  */
269
302
  export function prLoopWithWaves(prLoop: PrLoopPipelineOptions, gatedWaves?: GatedWavePipelineOptions): PrLoopPipelineOptions {
270
303
  if (!gatedWaves) return prLoop;
304
+ if (!prLoop.applyOnMerge) {
305
+ throw new Error("--wave-gate beside --pr-loop gates the apply on merge, so it needs --apply-on-merge (#4168); without it a merge records the plan and a release deploys it");
306
+ }
271
307
  if (gatedWaves.canary?.length) throw new Error("a pull-request pipeline's waves are the change's derivation, so it takes no --canary; drop --canary");
272
308
  if (gatedWaves.base !== undefined) throw new Error("a pull-request pipeline measures each push from the commit before it, so it takes no --base; drop --base");
273
309
  return { ...prLoop, waveGate: gatedWaves.gate };
@@ -310,6 +346,11 @@ export function prApplyGroup(env: string, member?: string): string {
310
346
  return member ? `chant-apply.${member}.${env}` : `chant-apply-${env}`;
311
347
  }
312
348
 
349
+ /** The group that keeps one record job at a time per environment and member (#4168), named as {@link prApplyGroup} names the apply's. */
350
+ export function prRecordGroup(env: string, member?: string): string {
351
+ return member ? `chant-record.${member}.${env}` : `chant-record-${env}`;
352
+ }
353
+
313
354
  /**
314
355
  * The cache key the apply job keeps its attempt record under (#3543), with
315
356
  * `commit` the forge's expression for the pushed commit. A push of another
@@ -737,7 +737,7 @@ export function waveNotCovered(approved: ChangeSetDocument, fresh: ChangeSetDocu
737
737
  }
738
738
 
739
739
  export interface PrReportInput {
740
- stage: "plan" | "apply";
740
+ stage: "plan" | "apply" | "record";
741
741
  pr: number | null;
742
742
  env: string;
743
743
  /** The workspace member whose pipeline runs the stage (#3465). */