@checkstack/automation-backend 0.2.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/CHANGELOG.md +453 -0
- package/drizzle/0000_acoustic_diamondback.sql +80 -0
- package/drizzle/0001_mute_vindicator.sql +12 -0
- package/drizzle/0002_silky_omega_red.sql +12 -0
- package/drizzle/meta/0000_snapshot.json +688 -0
- package/drizzle/meta/0001_snapshot.json +785 -0
- package/drizzle/meta/0002_snapshot.json +861 -0
- package/drizzle/meta/_journal.json +27 -0
- package/drizzle.config.ts +12 -0
- package/package.json +41 -0
- package/src/action-registry.ts +83 -0
- package/src/action-types.ts +324 -0
- package/src/artifact-store.ts +140 -0
- package/src/artifact-type-registry.ts +64 -0
- package/src/automation-store.ts +227 -0
- package/src/builtin-actions.test.ts +185 -0
- package/src/builtin-actions.ts +132 -0
- package/src/builtin-triggers.test.ts +264 -0
- package/src/builtin-triggers.ts +365 -0
- package/src/dispatch/action-kind.ts +44 -0
- package/src/dispatch/condition.ts +61 -0
- package/src/dispatch/delay-queue.ts +91 -0
- package/src/dispatch/engine.test.ts +1198 -0
- package/src/dispatch/engine.ts +1672 -0
- package/src/dispatch/path-nav.ts +65 -0
- package/src/dispatch/render.test.ts +75 -0
- package/src/dispatch/render.ts +136 -0
- package/src/dispatch/run-state-store.ts +143 -0
- package/src/dispatch/run-state.ts +298 -0
- package/src/dispatch/scope.test.ts +40 -0
- package/src/dispatch/scope.ts +125 -0
- package/src/dispatch/stalled-sweeper.ts +164 -0
- package/src/dispatch/test-fixtures.ts +558 -0
- package/src/dispatch/trigger-subscriber.ts +397 -0
- package/src/dispatch/types.ts +259 -0
- package/src/extension-points.ts +88 -0
- package/src/index.ts +379 -0
- package/src/migration/from-webhook-subscriptions.test.ts +237 -0
- package/src/migration/from-webhook-subscriptions.ts +398 -0
- package/src/registries.test.ts +357 -0
- package/src/router.test.ts +724 -0
- package/src/router.ts +556 -0
- package/src/schema.ts +310 -0
- package/src/trigger-registry.ts +99 -0
- package/src/validate-definition.test.ts +306 -0
- package/src/validate-definition.ts +304 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the deep automation-definition validator.
|
|
3
|
+
*
|
|
4
|
+
* Builds a tiny trigger + action registry, then asserts that
|
|
5
|
+
* `collectDefinitionIssues` surfaces structural errors, unknown
|
|
6
|
+
* trigger/action ids, and — the gap this module closes — invalid
|
|
7
|
+
* provider-action config values + unknown config keys.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from "bun:test";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { Versioned } from "@checkstack/backend-api";
|
|
12
|
+
import { definePluginMetadata } from "@checkstack/common";
|
|
13
|
+
import type { AutomationDefinition } from "@checkstack/automation-common";
|
|
14
|
+
import { createActionRegistry } from "./action-registry";
|
|
15
|
+
import { createTriggerRegistry } from "./trigger-registry";
|
|
16
|
+
import { collectDefinitionIssues } from "./validate-definition";
|
|
17
|
+
|
|
18
|
+
const meta = definePluginMetadata({ pluginId: "test" });
|
|
19
|
+
|
|
20
|
+
function makeDeps() {
|
|
21
|
+
const triggerRegistry = createTriggerRegistry();
|
|
22
|
+
const actionRegistry = createActionRegistry();
|
|
23
|
+
|
|
24
|
+
triggerRegistry.register(
|
|
25
|
+
{
|
|
26
|
+
id: "fired",
|
|
27
|
+
displayName: "Fired",
|
|
28
|
+
payloadSchema: z.object({ id: z.string() }),
|
|
29
|
+
configSchema: z.object({ intervalSeconds: z.number().int().min(1) }),
|
|
30
|
+
// A trigger must be reachable via a hook or setup; this validator
|
|
31
|
+
// only cares about the config schema, so a no-op setup suffices.
|
|
32
|
+
setup: async () => async () => {},
|
|
33
|
+
},
|
|
34
|
+
meta,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
actionRegistry.register(
|
|
38
|
+
{
|
|
39
|
+
id: "log",
|
|
40
|
+
displayName: "Log",
|
|
41
|
+
config: new Versioned({
|
|
42
|
+
version: 1,
|
|
43
|
+
schema: z.object({
|
|
44
|
+
message: z.string().min(1),
|
|
45
|
+
level: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
46
|
+
}),
|
|
47
|
+
}),
|
|
48
|
+
execute: async () => ({ success: true }),
|
|
49
|
+
},
|
|
50
|
+
meta,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return { triggerRegistry, actionRegistry };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Deps whose action registry includes a producing action (`test.create`,
|
|
58
|
+
* `produces: "thing"`) so the artifact-id invariants can be exercised.
|
|
59
|
+
*/
|
|
60
|
+
function makeProducerDeps() {
|
|
61
|
+
const { triggerRegistry, actionRegistry } = makeDeps();
|
|
62
|
+
actionRegistry.register(
|
|
63
|
+
{
|
|
64
|
+
id: "create",
|
|
65
|
+
displayName: "Create Thing",
|
|
66
|
+
config: new Versioned({ version: 1, schema: z.object({}) }),
|
|
67
|
+
produces: "thing",
|
|
68
|
+
execute: async () => ({ success: true, artifact: { ok: true } }),
|
|
69
|
+
},
|
|
70
|
+
meta,
|
|
71
|
+
);
|
|
72
|
+
return { triggerRegistry, actionRegistry };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function baseDefinition(
|
|
76
|
+
overrides: Partial<AutomationDefinition> = {},
|
|
77
|
+
): AutomationDefinition {
|
|
78
|
+
return {
|
|
79
|
+
name: "A",
|
|
80
|
+
triggers: [{ event: "test.fired", config: { intervalSeconds: 5 } }],
|
|
81
|
+
conditions: [],
|
|
82
|
+
actions: [
|
|
83
|
+
{
|
|
84
|
+
action: "test.log",
|
|
85
|
+
config: { message: "hi", level: "info" },
|
|
86
|
+
enabled: true,
|
|
87
|
+
continue_on_error: false,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
mode: "single",
|
|
91
|
+
max_runs: 1,
|
|
92
|
+
...overrides,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe("collectDefinitionIssues", () => {
|
|
97
|
+
it("returns no issues for a fully valid definition", () => {
|
|
98
|
+
const issues = collectDefinitionIssues(baseDefinition(), makeDeps());
|
|
99
|
+
expect(issues).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("flags an invalid enum value in a provider action config", () => {
|
|
103
|
+
const def = baseDefinition({
|
|
104
|
+
actions: [
|
|
105
|
+
{
|
|
106
|
+
action: "test.log",
|
|
107
|
+
config: { message: "hi", level: "debugthisiswrong" },
|
|
108
|
+
enabled: true,
|
|
109
|
+
continue_on_error: false,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
const issues = collectDefinitionIssues(def, makeDeps());
|
|
114
|
+
expect(issues.length).toBeGreaterThan(0);
|
|
115
|
+
const levelIssue = issues.find((i) => i.path.join(".") === "actions.0.config.level");
|
|
116
|
+
expect(levelIssue).toBeDefined();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("flags a missing required config field", () => {
|
|
120
|
+
const def = baseDefinition({
|
|
121
|
+
actions: [
|
|
122
|
+
{
|
|
123
|
+
action: "test.log",
|
|
124
|
+
config: { level: "info" },
|
|
125
|
+
enabled: true,
|
|
126
|
+
continue_on_error: false,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
const issues = collectDefinitionIssues(def, makeDeps());
|
|
131
|
+
expect(
|
|
132
|
+
issues.some((i) => i.path.join(".") === "actions.0.config.message"),
|
|
133
|
+
).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("flags an unknown config key (strict)", () => {
|
|
137
|
+
const def = baseDefinition({
|
|
138
|
+
actions: [
|
|
139
|
+
{
|
|
140
|
+
action: "test.log",
|
|
141
|
+
config: { message: "hi", level: "info", levle: "typo" },
|
|
142
|
+
enabled: true,
|
|
143
|
+
continue_on_error: false,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
const issues = collectDefinitionIssues(def, makeDeps());
|
|
148
|
+
expect(issues.length).toBeGreaterThan(0);
|
|
149
|
+
// The unknown key is reported under the action's config path.
|
|
150
|
+
expect(issues.some((i) => i.path[0] === "actions" && i.path.includes("config"))).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("flags an unknown action id", () => {
|
|
154
|
+
const def = baseDefinition({
|
|
155
|
+
actions: [
|
|
156
|
+
{
|
|
157
|
+
action: "test.does_not_exist",
|
|
158
|
+
config: {},
|
|
159
|
+
enabled: true,
|
|
160
|
+
continue_on_error: false,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
const issues = collectDefinitionIssues(def, makeDeps());
|
|
165
|
+
const issue = issues.find((i) => i.path.join(".") === "actions.0.action");
|
|
166
|
+
expect(issue?.message).toMatch(/Unknown action/);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("flags an unknown trigger event", () => {
|
|
170
|
+
const def = baseDefinition({
|
|
171
|
+
triggers: [{ event: "nope.gone" }],
|
|
172
|
+
});
|
|
173
|
+
const issues = collectDefinitionIssues(def, makeDeps());
|
|
174
|
+
const issue = issues.find((i) => i.path.join(".") === "triggers.0.event");
|
|
175
|
+
expect(issue?.message).toMatch(/Unknown trigger/);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("flags an invalid trigger config value", () => {
|
|
179
|
+
const def = baseDefinition({
|
|
180
|
+
triggers: [{ event: "test.fired", config: { intervalSeconds: 0 } }],
|
|
181
|
+
});
|
|
182
|
+
const issues = collectDefinitionIssues(def, makeDeps());
|
|
183
|
+
expect(
|
|
184
|
+
issues.some(
|
|
185
|
+
(i) => i.path.join(".") === "triggers.0.config.intervalSeconds",
|
|
186
|
+
),
|
|
187
|
+
).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("validates configs nested inside a choose branch", () => {
|
|
191
|
+
const def = baseDefinition({
|
|
192
|
+
actions: [
|
|
193
|
+
{
|
|
194
|
+
choose: [
|
|
195
|
+
{
|
|
196
|
+
when: "true",
|
|
197
|
+
sequence: [
|
|
198
|
+
{
|
|
199
|
+
action: "test.log",
|
|
200
|
+
config: { message: "hi", level: "bogus" },
|
|
201
|
+
enabled: true,
|
|
202
|
+
continue_on_error: false,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
enabled: true,
|
|
208
|
+
continue_on_error: false,
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
});
|
|
212
|
+
const issues = collectDefinitionIssues(def, makeDeps());
|
|
213
|
+
expect(
|
|
214
|
+
issues.some(
|
|
215
|
+
(i) =>
|
|
216
|
+
i.path.join(".") === "actions.0.choose.0.sequence.0.config.level",
|
|
217
|
+
),
|
|
218
|
+
).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("returns structural issues for a malformed top-level shape", () => {
|
|
222
|
+
const issues = collectDefinitionIssues(
|
|
223
|
+
{ name: "", triggers: [] },
|
|
224
|
+
makeDeps(),
|
|
225
|
+
);
|
|
226
|
+
expect(issues.length).toBeGreaterThan(0);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("rejects a duplicate action id", () => {
|
|
230
|
+
const def = baseDefinition({
|
|
231
|
+
actions: [
|
|
232
|
+
{
|
|
233
|
+
id: "dup",
|
|
234
|
+
action: "test.log",
|
|
235
|
+
config: { message: "a", level: "info" },
|
|
236
|
+
enabled: true,
|
|
237
|
+
continue_on_error: false,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: "dup",
|
|
241
|
+
action: "test.log",
|
|
242
|
+
config: { message: "b", level: "info" },
|
|
243
|
+
enabled: true,
|
|
244
|
+
continue_on_error: false,
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
});
|
|
248
|
+
const issues = collectDefinitionIssues(def, makeDeps());
|
|
249
|
+
const dupIssue = issues.find(
|
|
250
|
+
(i) => i.path.join(".") === "actions.1.id",
|
|
251
|
+
);
|
|
252
|
+
expect(dupIssue?.message).toMatch(/must be unique/);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("rejects a producing action that has no id", () => {
|
|
256
|
+
const deps = makeProducerDeps();
|
|
257
|
+
const def = baseDefinition({
|
|
258
|
+
actions: [
|
|
259
|
+
{
|
|
260
|
+
action: "test.create",
|
|
261
|
+
config: {},
|
|
262
|
+
enabled: true,
|
|
263
|
+
continue_on_error: false,
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
});
|
|
267
|
+
const issues = collectDefinitionIssues(def, deps);
|
|
268
|
+
const idIssue = issues.find((i) => i.path.join(".") === "actions.0.id");
|
|
269
|
+
expect(idIssue?.message).toMatch(/must have an id/);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("accepts a producing action that has an id", () => {
|
|
273
|
+
const deps = makeProducerDeps();
|
|
274
|
+
const def = baseDefinition({
|
|
275
|
+
actions: [
|
|
276
|
+
{
|
|
277
|
+
id: "make_thing",
|
|
278
|
+
action: "test.create",
|
|
279
|
+
config: {},
|
|
280
|
+
enabled: true,
|
|
281
|
+
continue_on_error: false,
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
});
|
|
285
|
+
const issues = collectDefinitionIssues(def, deps);
|
|
286
|
+
expect(issues).toEqual([]);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("rejects a hyphenated action id via the structural pass", () => {
|
|
290
|
+
const def = baseDefinition({
|
|
291
|
+
actions: [
|
|
292
|
+
{
|
|
293
|
+
id: "bad-id",
|
|
294
|
+
action: "test.log",
|
|
295
|
+
config: { message: "hi", level: "info" },
|
|
296
|
+
enabled: true,
|
|
297
|
+
continue_on_error: false,
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
});
|
|
301
|
+
const issues = collectDefinitionIssues(def, makeDeps());
|
|
302
|
+
expect(
|
|
303
|
+
issues.some((i) => i.path.includes("id")),
|
|
304
|
+
).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep validation of an automation definition.
|
|
3
|
+
*
|
|
4
|
+
* `AutomationDefinitionSchema` only validates the structural shape —
|
|
5
|
+
* action `config` is typed as `z.record(z.unknown())`, so it never
|
|
6
|
+
* checks a provider action's config against that action's own schema.
|
|
7
|
+
* This walker fills the gap so the editor can surface *any* wrong
|
|
8
|
+
* content, not just structural errors:
|
|
9
|
+
*
|
|
10
|
+
* - unknown trigger `event` / action `action` ids,
|
|
11
|
+
* - per-trigger `config` that violates the trigger's `configSchema`,
|
|
12
|
+
* - per-action `config` that violates the action's config schema
|
|
13
|
+
* (wrong enum value, missing required field, wrong type, AND —
|
|
14
|
+
* because we validate in strict mode — unknown/typo'd keys).
|
|
15
|
+
*
|
|
16
|
+
* Returned issue `path`s are dot-joinable for display, e.g.
|
|
17
|
+
* `actions.0.config.level` or `triggers.1.event`.
|
|
18
|
+
*/
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import {
|
|
21
|
+
AutomationDefinitionSchema,
|
|
22
|
+
type ActionInput,
|
|
23
|
+
type AutomationDefinition,
|
|
24
|
+
} from "@checkstack/automation-common";
|
|
25
|
+
import type { ActionRegistry } from "./action-registry";
|
|
26
|
+
import type { TriggerRegistry } from "./trigger-registry";
|
|
27
|
+
|
|
28
|
+
export interface DefinitionIssue {
|
|
29
|
+
path: Array<string | number>;
|
|
30
|
+
message: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ValidateDefinitionDeps {
|
|
34
|
+
triggerRegistry: TriggerRegistry;
|
|
35
|
+
actionRegistry: ActionRegistry;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate a definition both structurally and semantically. Returns an
|
|
40
|
+
* empty array when the definition is fully valid.
|
|
41
|
+
*
|
|
42
|
+
* Structural errors short-circuit the semantic pass: if the top-level
|
|
43
|
+
* shape is wrong we can't reliably walk the action tree, so we return
|
|
44
|
+
* the structural issues alone and let the operator fix those first.
|
|
45
|
+
*/
|
|
46
|
+
export function collectDefinitionIssues(
|
|
47
|
+
definition: unknown,
|
|
48
|
+
deps: ValidateDefinitionDeps,
|
|
49
|
+
): DefinitionIssue[] {
|
|
50
|
+
const parsed = AutomationDefinitionSchema.safeParse(definition);
|
|
51
|
+
if (!parsed.success) {
|
|
52
|
+
return parsed.error.issues.map((issue) => ({
|
|
53
|
+
path: issue.path.map((segment) => toPathSegment(segment)),
|
|
54
|
+
message: issue.message,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const issues: DefinitionIssue[] = [];
|
|
59
|
+
validateTriggers(parsed.data, deps, issues);
|
|
60
|
+
validateActionList(parsed.data.actions, ["actions"], deps, issues);
|
|
61
|
+
validateActionIds(parsed.data.actions, ["actions"], deps, issues);
|
|
62
|
+
return issues;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Semantic action-id validation ──────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Walk the entire action tree and enforce the two artifact-reference
|
|
69
|
+
* invariants the structural zod pass can't:
|
|
70
|
+
*
|
|
71
|
+
* 1. Every action `id` is unique within the automation (so
|
|
72
|
+
* `artifacts.<id>.<name>` is unambiguous).
|
|
73
|
+
* 2. Any provider action whose registered action declares a truthy
|
|
74
|
+
* `produces` MUST carry an `id` (so the produced artifact is
|
|
75
|
+
* referenceable).
|
|
76
|
+
*
|
|
77
|
+
* Identifier-format is already enforced by the zod schema in the
|
|
78
|
+
* structural pass, so we don't re-check it here.
|
|
79
|
+
*/
|
|
80
|
+
function validateActionIds(
|
|
81
|
+
actions: ActionInput[],
|
|
82
|
+
basePath: Array<string | number>,
|
|
83
|
+
deps: ValidateDefinitionDeps,
|
|
84
|
+
issues: DefinitionIssue[],
|
|
85
|
+
): void {
|
|
86
|
+
const seen = new Set<string>();
|
|
87
|
+
walkActionIds(actions, basePath, deps, seen, issues);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function walkActionIds(
|
|
91
|
+
actions: ActionInput[],
|
|
92
|
+
basePath: Array<string | number>,
|
|
93
|
+
deps: ValidateDefinitionDeps,
|
|
94
|
+
seen: Set<string>,
|
|
95
|
+
issues: DefinitionIssue[],
|
|
96
|
+
): void {
|
|
97
|
+
for (const [index, action] of actions.entries()) {
|
|
98
|
+
walkActionId(action, [...basePath, index], deps, seen, issues);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function walkActionId(
|
|
103
|
+
action: ActionInput,
|
|
104
|
+
path: Array<string | number>,
|
|
105
|
+
deps: ValidateDefinitionDeps,
|
|
106
|
+
seen: Set<string>,
|
|
107
|
+
issues: DefinitionIssue[],
|
|
108
|
+
): void {
|
|
109
|
+
if (typeof action.id === "string") {
|
|
110
|
+
if (seen.has(action.id)) {
|
|
111
|
+
issues.push({
|
|
112
|
+
path: [...path, "id"],
|
|
113
|
+
message: `Action id "${action.id}" must be unique within the automation`,
|
|
114
|
+
});
|
|
115
|
+
} else {
|
|
116
|
+
seen.add(action.id);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if ("action" in action) {
|
|
121
|
+
const registered = deps.actionRegistry.getAction(action.action);
|
|
122
|
+
if (registered?.produces && !action.id) {
|
|
123
|
+
issues.push({
|
|
124
|
+
path: [...path, "id"],
|
|
125
|
+
message:
|
|
126
|
+
"Actions that produce an artifact must have an id so the artifact can be referenced as artifacts.<id>.<name>",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if ("choose" in action) {
|
|
133
|
+
for (const [branchIndex, branch] of action.choose.entries()) {
|
|
134
|
+
walkActionIds(
|
|
135
|
+
branch.sequence,
|
|
136
|
+
[...path, "choose", branchIndex, "sequence"],
|
|
137
|
+
deps,
|
|
138
|
+
seen,
|
|
139
|
+
issues,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (action.else) {
|
|
143
|
+
walkActionIds(action.else, [...path, "else"], deps, seen, issues);
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if ("parallel" in action) {
|
|
149
|
+
walkActionIds(action.parallel, [...path, "parallel"], deps, seen, issues);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if ("repeat" in action) {
|
|
154
|
+
walkActionIds(
|
|
155
|
+
action.repeat.sequence,
|
|
156
|
+
[...path, "repeat", "sequence"],
|
|
157
|
+
deps,
|
|
158
|
+
seen,
|
|
159
|
+
issues,
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if ("sequence" in action) {
|
|
165
|
+
walkActionIds(action.sequence, [...path, "sequence"], deps, seen, issues);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// delay / variables / condition / stop / wait_for_trigger have no child
|
|
170
|
+
// action lists and don't produce artifacts — nothing more to walk.
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function validateTriggers(
|
|
174
|
+
definition: AutomationDefinition,
|
|
175
|
+
deps: ValidateDefinitionDeps,
|
|
176
|
+
issues: DefinitionIssue[],
|
|
177
|
+
): void {
|
|
178
|
+
for (const [index, trigger] of definition.triggers.entries()) {
|
|
179
|
+
const registered = deps.triggerRegistry.getTrigger(trigger.event);
|
|
180
|
+
if (!registered) {
|
|
181
|
+
issues.push({
|
|
182
|
+
path: ["triggers", index, "event"],
|
|
183
|
+
message: `Unknown trigger event "${trigger.event}"`,
|
|
184
|
+
});
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (registered.configSchema) {
|
|
188
|
+
const result = strictParse(registered.configSchema, trigger.config ?? {});
|
|
189
|
+
if (!result.success) {
|
|
190
|
+
pushZodIssues(result.error, ["triggers", index, "config"], issues);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function validateActionList(
|
|
197
|
+
actions: ActionInput[],
|
|
198
|
+
basePath: Array<string | number>,
|
|
199
|
+
deps: ValidateDefinitionDeps,
|
|
200
|
+
issues: DefinitionIssue[],
|
|
201
|
+
): void {
|
|
202
|
+
for (const [index, action] of actions.entries()) {
|
|
203
|
+
validateAction(action, [...basePath, index], deps, issues);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function validateAction(
|
|
208
|
+
action: ActionInput,
|
|
209
|
+
path: Array<string | number>,
|
|
210
|
+
deps: ValidateDefinitionDeps,
|
|
211
|
+
issues: DefinitionIssue[],
|
|
212
|
+
): void {
|
|
213
|
+
if ("action" in action) {
|
|
214
|
+
const registered = deps.actionRegistry.getAction(action.action);
|
|
215
|
+
if (!registered) {
|
|
216
|
+
issues.push({
|
|
217
|
+
path: [...path, "action"],
|
|
218
|
+
message: `Unknown action "${action.action}"`,
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const result = strictParse(registered.config.schema, action.config);
|
|
223
|
+
if (!result.success) {
|
|
224
|
+
pushZodIssues(result.error, [...path, "config"], issues);
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if ("choose" in action) {
|
|
230
|
+
for (const [branchIndex, branch] of action.choose.entries()) {
|
|
231
|
+
validateActionList(
|
|
232
|
+
branch.sequence,
|
|
233
|
+
[...path, "choose", branchIndex, "sequence"],
|
|
234
|
+
deps,
|
|
235
|
+
issues,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (action.else) {
|
|
239
|
+
validateActionList(action.else, [...path, "else"], deps, issues);
|
|
240
|
+
}
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if ("parallel" in action) {
|
|
245
|
+
validateActionList(action.parallel, [...path, "parallel"], deps, issues);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if ("repeat" in action) {
|
|
250
|
+
validateActionList(
|
|
251
|
+
action.repeat.sequence,
|
|
252
|
+
[...path, "repeat", "sequence"],
|
|
253
|
+
deps,
|
|
254
|
+
issues,
|
|
255
|
+
);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if ("sequence" in action) {
|
|
260
|
+
validateActionList(action.sequence, [...path, "sequence"], deps, issues);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// delay / variables / condition / stop / wait_for_trigger carry no
|
|
265
|
+
// provider config to deep-validate — their structure is already fully
|
|
266
|
+
// covered by AutomationDefinitionSchema.
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Parse against a schema in strict mode when it's a plain object schema,
|
|
271
|
+
* so unknown / typo'd config keys are reported rather than silently
|
|
272
|
+
* stripped. Non-object schemas (unions, records, primitives) fall back
|
|
273
|
+
* to a normal parse.
|
|
274
|
+
*/
|
|
275
|
+
function strictParse(schema: z.ZodType<unknown>, value: unknown) {
|
|
276
|
+
if (schema instanceof z.ZodObject) {
|
|
277
|
+
return schema.strict().safeParse(value);
|
|
278
|
+
}
|
|
279
|
+
return schema.safeParse(value);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function pushZodIssues(
|
|
283
|
+
error: z.ZodError,
|
|
284
|
+
basePath: Array<string | number>,
|
|
285
|
+
issues: DefinitionIssue[],
|
|
286
|
+
): void {
|
|
287
|
+
for (const issue of error.issues) {
|
|
288
|
+
issues.push({
|
|
289
|
+
path: [...basePath, ...issue.path.map((segment) => toPathSegment(segment))],
|
|
290
|
+
message: issue.message,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Zod issue paths are `PropertyKey[]` (string | number | symbol). The
|
|
297
|
+
* automation contract's issue path is `(string | number)[]`, so coerce
|
|
298
|
+
* the rare symbol segment to its string form.
|
|
299
|
+
*/
|
|
300
|
+
function toPathSegment(segment: PropertyKey): string | number {
|
|
301
|
+
return typeof segment === "number" || typeof segment === "string"
|
|
302
|
+
? segment
|
|
303
|
+
: String(segment);
|
|
304
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@checkstack/tsconfig/backend.json",
|
|
3
|
+
"include": [
|
|
4
|
+
"src"
|
|
5
|
+
],
|
|
6
|
+
"references": [
|
|
7
|
+
{
|
|
8
|
+
"path": "../automation-common"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"path": "../backend-api"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"path": "../command-backend"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"path": "../common"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "../drizzle-helper"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "../integration-common"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "../notification-common"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"path": "../queue-api"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"path": "../signal-common"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"path": "../template-engine"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"path": "../test-utils-backend"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|