@intentius/chant 0.1.4 → 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/bin/chant +1 -10
- package/package.json +4 -5
- package/src/attrref.test.ts +1 -1
- package/src/bench.test.ts +1 -1
- package/src/build.test.ts +3 -5
- package/src/builder.test.ts +1 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/justfile +8 -8
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +4 -4
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +1 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +3 -3
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate-cli.ts +1 -1
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +7 -7
- package/src/cli/commands/build.test.ts +1 -1
- package/src/cli/commands/diff.test.ts +1 -1
- package/src/cli/commands/doctor.test.ts +1 -1
- package/src/cli/commands/doctor.ts +7 -7
- package/src/cli/commands/import.test.ts +1 -1
- package/src/cli/commands/init-lexicon/templates/codegen.ts +1 -1
- package/src/cli/commands/init-lexicon/templates/plugin.ts +3 -3
- package/src/cli/commands/init-lexicon/templates/project.ts +13 -13
- package/src/cli/commands/init-lexicon/templates/tests.ts +4 -4
- package/src/cli/commands/init-lexicon.test.ts +2 -2
- package/src/cli/commands/init-lexicon.ts +1 -1
- package/src/cli/commands/init.test.ts +1 -1
- package/src/cli/commands/init.ts +1 -2
- package/src/cli/commands/lint.test.ts +3 -3
- package/src/cli/commands/list.test.ts +2 -2
- package/src/cli/commands/onboard.test.ts +33 -33
- package/src/cli/commands/onboard.ts +13 -13
- package/src/cli/commands/update.test.ts +1 -1
- package/src/cli/conflict-check.test.ts +7 -2
- package/src/cli/format.test.ts +1 -1
- package/src/cli/lsp/server.test.ts +8 -4
- package/src/cli/main.test.ts +1 -1
- package/src/cli/main.ts +6 -5
- package/src/cli/mcp/server.test.ts +1 -1
- package/src/cli/plugins.test.ts +1 -1
- package/src/cli/reporters/stylish.test.ts +1 -1
- package/src/cli/watch.test.ts +1 -1
- package/src/codegen/docs-interpolation.test.ts +3 -3
- package/src/codegen/docs-rules.test.ts +1 -1
- package/src/codegen/docs.ts +1 -1
- package/src/codegen/fetch.test.ts +1 -1
- package/src/codegen/generate-registry.test.ts +1 -1
- package/src/codegen/generate-runtime-index.test.ts +1 -1
- package/src/codegen/generate-typescript.test.ts +1 -1
- package/src/codegen/generate.test.ts +1 -1
- package/src/codegen/json-patch.test.ts +1 -1
- package/src/codegen/json-schema.test.ts +1 -1
- package/src/codegen/topo-sort.test.ts +1 -1
- package/src/codegen/typecheck.test.ts +1 -1
- package/src/codegen/typecheck.ts +18 -5
- package/src/codegen/validate.test.ts +1 -1
- package/src/composite.test.ts +17 -16
- package/src/composite.ts +5 -4
- package/src/config.test.ts +3 -3
- package/src/config.ts +0 -4
- package/src/declarable.test.ts +1 -1
- package/src/detectLexicon.test.ts +1 -1
- package/src/discovery/cache.test.ts +1 -1
- package/src/discovery/collect.test.ts +1 -1
- package/src/discovery/cycles.test.ts +1 -1
- package/src/discovery/files.test.ts +1 -1
- package/src/discovery/graph.test.ts +1 -1
- package/src/discovery/import.test.ts +4 -3
- package/src/discovery/index.test.ts +1 -1
- package/src/discovery/resolve.test.ts +1 -1
- package/src/errors.test.ts +1 -1
- package/src/import/base-parser.test.ts +1 -1
- package/src/import/ir-utils.test.ts +1 -1
- package/src/index.ts +4 -0
- package/src/intrinsic-interpolation.test.ts +1 -1
- package/src/intrinsic.test.ts +1 -1
- package/src/lexicon-integrity.test.ts +2 -2
- package/src/lexicon-manifest.test.ts +1 -1
- package/src/lexicon-output.test.ts +1 -1
- package/src/lexicon-schema.test.ts +1 -1
- package/src/lint/config-overrides.test.ts +2 -2
- package/src/lint/config.test.ts +2 -2
- package/src/lint/config.ts +1 -1
- package/src/lint/declarative.test.ts +1 -1
- package/src/lint/discover.test.ts +1 -1
- package/src/lint/discover.ts +10 -0
- package/src/lint/engine.test.ts +1 -1
- package/src/lint/named-checks.test.ts +1 -1
- package/src/lint/parser.test.ts +2 -2
- package/src/lint/post-synth.test.ts +1 -1
- package/src/lint/rule-loader.test.ts +2 -2
- package/src/lint/rule-options.test.ts +2 -2
- package/src/lint/rule-registry.test.ts +1 -1
- package/src/lint/rule.test.ts +1 -1
- package/src/lint/rules/cor017-composite-name-match.test.ts +1 -1
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.test.ts +1 -1
- package/src/lint/rules/declarable-naming-convention.test.ts +1 -1
- package/src/lint/rules/evl001-non-literal-expression.test.ts +1 -1
- package/src/lint/rules/evl002-control-flow-resource.test.ts +1 -1
- package/src/lint/rules/evl003-dynamic-property-access.test.ts +1 -1
- package/src/lint/rules/evl004-spread-non-const.test.ts +1 -1
- package/src/lint/rules/evl005-resource-block-body.test.ts +1 -1
- package/src/lint/rules/evl007-invalid-siblings.test.ts +1 -1
- package/src/lint/rules/evl009-composite-no-constant.test.ts +1 -1
- package/src/lint/rules/evl010-composite-no-transform.test.ts +1 -1
- package/src/lint/rules/export-required.test.ts +1 -1
- package/src/lint/rules/file-declarable-limit.test.ts +1 -1
- package/src/lint/rules/flat-declarations.test.ts +1 -1
- package/src/lint/rules/no-cyclic-declarable-ref.test.ts +1 -1
- package/src/lint/rules/no-redundant-type-import.test.ts +1 -1
- package/src/lint/rules/no-redundant-value-cast.test.ts +1 -1
- package/src/lint/rules/no-string-ref.test.ts +1 -1
- package/src/lint/rules/no-unused-declarable-import.test.ts +1 -1
- package/src/lint/rules/no-unused-declarable.test.ts +1 -1
- package/src/lint/rules/single-concern-file.test.ts +1 -1
- package/src/lint/selectors.test.ts +1 -1
- 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/src/project-validation.test.ts +2 -2
- package/src/pseudo-parameter.test.ts +1 -1
- package/src/resource-attributes.test.ts +2 -2
- package/src/runtime-adapter.ts +13 -68
- package/src/serializer-walker.test.ts +2 -1
- package/src/sort.test.ts +1 -1
- package/src/stack-output.ts +2 -2
- package/src/toml.test.ts +2 -2
- package/src/types.test.ts +1 -1
- package/src/utils.test.ts +1 -1
- package/src/utils.ts +2 -2
- package/src/validation.test.ts +1 -1
- package/src/yaml.test.ts +1 -1
|
@@ -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
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from "
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { validateProjectStructure } from "./project-validation";
|
|
3
3
|
import { mkdirSync, writeFileSync, rmSync } from "fs";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
|
|
6
|
-
const TEST_DIR = join(import.meta.
|
|
6
|
+
const TEST_DIR = join(import.meta.dirname, "__test_project_validation__");
|
|
7
7
|
|
|
8
8
|
beforeEach(() => {
|
|
9
9
|
mkdirSync(TEST_DIR, { recursive: true });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect,
|
|
1
|
+
import { describe, test, expect, vi } from "vitest";
|
|
2
2
|
import { resolveDependsOn } from "./resource-attributes";
|
|
3
3
|
import { DECLARABLE_MARKER, type Declarable } from "./declarable";
|
|
4
4
|
|
|
@@ -55,7 +55,7 @@ describe("resolveDependsOn", () => {
|
|
|
55
55
|
test("warns and skips Declarable not found in entityNames", () => {
|
|
56
56
|
const bucket = mockDeclarable();
|
|
57
57
|
const entityNames = new Map<Declarable, string>(); // bucket not registered
|
|
58
|
-
const spy = spyOn(console, "warn").mockImplementation(() => {});
|
|
58
|
+
const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
59
59
|
|
|
60
60
|
const result = resolveDependsOn(bucket, entityNames, "MyResource");
|
|
61
61
|
expect(result).toEqual([]);
|
package/src/runtime-adapter.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime adapter — abstracts
|
|
2
|
+
* Runtime adapter — abstracts child-process and filesystem APIs for chant.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* not which adapter class is used.
|
|
4
|
+
* The `target` parameter (from config) controls what commands get spawned
|
|
5
|
+
* (node/npx/npm), not which adapter class is used.
|
|
7
6
|
*/
|
|
8
7
|
import { dirname } from "path";
|
|
9
8
|
import { fileURLToPath } from "url";
|
|
@@ -19,16 +18,16 @@ export interface SpawnResult {
|
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
export interface RuntimeCommands {
|
|
22
|
-
/** Runtime binary: "
|
|
21
|
+
/** Runtime binary: "node" */
|
|
23
22
|
runner: string;
|
|
24
|
-
/** Package executor: "
|
|
23
|
+
/** Package executor: "npx" */
|
|
25
24
|
exec: string;
|
|
26
|
-
/** Pack command: ["
|
|
25
|
+
/** Pack command: ["npm", "pack"] */
|
|
27
26
|
packCmd: string[];
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
export interface RuntimeAdapter {
|
|
31
|
-
readonly name: "
|
|
30
|
+
readonly name: "node";
|
|
32
31
|
/** Hash content and return hex string */
|
|
33
32
|
hash(content: string): string;
|
|
34
33
|
/** Algorithm name recorded in integrity.json */
|
|
@@ -41,44 +40,6 @@ export interface RuntimeAdapter {
|
|
|
41
40
|
readonly commands: RuntimeCommands;
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
// ── Bun adapter ──────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
class BunRuntimeAdapter implements RuntimeAdapter {
|
|
47
|
-
readonly name = "bun" as const;
|
|
48
|
-
readonly hashAlgorithm = "xxhash64";
|
|
49
|
-
readonly commands: RuntimeCommands;
|
|
50
|
-
|
|
51
|
-
constructor(target: "bun" | "node") {
|
|
52
|
-
this.commands =
|
|
53
|
-
target === "node"
|
|
54
|
-
? { runner: "node", exec: "npx", packCmd: ["npm", "pack"] }
|
|
55
|
-
: { runner: "bun", exec: "bunx", packCmd: ["bun", "pm", "pack"] };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
hash(content: string): string {
|
|
59
|
-
return Bun.hash(content).toString(16);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
globMatch(pattern: string, filePath: string): boolean {
|
|
63
|
-
const glob = new Bun.Glob(pattern);
|
|
64
|
-
return glob.match(filePath);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async spawn(cmd: string[], opts?: { cwd?: string }): Promise<SpawnResult> {
|
|
68
|
-
const proc = Bun.spawn(cmd, {
|
|
69
|
-
cwd: opts?.cwd,
|
|
70
|
-
stdout: "pipe",
|
|
71
|
-
stderr: "pipe",
|
|
72
|
-
});
|
|
73
|
-
const [stdout, stderr] = await Promise.all([
|
|
74
|
-
new Response(proc.stdout).text(),
|
|
75
|
-
new Response(proc.stderr).text(),
|
|
76
|
-
]);
|
|
77
|
-
const exitCode = await proc.exited;
|
|
78
|
-
return { stdout, stderr, exitCode };
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
43
|
// ── Node adapter ─────────────────────────────────────────────────
|
|
83
44
|
|
|
84
45
|
class NodeRuntimeAdapter implements RuntimeAdapter {
|
|
@@ -86,11 +47,8 @@ class NodeRuntimeAdapter implements RuntimeAdapter {
|
|
|
86
47
|
readonly hashAlgorithm = "sha256";
|
|
87
48
|
readonly commands: RuntimeCommands;
|
|
88
49
|
|
|
89
|
-
constructor(
|
|
90
|
-
this.commands =
|
|
91
|
-
target === "bun"
|
|
92
|
-
? { runner: "bun", exec: "bunx", packCmd: ["bun", "pm", "pack"] }
|
|
93
|
-
: { runner: "node", exec: "npx", packCmd: ["npm", "pack"] };
|
|
50
|
+
constructor() {
|
|
51
|
+
this.commands = { runner: "node", exec: "npx", packCmd: ["npm", "pack"] };
|
|
94
52
|
}
|
|
95
53
|
|
|
96
54
|
hash(content: string): string {
|
|
@@ -107,6 +65,7 @@ class NodeRuntimeAdapter implements RuntimeAdapter {
|
|
|
107
65
|
resolve({
|
|
108
66
|
stdout: stdout ?? "",
|
|
109
67
|
stderr: stderr ?? "",
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
110
69
|
exitCode: err ? (err as any).code ?? 1 : 0,
|
|
111
70
|
});
|
|
112
71
|
});
|
|
@@ -118,29 +77,16 @@ class NodeRuntimeAdapter implements RuntimeAdapter {
|
|
|
118
77
|
|
|
119
78
|
let _runtime: RuntimeAdapter | undefined;
|
|
120
79
|
|
|
121
|
-
/**
|
|
122
|
-
* Detect whether we're running under Bun.
|
|
123
|
-
*/
|
|
124
|
-
function isBun(): boolean {
|
|
125
|
-
return typeof globalThis.Bun !== "undefined";
|
|
126
|
-
}
|
|
127
|
-
|
|
128
80
|
/**
|
|
129
81
|
* Initialize the runtime adapter singleton.
|
|
130
|
-
*
|
|
131
|
-
* @param target - Which commands to spawn ("bun" or "node"). Defaults to auto-detect.
|
|
132
|
-
* Controls the `commands` property, not which adapter class is used.
|
|
133
82
|
*/
|
|
134
|
-
export function initRuntime(
|
|
135
|
-
|
|
136
|
-
_runtime = isBun()
|
|
137
|
-
? new BunRuntimeAdapter(resolvedTarget)
|
|
138
|
-
: new NodeRuntimeAdapter(resolvedTarget);
|
|
83
|
+
export function initRuntime(): RuntimeAdapter {
|
|
84
|
+
_runtime = new NodeRuntimeAdapter();
|
|
139
85
|
return _runtime;
|
|
140
86
|
}
|
|
141
87
|
|
|
142
88
|
/**
|
|
143
|
-
* Get the runtime adapter. Lazily initializes
|
|
89
|
+
* Get the runtime adapter. Lazily initializes if not yet set.
|
|
144
90
|
*/
|
|
145
91
|
export function getRuntime(): RuntimeAdapter {
|
|
146
92
|
if (!_runtime) {
|
|
@@ -151,7 +97,6 @@ export function getRuntime(): RuntimeAdapter {
|
|
|
151
97
|
|
|
152
98
|
/**
|
|
153
99
|
* Convert `import.meta.url` to a directory path.
|
|
154
|
-
* Works on both Bun and Node (replaces Bun-only `import.meta.dir`).
|
|
155
100
|
*/
|
|
156
101
|
export function moduleDir(importMetaUrl: string): string {
|
|
157
102
|
return dirname(fileURLToPath(importMetaUrl));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { walkValue, type SerializerVisitor } from "./serializer-walker";
|
|
3
3
|
import { DECLARABLE_MARKER, type Declarable } from "./declarable";
|
|
4
4
|
import { INTRINSIC_MARKER } from "./intrinsic";
|
|
@@ -98,6 +98,7 @@ describe("walkValue", () => {
|
|
|
98
98
|
const TestTable = createResource("Test::Table", "test", {});
|
|
99
99
|
const resource = new TestTable({}) as unknown as Declarable;
|
|
100
100
|
const names = new Map<Declarable, string>([[resource, "MyTable"]]);
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
102
|
expect(walkValue((resource as any).Ref, names, mockVisitor)).toEqual({ __ref: "MyTable" });
|
|
102
103
|
});
|
|
103
104
|
|
package/src/sort.test.ts
CHANGED
package/src/stack-output.ts
CHANGED
|
@@ -64,8 +64,8 @@ export function stackOutput(
|
|
|
64
64
|
): StackOutput {
|
|
65
65
|
// Derive lexicon from the AttrRef's parent entity
|
|
66
66
|
const parent = ref.parent.deref();
|
|
67
|
-
const lexicon = parent && typeof (parent as
|
|
68
|
-
? (parent as
|
|
67
|
+
const lexicon = parent && typeof (parent as Record<string, unknown>).lexicon === "string"
|
|
68
|
+
? (parent as Record<string, unknown>).lexicon as string
|
|
69
69
|
: "unknown";
|
|
70
70
|
|
|
71
71
|
const output: StackOutput = {
|
package/src/toml.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { emitTOML, parseTOML } from "./toml";
|
|
3
3
|
|
|
4
4
|
describe("emitTOML", () => {
|
|
@@ -66,7 +66,7 @@ describe("emitTOML", () => {
|
|
|
66
66
|
|
|
67
67
|
test("emits header comment", () => {
|
|
68
68
|
const result = emitTOML({ name: "test" }, { header: "Generated by Chant" });
|
|
69
|
-
expect(result
|
|
69
|
+
expect(result.startsWith("# Generated by Chant\n")).toBe(true);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
test("respects key ordering", () => {
|
package/src/types.test.ts
CHANGED