@intentius/chant-lexicon-terraform 0.104.1 → 0.104.3

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.
@@ -25,6 +25,28 @@
25
25
  * Run without a wave plan (`chant run --components`, or a fan-out with no
26
26
  * wave gate), `run` plans and applies in one go, which is what `terraform
27
27
  * apply` with no plan file does too.
28
+ *
29
+ * ## choudoufu roots in a gated wave (#3459)
30
+ *
31
+ * A live choudoufu root that `live-plan-set` can plan as chant would (no
32
+ * `vars`, no var files, no backend config; see `choudoufuWaveRoot`) goes
33
+ * through choudoufu's own set commands (choudoufu#1754), which need
34
+ * choudoufu v0.22.0 (`MIN_CHOUDOUFU_WAVES_VERSION`):
35
+ *
36
+ * - `plan` runs `live-plan-set -json` over the root as a one-root set. The
37
+ * wave gate still binds chant's `terraformPlanDigest` of the plan the
38
+ * document embeds, the same string a single-root gate binds. choudoufu's
39
+ * set digest is kept for the apply, and its root digest is the change-set
40
+ * part's `nativeDigest`.
41
+ * - `run` hands that document and choudoufu's set digest to
42
+ * `live-wave-apply`, which plans the root again and applies nothing when
43
+ * the plan moved (exit 3).
44
+ * - choudoufu's resume file is kept in the fan-out's attempt record
45
+ * (`DeployContext.carry`) after every apply, landed or not, and written
46
+ * back on the next attempt when it was written for the same set digest.
47
+ *
48
+ * chant's waves stay the order. Each root is its own one-root set, so
49
+ * choudoufu's wave over it is always wave 1.
28
50
  */
29
51
  import type { Capability } from "@intentius/chant/components/capability";
30
52
  import { type CapabilityPlugin } from "@intentius/chant/components/capability-plugin";
@@ -1 +1 @@
1
- {"version":3,"file":"capability-plugin.d.ts","sourceRoot":"","sources":["../../src/components/capability-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAiC,MAAM,wCAAwC,CAAC;AAExG,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,+CAA+C,CAAC;AAGzG,0CAA0C;AAC1C,MAAM,WAAW,uBAAuB;IACtC,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,6FAA6F;AAC7F,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,YAAY,EAAE,OAAO,CAAC;IACtB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAgCD,eAAO,MAAM,wBAAwB,EAAE,UAAU,CAAC,uBAAuB,EAAE,wBAAwB,CA6BlG,CAAC;AAEF,eAAO,MAAM,uBAAuB;aAClC,KAAK,YAAG,iBAAiB;CACjB,CAAC;AAEX,eAAO,MAAM,yBAAyB,EAAE,gBAWvC,CAAC"}
1
+ {"version":3,"file":"capability-plugin.d.ts","sourceRoot":"","sources":["../../src/components/capability-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAiC,MAAM,wCAAwC,CAAC;AAExG,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,+CAA+C,CAAC;AAWzG,0CAA0C;AAC1C,MAAM,WAAW,uBAAuB;IACtC,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,6FAA6F;AAC7F,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,YAAY,EAAE,OAAO,CAAC;IACtB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAmED,eAAO,MAAM,wBAAwB,EAAE,UAAU,CAAC,uBAAuB,EAAE,wBAAwB,CAwClG,CAAC;AAEF,eAAO,MAAM,uBAAuB;aAClC,KAAK,YAAG,iBAAiB;CACjB,CAAC;AAEX,eAAO,MAAM,yBAAyB,EAAE,gBAWvC,CAAC"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "0bae71e990e47e95d5f8abf4d52da07e7d626b2758f7cd945d3b9fbb7066a451",
4
+ "manifest.json": "3ee2ce9ce0166f20d489f1628e86c43499330e1117ea31805b3103d569770d23",
5
5
  "meta.json": "ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356",
6
6
  "types/index.d.ts": "022927e6a7b351cc4cf52d5f973f60ccd140e5810e17b32fb8868965aa1db161",
7
7
  "rules/plan-before-apply.ts": "31b1b3176799fd365b2583fe3947edd187c2a863e3dc0094ed9e91ece2464c99",
@@ -56,5 +56,5 @@
56
56
  "rules/tf045.ts": "760b1f4f385fbe7a86897efe3078fdeac11bcc9bfb806285d63db50488174b7b",
57
57
  "skills/chant-terraform.md": "d9ca7b7c28aed0038ce775a3491150298492eed2858fd3651ed9bfca0e577ae8"
58
58
  },
59
- "composite": "bbad081aac99abd000d25a05a2735ee5306a7f18078e2eb3711b6a9d27b78ef1"
59
+ "composite": "57d4f388168f72ef71e52530804ecdc19628cf0707ede67c432bde5a0efeb45c"
60
60
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terraform",
3
- "version": "0.104.1",
3
+ "version": "0.104.3",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Terraform"
6
6
  }
@@ -71,6 +71,34 @@ export declare const DEFAULT_LIVE_PLAN_DOCUMENT_FILE = "chant.live-plan.json";
71
71
  * shipped the last of the three.
72
72
  */
73
73
  export declare const MIN_CHOUDOUFU_VERSION = "0.15.0";
74
+ /**
75
+ * The choudoufu release a gated wave needs for a choudoufu root (#3459):
76
+ * the set digest on `live-plan-set -json`, and `live-wave-apply` with its
77
+ * resume file (choudoufu#1754, merged as choudoufu PR 1816 after v0.21.0).
78
+ * choudoufu's changelog lists both under `v0.22.0 (Unreleased)`, so this is
79
+ * the version main releases as.
80
+ *
81
+ * Only the wave path checks it, so plain `terraform` and `tofu` roots, and a
82
+ * choudoufu root run any other way, do not wait on it. A dev build prints no
83
+ * release version. For one, the check is the capability itself: a set plan
84
+ * document without a top-level `digest` came from a choudoufu older than
85
+ * choudoufu#1754, and {@link choudoufuPlanSet} refuses it.
86
+ */
87
+ export declare const MIN_CHOUDOUFU_WAVES_VERSION = "0.22.0";
88
+ /**
89
+ * Where a choudoufu root's wave files live, relative to the project root:
90
+ * `<root>/plan-set.json` (the set plan document chant planned and the
91
+ * approver approved), `<root>/resume-<digest>.json` (choudoufu's resume
92
+ * file) and `<root>/out/` (plan files and logs).
93
+ */
94
+ export declare const CHOUDOUFU_WAVES_DIR = ".chant/choudoufu-waves";
95
+ /** `live-wave-apply`'s exit statuses (choudoufu#1754). */
96
+ export declare const CHOUDOUFU_WAVE_EXIT: {
97
+ readonly landed: 0;
98
+ readonly error: 1;
99
+ readonly setMoved: 3;
100
+ readonly rootFailed: 4;
101
+ };
74
102
  /**
75
103
  * choudoufu's own refusal summary for `-out` on the `live-plan -estate`
76
104
  * surface (`internal/command/live_mode.go`). That is the one surface where a
@@ -550,6 +578,14 @@ export declare function isOlderVersion(version: string, min: string): boolean;
550
578
  export declare function parseChoudoufuVersion(versionOutput: string): string | undefined;
551
579
  /** Test-only: clear the cached version check so a test can simulate a fresh module load. */
552
580
  export declare function __resetChoudoufuVersionCheckForTests(): void;
581
+ /**
582
+ * Why `choudoufu version`'s output cannot drive a gated wave, or `undefined`
583
+ * when it can (#3459). A release older than
584
+ * {@link MIN_CHOUDOUFU_WAVES_VERSION} is refused. A dev build names no
585
+ * release and passes here; {@link parseChoudoufuPlanSet} then refuses a set
586
+ * plan document with no digest, which is what an older dev build prints.
587
+ */
588
+ export declare function choudoufuWavesVersionRefusal(versionOutput: string): string | undefined;
553
589
  /**
554
590
  * `terraform init` in the named root, with the root's `backendConfig` supplied
555
591
  * as `-backend-config` flags. Uses the longInfra profile: init downloads
@@ -605,6 +641,147 @@ export declare function terraformApply(args: TerraformApplyArgs, signal?: AbortS
605
641
  * asked for the values to hand them on.
606
642
  */
607
643
  export declare function terraformOutputs(args: TerraformRootArgs, signal?: AbortSignal): Promise<Record<string, unknown>>;
644
+ /**
645
+ * `live-plan-set -json` over one root, from the project root, so the
646
+ * document names the root by its path from there, which is what
647
+ * `live-wave-apply` resolves it against.
648
+ */
649
+ export declare function choudoufuPlanSetCommand(opts: {
650
+ binary: string;
651
+ root: string;
652
+ outDir: string;
653
+ }): string;
654
+ /**
655
+ * `live-wave-apply` over a one-root set. The set chant hands choudoufu is
656
+ * one root, so choudoufu's waves over it are a single wave and `-wave=1` is
657
+ * always that root's. chant's waves are the order; choudoufu's command is
658
+ * the apply, with its set check and its resume file.
659
+ */
660
+ export declare function choudoufuWaveApplyCommand(opts: {
661
+ binary: string;
662
+ planSet: string;
663
+ digest: string;
664
+ resume: string;
665
+ outDir: string;
666
+ }): string;
667
+ /**
668
+ * The root's directory as a set plan document names it: its path from the
669
+ * project root, with forward slashes. `undefined` when the root is the
670
+ * project root itself or outside it, since choudoufu takes only roots inside
671
+ * the working directory.
672
+ */
673
+ export declare function choudoufuSetRootPath(projectRoot: string, dir: string): string | undefined;
674
+ /** A root that a gated wave runs through `live-plan-set` and `live-wave-apply`. */
675
+ export interface ChoudoufuWaveRootArgs extends TerraformRootArgs {
676
+ /** The step's `-var` values. A root given any is not a wave root: see {@link choudoufuWaveRoot}. */
677
+ vars?: Record<string, unknown>;
678
+ }
679
+ /**
680
+ * Whether a gated wave runs this root through choudoufu's own set commands
681
+ * (#3459), and if not, why not. It does for a live choudoufu root that
682
+ * `live-plan-set` can plan as chant would: those commands run plain `init`
683
+ * and `plan` in each root, with no `-var-file`, no `-var` and no
684
+ * `-backend-config`, so a root that needs any of them goes the `plan -out`
685
+ * and `apply PLANFILE` way instead, where choudoufu's per-root refusal
686
+ * (choudoufu #878) still stands in front of the apply.
687
+ */
688
+ export declare function choudoufuWaveRoot(args: ChoudoufuWaveRootArgs, signal?: AbortSignal): Promise<{
689
+ wave: true;
690
+ root: string;
691
+ } | {
692
+ wave: false;
693
+ reason: string;
694
+ }>;
695
+ /** What {@link parseChoudoufuPlanSet} read out of a one-root set plan document. */
696
+ export interface ChoudoufuPlanSet {
697
+ /** choudoufu's set digest: what `live-wave-apply -digest` takes. */
698
+ setDigest: string;
699
+ /** choudoufu's digest of the one root. */
700
+ rootDigest: string;
701
+ /** chant's {@link terraformPlanDigest} over the root's embedded plan: what the wave gate binds. */
702
+ planDigest: string;
703
+ changed: boolean;
704
+ /** The root's `show -json` plan, as the document embeds it. */
705
+ json: unknown;
706
+ /** The member's change-set part, with choudoufu's root digest as `nativeDigest`. */
707
+ changeSet: ChangeSetPart;
708
+ }
709
+ /**
710
+ * Read `live-plan-set -json`'s document for one root (#3459). Refuses a
711
+ * document with no set digest: that choudoufu predates choudoufu#1754, and
712
+ * this is how a dev build, which names no release, is held to
713
+ * {@link MIN_CHOUDOUFU_WAVES_VERSION}. Refuses a root that did not plan,
714
+ * with choudoufu's own error.
715
+ */
716
+ export declare function parseChoudoufuPlanSet(stdout: string, opts: {
717
+ member: string;
718
+ root: string;
719
+ }): ChoudoufuPlanSet;
720
+ /** What {@link choudoufuPlanSet} planned. */
721
+ export interface ChoudoufuPlanSetResult extends ChoudoufuPlanSet {
722
+ /** The set plan document, written as choudoufu printed it. `live-wave-apply -plan-set` reads this file. */
723
+ planSetFile: string;
724
+ /** The root's path as the document names it. */
725
+ root: string;
726
+ }
727
+ /**
728
+ * Plan one live choudoufu root as a one-root set: `live-plan-set -json` from
729
+ * the project root (#3459). The document goes to
730
+ * `<CHOUDOUFU_WAVES_DIR>/<root>/plan-set.json`, the file a later
731
+ * `live-wave-apply` hands back to choudoufu with the set digest.
732
+ */
733
+ export declare function choudoufuPlanSet(args: TerraformRootArgs, signal?: AbortSignal): Promise<ChoudoufuPlanSetResult>;
734
+ /**
735
+ * What a choudoufu root keeps in the fan-out's attempt record (#3459):
736
+ * choudoufu's resume file, under the set digest it was written for.
737
+ */
738
+ export interface ChoudoufuCarriedResume {
739
+ kind: "choudoufu-wave-resume";
740
+ setDigest: string;
741
+ resume: unknown;
742
+ }
743
+ /**
744
+ * The resume file to hand choudoufu for `setDigest`, from what an earlier
745
+ * attempt carried, or `undefined`. choudoufu refuses a resume file written
746
+ * for another digest, and a re-planned root with a moved plan has another
747
+ * digest, so only a file for this same set comes back.
748
+ */
749
+ export declare function carriedChoudoufuResume(carried: unknown, setDigest: string): unknown;
750
+ export interface ChoudoufuWaveApplyArgs extends TerraformRootArgs {
751
+ /** {@link ChoudoufuPlanSetResult.planSetFile}: the approved document. */
752
+ planSetFile: string;
753
+ /** {@link ChoudoufuPlanSetResult.setDigest}: choudoufu's digest of that document. */
754
+ setDigest: string;
755
+ /** What the root carried from an earlier attempt (`DeployContext.carried[root]`). */
756
+ carried?: unknown;
757
+ }
758
+ export interface ChoudoufuWaveApplyResult {
759
+ /** `live-wave-apply`'s exit status. See {@link CHOUDOUFU_WAVE_EXIT}. */
760
+ exitCode: number;
761
+ /** Exit 0: the root landed. */
762
+ landed: boolean;
763
+ /** `-json`'s result document, when it printed one. */
764
+ result?: unknown;
765
+ /** choudoufu's resume file after the run, to carry. Absent when choudoufu wrote none. */
766
+ carry?: ChoudoufuCarriedResume;
767
+ /** One line for a person: what happened, in choudoufu's words where it gave some. */
768
+ message: string;
769
+ }
770
+ /** One line for each of `live-wave-apply`'s exit statuses, with choudoufu's own error when it gave one. */
771
+ export declare function describeChoudoufuWaveExit(exitCode: number, result: unknown, stderr: string): string;
772
+ /**
773
+ * Apply one live choudoufu root through `live-wave-apply` (#3459), with the
774
+ * set plan document and set digest {@link choudoufuPlanSet} produced. When
775
+ * the root carried a resume file for this same digest from an earlier
776
+ * attempt, it is written back first, so choudoufu sees what it already
777
+ * recorded. Its resume file after the run comes back as `carry`, landed or
778
+ * not.
779
+ *
780
+ * Never throws for a choudoufu answer: exit 3 (the set moved) and exit 4 (the
781
+ * root failed) come back with `landed: false`, and so does exit 1. An abort
782
+ * or a spawn failure propagates.
783
+ */
784
+ export declare function choudoufuWaveApply(args: ChoudoufuWaveApplyArgs, signal?: AbortSignal): Promise<ChoudoufuWaveApplyResult>;
608
785
  /**
609
786
  * `terraform show` over current state, or over a saved plan when `planFile` is
610
787
  * given. Returns the parsed `-json` output and the `-no-color` render, plus
@@ -1 +1 @@
1
- {"version":3,"file":"terraform.d.ts","sourceRoot":"","sources":["../../../src/op/activities/terraform.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"terraform.d.ts","sourceRoot":"","sources":["../../../src/op/activities/terraform.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-terraform",
3
- "version": "0.104.1",
3
+ "version": "0.104.3",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src/",
@@ -67,7 +67,7 @@
67
67
  "access": "public"
68
68
  },
69
69
  "peerDependencies": {
70
- "@intentius/chant": "^0.104.1",
70
+ "@intentius/chant": "^0.104.3",
71
71
  "typescript": "^5.9.3",
72
72
  "zod": "^4.3.6"
73
73
  },
@@ -8,8 +8,31 @@ import { beforeEach, describe, expect, test, vi } from "vitest";
8
8
  const calls: string[] = [];
9
9
  let applyResult: Record<string, unknown> = { applied: true };
10
10
  let changeSet: Record<string, unknown> | undefined;
11
+ // #3459: whether the root goes through choudoufu's set commands, and what they answer.
12
+ let waveRoot: { wave: true; root: string } | { wave: false; reason: string } = { wave: false, reason: "the root runs terraform" };
13
+ let waveApply: Record<string, unknown> = { exitCode: 0, landed: true, message: "landed" };
14
+ const waveApplyArgs: Array<Record<string, unknown>> = [];
11
15
 
12
16
  vi.mock("../op/activities/terraform", () => ({
17
+ choudoufuWaveRoot: vi.fn(async () => waveRoot),
18
+ choudoufuPlanSet: vi.fn(async (args: { root: string }) => {
19
+ calls.push(`plan-set ${args.root}`);
20
+ return {
21
+ setDigest: `sha256:set-${args.root}`,
22
+ rootDigest: `sha256:root-${args.root}`,
23
+ planDigest: `jcs1-sha256:${args.root}`,
24
+ changed: true,
25
+ json: {},
26
+ changeSet: { member: { member: args.root, nativeDigest: `sha256:root-${args.root}` }, entries: [] },
27
+ planSetFile: `/x/.chant/choudoufu-waves/${args.root}/plan-set.json`,
28
+ root: `estates/${args.root}`,
29
+ };
30
+ }),
31
+ choudoufuWaveApply: vi.fn(async (args: Record<string, unknown>) => {
32
+ calls.push(`wave-apply ${String(args.root)} ${String(args.setDigest)}`);
33
+ waveApplyArgs.push(args);
34
+ return waveApply;
35
+ }),
13
36
  terraformInit: vi.fn(async (args: { root: string }) => {
14
37
  calls.push(`init ${args.root}`);
15
38
  return { dir: `/x/${args.root}` };
@@ -34,6 +57,73 @@ beforeEach(() => {
34
57
  calls.length = 0;
35
58
  applyResult = { applied: true };
36
59
  changeSet = undefined;
60
+ waveRoot = { wave: false, reason: "the root runs terraform" };
61
+ waveApply = { exitCode: 0, landed: true, message: "landed" };
62
+ waveApplyArgs.length = 0;
63
+ });
64
+
65
+ describe("terraform-apply on a choudoufu root in a gated wave (#3459)", () => {
66
+ const resume = { format_version: "1", set_digest: "sha256:set-e01", roots: [{ root: "estates/e01", wave: 1, outcome: "landed" }] };
67
+ const carry = { kind: "choudoufu-wave-resume", setDigest: "sha256:set-e01", resume };
68
+ const wavePlan = {
69
+ planDigest: "jcs1-sha256:e01",
70
+ choudoufu: { planSetFile: "/x/.chant/choudoufu-waves/e01/plan-set.json", setDigest: "sha256:set-e01" },
71
+ };
72
+
73
+ test("plan runs live-plan-set and binds chant's plan digest, keeping choudoufu's set digest for the apply", async () => {
74
+ waveRoot = { wave: true, root: "estates/e01" };
75
+ const planned = await terraformApplyCapability.plan!({ env: "test", component: "e01" }, { root: "e01" });
76
+ expect(calls).toEqual(["plan-set e01"]);
77
+ expect(planned.member).toBe("e01");
78
+ expect(planned.planDigest).toBe("jcs1-sha256:e01");
79
+ expect(planned.artifact).toEqual(wavePlan);
80
+ expect(planned.changeSet).toEqual({ member: { member: "e01", nativeDigest: "sha256:root-e01" }, entries: [] });
81
+ });
82
+
83
+ test("a choudoufu root live-plan-set cannot plan as chant would keeps plan -out", async () => {
84
+ waveRoot = { wave: false, reason: "the step passes -var values, which live-plan-set does not take" };
85
+ await terraformApplyCapability.plan!({ env: "test", component: "e01" }, { root: "e01", vars: { a: "b" } });
86
+ expect(calls).toEqual(["init e01", 'plan e01 {"a":"b"}']);
87
+ });
88
+
89
+ test("run hands the approved document and set digest to live-wave-apply and carries the resume file", async () => {
90
+ waveApply = { exitCode: 0, landed: true, message: "landed", carry };
91
+ const kept: Array<[string, unknown]> = [];
92
+ const out = await terraformApplyCapability.run(
93
+ { env: "test", component: "e01", plans: { e01: wavePlan }, carry: (m, v) => kept.push([m, v]) },
94
+ { root: "e01" },
95
+ );
96
+ expect(calls).toEqual(["wave-apply e01 sha256:set-e01", "output e01"]);
97
+ expect(waveApplyArgs[0]).toMatchObject({ root: "e01", planSetFile: wavePlan.choudoufu.planSetFile, setDigest: "sha256:set-e01" });
98
+ expect(kept).toEqual([["e01", carry]]);
99
+ expect(out).toEqual({ root: "e01", planDigest: "jcs1-sha256:e01", fromWavePlan: true, outputs: { id: "e01-id" } });
100
+ });
101
+
102
+ test("run passes what an earlier attempt carried for the root", async () => {
103
+ await terraformApplyCapability.run(
104
+ { env: "test", component: "e01", plans: { e01: wavePlan }, carried: { e01: carry, other: { x: 1 } } },
105
+ { root: "e01" },
106
+ );
107
+ expect(waveApplyArgs[0]!.carried).toEqual(carry);
108
+ });
109
+
110
+ test("a moved set fails the step, applies nothing more, and still carries the resume file", async () => {
111
+ waveApply = {
112
+ exitCode: 3,
113
+ landed: false,
114
+ message: "choudoufu refused the approved set: the root's plan moved since it was approved, and nothing was applied",
115
+ carry,
116
+ };
117
+ const kept: unknown[] = [];
118
+ await expect(
119
+ terraformApplyCapability.run(
120
+ { env: "test", component: "e01", plans: { e01: wavePlan }, carry: (_m, v) => kept.push(v) },
121
+ { root: "e01" },
122
+ ),
123
+ ).rejects.toThrow(/root "e01": choudoufu refused the approved set/);
124
+ expect(calls).toEqual(["wave-apply e01 sha256:set-e01"]);
125
+ expect(kept).toEqual([carry]);
126
+ });
37
127
  });
38
128
 
39
129
  describe("terraform-apply", () => {
@@ -25,12 +25,42 @@
25
25
  * Run without a wave plan (`chant run --components`, or a fan-out with no
26
26
  * wave gate), `run` plans and applies in one go, which is what `terraform
27
27
  * apply` with no plan file does too.
28
+ *
29
+ * ## choudoufu roots in a gated wave (#3459)
30
+ *
31
+ * A live choudoufu root that `live-plan-set` can plan as chant would (no
32
+ * `vars`, no var files, no backend config; see `choudoufuWaveRoot`) goes
33
+ * through choudoufu's own set commands (choudoufu#1754), which need
34
+ * choudoufu v0.22.0 (`MIN_CHOUDOUFU_WAVES_VERSION`):
35
+ *
36
+ * - `plan` runs `live-plan-set -json` over the root as a one-root set. The
37
+ * wave gate still binds chant's `terraformPlanDigest` of the plan the
38
+ * document embeds, the same string a single-root gate binds. choudoufu's
39
+ * set digest is kept for the apply, and its root digest is the change-set
40
+ * part's `nativeDigest`.
41
+ * - `run` hands that document and choudoufu's set digest to
42
+ * `live-wave-apply`, which plans the root again and applies nothing when
43
+ * the plan moved (exit 3).
44
+ * - choudoufu's resume file is kept in the fan-out's attempt record
45
+ * (`DeployContext.carry`) after every apply, landed or not, and written
46
+ * back on the next attempt when it was written for the same set digest.
47
+ *
48
+ * chant's waves stay the order. Each root is its own one-root set, so
49
+ * choudoufu's wave over it is always wave 1.
28
50
  */
29
51
 
30
52
  import type { Capability, DeployContext, CapabilityPlan } from "@intentius/chant/components/capability";
31
53
  import type { ChangeSetPart } from "@intentius/chant/change-set";
32
54
  import { ownPackageVersion, type CapabilityPlugin } from "@intentius/chant/components/capability-plugin";
33
- import { terraformApply, terraformInit, terraformOutputs, terraformPlan } from "../op/activities/terraform";
55
+ import {
56
+ choudoufuPlanSet,
57
+ choudoufuWaveApply,
58
+ choudoufuWaveRoot,
59
+ terraformApply,
60
+ terraformInit,
61
+ terraformOutputs,
62
+ terraformPlan,
63
+ } from "../op/activities/terraform";
34
64
 
35
65
  /** The `terraform-apply` step's input. */
36
66
  export interface TerraformApplyStepInput {
@@ -82,12 +112,57 @@ function isPlannedRoot(value: unknown): value is PlannedRoot {
82
112
  return typeof v?.planFile === "string" && typeof v.planDigest === "string";
83
113
  }
84
114
 
115
+ /** What `plan` hands `run` for a choudoufu root planned as a one-root set (#3459). */
116
+ interface ChoudoufuWavePlannedRoot {
117
+ planDigest: string;
118
+ choudoufu: { planSetFile: string; setDigest: string };
119
+ }
120
+
121
+ function isChoudoufuWavePlan(value: unknown): value is ChoudoufuWavePlannedRoot {
122
+ const v = value as Partial<ChoudoufuWavePlannedRoot> | undefined;
123
+ return (
124
+ typeof v?.planDigest === "string" &&
125
+ typeof v.choudoufu?.planSetFile === "string" &&
126
+ typeof v.choudoufu.setDigest === "string"
127
+ );
128
+ }
129
+
130
+ /** Apply a choudoufu root through `live-wave-apply`, carrying its resume file either way. */
131
+ async function runChoudoufuWave(
132
+ ctx: DeployContext,
133
+ input: TerraformApplyStepInput,
134
+ planned: ChoudoufuWavePlannedRoot,
135
+ ): Promise<TerraformApplyStepOutput> {
136
+ const applied = await choudoufuWaveApply({
137
+ ...rootArgs(input),
138
+ planSetFile: planned.choudoufu.planSetFile,
139
+ setDigest: planned.choudoufu.setDigest,
140
+ ...(ctx.carried?.[input.root] !== undefined ? { carried: ctx.carried[input.root] } : {}),
141
+ });
142
+ if (applied.carry) ctx.carry?.(input.root, applied.carry);
143
+ if (!applied.landed) {
144
+ throw new Error(`terraform-apply: root "${input.root}": ${applied.message}`);
145
+ }
146
+ const outputs = await terraformOutputs(rootArgs(input));
147
+ return { root: input.root, planDigest: planned.planDigest, fromWavePlan: true, outputs };
148
+ }
149
+
85
150
  export const terraformApplyCapability: Capability<TerraformApplyStepInput, TerraformApplyStepOutput> = {
86
151
  kind: "terraform-apply",
87
152
  // A root's apply has no undo short of applying the previous configuration,
88
153
  // which is a change of its own. COMP003 asks the component to say so.
89
154
  rollbackPolicy: "needs-opt-out",
90
155
  async plan(_ctx: DeployContext, input: TerraformApplyStepInput): Promise<CapabilityPlan> {
156
+ requireRoot(input);
157
+ const wave = await choudoufuWaveRoot({ ...rootArgs(input), ...(input.vars ? { vars: input.vars } : {}) });
158
+ if (wave.wave) {
159
+ const set = await choudoufuPlanSet(rootArgs(input));
160
+ const artifact: ChoudoufuWavePlannedRoot = {
161
+ planDigest: set.planDigest,
162
+ choudoufu: { planSetFile: set.planSetFile, setDigest: set.setDigest },
163
+ };
164
+ return { member: input.root, planDigest: set.planDigest, artifact, changeSet: set.changeSet };
165
+ }
91
166
  const { changeSet, ...planned } = await planRoot(input);
92
167
  return { member: input.root, planDigest: planned.planDigest, artifact: planned, ...(changeSet ? { changeSet } : {}) };
93
168
  },
@@ -100,6 +175,7 @@ export const terraformApplyCapability: Capability<TerraformApplyStepInput, Terra
100
175
  },
101
176
  async run(ctx: DeployContext, input: TerraformApplyStepInput): Promise<TerraformApplyStepOutput> {
102
177
  const fromWave = ctx.plans?.[input.root];
178
+ if (isChoudoufuWavePlan(fromWave)) return runChoudoufuWave(ctx, input, fromWave);
103
179
  const planned: PlannedRoot = isPlannedRoot(fromWave) ? fromWave : await planRoot(input);
104
180
  const applied = await terraformApply({ ...rootArgs(input), planFile: planned.planFile });
105
181
  if (!applied.applied) {
@@ -0,0 +1,176 @@
1
+ /**
2
+ * choudoufu roots in a gated wave (#3459): the version floor, the one-root
3
+ * set plan document, the commands, and which resume file comes back.
4
+ */
5
+
6
+ import { describe, expect, test } from "vitest";
7
+ import { join } from "node:path";
8
+ import {
9
+ MIN_CHOUDOUFU_WAVES_VERSION,
10
+ carriedChoudoufuResume,
11
+ choudoufuPlanSetCommand,
12
+ choudoufuSetRootPath,
13
+ choudoufuWaveApplyCommand,
14
+ choudoufuWavesVersionRefusal,
15
+ describeChoudoufuWaveExit,
16
+ parseChoudoufuPlanSet,
17
+ terraformPlanDigest,
18
+ } from "./terraform";
19
+
20
+ const plan = {
21
+ format_version: "1.2",
22
+ resource_changes: [
23
+ {
24
+ address: "aws_sqs_queue.q",
25
+ mode: "managed",
26
+ type: "aws_sqs_queue",
27
+ name: "q",
28
+ change: { actions: ["create"], before: null, after: { name: "q" } },
29
+ },
30
+ ],
31
+ };
32
+
33
+ function setDocument(overrides: Record<string, unknown> = {}, root: Record<string, unknown> = {}): string {
34
+ return JSON.stringify({
35
+ format_version: "1",
36
+ roots: [
37
+ {
38
+ root: "estates/e01",
39
+ estate: "e01",
40
+ status: "planned",
41
+ error: "",
42
+ changes: true,
43
+ plan_file: ".chant/choudoufu-waves/e01/out/estates/e01.tfplan",
44
+ log_file: ".chant/choudoufu-waves/e01/out/estates/e01.log",
45
+ duration_ms: 10,
46
+ plan,
47
+ digest: "sha256:root",
48
+ ...root,
49
+ },
50
+ ],
51
+ summary: { roots: 1, planned: 1, changed: 1, failed: 0 },
52
+ digest: "sha256:set",
53
+ exit_code: 2,
54
+ ...overrides,
55
+ });
56
+ }
57
+
58
+ describe("the version floor", () => {
59
+ test("is the release choudoufu#1754 ships in", () => {
60
+ expect(MIN_CHOUDOUFU_WAVES_VERSION).toBe("0.22.0");
61
+ });
62
+
63
+ test("refuses a release older than it", () => {
64
+ const refusal = choudoufuWavesVersionRefusal("choudoufu v0.21.0 (based on OpenTofu v1.13.0)\non darwin_arm64");
65
+ expect(refusal).toMatch(/choudoufu 0\.21\.0 is older than v0\.22\.0/);
66
+ });
67
+
68
+ test("passes that release and later ones", () => {
69
+ expect(choudoufuWavesVersionRefusal("choudoufu v0.22.0 (based on OpenTofu v1.13.0)")).toBeUndefined();
70
+ expect(choudoufuWavesVersionRefusal("choudoufu v1.0.0 (based on OpenTofu v1.13.0)")).toBeUndefined();
71
+ });
72
+
73
+ test("passes a dev build, which names no release; the document's digest is the check there", () => {
74
+ expect(choudoufuWavesVersionRefusal("OpenTofu v1.13.0-dev\non darwin_arm64")).toBeUndefined();
75
+ });
76
+ });
77
+
78
+ describe("parseChoudoufuPlanSet", () => {
79
+ test("binds chant's digest of the embedded plan and keeps choudoufu's set and root digests", () => {
80
+ const parsed = parseChoudoufuPlanSet(setDocument(), { member: "e01", root: "estates/e01" });
81
+ expect(parsed.setDigest).toBe("sha256:set");
82
+ expect(parsed.rootDigest).toBe("sha256:root");
83
+ expect(parsed.planDigest).toBe(terraformPlanDigest(plan));
84
+ expect(parsed.changed).toBe(true);
85
+ expect(parsed.changeSet.member.member).toBe("e01");
86
+ expect(parsed.changeSet.member.nativeDigest).toBe("sha256:root");
87
+ });
88
+
89
+ test("refuses a document with no set digest: that choudoufu predates choudoufu#1754", () => {
90
+ expect(() => parseChoudoufuPlanSet(setDocument({ digest: undefined }), { member: "e01", root: "estates/e01" })).toThrow(
91
+ /carries no set digest.*v0\.22\.0/,
92
+ );
93
+ });
94
+
95
+ test("refuses a root that did not plan, with choudoufu's error", () => {
96
+ const doc = setDocument({}, { status: "failed", stage: "init", error: "provider not found", plan: null });
97
+ expect(() => parseChoudoufuPlanSet(doc, { member: "e01", root: "estates/e01" })).toThrow(
98
+ 'choudoufu could not plan root "e01" at init: provider not found',
99
+ );
100
+ });
101
+
102
+ test("refuses a document that is not about this root alone", () => {
103
+ expect(() => parseChoudoufuPlanSet(setDocument(), { member: "e01", root: "estates/e02" })).toThrow(/not only "estates\/e02"/);
104
+ });
105
+
106
+ test("refuses output that is not JSON", () => {
107
+ expect(() => parseChoudoufuPlanSet("Error: something", { member: "e01", root: "estates/e01" })).toThrow(/no JSON document/);
108
+ });
109
+ });
110
+
111
+ describe("commands", () => {
112
+ test("live-plan-set plans the one root, as a document", () => {
113
+ expect(choudoufuPlanSetCommand({ binary: "choudoufu", root: "estates/e01", outDir: ".chant/choudoufu-waves/e01/out" })).toBe(
114
+ "choudoufu live-plan-set -json -out-dir=.chant/choudoufu-waves/e01/out estates/e01",
115
+ );
116
+ });
117
+
118
+ test("live-wave-apply takes the approved document, its set digest, wave 1 and the resume file", () => {
119
+ expect(
120
+ choudoufuWaveApplyCommand({
121
+ binary: "choudoufu",
122
+ planSet: ".chant/choudoufu-waves/e01/plan-set.json",
123
+ digest: "sha256:set",
124
+ resume: ".chant/choudoufu-waves/e01/resume-set.json",
125
+ outDir: ".chant/choudoufu-waves/e01/out",
126
+ }),
127
+ ).toBe(
128
+ "choudoufu live-wave-apply -plan-set=.chant/choudoufu-waves/e01/plan-set.json -digest=sha256:set -wave=1 " +
129
+ "-resume=.chant/choudoufu-waves/e01/resume-set.json -out-dir=.chant/choudoufu-waves/e01/out -json",
130
+ );
131
+ });
132
+
133
+ test("a root is named by its path from the project root, and only inside it", () => {
134
+ expect(choudoufuSetRootPath("/p", join("/p", "estates", "e01"))).toBe("estates/e01");
135
+ expect(choudoufuSetRootPath("/p", "/p")).toBeUndefined();
136
+ expect(choudoufuSetRootPath("/p", "/elsewhere/e01")).toBeUndefined();
137
+ });
138
+ });
139
+
140
+ describe("the carried resume file", () => {
141
+ const resume = { format_version: "1", set_digest: "sha256:set", roots: [{ root: "estates/e01", wave: 1, outcome: "failed" }] };
142
+
143
+ test("comes back for the same set digest", () => {
144
+ expect(carriedChoudoufuResume({ kind: "choudoufu-wave-resume", setDigest: "sha256:set", resume }, "sha256:set")).toEqual(resume);
145
+ });
146
+
147
+ test("does not come back for another set digest, which choudoufu would refuse", () => {
148
+ expect(carriedChoudoufuResume({ kind: "choudoufu-wave-resume", setDigest: "sha256:set", resume }, "sha256:other")).toBeUndefined();
149
+ });
150
+
151
+ test("does not come back when the file inside names another digest, or for something else carried", () => {
152
+ const wrong = { ...resume, set_digest: "sha256:other" };
153
+ expect(carriedChoudoufuResume({ kind: "choudoufu-wave-resume", setDigest: "sha256:set", resume: wrong }, "sha256:set")).toBeUndefined();
154
+ expect(carriedChoudoufuResume({ kind: "something-else" }, "sha256:set")).toBeUndefined();
155
+ expect(carriedChoudoufuResume(undefined, "sha256:set")).toBeUndefined();
156
+ });
157
+ });
158
+
159
+ describe("describeChoudoufuWaveExit", () => {
160
+ test("exit 3 names the moved root and both digests", () => {
161
+ const message = describeChoudoufuWaveExit(
162
+ 3,
163
+ { exit_code: 3, moved: [{ root: "estates/e01", approved_digest: "sha256:a", fresh_digest: "sha256:b" }] },
164
+ "",
165
+ );
166
+ expect(message).toMatch(/nothing was applied/);
167
+ expect(message).toMatch(/estates\/e01 moved: approved sha256:a, now sha256:b/);
168
+ });
169
+
170
+ test("exit 4 points at the resume file, and exit 1 says the command could not run", () => {
171
+ expect(describeChoudoufuWaveExit(4, { exit_code: 4 }, "")).toMatch(/resume file records why/);
172
+ expect(describeChoudoufuWaveExit(1, undefined, "Error: Missing options\n")).toBe(
173
+ "choudoufu live-wave-apply could not run (exit 1) (Error: Missing options)",
174
+ );
175
+ });
176
+ });
@@ -40,13 +40,13 @@
40
40
  */
41
41
 
42
42
  import { exec } from "node:child_process";
43
- import { writeFileSync } from "node:fs";
43
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
44
44
  import { promisify } from "node:util";
45
- import { resolve, dirname, join } from "node:path";
45
+ import { resolve, dirname, join, relative, sep } from "node:path";
46
46
  import { loadChantConfigUpward } from "@intentius/chant/config";
47
47
  import { liveReadKey, memoLiveRead } from "@intentius/chant/live-read-session";
48
48
  import { terraformPlanDigest } from "../../plan-digest";
49
- import { plannerForBinary, terraformChangeSetPart } from "../../change-set";
49
+ import { choudoufuSetPlanParts, plannerForBinary, terraformChangeSetPart } from "../../change-set";
50
50
  import type { ChangeSetPart } from "@intentius/chant/change-set";
51
51
  import type { TerraformConfig, TerraformRootConfig } from "../../config";
52
52
  import { detectLiveEstate } from "./live-detect";
@@ -100,6 +100,32 @@ export const DEFAULT_LIVE_PLAN_DOCUMENT_FILE = "chant.live-plan.json";
100
100
  */
101
101
  export const MIN_CHOUDOUFU_VERSION = "0.15.0";
102
102
 
103
+ /**
104
+ * The choudoufu release a gated wave needs for a choudoufu root (#3459):
105
+ * the set digest on `live-plan-set -json`, and `live-wave-apply` with its
106
+ * resume file (choudoufu#1754, merged as choudoufu PR 1816 after v0.21.0).
107
+ * choudoufu's changelog lists both under `v0.22.0 (Unreleased)`, so this is
108
+ * the version main releases as.
109
+ *
110
+ * Only the wave path checks it, so plain `terraform` and `tofu` roots, and a
111
+ * choudoufu root run any other way, do not wait on it. A dev build prints no
112
+ * release version. For one, the check is the capability itself: a set plan
113
+ * document without a top-level `digest` came from a choudoufu older than
114
+ * choudoufu#1754, and {@link choudoufuPlanSet} refuses it.
115
+ */
116
+ export const MIN_CHOUDOUFU_WAVES_VERSION = "0.22.0";
117
+
118
+ /**
119
+ * Where a choudoufu root's wave files live, relative to the project root:
120
+ * `<root>/plan-set.json` (the set plan document chant planned and the
121
+ * approver approved), `<root>/resume-<digest>.json` (choudoufu's resume
122
+ * file) and `<root>/out/` (plan files and logs).
123
+ */
124
+ export const CHOUDOUFU_WAVES_DIR = ".chant/choudoufu-waves";
125
+
126
+ /** `live-wave-apply`'s exit statuses (choudoufu#1754). */
127
+ export const CHOUDOUFU_WAVE_EXIT = { landed: 0, error: 1, setMoved: 3, rootFailed: 4 } as const;
128
+
103
129
  /**
104
130
  * choudoufu's own refusal summary for `-out` on the `live-plan -estate`
105
131
  * surface (`internal/command/live_mode.go`). That is the one surface where a
@@ -745,6 +771,36 @@ async function ensureChoudoufuVersion(binary: string, signal?: AbortSignal): Pro
745
771
  /** Test-only: clear the cached version check so a test can simulate a fresh module load. */
746
772
  export function __resetChoudoufuVersionCheckForTests(): void {
747
773
  choudoufuVersionCheck = undefined;
774
+ choudoufuWavesVersionCheck = undefined;
775
+ }
776
+
777
+ /**
778
+ * Why `choudoufu version`'s output cannot drive a gated wave, or `undefined`
779
+ * when it can (#3459). A release older than
780
+ * {@link MIN_CHOUDOUFU_WAVES_VERSION} is refused. A dev build names no
781
+ * release and passes here; {@link parseChoudoufuPlanSet} then refuses a set
782
+ * plan document with no digest, which is what an older dev build prints.
783
+ */
784
+ export function choudoufuWavesVersionRefusal(versionOutput: string): string | undefined {
785
+ const version = parseChoudoufuVersion(versionOutput);
786
+ if (version === undefined || !isOlderVersion(version, MIN_CHOUDOUFU_WAVES_VERSION)) return undefined;
787
+ return (
788
+ `choudoufu ${version} is older than v${MIN_CHOUDOUFU_WAVES_VERSION}, the first release with the set digest and ` +
789
+ "live-wave-apply (choudoufu#1754) that a gated wave uses for a choudoufu root. Upgrade choudoufu, or run the " +
790
+ "fan-out without --wave-gate."
791
+ );
792
+ }
793
+
794
+ /** Cached like {@link choudoufuVersionCheck}, and only ever run on the wave path. */
795
+ let choudoufuWavesVersionCheck: Promise<void> | undefined;
796
+
797
+ async function ensureChoudoufuWavesVersion(binary: string, signal?: AbortSignal): Promise<void> {
798
+ choudoufuWavesVersionCheck ??= (async () => {
799
+ const { stdout } = await execAsync(`${binary} version`, { env: process.env, signal });
800
+ const refusal = choudoufuWavesVersionRefusal(stdout);
801
+ if (refusal) throw new Error(refusal);
802
+ })();
803
+ return choudoufuWavesVersionCheck;
748
804
  }
749
805
 
750
806
  // ── Root resolution ─────────────────────────────────────────────────────────
@@ -759,6 +815,8 @@ interface ResolvedRoot {
759
815
  estate?: string;
760
816
  /** `true` exactly when this root is live: `binary` is `"choudoufu"` and `estate` is set. */
761
817
  live: boolean;
818
+ /** The directory the project's `chant.config.*` lives in, which `root.dir` resolves against. */
819
+ projectRoot: string;
762
820
  }
763
821
 
764
822
  /**
@@ -802,6 +860,7 @@ async function resolveRoot(args: TerraformRootArgs, signal?: AbortSignal): Promi
802
860
  dir,
803
861
  ...(estate !== undefined ? { estate } : {}),
804
862
  live: estate !== undefined || configEstate !== undefined,
863
+ projectRoot,
805
864
  };
806
865
  }
807
866
 
@@ -1029,6 +1088,352 @@ export async function terraformOutputs(
1029
1088
  return Object.fromEntries(Object.entries(parsed).map(([name, entry]) => [name, entry?.value]));
1030
1089
  }
1031
1090
 
1091
+ // ── choudoufu gated waves (#3459) ───────────────────────────────────────────
1092
+
1093
+ /**
1094
+ * `live-plan-set -json` over one root, from the project root, so the
1095
+ * document names the root by its path from there, which is what
1096
+ * `live-wave-apply` resolves it against.
1097
+ */
1098
+ export function choudoufuPlanSetCommand(opts: { binary: string; root: string; outDir: string }): string {
1099
+ return [opts.binary, "live-plan-set", "-json", `-out-dir=${quoteArg(opts.outDir)}`, quoteArg(opts.root)].join(" ");
1100
+ }
1101
+
1102
+ /**
1103
+ * `live-wave-apply` over a one-root set. The set chant hands choudoufu is
1104
+ * one root, so choudoufu's waves over it are a single wave and `-wave=1` is
1105
+ * always that root's. chant's waves are the order; choudoufu's command is
1106
+ * the apply, with its set check and its resume file.
1107
+ */
1108
+ export function choudoufuWaveApplyCommand(opts: {
1109
+ binary: string;
1110
+ planSet: string;
1111
+ digest: string;
1112
+ resume: string;
1113
+ outDir: string;
1114
+ }): string {
1115
+ return [
1116
+ opts.binary,
1117
+ "live-wave-apply",
1118
+ `-plan-set=${quoteArg(opts.planSet)}`,
1119
+ `-digest=${quoteArg(opts.digest)}`,
1120
+ "-wave=1",
1121
+ `-resume=${quoteArg(opts.resume)}`,
1122
+ `-out-dir=${quoteArg(opts.outDir)}`,
1123
+ "-json",
1124
+ ].join(" ");
1125
+ }
1126
+
1127
+ /**
1128
+ * The root's directory as a set plan document names it: its path from the
1129
+ * project root, with forward slashes. `undefined` when the root is the
1130
+ * project root itself or outside it, since choudoufu takes only roots inside
1131
+ * the working directory.
1132
+ */
1133
+ export function choudoufuSetRootPath(projectRoot: string, dir: string): string | undefined {
1134
+ const rel = relative(projectRoot, dir);
1135
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || resolve(projectRoot, rel) !== resolve(dir)) return undefined;
1136
+ return rel.split(sep).join("/");
1137
+ }
1138
+
1139
+ /** A root that a gated wave runs through `live-plan-set` and `live-wave-apply`. */
1140
+ export interface ChoudoufuWaveRootArgs extends TerraformRootArgs {
1141
+ /** The step's `-var` values. A root given any is not a wave root: see {@link choudoufuWaveRoot}. */
1142
+ vars?: Record<string, unknown>;
1143
+ }
1144
+
1145
+ /**
1146
+ * Whether a gated wave runs this root through choudoufu's own set commands
1147
+ * (#3459), and if not, why not. It does for a live choudoufu root that
1148
+ * `live-plan-set` can plan as chant would: those commands run plain `init`
1149
+ * and `plan` in each root, with no `-var-file`, no `-var` and no
1150
+ * `-backend-config`, so a root that needs any of them goes the `plan -out`
1151
+ * and `apply PLANFILE` way instead, where choudoufu's per-root refusal
1152
+ * (choudoufu #878) still stands in front of the apply.
1153
+ */
1154
+ export async function choudoufuWaveRoot(
1155
+ args: ChoudoufuWaveRootArgs,
1156
+ signal?: AbortSignal,
1157
+ ): Promise<{ wave: true; root: string } | { wave: false; reason: string }> {
1158
+ const resolved = await resolveRoot(args, signal);
1159
+ if (resolved.binary !== "choudoufu") return { wave: false, reason: `the root runs ${resolved.binary}` };
1160
+ if (!resolved.live) return { wave: false, reason: "the root declares no live estate" };
1161
+ if (args.vars && Object.values(args.vars).some((v) => v !== undefined)) {
1162
+ return { wave: false, reason: "the step passes -var values, which live-plan-set does not take" };
1163
+ }
1164
+ if (resolved.root.varFiles && resolved.root.varFiles.length > 0) {
1165
+ return { wave: false, reason: "the root has var files, which live-plan-set does not take" };
1166
+ }
1167
+ if (resolved.root.backendConfig && Object.keys(resolved.root.backendConfig).length > 0) {
1168
+ return { wave: false, reason: "the root has backend config, which live-plan-set does not take" };
1169
+ }
1170
+ const root = choudoufuSetRootPath(resolved.projectRoot, resolved.dir);
1171
+ if (root === undefined) return { wave: false, reason: "the root's directory is not inside the project" };
1172
+ return { wave: true, root };
1173
+ }
1174
+
1175
+ /** What {@link parseChoudoufuPlanSet} read out of a one-root set plan document. */
1176
+ export interface ChoudoufuPlanSet {
1177
+ /** choudoufu's set digest: what `live-wave-apply -digest` takes. */
1178
+ setDigest: string;
1179
+ /** choudoufu's digest of the one root. */
1180
+ rootDigest: string;
1181
+ /** chant's {@link terraformPlanDigest} over the root's embedded plan: what the wave gate binds. */
1182
+ planDigest: string;
1183
+ changed: boolean;
1184
+ /** The root's `show -json` plan, as the document embeds it. */
1185
+ json: unknown;
1186
+ /** The member's change-set part, with choudoufu's root digest as `nativeDigest`. */
1187
+ changeSet: ChangeSetPart;
1188
+ }
1189
+
1190
+ const isObjectValue = (value: unknown): value is Record<string, unknown> =>
1191
+ typeof value === "object" && value !== null && !Array.isArray(value);
1192
+
1193
+ /**
1194
+ * Read `live-plan-set -json`'s document for one root (#3459). Refuses a
1195
+ * document with no set digest: that choudoufu predates choudoufu#1754, and
1196
+ * this is how a dev build, which names no release, is held to
1197
+ * {@link MIN_CHOUDOUFU_WAVES_VERSION}. Refuses a root that did not plan,
1198
+ * with choudoufu's own error.
1199
+ */
1200
+ export function parseChoudoufuPlanSet(stdout: string, opts: { member: string; root: string }): ChoudoufuPlanSet {
1201
+ let doc: unknown;
1202
+ try {
1203
+ doc = JSON.parse(stdout);
1204
+ } catch {
1205
+ throw new Error(`choudoufu live-plan-set printed no JSON document for root "${opts.member}"`);
1206
+ }
1207
+ if (!isObjectValue(doc) || !Array.isArray(doc.roots)) {
1208
+ throw new Error(`choudoufu live-plan-set printed no set plan document for root "${opts.member}"`);
1209
+ }
1210
+ if (typeof doc.digest !== "string" || doc.digest === "") {
1211
+ throw new Error(
1212
+ `choudoufu's set plan document for root "${opts.member}" carries no set digest, so this choudoufu predates ` +
1213
+ `choudoufu#1754. A gated wave over a choudoufu root needs v${MIN_CHOUDOUFU_WAVES_VERSION} or later.`,
1214
+ );
1215
+ }
1216
+ const roots = doc.roots.filter(isObjectValue);
1217
+ const entry = roots.find((r) => r.root === opts.root);
1218
+ if (roots.length !== 1 || !entry) {
1219
+ throw new Error(
1220
+ `choudoufu's set plan document for root "${opts.member}" names ${roots.map((r) => String(r.root)).join(", ") || "no root"}, ` +
1221
+ `not only "${opts.root}"`,
1222
+ );
1223
+ }
1224
+ if (entry.status !== "planned" || !isObjectValue(entry.plan)) {
1225
+ const stage = typeof entry.stage === "string" && entry.stage ? ` at ${entry.stage}` : "";
1226
+ const error = typeof entry.error === "string" && entry.error ? `: ${entry.error}` : "";
1227
+ throw new Error(`choudoufu could not plan root "${opts.member}"${stage}${error}`);
1228
+ }
1229
+ if (typeof entry.digest !== "string" || entry.digest === "") {
1230
+ throw new Error(`choudoufu's set plan document gives root "${opts.member}" no digest`);
1231
+ }
1232
+ const [changeSet] = choudoufuSetPlanParts({ document: doc, memberFor: () => opts.member });
1233
+ return {
1234
+ setDigest: doc.digest,
1235
+ rootDigest: entry.digest,
1236
+ planDigest: terraformPlanDigest(entry.plan),
1237
+ changed: entry.changes === true,
1238
+ json: entry.plan,
1239
+ changeSet: changeSet!,
1240
+ };
1241
+ }
1242
+
1243
+ /** The directory a root's wave files go in, absolute. */
1244
+ function choudoufuWavesDir(projectRoot: string, root: string): string {
1245
+ return join(projectRoot, CHOUDOUFU_WAVES_DIR, root.replace(/[^A-Za-z0-9._-]/g, "_"));
1246
+ }
1247
+
1248
+ /** A path under the project root as choudoufu, run from there, should be given it. */
1249
+ function fromProject(projectRoot: string, path: string): string {
1250
+ return relative(projectRoot, path).split(sep).join("/");
1251
+ }
1252
+
1253
+ /** What {@link choudoufuPlanSet} planned. */
1254
+ export interface ChoudoufuPlanSetResult extends ChoudoufuPlanSet {
1255
+ /** The set plan document, written as choudoufu printed it. `live-wave-apply -plan-set` reads this file. */
1256
+ planSetFile: string;
1257
+ /** The root's path as the document names it. */
1258
+ root: string;
1259
+ }
1260
+
1261
+ /**
1262
+ * Plan one live choudoufu root as a one-root set: `live-plan-set -json` from
1263
+ * the project root (#3459). The document goes to
1264
+ * `<CHOUDOUFU_WAVES_DIR>/<root>/plan-set.json`, the file a later
1265
+ * `live-wave-apply` hands back to choudoufu with the set digest.
1266
+ */
1267
+ export async function choudoufuPlanSet(args: TerraformRootArgs, signal?: AbortSignal): Promise<ChoudoufuPlanSetResult> {
1268
+ const resolved = await resolveRoot(args, signal);
1269
+ const { binary, root, dir, projectRoot } = resolved;
1270
+ if (binary !== "choudoufu") throw new Error(`choudoufuPlanSet: root "${args.root}" runs ${binary}, not choudoufu`);
1271
+ await ensureChoudoufuWavesVersion(binary, signal);
1272
+ const rootPath = choudoufuSetRootPath(projectRoot, dir);
1273
+ if (rootPath === undefined) throw new Error(`choudoufuPlanSet: root "${args.root}" (${dir}) is not inside ${projectRoot}`);
1274
+ const wavesDir = choudoufuWavesDir(projectRoot, args.root);
1275
+ mkdirSync(wavesDir, { recursive: true });
1276
+ const cmd = choudoufuPlanSetCommand({ binary, root: rootPath, outDir: fromProject(projectRoot, join(wavesDir, "out")) });
1277
+
1278
+ let stdout: string;
1279
+ try {
1280
+ const out = await run(cmd, projectRoot, terraformEnvironment(root), signal);
1281
+ report("", out.stderr);
1282
+ stdout = out.stdout;
1283
+ } catch (err) {
1284
+ // 2 is "planned, with changes" and 4 is "a root failed", which the
1285
+ // document itself explains. Anything else is the command not running.
1286
+ const failure = err as ExecFailure;
1287
+ if (failure.code !== 2 && failure.code !== 4) throw err;
1288
+ report("", failure.stderr ?? "");
1289
+ stdout = failure.stdout ?? "";
1290
+ }
1291
+
1292
+ const parsed = parseChoudoufuPlanSet(stdout, { member: args.root, root: rootPath });
1293
+ const planSetFile = join(wavesDir, "plan-set.json");
1294
+ writeFileSync(planSetFile, stdout);
1295
+ return { ...parsed, planSetFile, root: rootPath };
1296
+ }
1297
+
1298
+ /**
1299
+ * What a choudoufu root keeps in the fan-out's attempt record (#3459):
1300
+ * choudoufu's resume file, under the set digest it was written for.
1301
+ */
1302
+ export interface ChoudoufuCarriedResume {
1303
+ kind: "choudoufu-wave-resume";
1304
+ setDigest: string;
1305
+ resume: unknown;
1306
+ }
1307
+
1308
+ /**
1309
+ * The resume file to hand choudoufu for `setDigest`, from what an earlier
1310
+ * attempt carried, or `undefined`. choudoufu refuses a resume file written
1311
+ * for another digest, and a re-planned root with a moved plan has another
1312
+ * digest, so only a file for this same set comes back.
1313
+ */
1314
+ export function carriedChoudoufuResume(carried: unknown, setDigest: string): unknown {
1315
+ if (!isObjectValue(carried) || carried.kind !== "choudoufu-wave-resume") return undefined;
1316
+ if (carried.setDigest !== setDigest || !isObjectValue(carried.resume)) return undefined;
1317
+ return carried.resume.set_digest === setDigest ? carried.resume : undefined;
1318
+ }
1319
+
1320
+ export interface ChoudoufuWaveApplyArgs extends TerraformRootArgs {
1321
+ /** {@link ChoudoufuPlanSetResult.planSetFile}: the approved document. */
1322
+ planSetFile: string;
1323
+ /** {@link ChoudoufuPlanSetResult.setDigest}: choudoufu's digest of that document. */
1324
+ setDigest: string;
1325
+ /** What the root carried from an earlier attempt (`DeployContext.carried[root]`). */
1326
+ carried?: unknown;
1327
+ }
1328
+
1329
+ export interface ChoudoufuWaveApplyResult {
1330
+ /** `live-wave-apply`'s exit status. See {@link CHOUDOUFU_WAVE_EXIT}. */
1331
+ exitCode: number;
1332
+ /** Exit 0: the root landed. */
1333
+ landed: boolean;
1334
+ /** `-json`'s result document, when it printed one. */
1335
+ result?: unknown;
1336
+ /** choudoufu's resume file after the run, to carry. Absent when choudoufu wrote none. */
1337
+ carry?: ChoudoufuCarriedResume;
1338
+ /** One line for a person: what happened, in choudoufu's words where it gave some. */
1339
+ message: string;
1340
+ }
1341
+
1342
+ /** One line for each of `live-wave-apply`'s exit statuses, with choudoufu's own error when it gave one. */
1343
+ export function describeChoudoufuWaveExit(exitCode: number, result: unknown, stderr: string): string {
1344
+ const error = isObjectValue(result) && typeof result.error === "string" && result.error ? result.error : "";
1345
+ const moved =
1346
+ isObjectValue(result) && Array.isArray(result.moved)
1347
+ ? result.moved
1348
+ .filter(isObjectValue)
1349
+ .map((m) => `${String(m.root)} moved: approved ${String(m.approved_digest)}, now ${String(m.fresh_digest)}`)
1350
+ : [];
1351
+ const detail = error || moved.join("; ") || stderr.trim().split("\n").find((l) => l.trim() !== "")?.trim() || "";
1352
+ const suffix = detail ? ` (${detail})` : "";
1353
+ switch (exitCode) {
1354
+ case CHOUDOUFU_WAVE_EXIT.landed:
1355
+ return "landed";
1356
+ case CHOUDOUFU_WAVE_EXIT.setMoved:
1357
+ return `choudoufu refused the approved set: the root's plan moved since it was approved, and nothing was applied${suffix}`;
1358
+ case CHOUDOUFU_WAVE_EXIT.rootFailed:
1359
+ return `choudoufu could not land the root; its resume file records why${suffix}`;
1360
+ default:
1361
+ return `choudoufu live-wave-apply could not run (exit ${exitCode})${suffix}`;
1362
+ }
1363
+ }
1364
+
1365
+ /**
1366
+ * Apply one live choudoufu root through `live-wave-apply` (#3459), with the
1367
+ * set plan document and set digest {@link choudoufuPlanSet} produced. When
1368
+ * the root carried a resume file for this same digest from an earlier
1369
+ * attempt, it is written back first, so choudoufu sees what it already
1370
+ * recorded. Its resume file after the run comes back as `carry`, landed or
1371
+ * not.
1372
+ *
1373
+ * Never throws for a choudoufu answer: exit 3 (the set moved) and exit 4 (the
1374
+ * root failed) come back with `landed: false`, and so does exit 1. An abort
1375
+ * or a spawn failure propagates.
1376
+ */
1377
+ export async function choudoufuWaveApply(args: ChoudoufuWaveApplyArgs, signal?: AbortSignal): Promise<ChoudoufuWaveApplyResult> {
1378
+ const resolved = await resolveRoot(args, signal);
1379
+ const { binary, root, projectRoot } = resolved;
1380
+ if (binary !== "choudoufu") throw new Error(`choudoufuWaveApply: root "${args.root}" runs ${binary}, not choudoufu`);
1381
+ await ensureChoudoufuWavesVersion(binary, signal);
1382
+ const wavesDir = choudoufuWavesDir(projectRoot, args.root);
1383
+ mkdirSync(wavesDir, { recursive: true });
1384
+ // One file per digest: choudoufu refuses a resume file written for
1385
+ // another, and a file left by an earlier set must not block this one.
1386
+ const resumeFile = join(wavesDir, `resume-${args.setDigest.replace(/^sha256:/, "").slice(0, 16)}.json`);
1387
+ const carried = carriedChoudoufuResume(args.carried, args.setDigest);
1388
+ if (carried !== undefined) writeFileSync(resumeFile, JSON.stringify(carried, null, 2) + "\n");
1389
+
1390
+ const cmd = choudoufuWaveApplyCommand({
1391
+ binary,
1392
+ planSet: fromProject(projectRoot, args.planSetFile),
1393
+ digest: args.setDigest,
1394
+ resume: fromProject(projectRoot, resumeFile),
1395
+ outDir: fromProject(projectRoot, join(wavesDir, "out")),
1396
+ });
1397
+
1398
+ let exitCode = 0;
1399
+ let stdout = "";
1400
+ let stderr = "";
1401
+ try {
1402
+ const out = await run(cmd, projectRoot, terraformEnvironment(root), signal);
1403
+ stdout = out.stdout;
1404
+ stderr = out.stderr;
1405
+ } catch (err) {
1406
+ const failure = err as ExecFailure;
1407
+ if (typeof failure.code !== "number") throw err;
1408
+ exitCode = failure.code;
1409
+ stdout = failure.stdout ?? "";
1410
+ stderr = failure.stderr ?? "";
1411
+ }
1412
+ report(stdout, stderr);
1413
+
1414
+ let result: unknown;
1415
+ try {
1416
+ result = stdout.trim() ? JSON.parse(stdout) : undefined;
1417
+ } catch {
1418
+ result = undefined;
1419
+ }
1420
+ let carry: ChoudoufuCarriedResume | undefined;
1421
+ if (existsSync(resumeFile)) {
1422
+ try {
1423
+ carry = { kind: "choudoufu-wave-resume", setDigest: args.setDigest, resume: JSON.parse(readFileSync(resumeFile, "utf8")) };
1424
+ } catch {
1425
+ carry = undefined;
1426
+ }
1427
+ }
1428
+ return {
1429
+ exitCode,
1430
+ landed: exitCode === CHOUDOUFU_WAVE_EXIT.landed,
1431
+ ...(result !== undefined ? { result } : {}),
1432
+ ...(carry ? { carry } : {}),
1433
+ message: describeChoudoufuWaveExit(exitCode, result, stderr),
1434
+ };
1435
+ }
1436
+
1032
1437
  /**
1033
1438
  * `terraform show` over current state, or over a saved plan when `planFile` is
1034
1439
  * given. Returns the parsed `-json` output and the `-no-color` render, plus