@intentius/chant 0.1.5 → 0.1.6
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/package.json +2 -1
- package/src/index.ts +4 -0
- package/src/op/builders.ts +96 -0
- package/src/op/index.ts +4 -0
- package/src/op/op.test.ts +199 -0
- package/src/op/resource.ts +8 -0
- package/src/op/types.ts +66 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
".": "./src/index.ts",
|
|
37
37
|
"./cli": "./src/cli/index.ts",
|
|
38
38
|
"./cli/*": "./src/cli/*.ts",
|
|
39
|
+
"./op": "./src/op/index.ts",
|
|
39
40
|
"./*": "./src/*.ts"
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
package/src/index.ts
CHANGED
|
@@ -61,3 +61,7 @@ export * from "./lsp/lexicon-providers";
|
|
|
61
61
|
export * from "./mcp/types";
|
|
62
62
|
export * from "./state/index";
|
|
63
63
|
export * from "./spell/index";
|
|
64
|
+
// Op builders — use explicit exports to avoid collision with the core `build` function
|
|
65
|
+
export { Op, phase, activity, gate, kubectlApply, helmInstall, waitForStack,
|
|
66
|
+
gitlabPipeline, stateSnapshot, shell, teardown, OpResource } from "./op/index";
|
|
67
|
+
export type { OpConfig, PhaseDefinition, StepDefinition, ActivityStep, GateStep, RetryPolicy } from "./op/index";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { OpResource } from "./resource";
|
|
2
|
+
import type { OpConfig, PhaseDefinition, StepDefinition, ActivityStep, GateStep } from "./types";
|
|
3
|
+
|
|
4
|
+
// ── Core builders ─────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Declare a named, phased Temporal workflow.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* export default Op({
|
|
12
|
+
* name: "alb-deploy",
|
|
13
|
+
* overview: "Build and deploy the ALB multi-service stack",
|
|
14
|
+
* phases: [
|
|
15
|
+
* phase("Build", [build("examples/gitlab-aws-alb-infra")], { parallel: true }),
|
|
16
|
+
* phase("Deploy", [kubectlApply("dist/alb-infra.yaml")]),
|
|
17
|
+
* ],
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function Op(config: OpConfig): InstanceType<typeof OpResource> {
|
|
22
|
+
return new OpResource(config as unknown as Record<string, unknown>);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Define a named execution phase containing one or more steps. */
|
|
26
|
+
export function phase(
|
|
27
|
+
name: string,
|
|
28
|
+
steps: StepDefinition[],
|
|
29
|
+
opts?: { parallel?: boolean },
|
|
30
|
+
): PhaseDefinition {
|
|
31
|
+
return { name, steps, ...(opts?.parallel ? { parallel: true } : {}) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Reference a pre-built or custom activity by function name. */
|
|
35
|
+
export function activity(
|
|
36
|
+
fn: string,
|
|
37
|
+
args?: Record<string, unknown>,
|
|
38
|
+
profile?: ActivityStep["profile"],
|
|
39
|
+
): ActivityStep {
|
|
40
|
+
return {
|
|
41
|
+
kind: "activity",
|
|
42
|
+
fn,
|
|
43
|
+
...(args && Object.keys(args).length > 0 ? { args } : {}),
|
|
44
|
+
...(profile ? { profile } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Insert a human gate — the workflow pauses until the named signal is received. */
|
|
49
|
+
export function gate(
|
|
50
|
+
signalName: string,
|
|
51
|
+
opts?: { timeout?: string; description?: string },
|
|
52
|
+
): GateStep {
|
|
53
|
+
return {
|
|
54
|
+
kind: "gate",
|
|
55
|
+
signalName,
|
|
56
|
+
...(opts?.timeout ? { timeout: opts.timeout } : {}),
|
|
57
|
+
...(opts?.description ? { description: opts.description } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Pre-built activity shortcuts ──────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/** Run `npm run build` (or `chant build`) in the given project directory. */
|
|
64
|
+
export const build = (path: string, opts?: Record<string, unknown>): ActivityStep =>
|
|
65
|
+
activity("chantBuild", { path, ...opts });
|
|
66
|
+
|
|
67
|
+
/** Run `kubectl apply -f <manifest>`. Uses `longInfra` profile. */
|
|
68
|
+
export const kubectlApply = (manifest: string, opts?: Record<string, unknown>): ActivityStep =>
|
|
69
|
+
activity("kubectlApply", { manifest, ...opts }, "longInfra");
|
|
70
|
+
|
|
71
|
+
/** Run `helm upgrade --install`. Uses `longInfra` profile. */
|
|
72
|
+
export const helmInstall = (
|
|
73
|
+
name: string,
|
|
74
|
+
chart: string,
|
|
75
|
+
opts?: { values?: string; namespace?: string; [k: string]: unknown },
|
|
76
|
+
): ActivityStep => activity("helmInstall", { name, chart, ...opts }, "longInfra");
|
|
77
|
+
|
|
78
|
+
/** Poll for stack readiness (kubectl rollout, CloudFormation complete, etc). Uses `k8sWait` profile. */
|
|
79
|
+
export const waitForStack = (name: string, opts?: Record<string, unknown>): ActivityStep =>
|
|
80
|
+
activity("waitForStack", { name, ...opts }, "k8sWait");
|
|
81
|
+
|
|
82
|
+
/** Trigger and wait for a GitLab CI pipeline to complete. Uses `longInfra` profile. */
|
|
83
|
+
export const gitlabPipeline = (name: string, opts?: Record<string, unknown>): ActivityStep =>
|
|
84
|
+
activity("gitlabPipeline", { name, ...opts }, "longInfra");
|
|
85
|
+
|
|
86
|
+
/** Take a chant state snapshot for the given environment. */
|
|
87
|
+
export const stateSnapshot = (env: string): ActivityStep =>
|
|
88
|
+
activity("stateSnapshot", { env });
|
|
89
|
+
|
|
90
|
+
/** Run an arbitrary shell command. */
|
|
91
|
+
export const shell = (cmd: string, opts?: { env?: Record<string, string> }): ActivityStep =>
|
|
92
|
+
activity("shellCmd", { cmd, ...opts });
|
|
93
|
+
|
|
94
|
+
/** Run `chant teardown` in the given project directory. Uses `longInfra` profile. */
|
|
95
|
+
export const teardown = (path: string): ActivityStep =>
|
|
96
|
+
activity("chantTeardown", { path }, "longInfra");
|
package/src/op/index.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { Op, phase, activity, gate, build, kubectlApply, helmInstall, waitForStack,
|
|
2
|
+
gitlabPipeline, stateSnapshot, shell, teardown } from "./builders";
|
|
3
|
+
export { OpResource } from "./resource";
|
|
4
|
+
export type { OpConfig, PhaseDefinition, StepDefinition, ActivityStep, GateStep, RetryPolicy } from "./types";
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Op builder tests.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { Op, phase, activity, gate, build, kubectlApply, helmInstall,
|
|
7
|
+
waitForStack, gitlabPipeline, stateSnapshot, shell, teardown } from "./builders";
|
|
8
|
+
import { DECLARABLE_MARKER, type Declarable } from "../declarable";
|
|
9
|
+
|
|
10
|
+
// ── Op() ──────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
describe("Op()", () => {
|
|
13
|
+
function opProps(op: Declarable): Record<string, unknown> {
|
|
14
|
+
return (op as unknown as { props: Record<string, unknown> }).props;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
it("returns a Declarable with correct lexicon and entityType", () => {
|
|
18
|
+
const op = Op({ name: "my-op", overview: "Test op", phases: [] });
|
|
19
|
+
expect(op[DECLARABLE_MARKER]).toBe(true);
|
|
20
|
+
expect(op.lexicon).toBe("temporal");
|
|
21
|
+
expect(op.entityType).toBe("Temporal::Op");
|
|
22
|
+
expect(op.kind).toBe("resource");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("stores name and overview in props", () => {
|
|
26
|
+
const op = Op({ name: "deploy-op", overview: "Deploy infra", phases: [] });
|
|
27
|
+
const props = opProps(op);
|
|
28
|
+
expect(props.name).toBe("deploy-op");
|
|
29
|
+
expect(props.overview).toBe("Deploy infra");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("stores phases in props", () => {
|
|
33
|
+
const p = phase("Build", [build("path/to/project")]);
|
|
34
|
+
const op = Op({ name: "op", overview: "o", phases: [p] });
|
|
35
|
+
const props = opProps(op);
|
|
36
|
+
expect(Array.isArray(props.phases)).toBe(true);
|
|
37
|
+
expect((props.phases as unknown[]).length).toBe(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("stores optional depends in props", () => {
|
|
41
|
+
const op = Op({ name: "second-op", overview: "o", phases: [], depends: ["first-op"] });
|
|
42
|
+
const props = opProps(op);
|
|
43
|
+
expect(props.depends).toEqual(["first-op"]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("stores optional onFailure in props", () => {
|
|
47
|
+
const compensation = phase("Rollback", [shell("echo rollback")]);
|
|
48
|
+
const op = Op({ name: "op", overview: "o", phases: [], onFailure: [compensation] });
|
|
49
|
+
const props = opProps(op);
|
|
50
|
+
expect(Array.isArray(props.onFailure)).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("stores optional taskQueue in props", () => {
|
|
54
|
+
const op = Op({ name: "op", overview: "o", phases: [], taskQueue: "custom-queue" });
|
|
55
|
+
const props = opProps(op);
|
|
56
|
+
expect(props.taskQueue).toBe("custom-queue");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ── phase() ───────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
describe("phase()", () => {
|
|
63
|
+
it("returns correct shape with name and steps", () => {
|
|
64
|
+
const p = phase("Deploy", [build("./")]);
|
|
65
|
+
expect(p.name).toBe("Deploy");
|
|
66
|
+
expect(p.steps).toHaveLength(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("sets parallel: true when passed as option", () => {
|
|
70
|
+
const p = phase("Build", [build("a"), build("b")], { parallel: true });
|
|
71
|
+
expect(p.parallel).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("omits parallel key when not set", () => {
|
|
75
|
+
const p = phase("Build", [build("a")]);
|
|
76
|
+
expect("parallel" in p).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ── activity() ────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
describe("activity()", () => {
|
|
83
|
+
it("returns ActivityStep with kind 'activity'", () => {
|
|
84
|
+
const a = activity("myFn");
|
|
85
|
+
expect(a.kind).toBe("activity");
|
|
86
|
+
expect(a.fn).toBe("myFn");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("includes args when provided", () => {
|
|
90
|
+
const a = activity("myFn", { key: "val" });
|
|
91
|
+
expect(a.args).toEqual({ key: "val" });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("omits args key when no args provided", () => {
|
|
95
|
+
const a = activity("myFn");
|
|
96
|
+
expect("args" in a).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("includes profile when provided", () => {
|
|
100
|
+
const a = activity("myFn", {}, "longInfra");
|
|
101
|
+
expect(a.profile).toBe("longInfra");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("omits profile key when not provided", () => {
|
|
105
|
+
const a = activity("myFn");
|
|
106
|
+
expect("profile" in a).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ── gate() ────────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
describe("gate()", () => {
|
|
113
|
+
it("returns GateStep with kind 'gate' and signalName", () => {
|
|
114
|
+
const g = gate("dns-delegation");
|
|
115
|
+
expect(g.kind).toBe("gate");
|
|
116
|
+
expect(g.signalName).toBe("dns-delegation");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("includes timeout when provided", () => {
|
|
120
|
+
const g = gate("approval", { timeout: "24h" });
|
|
121
|
+
expect(g.timeout).toBe("24h");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("includes description when provided", () => {
|
|
125
|
+
const g = gate("approval", { description: "Awaiting DNS delegation" });
|
|
126
|
+
expect(g.description).toBe("Awaiting DNS delegation");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("omits timeout and description when not provided", () => {
|
|
130
|
+
const g = gate("sig");
|
|
131
|
+
expect("timeout" in g).toBe(false);
|
|
132
|
+
expect("description" in g).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ── Pre-built shortcuts ───────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
describe("pre-built shortcuts", () => {
|
|
139
|
+
it("build() produces chantBuild activity with path arg", () => {
|
|
140
|
+
const a = build("./my-project");
|
|
141
|
+
expect(a.kind).toBe("activity");
|
|
142
|
+
expect(a.fn).toBe("chantBuild");
|
|
143
|
+
expect(a.args?.path).toBe("./my-project");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("build() uses default fastIdempotent profile (no profile key)", () => {
|
|
147
|
+
const a = build("./p");
|
|
148
|
+
expect("profile" in a).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("kubectlApply() produces kubectlApply activity with longInfra profile", () => {
|
|
152
|
+
const a = kubectlApply("dist/infra.yaml");
|
|
153
|
+
expect(a.fn).toBe("kubectlApply");
|
|
154
|
+
expect(a.args?.manifest).toBe("dist/infra.yaml");
|
|
155
|
+
expect(a.profile).toBe("longInfra");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("helmInstall() produces helmInstall activity with name and chart", () => {
|
|
159
|
+
const a = helmInstall("my-release", "charts/app");
|
|
160
|
+
expect(a.fn).toBe("helmInstall");
|
|
161
|
+
expect(a.args?.name).toBe("my-release");
|
|
162
|
+
expect(a.args?.chart).toBe("charts/app");
|
|
163
|
+
expect(a.profile).toBe("longInfra");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("waitForStack() produces waitForStack activity with k8sWait profile", () => {
|
|
167
|
+
const a = waitForStack("my-stack");
|
|
168
|
+
expect(a.fn).toBe("waitForStack");
|
|
169
|
+
expect(a.args?.name).toBe("my-stack");
|
|
170
|
+
expect(a.profile).toBe("k8sWait");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("gitlabPipeline() produces gitlabPipeline activity with longInfra profile", () => {
|
|
174
|
+
const a = gitlabPipeline("group/project");
|
|
175
|
+
expect(a.fn).toBe("gitlabPipeline");
|
|
176
|
+
expect(a.args?.name).toBe("group/project");
|
|
177
|
+
expect(a.profile).toBe("longInfra");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("stateSnapshot() produces stateSnapshot activity with env arg", () => {
|
|
181
|
+
const a = stateSnapshot("prod");
|
|
182
|
+
expect(a.fn).toBe("stateSnapshot");
|
|
183
|
+
expect(a.args?.env).toBe("prod");
|
|
184
|
+
expect("profile" in a).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("shell() produces shellCmd activity with cmd arg", () => {
|
|
188
|
+
const a = shell("echo hello");
|
|
189
|
+
expect(a.fn).toBe("shellCmd");
|
|
190
|
+
expect(a.args?.cmd).toBe("echo hello");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("teardown() produces chantTeardown activity with longInfra profile", () => {
|
|
194
|
+
const a = teardown("./project");
|
|
195
|
+
expect(a.fn).toBe("chantTeardown");
|
|
196
|
+
expect(a.args?.path).toBe("./project");
|
|
197
|
+
expect(a.profile).toBe("longInfra");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createResource } from "../runtime";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The Declarable resource backing an Op definition.
|
|
5
|
+
* entityType: "Temporal::Op", lexicon: "temporal"
|
|
6
|
+
* Discovered automatically alongside infra files — no pipeline changes needed.
|
|
7
|
+
*/
|
|
8
|
+
export const OpResource = createResource("Temporal::Op", "temporal", {});
|
package/src/op/types.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Op type definitions — the data model for a named, phased Temporal workflow.
|
|
3
|
+
*
|
|
4
|
+
* These types are intentionally free of Temporal SDK imports so they can live
|
|
5
|
+
* in core without pulling in @temporalio/* as a dependency.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface OpConfig {
|
|
9
|
+
/** Kebab-case identifier. Used as the workflow function name (camelCase) and output directory name. */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Human-readable description shown in `chant run list` and deployment reports. */
|
|
12
|
+
overview: string;
|
|
13
|
+
/** Temporal task queue. Defaults to `name`. */
|
|
14
|
+
taskQueue?: string;
|
|
15
|
+
/** Temporal namespace. Defaults to chant.config.ts defaultProfile's namespace. */
|
|
16
|
+
namespace?: string;
|
|
17
|
+
/** Ordered list of execution phases. */
|
|
18
|
+
phases: PhaseDefinition[];
|
|
19
|
+
/** Other Op names that must be complete before this Op can run. */
|
|
20
|
+
depends?: string[];
|
|
21
|
+
/** Compensation phases executed on terminal failure (run in reverse order). */
|
|
22
|
+
onFailure?: PhaseDefinition[];
|
|
23
|
+
/** Search attributes to upsert at workflow start. */
|
|
24
|
+
searchAttributes?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PhaseDefinition {
|
|
28
|
+
/** Display name shown in progress output and Temporal UI. */
|
|
29
|
+
name: string;
|
|
30
|
+
/** Ordered steps within the phase. */
|
|
31
|
+
steps: StepDefinition[];
|
|
32
|
+
/** Run all steps concurrently via Promise.all. Default: false. */
|
|
33
|
+
parallel?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type StepDefinition = ActivityStep | GateStep;
|
|
37
|
+
|
|
38
|
+
export interface ActivityStep {
|
|
39
|
+
kind: "activity";
|
|
40
|
+
/** Name of the exported activity function in the pre-built activity library. */
|
|
41
|
+
fn: string;
|
|
42
|
+
/** Arguments passed to the activity function. */
|
|
43
|
+
args?: Record<string, unknown>;
|
|
44
|
+
/**
|
|
45
|
+
* Key from TEMPORAL_ACTIVITY_PROFILES controlling timeout + retry.
|
|
46
|
+
* Default: "fastIdempotent"
|
|
47
|
+
*/
|
|
48
|
+
profile?: "fastIdempotent" | "longInfra" | "k8sWait" | "humanGate";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface GateStep {
|
|
52
|
+
kind: "gate";
|
|
53
|
+
/** Signal name. The generated workflow waits for this signal before continuing. */
|
|
54
|
+
signalName: string;
|
|
55
|
+
/** Temporal duration string. Default: "48h". */
|
|
56
|
+
timeout?: string;
|
|
57
|
+
/** Human-readable description of the action required to unblock this gate. */
|
|
58
|
+
description?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RetryPolicy {
|
|
62
|
+
initialInterval?: string;
|
|
63
|
+
backoffCoefficient?: number;
|
|
64
|
+
maximumAttempts?: number;
|
|
65
|
+
maximumInterval?: string;
|
|
66
|
+
}
|