@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
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Rounds and per-wave gates at promote (#4168 step 4): a dependent planned
3
+ * against its upstream's planned outputs is planned again once the upstream
4
+ * applied as another value, and `--wave-gate` checks each wave.
5
+ */
6
+
7
+ import { describe, expect, test } from "vitest";
8
+ import { composeChangeSet, type ChangeSetDocument } from "../change-set";
9
+ import type { DriverComponent, DriverRunResult, ReleaseGateBinding } from "./driver";
10
+ import { movedInputs, renameGates, runPromotionInRounds } from "./promote-rounds";
11
+
12
+ const sha = (c: string) => `sha256:${c.repeat(64)}`;
13
+
14
+ const net: DriverComponent = { name: "net", deploy: [{ phase: "Apply", steps: [{ kind: "gate", gate: "prod-release" }, { kind: "tf", name: "net" }] }] };
15
+ const svc: DriverComponent = {
16
+ name: "svc",
17
+ dependsOn: ["net"],
18
+ deploy: [{ phase: "Apply", steps: [{ kind: "gate", gate: "prod-release" }, { kind: "tf", name: "svc", vpc: { stackOutput: { stack: "net", name: "id" } } }] }],
19
+ };
20
+
21
+ /** A change set of one planned member per name, each at the digest given; `writes` adds an update of that member's `vpc`. */
22
+ const doc = (members: Record<string, string>, writes: Record<string, string> = {}): ChangeSetDocument =>
23
+ composeChangeSet(
24
+ Object.entries(members).map(([member, digest]) => ({
25
+ member: { member, lexicon: "terraform", planner: "terraform" as const, status: "planned" as const, planDigest: digest, holes: [] },
26
+ entries: writes[member]
27
+ ? [{ member, lexicon: "terraform", planner: "terraform" as const, address: `${member}.main`, type: "x", name: "main", action: "update" as const, attributes: [{ path: "vpc", after: writes[member] }] }]
28
+ : [],
29
+ })),
30
+ );
31
+
32
+ const approved = doc({ net: sha("1"), svc: sha("2") }, { svc: "vpc-new" });
33
+ const binding: ReleaseGateBinding = { op: "release", planDigest: approved.digest, releaseDigest: sha("r") };
34
+
35
+ /** A run that applies every component and leaves `exposes` as its outputs, recording what it was handed. */
36
+ function runner(exposes: Record<string, Record<string, unknown>>) {
37
+ const calls: Array<{ names: string[]; gates: string[]; binding: string; outputs: Record<string, Record<string, unknown>> }> = [];
38
+ const run = async (components: DriverComponent[], outputs: Record<string, Record<string, unknown>>, b: ReleaseGateBinding): Promise<DriverRunResult> => {
39
+ const gates = components.flatMap((c) => c.deploy.flatMap((p) => p.steps.filter((s) => (s as { kind?: string }).kind === "gate").map((s) => (s as { gate: string }).gate)));
40
+ calls.push({ names: components.map((c) => c.name), gates, binding: b.planDigest, outputs });
41
+ const out = { ...outputs };
42
+ for (const c of components) if (exposes[c.name]) out[c.name] = exposes[c.name];
43
+ return {
44
+ order: components.map((c) => c.name),
45
+ waves: [components.map((c) => c.name)],
46
+ results: components.map((c) => ({ component: c.name, ok: true, status: "ok" as const, records: [] })),
47
+ ok: true,
48
+ status: "ok",
49
+ componentOutputs: out,
50
+ };
51
+ };
52
+ return { run, calls };
53
+ }
54
+
55
+ const bindings: string[][] = [];
56
+ const bind = async (d: ChangeSetDocument, gates: string[]): Promise<ReleaseGateBinding> => {
57
+ bindings.push(gates);
58
+ return { op: "release", planDigest: d.digest, releaseDigest: sha("r") };
59
+ };
60
+
61
+ describe("movedInputs", () => {
62
+ test("names each reference that resolves to another value than its plan read", () => {
63
+ expect(movedInputs(svc, { net: { id: "vpc-new" } }, { net: { id: "vpc-new" } })).toEqual([]);
64
+ expect(movedInputs(svc, { net: { id: "vpc-new" } }, { net: { id: "vpc-other" } })).toEqual(["net.id"]);
65
+ expect(movedInputs(svc, undefined, { net: { id: "x" } })).toEqual([]);
66
+ });
67
+ });
68
+
69
+ describe("renameGates", () => {
70
+ test("renames every gate, nested phases included, and drops the old key", () => {
71
+ const c: DriverComponent = { name: "x", deploy: [{ phase: "A", steps: [{ phase: "B", steps: [{ kind: "gate", signalName: "old" }] }, { kind: "gate", gate: "g" }] }] };
72
+ const renamed = renameGates(c, (g) => `${g}-round-2`);
73
+ expect(JSON.stringify(renamed)).toContain('"gate":"old-round-2"');
74
+ expect(JSON.stringify(renamed)).toContain('"gate":"g-round-2"');
75
+ expect(JSON.stringify(renamed)).not.toContain("signalName");
76
+ });
77
+ });
78
+
79
+ describe("runPromotionInRounds", () => {
80
+ test("an upstream that applies as its plan said leaves the dependent under the approval it has", async () => {
81
+ const { run, calls } = runner({ net: { id: "vpc-new" } });
82
+ const result = await runPromotionInRounds({
83
+ components: [svc, net],
84
+ outputs: {},
85
+ binding,
86
+ approved,
87
+ reads: { svc: { net: { id: "vpc-new" } } },
88
+ run,
89
+ replan: async () => { throw new Error("not planned again"); },
90
+ bind,
91
+ });
92
+ expect(result.run.status).toBe("ok");
93
+ expect(result.stages).toEqual([]);
94
+ expect(calls.map((c) => [c.names, c.binding])).toEqual([[["net"], approved.digest], [["svc"], approved.digest]]);
95
+ expect(calls[1].outputs).toEqual({ net: { id: "vpc-new" } });
96
+ expect(result.completed).toEqual(["net", "svc"]);
97
+ });
98
+
99
+ test("an upstream that applies as another value plans the dependent again; a covered plan applies under the approval", async () => {
100
+ const { run, calls } = runner({ net: { id: "vpc-other" } });
101
+ const result = await runPromotionInRounds({
102
+ components: [net, svc],
103
+ outputs: {},
104
+ binding,
105
+ approved,
106
+ reads: { svc: { net: { id: "vpc-new" } } },
107
+ run,
108
+ replan: async () => doc({ svc: sha("2") }, { svc: "vpc-new" }),
109
+ bind,
110
+ });
111
+ expect(result.stages).toEqual([]);
112
+ expect(calls[1]).toMatchObject({ names: ["svc"], binding: approved.digest, gates: ["prod-release"] });
113
+ });
114
+
115
+ test("a moved plan the approval does not cover waits at <gate>-round-2, bound to the new digest", async () => {
116
+ const { run, calls } = runner({ net: { id: "vpc-other" } });
117
+ const fresh = doc({ svc: sha("9") }, { svc: "vpc-other" });
118
+ const result = await runPromotionInRounds({
119
+ components: [net, svc],
120
+ outputs: {},
121
+ binding,
122
+ approved,
123
+ reads: { svc: { net: { id: "vpc-new" } } },
124
+ run,
125
+ replan: async (only, outputs) => {
126
+ expect([...only]).toEqual(["svc"]);
127
+ expect(outputs).toEqual({ net: { id: "vpc-other" } });
128
+ return fresh;
129
+ },
130
+ bind,
131
+ });
132
+ expect(result.stages).toHaveLength(1);
133
+ expect(result.stages[0]).toMatchObject({ kind: "round", number: 2, components: ["svc"], digest: fresh.digest, status: "applied" });
134
+ expect(result.stages[0].notCovered[0]).toMatch(/svc/);
135
+ expect(calls[1]).toMatchObject({ names: ["svc"], binding: fresh.digest, gates: ["prod-release-round-2"] });
136
+ });
137
+
138
+ test("--wave-gate checks every wave after the first and names one gate per wave", async () => {
139
+ const { run, calls } = runner({ net: { id: "vpc-new" } });
140
+ const fresh = doc({ svc: sha("8") }, { svc: "vpc-other" });
141
+ const result = await runPromotionInRounds({
142
+ components: [net, svc],
143
+ outputs: {},
144
+ binding,
145
+ approved,
146
+ reads: { svc: { net: { id: "vpc-new" } } },
147
+ waveGate: "deploy",
148
+ run,
149
+ replan: async () => fresh,
150
+ bind,
151
+ });
152
+ expect(result.stages[0]).toMatchObject({ kind: "wave", number: 2, digest: fresh.digest });
153
+ expect(calls[1].gates).toEqual(["deploy-wave-2"]);
154
+ });
155
+
156
+ test("a stage whose components declare no gate stops with nothing more deployed", async () => {
157
+ const plain: DriverComponent = { ...svc, deploy: [{ phase: "Apply", steps: [{ kind: "tf", name: "svc", vpc: { stackOutput: { stack: "net", name: "id" } } }] }] };
158
+ const { run, calls } = runner({ net: { id: "vpc-other" } });
159
+ const result = await runPromotionInRounds({
160
+ components: [net, plain],
161
+ outputs: {},
162
+ binding,
163
+ approved,
164
+ reads: { svc: { net: { id: "vpc-new" } } },
165
+ run,
166
+ replan: async () => doc({ svc: sha("9") }, { svc: "vpc-other" }),
167
+ bind,
168
+ });
169
+ expect(result.run.status).toBe("fail");
170
+ expect(result.stages[0]).toMatchObject({ status: "failed" });
171
+ expect(result.stages[0].message).toMatch(/no gate/);
172
+ expect(calls.map((c) => c.names)).toEqual([["net"]]);
173
+ });
174
+
175
+ test("a resume skips what applied and starts from the outputs it kept", async () => {
176
+ const { run, calls } = runner({});
177
+ const result = await runPromotionInRounds({
178
+ components: [net, svc],
179
+ outputs: { net: { id: "vpc-new" } },
180
+ binding,
181
+ approved,
182
+ reads: { svc: { net: { id: "vpc-new" } } },
183
+ completed: ["net"],
184
+ run,
185
+ replan: async () => { throw new Error("not planned again"); },
186
+ bind,
187
+ });
188
+ expect(calls.map((c) => c.names)).toEqual([["svc"]]);
189
+ expect(result.completed).toEqual(["net", "svc"]);
190
+ });
191
+
192
+ test("a gated wave stops the run and leaves what applied", async () => {
193
+ const calls: string[][] = [];
194
+ const result = await runPromotionInRounds({
195
+ components: [net, svc],
196
+ outputs: {},
197
+ binding,
198
+ approved,
199
+ reads: {},
200
+ run: async (components, outputs) => {
201
+ calls.push(components.map((c) => c.name));
202
+ return {
203
+ order: ["net"],
204
+ waves: [["net"]],
205
+ results: [{ component: "net", ok: false, status: "gated", records: [] }],
206
+ ok: false,
207
+ status: "gated",
208
+ gatedComponent: "net",
209
+ componentOutputs: outputs,
210
+ };
211
+ },
212
+ replan: async () => approved,
213
+ bind,
214
+ });
215
+ expect(result.run.status).toBe("gated");
216
+ expect(result.run.gatedComponent).toBe("net");
217
+ expect(calls).toEqual([["net"]]);
218
+ expect(result.completed).toEqual([]);
219
+ });
220
+ });
@@ -0,0 +1,232 @@
1
+ /**
2
+ * A promote under a release that plans again once an upstream applied, and a
3
+ * gate per wave (#4168 step 4). Merging deploys nothing since step 1, so what
4
+ * `pr-apply` did on merge happens here, at promote:
5
+ *
6
+ * - **Rounds** (#3999's rule, `../pr-loop.ts`). The release gate approves a
7
+ * change set in which each dependent was planned against its upstream's
8
+ * planned outputs (`planReleaseSet` in ./release-gate.ts). The promote runs
9
+ * a wave at a time. Before a wave runs, a component whose plan read an
10
+ * upstream output that the run left as another value
11
+ * (`movedWiringReferences`) is planned again against what stands. When the
12
+ * approved change set covers the new plan (`waveNotCovered`, the test a
13
+ * pull request's resume uses), it applies under the approval it has.
14
+ * Otherwise its gates are decided as `<gate>-round-<n>` (`prRoundGate`),
15
+ * bound to the new plan's digest.
16
+ * - **A gate per wave** (`--wave-gate <gate>`, #4157's rule). Every wave
17
+ * after the first is planned again against what the waves before it left,
18
+ * and a wave the approved change set does not cover waits at
19
+ * `<gate>-wave-<k>` (`waveGateName`), one gate for the whole wave, bound to
20
+ * that wave's digest.
21
+ *
22
+ * A stage whose components declare no gate has nowhere to wait, so the
23
+ * promote stops there with nothing more deployed.
24
+ *
25
+ * A promote that stops at a round's or a wave's gate writes a resume record
26
+ * ({@link PromoteResume}): the components that applied and the outputs they
27
+ * left. The same promote run again with it skips what applied and carries on
28
+ * from the wave it stopped at, so an approval of the round or wave it waits on
29
+ * is decided against the same plan.
30
+ */
31
+
32
+ import { movedWiringReferences, type DriverComponent, type DriverGate, type DriverPhase, type DriverRunResult, type DriverStep, type ReleaseGateBinding } from "./driver";
33
+ import { resolveComponentGraph } from "./driver";
34
+ import { waveNotCovered } from "./pr-run";
35
+ import { prRoundGate } from "../pr-loop";
36
+ import { waveGateName } from "../gated-waves";
37
+ import { capabilitySteps } from "./wave-plan";
38
+ import type { ChangeSetDocument } from "../change-set";
39
+ import type { ReleasePlan } from "./release-gate";
40
+
41
+ export const PROMOTE_RESUME_SCHEMA = "chant-promote-resume/v1";
42
+
43
+ /** What a promote that stopped at a round's or a wave's gate leaves for the next attempt. */
44
+ export interface PromoteResume {
45
+ schema: typeof PROMOTE_RESUME_SCHEMA;
46
+ env: string;
47
+ releaseDigest: string;
48
+ /** The change set the release gate approved before anything applied. */
49
+ approvedDigest: string;
50
+ /** Components that applied, in run order. */
51
+ completed: string[];
52
+ /** The outputs standing after them. */
53
+ outputs: Record<string, Record<string, unknown>>;
54
+ /** ISO 8601. */
55
+ at: string;
56
+ }
57
+
58
+ /** One round or wave that needed its own approval. */
59
+ export interface PromoteStage {
60
+ kind: "round" | "wave";
61
+ /** The round number (2 and up) or the wave number (1-based). */
62
+ number: number;
63
+ components: string[];
64
+ /** The plan the stage's gates bind. */
65
+ digest: string;
66
+ /** Why the approved change set did not cover it, one line per member. */
67
+ notCovered: string[];
68
+ status: "applied" | "waiting" | "failed";
69
+ /** With `failed` before anything ran: why. */
70
+ message?: string;
71
+ }
72
+
73
+ export interface PromoteRoundsOptions {
74
+ /** The promoted compositions, in no particular order. */
75
+ components: DriverComponent[];
76
+ /** Outputs standing before the run: the seeds, and on a resume the record's. */
77
+ outputs: Record<string, Record<string, unknown>>;
78
+ /** The release gate the change set was approved under. */
79
+ binding: ReleaseGateBinding;
80
+ /** The approved change set, and what each component's plan read. */
81
+ approved: ChangeSetDocument;
82
+ reads: ReleasePlan["reads"];
83
+ /** Components an earlier attempt applied. Skipped. */
84
+ completed?: readonly string[];
85
+ /** `--wave-gate <gate>`: check every wave after the first, not only the components whose inputs moved, and name a wave's gate `<gate>-wave-<k>`. */
86
+ waveGate?: string;
87
+ /** Run one subset of the components with `outputs` standing (`runPromotion` with the binding). */
88
+ run: (components: DriverComponent[], outputs: Record<string, Record<string, unknown>>, binding: ReleaseGateBinding) => Promise<DriverRunResult>;
89
+ /** Plan `only` again with `outputs` standing (`planReleaseSet`). */
90
+ replan: (only: ReadonlySet<string>, outputs: Record<string, Record<string, unknown>>) => Promise<ChangeSetDocument>;
91
+ /** Store a stage's plan and bind its gates (empty resolutions included). */
92
+ bind: (doc: ChangeSetDocument, gates: string[]) => Promise<ReleaseGateBinding>;
93
+ }
94
+
95
+ export interface PromoteRoundsResult {
96
+ run: DriverRunResult;
97
+ /** The rounds and waves that needed their own approval. */
98
+ stages: PromoteStage[];
99
+ /** Every component that applied, earlier attempts included, in run order. */
100
+ completed: string[];
101
+ outputs: Record<string, Record<string, unknown>>;
102
+ }
103
+
104
+ function isGate(step: DriverStep | DriverGate | DriverPhase): step is DriverGate {
105
+ return (step as { kind?: unknown }).kind === "gate";
106
+ }
107
+
108
+ function isPhase(step: DriverStep | DriverGate | DriverPhase): step is DriverPhase {
109
+ return typeof (step as { phase?: unknown }).phase === "string" && Array.isArray((step as DriverPhase).steps);
110
+ }
111
+
112
+ /** `component` with each gate renamed by `rename`, nested phases included. */
113
+ export function renameGates(component: DriverComponent, rename: (gate: string) => string): DriverComponent {
114
+ const walk = (phase: DriverPhase): DriverPhase => ({
115
+ ...phase,
116
+ steps: phase.steps.map((step) => {
117
+ if (isPhase(step)) return walk(step);
118
+ if (!isGate(step)) return step;
119
+ const { signalName: _old, ...rest } = step;
120
+ return { ...rest, gate: rename(step.gate ?? step.signalName ?? "") };
121
+ }),
122
+ });
123
+ return { ...component, deploy: component.deploy.map(walk) };
124
+ }
125
+
126
+ /** The gate names `components` declare, sorted and distinct. */
127
+ function gatesOf(components: readonly DriverComponent[]): string[] {
128
+ const names = new Set<string>();
129
+ const walk = (steps: DriverPhase["steps"]): void => {
130
+ for (const step of steps) {
131
+ if (isPhase(step)) walk(step.steps);
132
+ else if (isGate(step)) names.add(step.gate ?? step.signalName ?? "");
133
+ }
134
+ };
135
+ for (const c of components) for (const phase of c.deploy) walk(phase.steps);
136
+ return [...names].sort();
137
+ }
138
+
139
+ /** The references `component`'s steps make that resolve to another value now than when its plan read them. */
140
+ export function movedInputs(component: DriverComponent, read: Record<string, Record<string, unknown>> | undefined, now: Record<string, Record<string, unknown>>): string[] {
141
+ if (!read) return [];
142
+ const moved = new Set<string>();
143
+ for (const step of capabilitySteps(component.deploy)) {
144
+ const { kind: _kind, ...rest } = step;
145
+ for (const r of movedWiringReferences(rest, read, now)) if (!r.publish) moved.add(r.ref);
146
+ }
147
+ return [...moved].sort();
148
+ }
149
+
150
+ function joinRuns(runs: DriverRunResult[], outputs: Record<string, Record<string, unknown>>): DriverRunResult {
151
+ const last = runs[runs.length - 1];
152
+ const base: DriverRunResult = {
153
+ order: runs.flatMap((r) => r.order),
154
+ waves: runs.flatMap((r) => r.waves),
155
+ results: runs.flatMap((r) => r.results),
156
+ ok: runs.every((r) => r.ok),
157
+ status: "ok",
158
+ componentOutputs: outputs,
159
+ };
160
+ if (!last || last.status === "ok") return base;
161
+ const { order: _o, waves: _w, results: _r, ok: _ok, componentOutputs: _c, ...stop } = last;
162
+ return { ...base, ...stop, ok: false };
163
+ }
164
+
165
+ /**
166
+ * Run the promote a wave at a time, planning again where an upstream applied
167
+ * as another value, and with `waveGate`, checking each wave. Stops at the
168
+ * first wave that fails or waits on a gate.
169
+ */
170
+ export async function runPromotionInRounds(options: PromoteRoundsOptions): Promise<PromoteRoundsResult> {
171
+ const byName = new Map(options.components.map((c) => [c.name, c]));
172
+ const { waves } = resolveComponentGraph(options.components.map((c) => ({ ...c, dependsOn: (c.dependsOn ?? []).filter((d) => byName.has(d)) })));
173
+ const completed = [...(options.completed ?? [])];
174
+ const done = new Set(completed);
175
+ let outputs = { ...options.outputs };
176
+ const runs: DriverRunResult[] = [];
177
+ const stages: PromoteStage[] = [];
178
+ let round = 1;
179
+
180
+ for (const [index, wave] of waves.entries()) {
181
+ const waveNumber = index + 1;
182
+ const pending = wave.filter((name) => !done.has(name));
183
+ if (pending.length === 0) continue;
184
+ const check = options.waveGate !== undefined && waveNumber > 1
185
+ ? pending
186
+ : pending.filter((name) => movedInputs(byName.get(name)!, options.reads[name], outputs).length > 0);
187
+ const batches: Array<{ names: string[]; binding: ReleaseGateBinding; stage?: PromoteStage; rename?: (g: string) => string }> = [];
188
+ const rest = pending.filter((name) => !check.includes(name));
189
+ if (check.length > 0) {
190
+ const fresh = await options.replan(new Set(check), outputs);
191
+ const notCovered = waveNotCovered(options.approved, fresh);
192
+ if (notCovered.length === 0) {
193
+ rest.push(...check);
194
+ } else {
195
+ const kind: PromoteStage["kind"] = options.waveGate !== undefined ? "wave" : "round";
196
+ if (kind === "round") round += 1;
197
+ const number = kind === "wave" ? waveNumber : round;
198
+ const base = options.waveGate;
199
+ const rename = (g: string) => (base !== undefined ? waveGateName(base, number) : prRoundGate(g, number));
200
+ const renamed = check.map((name) => renameGates(byName.get(name)!, rename));
201
+ const gates = gatesOf(renamed);
202
+ const stage: PromoteStage = { kind, number, components: check, digest: fresh.digest, notCovered, status: "waiting" };
203
+ stages.push(stage);
204
+ if (gates.length === 0) {
205
+ stage.status = "failed";
206
+ stage.message = `${check.join(", ")} planned again past the approved change set and declare${check.length === 1 ? "s" : ""} no gate to approve the new plan on`;
207
+ const stop: DriverRunResult = { order: [], waves: [], results: [], ok: false, status: "fail", failedComponent: check[0], componentOutputs: outputs };
208
+ return { run: joinRuns([...runs, stop], outputs), stages, completed, outputs };
209
+ }
210
+ const binding = await options.bind(fresh, gates);
211
+ batches.push({ names: check, binding, stage, rename });
212
+ }
213
+ }
214
+ if (rest.length > 0) batches.unshift({ names: pending.filter((n) => rest.includes(n)), binding: options.binding });
215
+
216
+ for (const batch of batches) {
217
+ const components = batch.names.map((name) => (batch.rename ? renameGates(byName.get(name)!, batch.rename) : byName.get(name)!));
218
+ const run = await options.run(components, outputs, batch.binding);
219
+ runs.push(run);
220
+ outputs = { ...outputs, ...run.componentOutputs };
221
+ for (const r of run.results) {
222
+ if (r.status === "ok" && !done.has(r.component)) {
223
+ done.add(r.component);
224
+ completed.push(r.component);
225
+ }
226
+ }
227
+ if (batch.stage) batch.stage.status = run.status === "ok" ? "applied" : run.status === "gated" ? "waiting" : "failed";
228
+ if (run.status !== "ok") return { run: joinRuns(runs, outputs), stages, completed, outputs };
229
+ }
230
+ }
231
+ return { run: joinRuns(runs, outputs), stages, completed, outputs };
232
+ }
@@ -13,6 +13,7 @@ import {
13
13
  buildReleaseArtifacts,
14
14
  buildStepsOf,
15
15
  compileEnvironment,
16
+ recordedInputRefs,
16
17
  ReleaseCompileError,
17
18
  templateRefs,
18
19
  withOnlyArchiveSteps,
@@ -49,6 +50,18 @@ describe("templateRefs", () => {
49
50
  });
50
51
  });
51
52
 
53
+ describe("recordedInputRefs (#4155)", () => {
54
+ test("adds each kind's own fields to the template fields: a chart; a missing one need not exist", () => {
55
+ expect(recordedInputRefs(api).map((r) => [r.kind, r.field, r.path, r.required])).toEqual([
56
+ ["cfn-deploy", "template", "dist/api.template.json", true],
57
+ ["cfn-deploy", "template", "dist/edge.template.json", true],
58
+ ["helm-upgrade", "values", "values/base.yaml", true],
59
+ ["helm-upgrade", "chart", "charts/api", false],
60
+ ["kubectl-apply", "manifest", "k8s/api.yaml", true],
61
+ ]);
62
+ });
63
+ });
64
+
52
65
  describe("withOnlyArchiveSteps", () => {
53
66
  test("keeps the archive steps alone, drops gates and every other step, and the dependencies", () => {
54
67
  expect(withOnlyArchiveSteps(api)).toEqual({
@@ -100,6 +113,27 @@ describe("compileEnvironment", () => {
100
113
  });
101
114
  });
102
115
 
116
+ test("records a chart directory file by file and names it in trees; a file that is not UTF-8 as base64 (#4155)", async () => {
117
+ await withTestDir(async (dir) => {
118
+ writeFileSync(
119
+ join(dir, "web.component.ts"),
120
+ `export const web = { name: "web", dependsOn: [], deploy: [{ phase: "Deploy", steps: [
121
+ { kind: "helm-upgrade", release: "web", chart: "chart" },
122
+ { kind: "helm-upgrade", release: "remote", chart: "bitnami/nginx", repo: "https://charts.example" },
123
+ ] }] };`,
124
+ );
125
+ mkdirSync(join(dir, "chart", "charts"), { recursive: true });
126
+ writeFileSync(join(dir, "chart", "Chart.yaml"), "name: web\n");
127
+ writeFileSync(join(dir, "chart", "charts", "dep.tgz"), Buffer.from([0x1f, 0x8b, 0xff, 0x00]));
128
+ const target = await compileEnvironment({ projectDir: dir }, "prod");
129
+ expect(target.trees).toEqual([{ component: "web", path: "chart" }]);
130
+ expect(target.templates).toEqual([
131
+ { component: "web", path: "chart/Chart.yaml", content: "name: web\n" },
132
+ { component: "web", path: "chart/charts/dep.tgz", content: Buffer.from([0x1f, 0x8b, 0xff, 0x00]).toString("base64"), encoding: "base64" },
133
+ ]);
134
+ });
135
+ });
136
+
103
137
  test("refuses when a template a deploy names does not exist after the build", async () => {
104
138
  await withTestDir(async (dir) => {
105
139
  project(dir);