@checkstack/automation-common 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/src/schemas.ts ADDED
@@ -0,0 +1,682 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Schemas for the Automation platform.
5
+ *
6
+ * The data model mirrors Home Assistant's automation editor:
7
+ *
8
+ * - An **automation** has a list of **triggers** (event entry points),
9
+ * optional **conditions** that gate the whole run, and a list of
10
+ * **actions** (the work to do).
11
+ * - Each action is one of nine primitive types. The discriminator is the
12
+ * presence of a key (`action`, `choose`, `parallel`, `delay`, `repeat`,
13
+ * `variables`, `condition`, `stop`, `wait_for_trigger`) — there's no
14
+ * `kind:` field.
15
+ * - Conditions and templated values use the shared template engine
16
+ * (`@checkstack/template-engine`).
17
+ *
18
+ * The automation definition is the source of truth — stored as JSON in the
19
+ * `automations.definition` column, also round-tripped from YAML in the UI.
20
+ */
21
+
22
+ // ─── Trigger ─────────────────────────────────────────────────────────────
23
+
24
+ /**
25
+ * A trigger is an entry point for an automation. When the underlying event
26
+ * fires (and the filter passes), the automation runs.
27
+ *
28
+ * - `event` is the fully-qualified event id (e.g. `incident.incident.created`).
29
+ * - `id` is an optional discriminator the operator can reference in
30
+ * `choose: when` clauses (auto-derived from `event` when unique).
31
+ * - `filter` is an optional template returning truthy/falsy to gate the
32
+ * trigger itself before any action runs.
33
+ * - `config` is the trigger's own configuration — only used by triggers
34
+ * that need extra setup (cron pattern for `time.cron`, etc.).
35
+ */
36
+ export const TriggerSchema = z.object({
37
+ id: z
38
+ .string()
39
+ .min(1)
40
+ .max(64)
41
+ .regex(/^[a-z][a-z0-9_-]*$/i, "Trigger id must be a slug")
42
+ .optional()
43
+ .describe(
44
+ "Operator-assigned discriminator. Auto-derives from event when unique.",
45
+ ),
46
+ event: z
47
+ .string()
48
+ .min(1)
49
+ .describe("Fully qualified event id (pluginId.hookId)."),
50
+ filter: z
51
+ .string()
52
+ .optional()
53
+ .describe(
54
+ "Optional template returning truthy/falsy. Gates the trigger before the run starts.",
55
+ ),
56
+ config: z
57
+ .record(z.string(), z.unknown())
58
+ .optional()
59
+ .describe(
60
+ "Per-trigger configuration (e.g. cron pattern for time.cron, interval seconds for time.interval).",
61
+ ),
62
+ });
63
+
64
+ export type Trigger = z.infer<typeof TriggerSchema>;
65
+
66
+ // ─── Condition (recursive) ────────────────────────────────────────────────
67
+
68
+ /**
69
+ * A condition is either:
70
+ * - A template string that evaluates truthy/falsy, OR
71
+ * - A `{ and | or | not }` combinator wrapping nested conditions.
72
+ *
73
+ * Recursive — uses `z.lazy` for the combinator branches.
74
+ */
75
+ export type ConditionInput =
76
+ | string
77
+ | { and: ConditionInput[] }
78
+ | { or: ConditionInput[] }
79
+ | { not: ConditionInput };
80
+
81
+ export const ConditionSchema: z.ZodType<ConditionInput> = z.lazy(() =>
82
+ z.union([
83
+ z
84
+ .string()
85
+ .min(1)
86
+ .describe("Template returning truthy/falsy."),
87
+ z.object({
88
+ and: z.array(ConditionSchema).min(1),
89
+ }),
90
+ z.object({
91
+ or: z.array(ConditionSchema).min(1),
92
+ }),
93
+ z.object({
94
+ not: ConditionSchema,
95
+ }),
96
+ ]),
97
+ );
98
+
99
+ export type Condition = z.infer<typeof ConditionSchema>;
100
+
101
+ // ─── Action primitives (recursive discriminated union) ────────────────────
102
+
103
+ /**
104
+ * Base fields every action carries. `id` is referenced by downstream
105
+ * artifact lookups (`artifacts.<id>.<name>.<field>`) and by run-step audit
106
+ * logs. Every action that PRODUCES an artifact MUST have an `id` (enforced
107
+ * semantically in validate-definition, since the schema can't know which
108
+ * actions produce). `id` must be a valid identifier so it can be used as a
109
+ * plain template segment.
110
+ */
111
+ const ActionBase = {
112
+ id: z
113
+ .string()
114
+ .min(1)
115
+ .max(64)
116
+ .regex(
117
+ /^[a-zA-Z_][a-zA-Z0-9_]*$/,
118
+ "Action id must be a valid identifier (letters, digits, underscore; no leading digit)",
119
+ )
120
+ .optional(),
121
+ description: z.string().optional(),
122
+ enabled: z.boolean().default(true),
123
+ continue_on_error: z.boolean().default(false),
124
+ };
125
+
126
+ /**
127
+ * 1. Provider action — calls a registered action by namespaced id.
128
+ */
129
+ export const ProviderActionSchema = z.object({
130
+ ...ActionBase,
131
+ action: z
132
+ .string()
133
+ .min(1)
134
+ .regex(
135
+ /^[a-z][a-z0-9_-]*\.[a-z][a-z0-9_-]*$/i,
136
+ "Action id must be namespaced (plugin.action_name)",
137
+ ),
138
+ config: z.record(z.string(), z.unknown()).default({}),
139
+ });
140
+
141
+ export type ProviderAction = z.infer<typeof ProviderActionSchema>;
142
+
143
+ // Forward declaration for self-referential Action schemas via z.lazy.
144
+ // Exported so downstream packages (and the oRPC contract) can name them.
145
+ export type ActionInput =
146
+ | z.infer<typeof ProviderActionSchema>
147
+ | ChooseInput
148
+ | ParallelInput
149
+ | DelayInput
150
+ | RepeatInput
151
+ | VariablesInput
152
+ | ConditionGuardInput
153
+ | StopInput
154
+ | WaitForTriggerInput
155
+ | SequenceInput;
156
+
157
+ export interface ChooseInput {
158
+ id?: string;
159
+ description?: string;
160
+ enabled?: boolean;
161
+ continue_on_error?: boolean;
162
+ choose: Array<{ when: ConditionInput; sequence: ActionInput[] }>;
163
+ else?: ActionInput[];
164
+ }
165
+
166
+ export interface ParallelInput {
167
+ id?: string;
168
+ description?: string;
169
+ enabled?: boolean;
170
+ continue_on_error?: boolean;
171
+ parallel: ActionInput[];
172
+ }
173
+
174
+ export interface DelayInput {
175
+ id?: string;
176
+ description?: string;
177
+ enabled?: boolean;
178
+ continue_on_error?: boolean;
179
+ delay: { seconds: number } | { template: string };
180
+ }
181
+
182
+ export type RepeatMode =
183
+ | { count: number }
184
+ | { for_each: string }
185
+ | { while: string; max_iterations?: number }
186
+ | { until: string; max_iterations?: number };
187
+
188
+ export interface RepeatInput {
189
+ id?: string;
190
+ description?: string;
191
+ enabled?: boolean;
192
+ continue_on_error?: boolean;
193
+ repeat: RepeatMode & { sequence: ActionInput[] };
194
+ }
195
+
196
+ export interface VariablesInput {
197
+ id?: string;
198
+ description?: string;
199
+ enabled?: boolean;
200
+ continue_on_error?: boolean;
201
+ variables: Record<string, unknown>;
202
+ }
203
+
204
+ export interface ConditionGuardInput {
205
+ id?: string;
206
+ description?: string;
207
+ enabled?: boolean;
208
+ continue_on_error?: boolean;
209
+ condition: ConditionInput;
210
+ }
211
+
212
+ export interface StopInput {
213
+ id?: string;
214
+ description?: string;
215
+ enabled?: boolean;
216
+ continue_on_error?: boolean;
217
+ stop: { reason?: string; error?: boolean };
218
+ }
219
+
220
+ export interface WaitForTriggerInput {
221
+ id?: string;
222
+ description?: string;
223
+ enabled?: boolean;
224
+ continue_on_error?: boolean;
225
+ wait_for_trigger: {
226
+ event: string;
227
+ filter?: string;
228
+ timeout_seconds?: number;
229
+ context_key?: string;
230
+ };
231
+ }
232
+
233
+ export interface SequenceInput {
234
+ id?: string;
235
+ description?: string;
236
+ enabled?: boolean;
237
+ continue_on_error?: boolean;
238
+ sequence: ActionInput[];
239
+ }
240
+
241
+ /**
242
+ * 2. Choose — if/elif/else branching.
243
+ */
244
+ export const ChooseActionSchema: z.ZodType<ChooseInput> = z.lazy(() =>
245
+ z.object({
246
+ ...ActionBase,
247
+ choose: z
248
+ .array(
249
+ z.object({
250
+ when: ConditionSchema,
251
+ sequence: z.array(ActionSchema).min(1),
252
+ }),
253
+ )
254
+ .min(1),
255
+ else: z.array(ActionSchema).optional(),
256
+ }),
257
+ );
258
+
259
+ /**
260
+ * 3. Parallel — fan out actions concurrently; wait for all.
261
+ */
262
+ export const ParallelActionSchema: z.ZodType<ParallelInput> = z.lazy(() =>
263
+ z.object({
264
+ ...ActionBase,
265
+ parallel: z.array(ActionSchema).min(1),
266
+ }),
267
+ );
268
+
269
+ /**
270
+ * 4. Delay — sleep for a fixed or templated number of seconds.
271
+ *
272
+ * Either a plain number of seconds OR a template that renders to one.
273
+ */
274
+ export const DelayActionSchema = z.object({
275
+ ...ActionBase,
276
+ delay: z.union([
277
+ z.object({ seconds: z.number().int().min(0).max(86_400) }),
278
+ z.object({ template: z.string().min(1) }),
279
+ ]),
280
+ });
281
+
282
+ /**
283
+ * 5. Repeat — count / for_each / while / until modes.
284
+ *
285
+ * - `count` runs the sequence N times. `repeat.index` is exposed.
286
+ * - `for_each` is a template that renders to a JSON array; the sequence
287
+ * runs once per item, with `repeat.item` and `repeat.index` exposed.
288
+ * - `while` evaluates a condition before each iteration; loop stops when
289
+ * it goes false. `max_iterations` defaults to 1000 as a safety net.
290
+ * - `until` evaluates after each iteration; loop stops when it goes true.
291
+ */
292
+ export const RepeatActionSchema: z.ZodType<RepeatInput> = z.lazy(() =>
293
+ z.object({
294
+ ...ActionBase,
295
+ repeat: z.union([
296
+ z.object({
297
+ count: z.number().int().min(1).max(10_000),
298
+ sequence: z.array(ActionSchema).min(1),
299
+ }),
300
+ z.object({
301
+ for_each: z.string().min(1),
302
+ sequence: z.array(ActionSchema).min(1),
303
+ }),
304
+ z.object({
305
+ while: z.string().min(1),
306
+ max_iterations: z.number().int().min(1).max(10_000).optional(),
307
+ sequence: z.array(ActionSchema).min(1),
308
+ }),
309
+ z.object({
310
+ until: z.string().min(1),
311
+ max_iterations: z.number().int().min(1).max(10_000).optional(),
312
+ sequence: z.array(ActionSchema).min(1),
313
+ }),
314
+ ]),
315
+ }),
316
+ );
317
+
318
+ /**
319
+ * 6. Variables — define local scoped values for downstream actions.
320
+ *
321
+ * Values can be literals or templates. Templates render at execution time
322
+ * and the rendered value is stored under the variable name in scope.
323
+ */
324
+ export const VariablesActionSchema = z.object({
325
+ ...ActionBase,
326
+ variables: z.record(z.string().min(1), z.unknown()).refine(
327
+ (record) => Object.keys(record).length > 0,
328
+ { message: "At least one variable required" },
329
+ ),
330
+ });
331
+
332
+ /**
333
+ * 7. Condition (guard) — mid-run assertion. If false, the run halts
334
+ * (unless `continue_on_error: true`).
335
+ */
336
+ export const ConditionGuardActionSchema: z.ZodType<ConditionGuardInput> = z.lazy(
337
+ () =>
338
+ z.object({
339
+ ...ActionBase,
340
+ condition: ConditionSchema,
341
+ }),
342
+ );
343
+
344
+ /**
345
+ * 8. Stop — explicit halt with optional reason and error flag.
346
+ */
347
+ export const StopActionSchema = z.object({
348
+ ...ActionBase,
349
+ stop: z.object({
350
+ reason: z.string().optional(),
351
+ error: z.boolean().default(false),
352
+ }),
353
+ });
354
+
355
+ /**
356
+ * 10. Sequence — wrap an ordered list of actions as a single action.
357
+ *
358
+ * Mirrors HA's `sequence:` wrapping. Primary use case: providing a
359
+ * multi-action branch inside `parallel` (each parallel branch is itself
360
+ * an Action, so without this wrapper you'd be limited to one action per
361
+ * branch). Also useful when an `id` / `continue_on_error` should apply
362
+ * to a group of actions atomically.
363
+ *
364
+ * Implementation: identical to walking a top-level `actions:` list.
365
+ */
366
+ export const SequenceActionSchema: z.ZodType<SequenceInput> = z.lazy(() =>
367
+ z.object({
368
+ ...ActionBase,
369
+ sequence: z.array(ActionSchema).min(1),
370
+ }),
371
+ );
372
+
373
+ /**
374
+ * 9. Wait for trigger — suspend the run until a matching event arrives.
375
+ *
376
+ * `context_key` defaults to the trigger event's declared context key
377
+ * (typically `incidentId`), so a wait inside an incident.created run
378
+ * matches the incident.resolved event for the same incident.
379
+ */
380
+ export const WaitForTriggerActionSchema = z.object({
381
+ ...ActionBase,
382
+ wait_for_trigger: z.object({
383
+ event: z.string().min(1),
384
+ filter: z.string().optional(),
385
+ timeout_seconds: z
386
+ .number()
387
+ .int()
388
+ .min(1)
389
+ .max(60 * 60 * 24 * 30) // 30 days
390
+ .optional(),
391
+ context_key: z.string().optional(),
392
+ }),
393
+ });
394
+
395
+ /**
396
+ * The discriminated union of all 9 action primitives. Discrimination is by
397
+ * presence of a key (action / choose / parallel / etc.), matching the
398
+ * YAML-friendly authoring style.
399
+ *
400
+ * Implemented as `z.union` (zod's `discriminatedUnion` requires a single
401
+ * tag field, which we don't use). The dispatch engine narrows manually.
402
+ */
403
+ export const ActionSchema: z.ZodType<ActionInput> = z.lazy(() =>
404
+ z.union([
405
+ ProviderActionSchema,
406
+ ChooseActionSchema,
407
+ ParallelActionSchema,
408
+ DelayActionSchema,
409
+ RepeatActionSchema,
410
+ VariablesActionSchema,
411
+ ConditionGuardActionSchema,
412
+ StopActionSchema,
413
+ WaitForTriggerActionSchema,
414
+ SequenceActionSchema,
415
+ ]),
416
+ );
417
+
418
+ export type Action = z.infer<typeof ActionSchema>;
419
+
420
+ // ─── Execution mode ───────────────────────────────────────────────────────
421
+
422
+ /**
423
+ * What to do when a trigger fires while a previous run of the same
424
+ * automation is still in progress.
425
+ *
426
+ * - `single`: skip the new trigger (default; safest for stateful flows).
427
+ * - `parallel`: start a new run alongside; runs are independent.
428
+ * - `queued`: queue the new trigger; process after current completes.
429
+ * - `restart`: abort the in-flight run, start fresh.
430
+ */
431
+ export const AutomationModeSchema = z
432
+ .enum(["single", "parallel", "queued", "restart"])
433
+ .default("single");
434
+
435
+ export type AutomationMode = z.infer<typeof AutomationModeSchema>;
436
+
437
+ // ─── Automation definition (top-level) ────────────────────────────────────
438
+
439
+ /**
440
+ * The full definition of an automation. Persisted as JSON in
441
+ * `automations.definition`. Round-trippable to YAML.
442
+ */
443
+ export const AutomationDefinitionSchema = z.object({
444
+ /** Operator-visible name. */
445
+ name: z.string().min(1).max(200),
446
+ /** Optional description for the UI / docs. */
447
+ description: z.string().optional(),
448
+ /** Entry points. At least one required. */
449
+ triggers: z.array(TriggerSchema).min(1),
450
+ /** Pre-run gate conditions. All must pass for the actions to run. */
451
+ conditions: z.array(ConditionSchema).default([]),
452
+ /** Ordered list of actions. */
453
+ actions: z.array(ActionSchema).default([]),
454
+ /** Concurrency mode. */
455
+ mode: AutomationModeSchema,
456
+ /** Max parallel runs (only meaningful in parallel/queued modes). */
457
+ max_runs: z.number().int().min(1).max(1000).default(10),
458
+ });
459
+
460
+ export type AutomationDefinition = z.infer<typeof AutomationDefinitionSchema>;
461
+
462
+ // ─── Automation persistence row schemas (API surface) ─────────────────────
463
+
464
+ export const AutomationStatusSchema = z.enum(["enabled", "disabled"]);
465
+ export type AutomationStatus = z.infer<typeof AutomationStatusSchema>;
466
+
467
+ export const AutomationSchema = z.object({
468
+ id: z.string(),
469
+ name: z.string(),
470
+ description: z.string().optional(),
471
+ status: AutomationStatusSchema,
472
+ definition: AutomationDefinitionSchema,
473
+ managedBy: z.string().optional().describe("GitOps provider id when managed declaratively"),
474
+ createdAt: z.coerce.date(),
475
+ updatedAt: z.coerce.date(),
476
+ });
477
+
478
+ export type Automation = z.infer<typeof AutomationSchema>;
479
+
480
+ // ─── Run state ────────────────────────────────────────────────────────────
481
+
482
+ export const RunStatusSchema = z.enum([
483
+ "pending",
484
+ "running",
485
+ "waiting",
486
+ "success",
487
+ "failed",
488
+ "cancelled",
489
+ "skipped",
490
+ ]);
491
+
492
+ export type RunStatus = z.infer<typeof RunStatusSchema>;
493
+
494
+ export const StepStatusSchema = z.enum([
495
+ "pending",
496
+ "running",
497
+ "success",
498
+ "failed",
499
+ "skipped",
500
+ "waiting",
501
+ ]);
502
+
503
+ export type StepStatus = z.infer<typeof StepStatusSchema>;
504
+
505
+ export const AutomationRunSchema = z.object({
506
+ id: z.string(),
507
+ automationId: z.string(),
508
+ triggerId: z.string().describe("Which trigger fired this run"),
509
+ triggerEventId: z.string(),
510
+ triggerPayload: z.record(z.string(), z.unknown()),
511
+ contextKey: z
512
+ .string()
513
+ .nullable()
514
+ .describe("Durable key linking this run to a domain entity (e.g. incidentId)"),
515
+ status: RunStatusSchema,
516
+ errorMessage: z.string().optional(),
517
+ startedAt: z.coerce.date(),
518
+ finishedAt: z.coerce.date().optional(),
519
+ });
520
+
521
+ export type AutomationRun = z.infer<typeof AutomationRunSchema>;
522
+
523
+ export const AutomationRunStepSchema = z.object({
524
+ id: z.string(),
525
+ runId: z.string(),
526
+ /** Hierarchical path of the action in the action tree, e.g. "actions[0].choose[1].then[2]". */
527
+ actionPath: z.string(),
528
+ /** Action id when the user assigned one, else null. */
529
+ actionId: z.string().nullable(),
530
+ /** Action kind discriminator: "action" | "choose" | "parallel" | ... */
531
+ actionKind: z.string(),
532
+ /** For provider actions, the resolved action id (e.g. "jira.create_issue"). */
533
+ providerActionId: z.string().nullable(),
534
+ status: StepStatusSchema,
535
+ attempts: z.number().int(),
536
+ errorMessage: z.string().optional(),
537
+ resultPayload: z
538
+ .record(z.string(), z.unknown())
539
+ .optional()
540
+ .describe("Result data returned by the action — typically the artifact."),
541
+ startedAt: z.coerce.date(),
542
+ finishedAt: z.coerce.date().optional(),
543
+ });
544
+
545
+ export type AutomationRunStep = z.infer<typeof AutomationRunStepSchema>;
546
+
547
+ // ─── Artifact ─────────────────────────────────────────────────────────────
548
+
549
+ /**
550
+ * A persisted artifact — what an action produced in an external system,
551
+ * recorded so future actions in the same automation (or future runs) can
552
+ * find and act on it.
553
+ *
554
+ * Keyed by `(automationId, contextKey, artifactType)` for the common case
555
+ * and additionally indexed by `actionId` when the operator assigned one.
556
+ */
557
+ export const AutomationArtifactSchema = z.object({
558
+ id: z.string(),
559
+ automationId: z.string(),
560
+ runId: z.string(),
561
+ /** Step that produced this artifact. */
562
+ stepId: z.string(),
563
+ /** Optional operator-assigned action id (preferred lookup key). */
564
+ actionId: z.string().nullable(),
565
+ /** Provider-declared artifact type id (e.g. `jira.issue`, `teams.message`). */
566
+ artifactType: z.string(),
567
+ /** Free-form payload — the artifact data. */
568
+ data: z.record(z.string(), z.unknown()),
569
+ /** Durable lookup key (typically `incidentId`). */
570
+ contextKey: z.string().nullable(),
571
+ /** When the upstream artifact was closed/resolved (set by close actions). */
572
+ closedAt: z.coerce.date().optional(),
573
+ createdAt: z.coerce.date(),
574
+ });
575
+
576
+ export type AutomationArtifact = z.infer<typeof AutomationArtifactSchema>;
577
+
578
+ // ─── API inputs ──────────────────────────────────────────────────────────
579
+
580
+ export const CreateAutomationInputSchema = z.object({
581
+ name: z.string().min(1).max(200),
582
+ description: z.string().optional(),
583
+ status: AutomationStatusSchema.default("enabled"),
584
+ definition: AutomationDefinitionSchema,
585
+ });
586
+
587
+ export type CreateAutomationInput = z.infer<typeof CreateAutomationInputSchema>;
588
+
589
+ export const UpdateAutomationInputSchema = z.object({
590
+ id: z.string(),
591
+ name: z.string().min(1).max(200).optional(),
592
+ description: z.string().optional(),
593
+ status: AutomationStatusSchema.optional(),
594
+ definition: AutomationDefinitionSchema.optional(),
595
+ });
596
+
597
+ export type UpdateAutomationInput = z.infer<typeof UpdateAutomationInputSchema>;
598
+
599
+ export const ValidateDefinitionInputSchema = z.object({
600
+ definition: z.unknown(),
601
+ });
602
+
603
+ export const ValidateDefinitionResultSchema = z.object({
604
+ valid: z.boolean(),
605
+ errors: z.array(
606
+ z.object({
607
+ path: z.array(z.union([z.string(), z.number()])),
608
+ message: z.string(),
609
+ }),
610
+ ),
611
+ });
612
+
613
+ export type ValidateDefinitionResult = z.infer<typeof ValidateDefinitionResultSchema>;
614
+
615
+ export const ListRunsInputSchema = z.object({
616
+ automationId: z.string().optional(),
617
+ status: RunStatusSchema.optional(),
618
+ limit: z.number().int().min(1).max(200).default(50),
619
+ offset: z.number().int().min(0).default(0),
620
+ });
621
+
622
+ export const ManualRunInputSchema = z.object({
623
+ automationId: z.string(),
624
+ triggerId: z.string().optional(),
625
+ payload: z.record(z.string(), z.unknown()).default({}),
626
+ });
627
+
628
+ // ─── Plugin-contributed trigger / action / artifact info (registry API) ──
629
+
630
+ /**
631
+ * Wire format for "what triggers does this plugin offer" listing endpoints.
632
+ */
633
+ export const TriggerInfoSchema = z.object({
634
+ qualifiedId: z.string(),
635
+ displayName: z.string(),
636
+ description: z.string().optional(),
637
+ category: z.string().default("Uncategorized"),
638
+ ownerPluginId: z.string(),
639
+ payloadSchema: z.record(z.string(), z.unknown()).describe("JSON Schema"),
640
+ configSchema: z
641
+ .record(z.string(), z.unknown())
642
+ .optional()
643
+ .describe("JSON Schema, when the trigger needs configuration"),
644
+ contextKey: z
645
+ .string()
646
+ .optional()
647
+ .describe("Default context key path inside the payload (e.g. 'incidentId')"),
648
+ });
649
+
650
+ export type TriggerInfo = z.infer<typeof TriggerInfoSchema>;
651
+
652
+ export const ActionInfoSchema = z.object({
653
+ qualifiedId: z.string(),
654
+ displayName: z.string(),
655
+ description: z.string().optional(),
656
+ category: z.string().default("Uncategorized"),
657
+ ownerPluginId: z.string(),
658
+ configSchema: z.record(z.string(), z.unknown()),
659
+ produces: z.string().optional().describe("Artifact type id this action produces, if any"),
660
+ consumes: z
661
+ .array(z.string())
662
+ .default([])
663
+ .describe("Artifact type ids this action consumes"),
664
+ connectionProviderId: z
665
+ .string()
666
+ .optional()
667
+ .describe(
668
+ "Fully-qualified integration provider id whose connection store + option resolvers supply this action's config dropdowns",
669
+ ),
670
+ });
671
+
672
+ export type ActionInfo = z.infer<typeof ActionInfoSchema>;
673
+
674
+ export const ArtifactTypeInfoSchema = z.object({
675
+ qualifiedId: z.string(),
676
+ displayName: z.string(),
677
+ description: z.string().optional(),
678
+ ownerPluginId: z.string(),
679
+ schema: z.record(z.string(), z.unknown()).describe("JSON Schema for the data field"),
680
+ });
681
+
682
+ export type ArtifactTypeInfo = z.infer<typeof ArtifactTypeInfoSchema>;
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { SHELL_ENV_PREFIX, toShellEnvKey } from "./shell-env";
3
+
4
+ describe("toShellEnvKey", () => {
5
+ it("prefixes, uppercases, and underscores a dotted scope path", () => {
6
+ expect(toShellEnvKey("trigger.payload.title")).toBe(
7
+ "CHECKSTACK_TRIGGER_PAYLOAD_TITLE",
8
+ );
9
+ });
10
+
11
+ it("collapses the dots inside a dotted artifact type", () => {
12
+ expect(toShellEnvKey("artifact.jira.issue.key")).toBe(
13
+ "CHECKSTACK_ARTIFACT_JIRA_ISSUE_KEY",
14
+ );
15
+ });
16
+
17
+ it("collapses any run of non-alphanumeric characters to a single underscore", () => {
18
+ expect(toShellEnvKey("var.my-weird key")).toBe("CHECKSTACK_VAR_MY_WEIRD_KEY");
19
+ });
20
+
21
+ it("trims leading and trailing separators", () => {
22
+ expect(toShellEnvKey(".trigger.event.")).toBe("CHECKSTACK_TRIGGER_EVENT");
23
+ });
24
+
25
+ it("uses the exported prefix constant", () => {
26
+ expect(toShellEnvKey("x").startsWith(SHELL_ENV_PREFIX)).toBe(true);
27
+ });
28
+ });