@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/CHANGELOG.md +442 -0
- package/package.json +33 -0
- package/src/access.ts +32 -0
- package/src/index.ts +8 -0
- package/src/plugin-metadata.ts +9 -0
- package/src/routes.ts +19 -0
- package/src/rpc-contract.ts +246 -0
- package/src/schemas.ts +682 -0
- package/src/shell-env.test.ts +28 -0
- package/src/shell-env.ts +33 -0
- package/src/signals.ts +58 -0
- package/src/variable-scope.test.ts +1045 -0
- package/src/variable-scope.ts +1029 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variable-scope resolver.
|
|
3
|
+
*
|
|
4
|
+
* Given an automation definition + the registry info for its triggers and
|
|
5
|
+
* actions + the path to a specific leaf action inside the definition, returns
|
|
6
|
+
* the set of template variables that are in scope at that point.
|
|
7
|
+
*
|
|
8
|
+
* The scope rules — kept deliberately conservative so the editor never
|
|
9
|
+
* over-promises:
|
|
10
|
+
*
|
|
11
|
+
* 1. `trigger.event` (string-literal union of subscribed trigger ids).
|
|
12
|
+
* 2. `trigger.payload.*` — union of every field across the subscribed
|
|
13
|
+
* triggers' payload JSON Schemas, with each leaf annotated by the
|
|
14
|
+
* set of triggers that contribute it. The `generateTypeDeclarations`
|
|
15
|
+
* utility consumes this to emit a discriminated union typed by
|
|
16
|
+
* `trigger.event`, so Monaco narrows correctly inside a branch that
|
|
17
|
+
* gates on a specific event id. The picker shows everything;
|
|
18
|
+
* conditional fields surface a `Only when …` hint.
|
|
19
|
+
* 3. `var.<name>` — accumulated from `variables:` actions that linearly
|
|
20
|
+
* precede the target action _in the same sequence slot_. We do NOT
|
|
21
|
+
* bubble variables out of a `choose` / `parallel` / `repeat` branch
|
|
22
|
+
* into the parent sequence, even though the runtime would keep them
|
|
23
|
+
* live, because the editor can't statically prove which branch ran.
|
|
24
|
+
* 4. `artifact.<actionId>.<localArtifactName>` — accumulated from
|
|
25
|
+
* provider actions with a declared `produces` (and an `id`) in the
|
|
26
|
+
* same sequence slot, with the same branch-isolation rule as
|
|
27
|
+
* variables. The runtime exposes the data at
|
|
28
|
+
* `artifacts.<actionId>.<localArtifactName>.<field>`.
|
|
29
|
+
* 5. `repeat.index` and (when the parent is `for_each`) `repeat.item` —
|
|
30
|
+
* whenever the path descends through a `repeat` container.
|
|
31
|
+
*
|
|
32
|
+
* The resolver is pure — no I/O, no side effects. Used by both the editor
|
|
33
|
+
* (for IntelliSense and the variable picker) and the validator (to flag
|
|
34
|
+
* references to undeclared variables).
|
|
35
|
+
*/
|
|
36
|
+
import type { Expr } from "@checkstack/template-engine";
|
|
37
|
+
import { parseCondition } from "@checkstack/template-engine";
|
|
38
|
+
import type {
|
|
39
|
+
ActionInfo,
|
|
40
|
+
ActionInput,
|
|
41
|
+
ArtifactTypeInfo,
|
|
42
|
+
AutomationDefinition,
|
|
43
|
+
ChooseInput,
|
|
44
|
+
ConditionInput,
|
|
45
|
+
ParallelInput,
|
|
46
|
+
ProviderAction,
|
|
47
|
+
RepeatInput,
|
|
48
|
+
SequenceInput,
|
|
49
|
+
Trigger,
|
|
50
|
+
TriggerInfo,
|
|
51
|
+
VariablesInput,
|
|
52
|
+
} from "./schemas";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Hierarchical entry in the resolved scope. Used by the variable picker
|
|
56
|
+
* for the tree view; the flat `path` is what gets inserted as `{{path}}`.
|
|
57
|
+
*/
|
|
58
|
+
export interface VariableEntry {
|
|
59
|
+
/** Dot-separated path used at the template site, e.g. `trigger.payload.systemId`. */
|
|
60
|
+
path: string;
|
|
61
|
+
/**
|
|
62
|
+
* Runtime-parseable `{{ }}` insertion form for this entry. Differs from
|
|
63
|
+
* `path` in two ways: the top-level namespace is the plural runtime
|
|
64
|
+
* context key (`var` → `variables`, `artifact` → `artifacts`; `trigger`,
|
|
65
|
+
* `repeat`, `now` already match the runtime context and stay as-is), and
|
|
66
|
+
* any segment that is not a valid template identifier (artifact ids with
|
|
67
|
+
* dots/hyphens, oddly-named variables, hyphenated payload keys) is
|
|
68
|
+
* emitted in bracket notation (`artifacts["integration-jira.issue"]`) so
|
|
69
|
+
* the template engine's tokenizer can read it. Consumers that need the
|
|
70
|
+
* text to actually insert into `{{ }}` should prefer this field and fall
|
|
71
|
+
* back to `path` only when it is absent.
|
|
72
|
+
*/
|
|
73
|
+
templateRef?: string;
|
|
74
|
+
/** Human-readable type label. */
|
|
75
|
+
type: string;
|
|
76
|
+
description?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Underlying JSON Schema fragment for this node, when known. The type
|
|
79
|
+
* declaration generator consumes this to emit a strongly-typed
|
|
80
|
+
* `declare const context` block for Monaco.
|
|
81
|
+
*/
|
|
82
|
+
jsonSchema?: Record<string, unknown>;
|
|
83
|
+
/** Nested entries for object types — populated for the picker's tree. */
|
|
84
|
+
children?: VariableEntry[];
|
|
85
|
+
/**
|
|
86
|
+
* Emit as an insertable field even when it has children — used for
|
|
87
|
+
* arrays, where both the whole array (`{{ ...tags }}`) and its elements
|
|
88
|
+
* (`{{ ...tags[0] }}`) are referenceable. Leaf entries (no children) are
|
|
89
|
+
* always insertable regardless of this flag.
|
|
90
|
+
*/
|
|
91
|
+
referenceable?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* When set, the entry only exists when `trigger.event` is one of these
|
|
94
|
+
* qualified ids. Populated on `trigger.payload.*` entries that come from
|
|
95
|
+
* a subset of the automation's subscribed triggers, so the picker can
|
|
96
|
+
* render an "Only when …" hint and the type generator can emit a
|
|
97
|
+
* discriminated-union shape.
|
|
98
|
+
*/
|
|
99
|
+
conditionalOnTriggers?: string[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface VariableScope {
|
|
103
|
+
/** Tree of in-scope entries, grouped under top-level namespaces. */
|
|
104
|
+
entries: VariableEntry[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Template-ref helpers ────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Whether `segment` is a bare identifier the template engine can read in dot
|
|
111
|
+
* notation. MIRRORS the template-engine tokenizer's identifier rule: a start
|
|
112
|
+
* character in `[A-Za-z_$]` followed by zero or more `[A-Za-z0-9_$]`. Anything
|
|
113
|
+
* else (dots, hyphens, leading digits, empty) must be bracketed instead.
|
|
114
|
+
*/
|
|
115
|
+
export function isTemplateIdentifier(segment: string): boolean {
|
|
116
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Append `segment` to `base`, producing a runtime-parseable `{{ }}` member
|
|
121
|
+
* access. Identifier segments use dot notation (`base.segment`); everything
|
|
122
|
+
* else uses bracket notation with a JSON-quoted string literal
|
|
123
|
+
* (`base["seg-ment"]`). `JSON.stringify` yields a valid double-quoted string
|
|
124
|
+
* literal — which the engine tokenizer supports — and safely escapes any
|
|
125
|
+
* dots, hyphens, or quotes inside the segment.
|
|
126
|
+
*/
|
|
127
|
+
export function appendTemplateSegment({
|
|
128
|
+
base,
|
|
129
|
+
segment,
|
|
130
|
+
}: {
|
|
131
|
+
base: string;
|
|
132
|
+
segment: string;
|
|
133
|
+
}): string {
|
|
134
|
+
return isTemplateIdentifier(segment)
|
|
135
|
+
? `${base}.${segment}`
|
|
136
|
+
: `${base}[${JSON.stringify(segment)}]`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Append a numeric array-element index to `base`, producing a
|
|
141
|
+
* runtime-parseable `{{ }}` index access (`base[0]`). The index is a bare
|
|
142
|
+
* number with NO quotes — the template engine's tokenizer reads a NUMBER
|
|
143
|
+
* inside the brackets and the renderer resolves it as an array index. This
|
|
144
|
+
* MUST match the tokenizer's bracket-number reconstruction byte-for-byte.
|
|
145
|
+
*/
|
|
146
|
+
export function appendArrayIndex({
|
|
147
|
+
base,
|
|
148
|
+
index,
|
|
149
|
+
}: {
|
|
150
|
+
base: string;
|
|
151
|
+
index: number;
|
|
152
|
+
}): string {
|
|
153
|
+
return `${base}[${index}]`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* One segment of a navigation path from the root of `definition.actions` to
|
|
158
|
+
* a particular leaf action.
|
|
159
|
+
*
|
|
160
|
+
* - `slot: "root"` is reserved for the first segment of the path; `index`
|
|
161
|
+
* is the index into `definition.actions`.
|
|
162
|
+
* - All other slots correspond to a child list inside a composite action.
|
|
163
|
+
* - When `slot === "choose-when"`, `whenIndex` identifies the when-branch.
|
|
164
|
+
*/
|
|
165
|
+
export interface ActionPathStep {
|
|
166
|
+
slot:
|
|
167
|
+
| "root"
|
|
168
|
+
| "choose-when"
|
|
169
|
+
| "choose-else"
|
|
170
|
+
| "parallel"
|
|
171
|
+
| "repeat"
|
|
172
|
+
| "sequence";
|
|
173
|
+
/** When `slot === "choose-when"`, which when-branch (index into `choose:`). */
|
|
174
|
+
whenIndex?: number;
|
|
175
|
+
/** Index of the step within the slot's child list. */
|
|
176
|
+
index: number;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export type ActionPath = ActionPathStep[];
|
|
180
|
+
|
|
181
|
+
export interface ResolveVariableScopeInput {
|
|
182
|
+
definition: AutomationDefinition;
|
|
183
|
+
triggers: TriggerInfo[];
|
|
184
|
+
actions: ActionInfo[];
|
|
185
|
+
artifactTypes: ArtifactTypeInfo[];
|
|
186
|
+
/** Position of the target action inside `definition.actions`. */
|
|
187
|
+
path: ActionPath;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
function getChildList(
|
|
193
|
+
action: ActionInput,
|
|
194
|
+
slot: ActionPathStep["slot"],
|
|
195
|
+
whenIndex: number | undefined,
|
|
196
|
+
): ActionInput[] {
|
|
197
|
+
if (slot === "root") return [];
|
|
198
|
+
if (slot === "choose-when") {
|
|
199
|
+
const choose = action as ChooseInput;
|
|
200
|
+
if (whenIndex === undefined) return [];
|
|
201
|
+
return choose.choose[whenIndex]?.sequence ?? [];
|
|
202
|
+
}
|
|
203
|
+
if (slot === "choose-else") {
|
|
204
|
+
const choose = action as ChooseInput;
|
|
205
|
+
return choose.else ?? [];
|
|
206
|
+
}
|
|
207
|
+
if (slot === "parallel") {
|
|
208
|
+
return (action as ParallelInput).parallel;
|
|
209
|
+
}
|
|
210
|
+
if (slot === "repeat") {
|
|
211
|
+
return (action as RepeatInput).repeat.sequence;
|
|
212
|
+
}
|
|
213
|
+
// sequence
|
|
214
|
+
return (action as SequenceInput).sequence;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isProviderAction(a: ActionInput): a is ProviderAction {
|
|
218
|
+
return typeof a === "object" && a !== null && "action" in a;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isVariablesAction(a: ActionInput): a is VariablesInput {
|
|
222
|
+
return typeof a === "object" && a !== null && "variables" in a;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isRepeatAction(a: ActionInput): a is RepeatInput {
|
|
226
|
+
return typeof a === "object" && a !== null && "repeat" in a;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function repeatMode(action: RepeatInput): "count" | "for_each" | "while" | "until" {
|
|
230
|
+
const r = action.repeat as Record<string, unknown>;
|
|
231
|
+
if ("for_each" in r) return "for_each";
|
|
232
|
+
if ("while" in r) return "while";
|
|
233
|
+
if ("until" in r) return "until";
|
|
234
|
+
return "count";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ─── Condition-aware trigger narrowing ─────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* When the path descends through a `choose-when`, the branch's `when:`
|
|
241
|
+
* condition may statically pin `trigger.event` to a specific id (or a
|
|
242
|
+
* specific subset). We use that to narrow `trigger.payload` from the
|
|
243
|
+
* full discriminated union down to the variants the operator is
|
|
244
|
+
* actually inside — so `{{ trigger.payload.title }}` works without the
|
|
245
|
+
* "Only when …" hint when the branch already gates on
|
|
246
|
+
* `trigger.event == "incident.created"`.
|
|
247
|
+
*
|
|
248
|
+
* Returns `undefined` when the condition doesn't tell us anything about
|
|
249
|
+
* `trigger.event` — in that case we keep all subscribed triggers in scope.
|
|
250
|
+
* Returns a `Set<string>` of event ids the condition allows otherwise.
|
|
251
|
+
*
|
|
252
|
+
* Pattern coverage (conservative — anything outside this list falls back
|
|
253
|
+
* to "no narrowing"):
|
|
254
|
+
*
|
|
255
|
+
* - `trigger.event == "X"` (either operand order) → {X}
|
|
256
|
+
* - `trigger.event != "X"` → all-minus-{X}, but only when we know the
|
|
257
|
+
* full universe (the caller passes `allTriggers`). When the universe
|
|
258
|
+
* is unknown we bail.
|
|
259
|
+
* - `A || B`, `A && B` where A and B are themselves narrowable.
|
|
260
|
+
*
|
|
261
|
+
* Complex predicates (`not`, function calls, references to other fields,
|
|
262
|
+
* dynamic comparisons) deliberately bail to `undefined`. We'd rather
|
|
263
|
+
* leave the picker showing every field than guess wrong.
|
|
264
|
+
*/
|
|
265
|
+
function narrowTriggersFromCondition(
|
|
266
|
+
condition: ConditionInput,
|
|
267
|
+
universe: string[],
|
|
268
|
+
): Set<string> | undefined {
|
|
269
|
+
if (typeof condition === "string") {
|
|
270
|
+
let parsed;
|
|
271
|
+
try {
|
|
272
|
+
parsed = parseCondition(condition);
|
|
273
|
+
} catch {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
return narrowFromExpr(parsed.root, universe);
|
|
277
|
+
}
|
|
278
|
+
if ("and" in condition) {
|
|
279
|
+
let result: Set<string> | undefined;
|
|
280
|
+
for (const child of condition.and) {
|
|
281
|
+
const childNarrow = narrowTriggersFromCondition(child, universe);
|
|
282
|
+
if (childNarrow === undefined) continue;
|
|
283
|
+
result =
|
|
284
|
+
result === undefined
|
|
285
|
+
? new Set(childNarrow)
|
|
286
|
+
: new Set([...result].filter((id) => childNarrow.has(id)));
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
if ("or" in condition) {
|
|
291
|
+
let result: Set<string> | undefined;
|
|
292
|
+
for (const child of condition.or) {
|
|
293
|
+
const childNarrow = narrowTriggersFromCondition(child, universe);
|
|
294
|
+
if (childNarrow === undefined) return undefined;
|
|
295
|
+
result =
|
|
296
|
+
result === undefined
|
|
297
|
+
? new Set(childNarrow)
|
|
298
|
+
: new Set([...result, ...childNarrow]);
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
// `not` — could in principle return all-minus-{X} but combining with
|
|
303
|
+
// outer AND/OR gets surprising fast. Bail.
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function narrowFromExpr(expr: Expr, universe: string[]): Set<string> | undefined {
|
|
308
|
+
if (expr.kind !== "binary") return undefined;
|
|
309
|
+
if (expr.op === "==") {
|
|
310
|
+
const event = extractTriggerEventLiteral(expr.left, expr.right);
|
|
311
|
+
return event === undefined ? undefined : new Set([event]);
|
|
312
|
+
}
|
|
313
|
+
if (expr.op === "!=") {
|
|
314
|
+
const event = extractTriggerEventLiteral(expr.left, expr.right);
|
|
315
|
+
if (event === undefined || universe.length === 0) return undefined;
|
|
316
|
+
return new Set(universe.filter((id) => id !== event));
|
|
317
|
+
}
|
|
318
|
+
if (expr.op === "||") {
|
|
319
|
+
const left = narrowFromExpr(expr.left, universe);
|
|
320
|
+
const right = narrowFromExpr(expr.right, universe);
|
|
321
|
+
if (left === undefined || right === undefined) return undefined;
|
|
322
|
+
return new Set([...left, ...right]);
|
|
323
|
+
}
|
|
324
|
+
if (expr.op === "&&") {
|
|
325
|
+
const left = narrowFromExpr(expr.left, universe);
|
|
326
|
+
const right = narrowFromExpr(expr.right, universe);
|
|
327
|
+
if (left === undefined && right === undefined) return undefined;
|
|
328
|
+
if (left === undefined) return right;
|
|
329
|
+
if (right === undefined) return left;
|
|
330
|
+
return new Set([...left].filter((id) => right.has(id)));
|
|
331
|
+
}
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function extractTriggerEventLiteral(
|
|
336
|
+
a: Expr,
|
|
337
|
+
b: Expr,
|
|
338
|
+
): string | undefined {
|
|
339
|
+
const direction = (member: Expr, literal: Expr): string | undefined => {
|
|
340
|
+
if (member.kind !== "member" || member.property !== "event") return undefined;
|
|
341
|
+
if (member.object.kind !== "identifier" || member.object.name !== "trigger") {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
if (literal.kind !== "literal" || typeof literal.value !== "string") {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
return literal.value;
|
|
348
|
+
};
|
|
349
|
+
return direction(a, b) ?? direction(b, a);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ─── JSON Schema → VariableEntry tree ──────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
interface JsonSchemaLike {
|
|
355
|
+
type?: string | string[];
|
|
356
|
+
description?: string;
|
|
357
|
+
properties?: Record<string, JsonSchemaLike>;
|
|
358
|
+
required?: string[];
|
|
359
|
+
items?: JsonSchemaLike;
|
|
360
|
+
enum?: unknown[];
|
|
361
|
+
additionalProperties?: boolean | JsonSchemaLike;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function schemaTypeLabel(schema: JsonSchemaLike | undefined): string {
|
|
365
|
+
if (!schema) return "unknown";
|
|
366
|
+
if (schema.enum) {
|
|
367
|
+
return schema.enum.map((v) => JSON.stringify(v)).join(" | ");
|
|
368
|
+
}
|
|
369
|
+
if (Array.isArray(schema.type)) {
|
|
370
|
+
return schema.type.join(" | ");
|
|
371
|
+
}
|
|
372
|
+
if (schema.type === "array" && schema.items) {
|
|
373
|
+
return `${schemaTypeLabel(schema.items)}[]`;
|
|
374
|
+
}
|
|
375
|
+
if (schema.type === "object") return "object";
|
|
376
|
+
if (typeof schema.type === "string") return schema.type;
|
|
377
|
+
return "unknown";
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Build a tree of `VariableEntry` nodes from a JSON Schema, rooted at
|
|
382
|
+
* `parentPath`. Each entry's `path` is the dot-joined navigation key the
|
|
383
|
+
* user would type after the picker inserts it.
|
|
384
|
+
*/
|
|
385
|
+
function entriesFromSchema(
|
|
386
|
+
schema: JsonSchemaLike | undefined,
|
|
387
|
+
parentPath: string,
|
|
388
|
+
parentTemplateRef: string,
|
|
389
|
+
): VariableEntry[] {
|
|
390
|
+
if (!schema || schema.type !== "object" || !schema.properties) return [];
|
|
391
|
+
return Object.entries(schema.properties).map(([key, child]) => {
|
|
392
|
+
const childPath = `${parentPath}.${key}`;
|
|
393
|
+
const childTemplateRef = appendTemplateSegment({
|
|
394
|
+
base: parentTemplateRef,
|
|
395
|
+
segment: key,
|
|
396
|
+
});
|
|
397
|
+
return entryFromSchemaNode(child, childPath, childTemplateRef);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Build a single `VariableEntry` for a property's schema, recursing into
|
|
403
|
+
* objects and arrays.
|
|
404
|
+
*
|
|
405
|
+
* - `object` with `properties` → container node with `children`.
|
|
406
|
+
* - `array` with `items` → a referenceable whole-array node carrying a
|
|
407
|
+
* single representative-element child at index `0`. The element child
|
|
408
|
+
* recurses for nested objects (`tags[0].field`) / arrays (`matrix[0][0]`)
|
|
409
|
+
* or is a leaf for scalar items (`tags[0]`).
|
|
410
|
+
* - anything else → leaf.
|
|
411
|
+
*/
|
|
412
|
+
function entryFromSchemaNode(
|
|
413
|
+
schema: JsonSchemaLike,
|
|
414
|
+
path: string,
|
|
415
|
+
templateRef: string,
|
|
416
|
+
): VariableEntry {
|
|
417
|
+
if (schema.type === "array" && schema.items) {
|
|
418
|
+
const elemPath = `${path}[0]`;
|
|
419
|
+
const elemTemplateRef = appendArrayIndex({ base: templateRef, index: 0 });
|
|
420
|
+
const elementEntry = entryFromSchemaNode(
|
|
421
|
+
schema.items,
|
|
422
|
+
elemPath,
|
|
423
|
+
elemTemplateRef,
|
|
424
|
+
);
|
|
425
|
+
return {
|
|
426
|
+
path,
|
|
427
|
+
templateRef,
|
|
428
|
+
type: schemaTypeLabel(schema),
|
|
429
|
+
description: schema.description,
|
|
430
|
+
jsonSchema: schema as unknown as Record<string, unknown>,
|
|
431
|
+
// Both the whole array and its elements are referenceable.
|
|
432
|
+
referenceable: true,
|
|
433
|
+
children: [elementEntry],
|
|
434
|
+
} satisfies VariableEntry;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const subEntries =
|
|
438
|
+
schema.type === "object" && schema.properties
|
|
439
|
+
? entriesFromSchema(schema, path, templateRef)
|
|
440
|
+
: undefined;
|
|
441
|
+
return {
|
|
442
|
+
path,
|
|
443
|
+
templateRef,
|
|
444
|
+
type: schemaTypeLabel(schema),
|
|
445
|
+
description: schema.description,
|
|
446
|
+
jsonSchema: schema as unknown as Record<string, unknown>,
|
|
447
|
+
children: subEntries,
|
|
448
|
+
} satisfies VariableEntry;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ─── Trigger scope ─────────────────────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Build the `trigger.*` namespace as a **discriminated union over
|
|
455
|
+
* `trigger.event`**.
|
|
456
|
+
*
|
|
457
|
+
* Every payload field from every subscribed trigger is offered, and each
|
|
458
|
+
* leaf entry is annotated with the trigger ids that contribute it. Fields
|
|
459
|
+
* shared by all subscribed triggers carry no `conditionalOnTriggers` flag.
|
|
460
|
+
* The TS-declaration generator turns this into a real
|
|
461
|
+
* `trigger: { event: "A"; payload: PA } | { event: "B"; payload: PB }`
|
|
462
|
+
* shape so Monaco narrows correctly inside `{{#if trigger.event === "A"}}`
|
|
463
|
+
* branches.
|
|
464
|
+
*/
|
|
465
|
+
/**
|
|
466
|
+
* Static `trigger.actor` subtree. Every trigger carries an actor (who/what
|
|
467
|
+
* caused the event), injected by the platform as event metadata, so this is
|
|
468
|
+
* offered unconditionally on every automation - independent of which triggers
|
|
469
|
+
* are subscribed - to drive filters like
|
|
470
|
+
* `{{ trigger.actor.type == "system" }}`.
|
|
471
|
+
*/
|
|
472
|
+
function buildActorEntry(): VariableEntry {
|
|
473
|
+
return {
|
|
474
|
+
path: "trigger.actor",
|
|
475
|
+
templateRef: "trigger.actor",
|
|
476
|
+
type: "object",
|
|
477
|
+
description:
|
|
478
|
+
"Who or what caused the event (system, user, application, or service).",
|
|
479
|
+
jsonSchema: {
|
|
480
|
+
type: "object",
|
|
481
|
+
properties: {
|
|
482
|
+
type: {
|
|
483
|
+
type: "string",
|
|
484
|
+
enum: ["system", "user", "application", "service"],
|
|
485
|
+
},
|
|
486
|
+
id: { type: "string" },
|
|
487
|
+
name: { type: "string" },
|
|
488
|
+
},
|
|
489
|
+
required: ["type", "id"],
|
|
490
|
+
},
|
|
491
|
+
children: [
|
|
492
|
+
{
|
|
493
|
+
path: "trigger.actor.type",
|
|
494
|
+
templateRef: "trigger.actor.type",
|
|
495
|
+
type: '"system" | "user" | "application" | "service"',
|
|
496
|
+
description:
|
|
497
|
+
"Actor kind. Filter e.g. system-created vs user-created events.",
|
|
498
|
+
jsonSchema: {
|
|
499
|
+
type: "string",
|
|
500
|
+
enum: ["system", "user", "application", "service"],
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
path: "trigger.actor.id",
|
|
505
|
+
templateRef: "trigger.actor.id",
|
|
506
|
+
type: "string",
|
|
507
|
+
description:
|
|
508
|
+
'Stable id: user id, application id, plugin id (service), or "system".',
|
|
509
|
+
jsonSchema: { type: "string" },
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
path: "trigger.actor.name",
|
|
513
|
+
templateRef: "trigger.actor.name",
|
|
514
|
+
type: "string",
|
|
515
|
+
description: "Human-readable actor name when known.",
|
|
516
|
+
jsonSchema: { type: "string" },
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Derive a stable, identifier-safe id for a trigger from its event id, used
|
|
524
|
+
* when the operator hasn't assigned an explicit `id`. Mirrors the backend
|
|
525
|
+
* dispatcher's derivation so editor autocomplete, the generated script types,
|
|
526
|
+
* and the runtime `trigger.id` all agree.
|
|
527
|
+
*/
|
|
528
|
+
export function deriveTriggerId(event: string): string {
|
|
529
|
+
return event.replaceAll(/[^a-z0-9]+/gi, "_").toLowerCase();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Build the `trigger.id` entry — the id of the specific trigger declaration
|
|
534
|
+
* that fired. Typed as the literal union of the automation's trigger ids
|
|
535
|
+
* (explicit `id` or derived from the event), so it discriminates triggers
|
|
536
|
+
* even when two subscribe to the same `event`.
|
|
537
|
+
*/
|
|
538
|
+
function buildTriggerIdEntry(triggers: Trigger[]): VariableEntry {
|
|
539
|
+
const effectiveIds = [
|
|
540
|
+
...new Set(triggers.map((t) => t.id ?? deriveTriggerId(t.event))),
|
|
541
|
+
];
|
|
542
|
+
const idType =
|
|
543
|
+
effectiveIds.length > 0
|
|
544
|
+
? effectiveIds.map((id) => JSON.stringify(id)).join(" | ")
|
|
545
|
+
: "string";
|
|
546
|
+
return {
|
|
547
|
+
path: "trigger.id",
|
|
548
|
+
templateRef: "trigger.id",
|
|
549
|
+
type: idType,
|
|
550
|
+
description:
|
|
551
|
+
effectiveIds.length <= 1
|
|
552
|
+
? "Id of the trigger declaration that fired."
|
|
553
|
+
: "Id of the trigger that fired — distinguishes triggers, including two on the same event.",
|
|
554
|
+
jsonSchema:
|
|
555
|
+
effectiveIds.length > 0
|
|
556
|
+
? { type: "string", enum: effectiveIds }
|
|
557
|
+
: { type: "string" },
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function buildTriggerEntries(args: {
|
|
562
|
+
triggers: Trigger[];
|
|
563
|
+
registeredTriggers: TriggerInfo[];
|
|
564
|
+
}): VariableEntry[] {
|
|
565
|
+
const { triggers, registeredTriggers } = args;
|
|
566
|
+
const matched = triggers
|
|
567
|
+
.map((t) => registeredTriggers.find((r) => r.qualifiedId === t.event))
|
|
568
|
+
.filter((r): r is TriggerInfo => r !== undefined);
|
|
569
|
+
|
|
570
|
+
const allEventIds = matched.map((m) => m.qualifiedId);
|
|
571
|
+
const eventType =
|
|
572
|
+
allEventIds.length > 0
|
|
573
|
+
? allEventIds.map((id) => JSON.stringify(id)).join(" | ")
|
|
574
|
+
: "string";
|
|
575
|
+
|
|
576
|
+
const idEntry = buildTriggerIdEntry(triggers);
|
|
577
|
+
const eventEntry: VariableEntry = {
|
|
578
|
+
path: "trigger.event",
|
|
579
|
+
templateRef: "trigger.event",
|
|
580
|
+
type: eventType,
|
|
581
|
+
description:
|
|
582
|
+
allEventIds.length <= 1
|
|
583
|
+
? "Fully-qualified event id of the trigger that fired."
|
|
584
|
+
: "Discriminator — narrows trigger.payload to the matching variant.",
|
|
585
|
+
jsonSchema:
|
|
586
|
+
allEventIds.length > 0
|
|
587
|
+
? { type: "string", enum: allEventIds }
|
|
588
|
+
: { type: "string" },
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
if (matched.length === 0) {
|
|
592
|
+
return [
|
|
593
|
+
{
|
|
594
|
+
path: "trigger",
|
|
595
|
+
templateRef: "trigger",
|
|
596
|
+
type: "object",
|
|
597
|
+
description: "Trigger that fired this run.",
|
|
598
|
+
children: [
|
|
599
|
+
idEntry,
|
|
600
|
+
eventEntry,
|
|
601
|
+
buildActorEntry(),
|
|
602
|
+
{
|
|
603
|
+
path: "trigger.payload",
|
|
604
|
+
templateRef: "trigger.payload",
|
|
605
|
+
type: "unknown",
|
|
606
|
+
description: "Trigger payload (no registered schema).",
|
|
607
|
+
jsonSchema: {},
|
|
608
|
+
},
|
|
609
|
+
],
|
|
610
|
+
},
|
|
611
|
+
];
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const payloadEntry = buildPayloadUnion(matched);
|
|
615
|
+
|
|
616
|
+
return [
|
|
617
|
+
{
|
|
618
|
+
path: "trigger",
|
|
619
|
+
templateRef: "trigger",
|
|
620
|
+
type: "object",
|
|
621
|
+
description: "Trigger that fired this run.",
|
|
622
|
+
children: [idEntry, eventEntry, buildActorEntry(), payloadEntry],
|
|
623
|
+
},
|
|
624
|
+
];
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Merge several triggers' payload schemas into a single
|
|
629
|
+
* `trigger.payload` entry. Strategy:
|
|
630
|
+
*
|
|
631
|
+
* - Walk every property of every payload.
|
|
632
|
+
* - For each property name, collect the set of triggers that include it.
|
|
633
|
+
* - When every trigger includes it with the same JSON shape, emit a
|
|
634
|
+
* plain entry.
|
|
635
|
+
* - Otherwise emit it with `conditionalOnTriggers` listing the contributors
|
|
636
|
+
* — the picker shows it (with a hint), the type generator funnels it
|
|
637
|
+
* into the right discriminated-union variant.
|
|
638
|
+
*/
|
|
639
|
+
function buildPayloadUnion(triggers: TriggerInfo[]): VariableEntry {
|
|
640
|
+
if (triggers.length === 1) {
|
|
641
|
+
const only = triggers[0]!;
|
|
642
|
+
return {
|
|
643
|
+
path: "trigger.payload",
|
|
644
|
+
templateRef: "trigger.payload",
|
|
645
|
+
type: schemaTypeLabel(only.payloadSchema as JsonSchemaLike),
|
|
646
|
+
description: "Trigger payload.",
|
|
647
|
+
jsonSchema: only.payloadSchema,
|
|
648
|
+
children: entriesFromSchema(
|
|
649
|
+
only.payloadSchema as JsonSchemaLike,
|
|
650
|
+
"trigger.payload",
|
|
651
|
+
"trigger.payload",
|
|
652
|
+
),
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const allEventIds = triggers.map((t) => t.qualifiedId);
|
|
657
|
+
const propertyContributors = new Map<
|
|
658
|
+
string,
|
|
659
|
+
Array<{ triggerId: string; schema: JsonSchemaLike }>
|
|
660
|
+
>();
|
|
661
|
+
|
|
662
|
+
for (const trig of triggers) {
|
|
663
|
+
const schema = trig.payloadSchema as JsonSchemaLike;
|
|
664
|
+
if (schema.type !== "object" || !schema.properties) continue;
|
|
665
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
666
|
+
const list = propertyContributors.get(key) ?? [];
|
|
667
|
+
list.push({ triggerId: trig.qualifiedId, schema: propSchema });
|
|
668
|
+
propertyContributors.set(key, list);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const children: VariableEntry[] = [];
|
|
673
|
+
for (const [key, contributors] of propertyContributors) {
|
|
674
|
+
const contributorIds = contributors.map((c) => c.triggerId);
|
|
675
|
+
const universal = contributorIds.length === allEventIds.length;
|
|
676
|
+
const sameShape = contributors.every(
|
|
677
|
+
(c) => schemaTypeLabel(c.schema) === schemaTypeLabel(contributors[0]!.schema),
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
const childPath = `trigger.payload.${key}`;
|
|
681
|
+
const childTemplateRef = appendTemplateSegment({
|
|
682
|
+
base: "trigger.payload",
|
|
683
|
+
segment: key,
|
|
684
|
+
});
|
|
685
|
+
const baseSchema = contributors[0]!.schema;
|
|
686
|
+
|
|
687
|
+
// When all contributors agree on the shape, descend into objects/arrays
|
|
688
|
+
// via the shared builder so payload arrays expose element children too.
|
|
689
|
+
// Otherwise emit a flat union entry with no children (we can't pick one
|
|
690
|
+
// canonical shape to recurse into).
|
|
691
|
+
const node: VariableEntry = sameShape
|
|
692
|
+
? entryFromSchemaNode(baseSchema, childPath, childTemplateRef)
|
|
693
|
+
: {
|
|
694
|
+
path: childPath,
|
|
695
|
+
templateRef: childTemplateRef,
|
|
696
|
+
type: contributors
|
|
697
|
+
.map((c) => schemaTypeLabel(c.schema))
|
|
698
|
+
.filter((t, i, arr) => arr.indexOf(t) === i)
|
|
699
|
+
.join(" | "),
|
|
700
|
+
description: baseSchema.description,
|
|
701
|
+
jsonSchema: undefined,
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
children.push({
|
|
705
|
+
...node,
|
|
706
|
+
conditionalOnTriggers: universal ? undefined : contributorIds,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return {
|
|
711
|
+
path: "trigger.payload",
|
|
712
|
+
templateRef: "trigger.payload",
|
|
713
|
+
type: "object",
|
|
714
|
+
description:
|
|
715
|
+
"Trigger payload — discriminated union over trigger.event. Fields contributed by only some triggers carry an `Only when …` hint.",
|
|
716
|
+
jsonSchema: undefined,
|
|
717
|
+
children,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ─── Walk & accumulate ─────────────────────────────────────────────────────
|
|
722
|
+
|
|
723
|
+
interface AccumulatedScope {
|
|
724
|
+
vars: VariableEntry[];
|
|
725
|
+
artifacts: VariableEntry[];
|
|
726
|
+
repeats: VariableEntry[];
|
|
727
|
+
/**
|
|
728
|
+
* When defined, the trigger universe is narrowed to this set — populated
|
|
729
|
+
* by walking `choose-when` branches whose `when:` condition statically
|
|
730
|
+
* pins `trigger.event` (see `narrowTriggersFromCondition`). When
|
|
731
|
+
* `undefined`, no condition along the path narrowed anything and all
|
|
732
|
+
* subscribed triggers remain in scope.
|
|
733
|
+
*/
|
|
734
|
+
narrowedTriggers: Set<string> | undefined;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Walk the path step by step, descending into the action tree and
|
|
739
|
+
* accumulating in-scope vars / artifacts / repeat contexts.
|
|
740
|
+
*
|
|
741
|
+
* At each step we consume only the actions _before_ the target index in
|
|
742
|
+
* the current slot's child list — that's the linear upstream.
|
|
743
|
+
*/
|
|
744
|
+
function accumulateAlongPath(args: {
|
|
745
|
+
definition: AutomationDefinition;
|
|
746
|
+
actions: ActionInfo[];
|
|
747
|
+
artifactTypes: ArtifactTypeInfo[];
|
|
748
|
+
path: ActionPath;
|
|
749
|
+
subscribedTriggerIds: string[];
|
|
750
|
+
}): AccumulatedScope {
|
|
751
|
+
const { definition, actions, artifactTypes, path, subscribedTriggerIds } = args;
|
|
752
|
+
|
|
753
|
+
const scope: AccumulatedScope = {
|
|
754
|
+
vars: [],
|
|
755
|
+
artifacts: [],
|
|
756
|
+
repeats: [],
|
|
757
|
+
narrowedTriggers: undefined,
|
|
758
|
+
};
|
|
759
|
+
let currentList: ActionInput[] = definition.actions;
|
|
760
|
+
let parentAction: ActionInput | null = null;
|
|
761
|
+
|
|
762
|
+
for (let segment = 0; segment < path.length; segment++) {
|
|
763
|
+
const step = path[segment]!;
|
|
764
|
+
|
|
765
|
+
// Accumulate everything in `currentList` up to (but not including) the
|
|
766
|
+
// target index.
|
|
767
|
+
accumulatePrefix({
|
|
768
|
+
slice: currentList.slice(0, step.index),
|
|
769
|
+
actions,
|
|
770
|
+
artifactTypes,
|
|
771
|
+
scope,
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
// Last step — we don't descend further.
|
|
775
|
+
if (segment === path.length - 1) break;
|
|
776
|
+
|
|
777
|
+
// Descend: target action of this step becomes parent of next step.
|
|
778
|
+
parentAction = currentList[step.index] ?? null;
|
|
779
|
+
if (!parentAction) break;
|
|
780
|
+
|
|
781
|
+
const nextStep = path[segment + 1]!;
|
|
782
|
+
|
|
783
|
+
// Narrow triggers when stepping into a `choose-when`: read the parent
|
|
784
|
+
// Choose's matching when-branch and intersect its narrowing with what
|
|
785
|
+
// we already had.
|
|
786
|
+
if (
|
|
787
|
+
nextStep.slot === "choose-when" &&
|
|
788
|
+
nextStep.whenIndex !== undefined &&
|
|
789
|
+
typeof parentAction === "object" &&
|
|
790
|
+
parentAction !== null &&
|
|
791
|
+
"choose" in parentAction
|
|
792
|
+
) {
|
|
793
|
+
const choose = parentAction as ChooseInput;
|
|
794
|
+
const branch = choose.choose[nextStep.whenIndex];
|
|
795
|
+
if (branch !== undefined) {
|
|
796
|
+
const universe = scope.narrowedTriggers
|
|
797
|
+
? [...scope.narrowedTriggers]
|
|
798
|
+
: subscribedTriggerIds;
|
|
799
|
+
const branchNarrow = narrowTriggersFromCondition(
|
|
800
|
+
branch.when,
|
|
801
|
+
universe,
|
|
802
|
+
);
|
|
803
|
+
if (branchNarrow !== undefined) {
|
|
804
|
+
scope.narrowedTriggers =
|
|
805
|
+
scope.narrowedTriggers === undefined
|
|
806
|
+
? branchNarrow
|
|
807
|
+
: new Set(
|
|
808
|
+
[...scope.narrowedTriggers].filter((id) =>
|
|
809
|
+
branchNarrow.has(id),
|
|
810
|
+
),
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (nextStep.slot === "repeat" && isRepeatAction(parentAction)) {
|
|
817
|
+
scope.repeats.push(
|
|
818
|
+
...buildRepeatEntries({ action: parentAction, depth: scope.repeats.length / 2 }),
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
currentList = getChildList(parentAction, nextStep.slot, nextStep.whenIndex);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return scope;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function buildRepeatEntries(args: {
|
|
829
|
+
action: RepeatInput;
|
|
830
|
+
depth: number;
|
|
831
|
+
}): VariableEntry[] {
|
|
832
|
+
const { action } = args;
|
|
833
|
+
const mode = repeatMode(action);
|
|
834
|
+
const out: VariableEntry[] = [
|
|
835
|
+
{
|
|
836
|
+
path: "repeat.index",
|
|
837
|
+
templateRef: "repeat.index",
|
|
838
|
+
type: "number",
|
|
839
|
+
description: "Current iteration index (zero-based).",
|
|
840
|
+
jsonSchema: { type: "integer" },
|
|
841
|
+
},
|
|
842
|
+
];
|
|
843
|
+
if (mode === "for_each") {
|
|
844
|
+
out.push({
|
|
845
|
+
path: "repeat.item",
|
|
846
|
+
templateRef: "repeat.item",
|
|
847
|
+
type: "unknown",
|
|
848
|
+
description: "Current item in the iterated collection.",
|
|
849
|
+
jsonSchema: {},
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
return out;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function accumulatePrefix(args: {
|
|
856
|
+
slice: ActionInput[];
|
|
857
|
+
actions: ActionInfo[];
|
|
858
|
+
artifactTypes: ArtifactTypeInfo[];
|
|
859
|
+
scope: AccumulatedScope;
|
|
860
|
+
}): void {
|
|
861
|
+
const { slice, actions, artifactTypes, scope } = args;
|
|
862
|
+
|
|
863
|
+
for (const action of slice) {
|
|
864
|
+
if (isVariablesAction(action)) {
|
|
865
|
+
for (const [name, value] of Object.entries(action.variables)) {
|
|
866
|
+
if (scope.vars.some((v) => v.path === `var.${name}`)) continue;
|
|
867
|
+
scope.vars.push({
|
|
868
|
+
path: `var.${name}`,
|
|
869
|
+
templateRef: appendTemplateSegment({
|
|
870
|
+
base: "variables",
|
|
871
|
+
segment: name,
|
|
872
|
+
}),
|
|
873
|
+
type: typeof value === "string" ? "string | template" : typeof value,
|
|
874
|
+
description: "Operator-defined variable.",
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (isProviderAction(action)) {
|
|
879
|
+
const registered = actions.find((a) => a.qualifiedId === action.action);
|
|
880
|
+
const produces = registered?.produces;
|
|
881
|
+
// The runtime exposes a produced artifact as
|
|
882
|
+
// `artifacts.<actionId>.<localName>.<field>`. Only producing actions
|
|
883
|
+
// that carry an `id` are referenceable, so skip the rest.
|
|
884
|
+
if (produces && action.id) {
|
|
885
|
+
const artifactInfo = artifactTypes.find(
|
|
886
|
+
(t) => t.qualifiedId === produces,
|
|
887
|
+
);
|
|
888
|
+
// localName = produces with the owning plugin prefix stripped
|
|
889
|
+
// (e.g. `integration-jira.issue` → `issue`).
|
|
890
|
+
const prefix = registered?.ownerPluginId
|
|
891
|
+
? `${registered.ownerPluginId}.`
|
|
892
|
+
: "";
|
|
893
|
+
const localName =
|
|
894
|
+
prefix && produces.startsWith(prefix)
|
|
895
|
+
? produces.slice(prefix.length)
|
|
896
|
+
: produces;
|
|
897
|
+
|
|
898
|
+
// Top-level node: `artifact.<id>` → templateRef `artifacts.<id>`.
|
|
899
|
+
const path = `artifact.${action.id}`;
|
|
900
|
+
if (scope.artifacts.some((a) => a.path === path)) continue;
|
|
901
|
+
const templateRef = appendTemplateSegment({
|
|
902
|
+
base: "artifacts",
|
|
903
|
+
segment: action.id,
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
// Intermediate localName node: `artifact.<id>.<localName>` →
|
|
907
|
+
// templateRef `artifacts.<id>.<localName>`, whose children are the
|
|
908
|
+
// artifact schema fields.
|
|
909
|
+
const localPath = `${path}.${localName}`;
|
|
910
|
+
const localTemplateRef = appendTemplateSegment({
|
|
911
|
+
base: templateRef,
|
|
912
|
+
segment: localName,
|
|
913
|
+
});
|
|
914
|
+
const fieldEntries = entriesFromSchema(
|
|
915
|
+
artifactInfo?.schema as JsonSchemaLike | undefined,
|
|
916
|
+
localPath,
|
|
917
|
+
localTemplateRef,
|
|
918
|
+
);
|
|
919
|
+
const localNode: VariableEntry = {
|
|
920
|
+
path: localPath,
|
|
921
|
+
templateRef: localTemplateRef,
|
|
922
|
+
type: artifactInfo?.displayName ?? produces,
|
|
923
|
+
description: artifactInfo?.description,
|
|
924
|
+
jsonSchema: artifactInfo?.schema,
|
|
925
|
+
children: fieldEntries.length > 0 ? fieldEntries : undefined,
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
scope.artifacts.push({
|
|
929
|
+
path,
|
|
930
|
+
templateRef,
|
|
931
|
+
type: artifactInfo?.displayName ?? produces,
|
|
932
|
+
description: artifactInfo?.description,
|
|
933
|
+
jsonSchema: artifactInfo?.schema,
|
|
934
|
+
children: [localNode],
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// ─── Entry point ───────────────────────────────────────────────────────────
|
|
942
|
+
|
|
943
|
+
export function resolveVariableScope(
|
|
944
|
+
input: ResolveVariableScopeInput,
|
|
945
|
+
): VariableScope {
|
|
946
|
+
const { definition, triggers, actions, artifactTypes, path } = input;
|
|
947
|
+
|
|
948
|
+
if (path.length === 0 || path[0]!.slot !== "root") {
|
|
949
|
+
return { entries: [] };
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const subscribedTriggerIds = definition.triggers
|
|
953
|
+
.map((t) => t.event)
|
|
954
|
+
.filter((id) => triggers.some((r) => r.qualifiedId === id));
|
|
955
|
+
|
|
956
|
+
const accumulated = accumulateAlongPath({
|
|
957
|
+
definition,
|
|
958
|
+
actions,
|
|
959
|
+
artifactTypes,
|
|
960
|
+
path,
|
|
961
|
+
subscribedTriggerIds,
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
// When the path's `choose-when` conditions narrowed `trigger.event` to a
|
|
965
|
+
// specific subset, hand the narrowed Trigger list to buildTriggerEntries
|
|
966
|
+
// so the resulting union has only those variants. Picker rows that were
|
|
967
|
+
// previously conditionalOnTriggers become unconditional inside the branch.
|
|
968
|
+
const effectiveTriggers = accumulated.narrowedTriggers
|
|
969
|
+
? definition.triggers.filter((t) =>
|
|
970
|
+
accumulated.narrowedTriggers!.has(t.event),
|
|
971
|
+
)
|
|
972
|
+
: definition.triggers;
|
|
973
|
+
|
|
974
|
+
const triggerEntries = buildTriggerEntries({
|
|
975
|
+
triggers: effectiveTriggers,
|
|
976
|
+
registeredTriggers: triggers,
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
const entries: VariableEntry[] = [...triggerEntries];
|
|
980
|
+
|
|
981
|
+
if (accumulated.vars.length > 0) {
|
|
982
|
+
entries.push({
|
|
983
|
+
path: "var",
|
|
984
|
+
templateRef: "variables",
|
|
985
|
+
type: "object",
|
|
986
|
+
description: "Variables declared upstream in this run.",
|
|
987
|
+
children: accumulated.vars,
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (accumulated.artifacts.length > 0) {
|
|
992
|
+
entries.push({
|
|
993
|
+
path: "artifact",
|
|
994
|
+
templateRef: "artifacts",
|
|
995
|
+
type: "object",
|
|
996
|
+
description: "Artifacts produced by upstream actions in this run.",
|
|
997
|
+
children: accumulated.artifacts,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (accumulated.repeats.length > 0) {
|
|
1002
|
+
entries.push({
|
|
1003
|
+
path: "repeat",
|
|
1004
|
+
templateRef: "repeat",
|
|
1005
|
+
type: "object",
|
|
1006
|
+
description: "Current repeat-iteration context.",
|
|
1007
|
+
children: accumulated.repeats,
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return { entries };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Flatten a `VariableScope` tree to the leaf paths, in the order the
|
|
1016
|
+
* picker should display them. Object-typed parents are kept _before_ their
|
|
1017
|
+
* children so the picker can render headers.
|
|
1018
|
+
*/
|
|
1019
|
+
export function flattenScope(scope: VariableScope): VariableEntry[] {
|
|
1020
|
+
const out: VariableEntry[] = [];
|
|
1021
|
+
const visit = (entries: VariableEntry[]): void => {
|
|
1022
|
+
for (const e of entries) {
|
|
1023
|
+
out.push(e);
|
|
1024
|
+
if (e.children) visit(e.children);
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
visit(scope.entries);
|
|
1028
|
+
return out;
|
|
1029
|
+
}
|