@intentius/chant 0.61.0 → 0.63.0
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.
- package/dist/cli/handlers/operator.d.ts +13 -18
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +2 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +3 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/driver-output.d.ts.map +1 -1
- package/dist/components/driver.d.ts +12 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +10 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/lexicon.d.ts +51 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +61 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +117 -0
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/plan-digest.d.ts +33 -0
- package/dist/lifecycle/plan-digest.d.ts.map +1 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/op/activities/lexicon-upgrade.d.ts +33 -3
- package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -1
- package/dist/op/activities/lifecycle.d.ts +27 -0
- package/dist/op/activities/lifecycle.d.ts.map +1 -1
- package/dist/op/activities/reconcile.d.ts +394 -14
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/builders.d.ts +6 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/composites/apply-op.d.ts +6 -0
- package/dist/op/composites/apply-op.d.ts.map +1 -1
- package/dist/op/composites/reconcile-op.d.ts.map +1 -1
- package/dist/op/gate-summary.d.ts +16 -0
- package/dist/op/gate-summary.d.ts.map +1 -1
- package/dist/op/gate.d.ts +104 -13
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +3 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +34 -2
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-output.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +8 -1
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +2 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/types.d.ts +19 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +8 -0
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/dist/terraform/__fixtures__/build-graph.d.ts +8 -0
- package/dist/terraform/__fixtures__/build-graph.d.ts.map +1 -1
- package/dist/terraform/graph.d.ts +18 -2
- package/dist/terraform/graph.d.ts.map +1 -1
- package/dist/terraform/parse.d.ts.map +1 -1
- package/dist/terraform/types.d.ts +7 -0
- package/dist/terraform/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/operator.test.ts +232 -1
- package/src/cli/handlers/operator.ts +130 -6
- package/src/cli/handlers/run.ts +19 -0
- package/src/cli/main.ts +2 -0
- package/src/cli/registry.ts +2 -0
- package/src/components/cli-support.ts +29 -4
- package/src/components/driver-output.ts +10 -0
- package/src/components/driver.test.ts +31 -0
- package/src/components/driver.ts +54 -8
- package/src/discovery/fold-import.test.ts +55 -0
- package/src/fold/fold.test.ts +152 -0
- package/src/fold/fold.ts +102 -2
- package/src/fold/subset-doc-parity.test.ts +35 -1
- package/src/fold/subset.ts +10 -0
- package/src/lexicon.ts +51 -0
- package/src/lifecycle/gate-ledger.test.ts +133 -1
- package/src/lifecycle/gate-ledger.ts +108 -0
- package/src/lifecycle/git.test.ts +49 -5
- package/src/lifecycle/git.ts +312 -11
- package/src/lifecycle/index.ts +1 -0
- package/src/lifecycle/plan-digest.test.ts +49 -0
- package/src/lifecycle/plan-digest.ts +86 -0
- package/src/lifecycle/run-ledger.ts +1 -0
- package/src/op/activities/lexicon-upgrade.test.ts +134 -39
- package/src/op/activities/lexicon-upgrade.ts +74 -12
- package/src/op/activities/lifecycle.ts +51 -2
- package/src/op/activities/reconcile.test.ts +1013 -1
- package/src/op/activities/reconcile.ts +721 -25
- package/src/op/builders.ts +7 -1
- package/src/op/composites/apply-op.ts +16 -0
- package/src/op/composites/composites.test.ts +15 -2
- package/src/op/composites/reconcile-op.test.ts +18 -0
- package/src/op/composites/reconcile-op.ts +7 -1
- package/src/op/gate-summary.test.ts +33 -0
- package/src/op/gate-summary.ts +31 -0
- package/src/op/gate.test.ts +614 -0
- package/src/op/gate.ts +190 -24
- package/src/op/index.ts +5 -2
- package/src/op/local-executor.test.ts +340 -2
- package/src/op/local-executor.ts +190 -32
- package/src/op/local-output.test.ts +38 -0
- package/src/op/local-output.ts +24 -1
- package/src/op/op-ir.test.ts +22 -0
- package/src/op/op-ir.ts +9 -0
- package/src/op/operator.test.ts +20 -0
- package/src/op/operator.ts +39 -1
- package/src/op/runtime.ts +2 -0
- package/src/op/runtimes/local.ts +11 -0
- package/src/op/types.ts +19 -0
- package/src/runtime-adapter.ts +17 -3
- package/src/terraform/__fixtures__/build-graph.ts +42 -0
- package/src/terraform/__fixtures__/carve-locals-data.test.ts +138 -0
- package/src/terraform/__fixtures__/depth-estate/main.tf +141 -0
- package/src/terraform/__fixtures__/depth-estate/terraform.tfstate +17 -0
- package/src/terraform/__fixtures__/depth-estate.test.ts +162 -0
- package/src/terraform/graph.test.ts +148 -1
- package/src/terraform/graph.ts +144 -6
- package/src/terraform/parse.ts +4 -1
- package/src/terraform/types.ts +7 -0
package/src/op/builders.ts
CHANGED
|
@@ -107,16 +107,22 @@ export function activity(
|
|
|
107
107
|
* that reaches this step with no resolution newer than its pending fact
|
|
108
108
|
* records the pending fact and ends `gated`. `chant approve <op> <gate>`
|
|
109
109
|
* writes the resolution, and the next run walks through carrying the approver.
|
|
110
|
+
*
|
|
111
|
+
* Pass `plan` to bind the approval to a specific plan (#2300) — normally the
|
|
112
|
+
* Plan phase's own digest, `plan.out.planDigest`. Then a resolution counts
|
|
113
|
+
* only for that plan, and a run whose fresh plan differs is refused by name
|
|
114
|
+
* instead of applying a change nobody approved. See {@link GateStep.plan}.
|
|
110
115
|
*/
|
|
111
116
|
export function gate(
|
|
112
117
|
name: string,
|
|
113
|
-
opts?: { timeout?: string; description?: string },
|
|
118
|
+
opts?: { timeout?: string; description?: string; plan?: GateStep["plan"] },
|
|
114
119
|
): GateStep {
|
|
115
120
|
return {
|
|
116
121
|
kind: "gate",
|
|
117
122
|
gate: name,
|
|
118
123
|
...(opts?.timeout ? { timeout: opts.timeout } : {}),
|
|
119
124
|
...(opts?.description ? { description: opts.description } : {}),
|
|
125
|
+
...(opts?.plan !== undefined ? { plan: opts.plan } : {}),
|
|
120
126
|
};
|
|
121
127
|
}
|
|
122
128
|
|
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
* approve <op> <gate>` line to run. Approve, run again, and the same Op
|
|
20
20
|
* converges — no durable workflow engine is involved.
|
|
21
21
|
*
|
|
22
|
+
* The gate binds the plan (#2300). The Plan phase's `lifecycleDiff` publishes
|
|
23
|
+
* a digest of its change set, the gate step references it, and a resolution
|
|
24
|
+
* counts only for that digest. Approve, change the declared source, re-run,
|
|
25
|
+
* and the run stops again with both digests named rather than applying a
|
|
26
|
+
* change set the approver never saw.
|
|
27
|
+
*
|
|
22
28
|
* @example
|
|
23
29
|
* ```typescript
|
|
24
30
|
* // additive, local executor
|
|
@@ -38,6 +44,7 @@
|
|
|
38
44
|
*/
|
|
39
45
|
|
|
40
46
|
import { Op, phase, activity, gate } from "../builders";
|
|
47
|
+
import { stepOutput } from "../step-output-ref";
|
|
41
48
|
import { gateName } from "../gate-name";
|
|
42
49
|
import type { OpResource } from "../resource";
|
|
43
50
|
import { defaultOutput, hasNativeRollback, type ApplyTarget, type DeleteMode } from "../activities/apply";
|
|
@@ -129,6 +136,10 @@ export function ApplyOp(config: ApplyOpConfig): ApplyOpResources {
|
|
|
129
136
|
phase("Plan", [
|
|
130
137
|
{
|
|
131
138
|
kind: "activity" as const,
|
|
139
|
+
// #2300: the gate below references this step's digest, and a
|
|
140
|
+
// reference needs an id. `plan` rather than `diff`, because what the
|
|
141
|
+
// Approve phase binds to is the plan this step produced.
|
|
142
|
+
id: "plan",
|
|
132
143
|
fn: "lifecycleDiff",
|
|
133
144
|
args: { env: config.env, live: true },
|
|
134
145
|
outcomeAttribute: { name: "Drift", from: "drifted" },
|
|
@@ -141,6 +152,11 @@ export function ApplyOp(config: ApplyOpConfig): ApplyOpResources {
|
|
|
141
152
|
phase("Approve", [
|
|
142
153
|
gate(gateName(config.gate ?? {}) || `approve-${config.name}`, {
|
|
143
154
|
...(config.gate?.timeout ? { timeout: config.gate.timeout } : {}),
|
|
155
|
+
// #2300: the approval is for the change set the Plan phase just
|
|
156
|
+
// produced. Edit the source between approving and re-running and
|
|
157
|
+
// the gate refuses, naming the approved digest and the planned one,
|
|
158
|
+
// instead of applying what nobody read.
|
|
159
|
+
plan: stepOutput("plan", "planDigest"),
|
|
144
160
|
description:
|
|
145
161
|
config.gate?.description ??
|
|
146
162
|
`Approve apply to ${config.env} (delete mode: ${deleteMode}` +
|
|
@@ -12,6 +12,7 @@ import { ReconcileOp } from "./reconcile-op";
|
|
|
12
12
|
import { ApplyOp } from "./apply-op";
|
|
13
13
|
import { DECLARABLE_MARKER } from "../../declarable";
|
|
14
14
|
import { EffectReceipt, receiptExpectation } from "../../effect-receipt";
|
|
15
|
+
import { isStepOutputRef } from "../step-output-ref";
|
|
15
16
|
|
|
16
17
|
function getProps(entity: unknown): Record<string, unknown> {
|
|
17
18
|
return (entity as { props: Record<string, unknown> }).props;
|
|
@@ -115,14 +116,14 @@ describe("ReconcileOp: configuration", () => {
|
|
|
115
116
|
expect(phases.map((p) => p.name)).toEqual(["Snapshot", "Plan", "Reconcile"]);
|
|
116
117
|
const reconcileStep = (phases[2].steps as Array<Record<string, unknown>>)[0];
|
|
117
118
|
expect(reconcileStep.fn).toBe("reconcilePr");
|
|
118
|
-
expect(reconcileStep.args).toEqual({ env: "prod", mode: "pull-request", owned: false });
|
|
119
|
+
expect(reconcileStep.args).toEqual({ env: "prod", op: "p", mode: "pull-request", owned: false });
|
|
119
120
|
});
|
|
120
121
|
|
|
121
122
|
test("scope.owned + onDrift flow into the reconcilePr step", () => {
|
|
122
123
|
const { op } = ReconcileOp({ name: "p", env: "prod", onDrift: "issue", scope: { owned: true } });
|
|
123
124
|
const phases = getProps(op).phases as Array<Record<string, unknown>>;
|
|
124
125
|
const reconcileStep = (phases[2].steps as Array<Record<string, unknown>>)[0];
|
|
125
|
-
expect(reconcileStep.args).toEqual({ env: "prod", mode: "issue", owned: true });
|
|
126
|
+
expect(reconcileStep.args).toEqual({ env: "prod", op: "p", mode: "issue", owned: true });
|
|
126
127
|
});
|
|
127
128
|
|
|
128
129
|
test("labels include Reconcile + Env", () => {
|
|
@@ -204,6 +205,18 @@ describe("ApplyOp: gating + deletes", () => {
|
|
|
204
205
|
const { op } = ApplyOp({ name: "p", env: "prod" });
|
|
205
206
|
expect(getProps(op).labels).toEqual({ Apply: "true", Env: "prod" });
|
|
206
207
|
});
|
|
208
|
+
|
|
209
|
+
// #2300 — the gate approves the change set the Plan phase produced, not the
|
|
210
|
+
// next run of the Op. The Plan step carries the id the reference needs.
|
|
211
|
+
test("the gate binds the Plan phase's own change-set digest", () => {
|
|
212
|
+
const { op } = ApplyOp({ name: "p", env: "prod", delete: "gated" });
|
|
213
|
+
const phases = getProps(op).phases as Array<Record<string, unknown>>;
|
|
214
|
+
const planStep = (phases[1].steps as Array<Record<string, unknown>>)[0];
|
|
215
|
+
expect(planStep.id).toBe("plan");
|
|
216
|
+
const gateStep = (phases[2].steps as Array<Record<string, unknown>>)[0];
|
|
217
|
+
expect(isStepOutputRef(gateStep.plan)).toBe(true);
|
|
218
|
+
expect(gateStep.plan).toMatchObject({ step: "plan", path: "planDigest" });
|
|
219
|
+
});
|
|
207
220
|
});
|
|
208
221
|
|
|
209
222
|
describe("ApplyOp: compensation (#125, total-or-refused in #1449)", () => {
|
|
@@ -34,6 +34,24 @@ describe("ReconcileOp composite — PR/issue URL outcome (#8)", () => {
|
|
|
34
34
|
});
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
describe("ReconcileOp composite — the finding step names its Op (#2319)", () => {
|
|
38
|
+
test("passes the Op's own name as `op`, which is what keeps the issue marker unique", () => {
|
|
39
|
+
const { op } = ReconcileOp({ name: "prod-reconcile", env: "prod", onDrift: "issue" });
|
|
40
|
+
expect((reconcileStep(op).args as { op: string }).op).toBe("prod-reconcile");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("two Ops over one env carry two identities", () => {
|
|
44
|
+
// The collision #2319 reports: `env` alone is shared here by construction,
|
|
45
|
+
// and a `TerraformWatchOp` over a root named "prod" would share it too.
|
|
46
|
+
const a = ReconcileOp({ name: "prod-reconcile", env: "prod", onDrift: "issue" });
|
|
47
|
+
const b = ReconcileOp({ name: "prod-reconcile-owned", env: "prod", onDrift: "issue" });
|
|
48
|
+
const argsA = reconcileStep(a.op).args as { op: string; env: string };
|
|
49
|
+
const argsB = reconcileStep(b.op).args as { op: string; env: string };
|
|
50
|
+
expect(argsA.env).toBe(argsB.env);
|
|
51
|
+
expect(argsA.op).not.toBe(argsB.op);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
37
55
|
describe("ReconcileOp composite — cadence on the op (#2120)", () => {
|
|
38
56
|
function opProps(op: unknown): Record<string, unknown> {
|
|
39
57
|
return (op as { props: Record<string, unknown> }).props;
|
|
@@ -96,10 +96,16 @@ export function ReconcileOp(config: ReconcileOpConfig): ReconcileOpResources {
|
|
|
96
96
|
// regenerates via `chant import --from`, and opens a PR. Surface the
|
|
97
97
|
// opened PR/issue URL as an outcome so `chant run` prints it — the
|
|
98
98
|
// reconcile's result is the link.
|
|
99
|
+
//
|
|
100
|
+
// `op` names this Op in the marker `issue` mode's sticky issue is
|
|
101
|
+
// found by (#2319). The env alone is not unique across Ops: it shares
|
|
102
|
+
// a marker namespace with `TerraformWatchOp`, which passes a terraform
|
|
103
|
+
// *root* as its `env`, so a chant environment and a root that happen
|
|
104
|
+
// to share a name resolved to one marker and overwrote each other.
|
|
99
105
|
{
|
|
100
106
|
kind: "activity" as const,
|
|
101
107
|
fn: "reconcilePr",
|
|
102
|
-
args: { env: config.env, mode: onDrift, owned },
|
|
108
|
+
args: { env: config.env, op: config.name, mode: onDrift, owned },
|
|
103
109
|
...(reconcileOutcome ? { outcomeAttribute: reconcileOutcome } : {}),
|
|
104
110
|
},
|
|
105
111
|
]),
|
|
@@ -28,6 +28,39 @@ describe("gatedRunSummaryMarkdown (#2243)", () => {
|
|
|
28
28
|
expect(md).toContain("chant approve app-apply approve-app-apply --approver <you>");
|
|
29
29
|
expect(md).toContain("_gates/app-apply.jsonl");
|
|
30
30
|
});
|
|
31
|
+
|
|
32
|
+
// #2310: `recordGateApproval`'s push was already reported (#2309); the
|
|
33
|
+
// gate's own `appendPending` push was not. The GitHub/Forgejo/Gitea step
|
|
34
|
+
// summary is exactly the surface an operator working from a different
|
|
35
|
+
// checkout would open, so it is where the gap mattered most.
|
|
36
|
+
test("says so when this run's own append never reached the remote", () => {
|
|
37
|
+
const md = gatedRunSummaryMarkdown({
|
|
38
|
+
...summary,
|
|
39
|
+
pushed: false,
|
|
40
|
+
pushWarning: "chant/lifecycle remote branch has moved since this run started",
|
|
41
|
+
});
|
|
42
|
+
expect(md).toContain("was not pushed to the remote");
|
|
43
|
+
expect(md).toContain("chant/lifecycle remote branch has moved since this run started");
|
|
44
|
+
expect(md).toContain("cannot see it to approve it");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("says nothing extra when the push landed or nothing was pushed this run", () => {
|
|
48
|
+
const md = gatedRunSummaryMarkdown(summary);
|
|
49
|
+
expect(md).not.toContain("was not pushed");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// #2300 — the block is where a CI approver reads what they are approving,
|
|
53
|
+
// so it names the plan and says the approval does not carry to the next one.
|
|
54
|
+
test("names the plan the approval binds to, when the gate binds one", () => {
|
|
55
|
+
const digest = `sha256:${"d".repeat(64)}`;
|
|
56
|
+
const md = gatedRunSummaryMarkdown({ ...summary, planDigest: digest });
|
|
57
|
+
expect(md).toContain(digest);
|
|
58
|
+
expect(md).toContain("and not the next run");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("a gate that binds no plan says nothing about one", () => {
|
|
62
|
+
expect(gatedRunSummaryMarkdown(summary)).not.toContain("and not the next run");
|
|
63
|
+
});
|
|
31
64
|
});
|
|
32
65
|
|
|
33
66
|
describe("writeGatedRunSummary surfaces (#2243, #2256)", () => {
|
package/src/op/gate-summary.ts
CHANGED
|
@@ -34,6 +34,22 @@ export interface GatedRunSummary {
|
|
|
34
34
|
expiresAt?: string;
|
|
35
35
|
/** The approval surface the run resolved, when it knew one. */
|
|
36
36
|
url?: string;
|
|
37
|
+
/**
|
|
38
|
+
* False when this run's own append reached only the local chant/lifecycle
|
|
39
|
+
* branch, not the remote (#2310) — the reason lives in `pushWarning`.
|
|
40
|
+
* Absent when this run left an already-standing pending fact alone, or when
|
|
41
|
+
* the push landed.
|
|
42
|
+
*/
|
|
43
|
+
pushed?: boolean;
|
|
44
|
+
/** Set when `pushed` is false. */
|
|
45
|
+
pushWarning?: string;
|
|
46
|
+
/**
|
|
47
|
+
* The plan the pending fact was recorded against (#2300), when the gate
|
|
48
|
+
* binds one. Shown because it is what the `chant approve` line below
|
|
49
|
+
* approves — and because a reader who comes back to a stale summary needs
|
|
50
|
+
* to see that the digest has moved on.
|
|
51
|
+
*/
|
|
52
|
+
planDigest?: string;
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
/**
|
|
@@ -51,6 +67,13 @@ export function gatedRunSummaryMarkdown(summary: GatedRunSummary): string {
|
|
|
51
67
|
if (summary.description) {
|
|
52
68
|
lines.push(summary.description, "");
|
|
53
69
|
}
|
|
70
|
+
if (summary.planDigest) {
|
|
71
|
+
lines.push(
|
|
72
|
+
`This approves one plan, \`${summary.planDigest}\`, and not the next run (#2300). ` +
|
|
73
|
+
"Change the configuration after approving and the next run refuses rather than applying.",
|
|
74
|
+
"",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
54
77
|
lines.push(
|
|
55
78
|
"Approve it, then re-run this workflow:",
|
|
56
79
|
"",
|
|
@@ -62,6 +85,14 @@ export function gatedRunSummaryMarkdown(summary: GatedRunSummary): string {
|
|
|
62
85
|
);
|
|
63
86
|
if (summary.expiresAt) lines.push(`Expires: ${summary.expiresAt}`);
|
|
64
87
|
if (summary.url) lines.push(`Approve at: ${summary.url}`);
|
|
88
|
+
if (summary.pushed === false) {
|
|
89
|
+
lines.push(
|
|
90
|
+
"",
|
|
91
|
+
`**This pending fact was not pushed to the remote** (${summary.pushWarning ?? "no reason given"}). ` +
|
|
92
|
+
"It exists only in this job's checkout. An operator working from a clone of the remote cannot " +
|
|
93
|
+
"see it to approve it until it reaches \`chant/lifecycle\` there.",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
65
96
|
lines.push("");
|
|
66
97
|
return lines.join("\n");
|
|
67
98
|
}
|