@async/pipeline 0.2.4 → 0.4.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/README.md +1 -1
- package/dist/internal/core/cache.d.ts.map +1 -1
- package/dist/internal/core/cache.js +10 -9
- package/dist/internal/core/cache.js.map +1 -1
- package/dist/internal/core/declaration.d.ts +15 -0
- package/dist/internal/core/declaration.d.ts.map +1 -0
- package/dist/internal/core/declaration.js +49 -0
- package/dist/internal/core/declaration.js.map +1 -0
- package/dist/internal/core/index.d.ts +16 -1
- package/dist/internal/core/index.d.ts.map +1 -1
- package/dist/internal/core/index.js +247 -54
- package/dist/internal/core/index.js.map +1 -1
- package/dist/internal/core/runtime.d.ts.map +1 -1
- package/dist/internal/core/runtime.js +14 -13
- package/dist/internal/core/runtime.js.map +1 -1
- package/dist/internal/node/cli.d.ts.map +1 -1
- package/dist/internal/node/cli.js +6 -3
- package/dist/internal/node/cli.js.map +1 -1
- package/dist/internal/node/loader.js +2 -2
- package/dist/internal/node/loader.js.map +1 -1
- package/dist/internal/node/sources.d.ts.map +1 -1
- package/dist/internal/node/sources.js +13 -3
- package/dist/internal/node/sources.js.map +1 -1
- package/dist/internal/node/store.js +2 -2
- package/dist/internal/node/store.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { assertCacheStore, defaultPipelineCache, defineCache, isCacheDirective, mergeWithDefaultCacheStores, parseCacheRef } from "./cache.js";
|
|
2
|
+
import { assertSupportedDeclaration, brandDeclaration, hasDeclarationKind, readDeclaration } from "./declaration.js";
|
|
2
3
|
import { pipelineError } from "./errors.js";
|
|
3
4
|
export * from "./cache.js";
|
|
5
|
+
export * from "./declaration.js";
|
|
4
6
|
export * from "./errors.js";
|
|
7
|
+
export const DEFAULT_PIPELINE_CONFIG_FILES = ["pipeline.ts", "pipeline.js", "pipeline.mjs", "pipeline.mts"];
|
|
8
|
+
const SOURCE_PIPELINE_DEFAULTED = Symbol.for("@async/pipeline.source.pipeline.defaulted");
|
|
5
9
|
export function sh(first, ...values) {
|
|
6
10
|
if (typeof first === "function") {
|
|
7
|
-
return { kind: "deferred-shell", command: first };
|
|
11
|
+
return brandDeclaration({ kind: "deferred-shell", command: first }, "deferred-shell");
|
|
8
12
|
}
|
|
9
13
|
let command = "";
|
|
10
14
|
for (let index = 0; index < first.length; index += 1) {
|
|
@@ -13,9 +17,11 @@ export function sh(first, ...values) {
|
|
|
13
17
|
command += String(values[index]);
|
|
14
18
|
}
|
|
15
19
|
}
|
|
16
|
-
return { kind: "shell", command };
|
|
20
|
+
return brandDeclaration({ kind: "shell", command }, "shell");
|
|
17
21
|
}
|
|
18
22
|
const AGENT_STEP_FIELDS = new Set(["use", "prompt", "model", "stdoutTo"]);
|
|
23
|
+
const DECLARED_AGENT_STEP_FIELDS = new Set(["kind", ...AGENT_STEP_FIELDS]);
|
|
24
|
+
const SHELL_STEP_FIELDS = new Set(["kind", "command"]);
|
|
19
25
|
export function agent(options) {
|
|
20
26
|
rejectUnknownFields(AGENT_STEP_FIELDS, options, "agent() step");
|
|
21
27
|
const use = options.use;
|
|
@@ -33,7 +39,7 @@ export function agent(options) {
|
|
|
33
39
|
throw pipelineError("ASYNC_PIPELINE_AGENT_INVALID", 'agent() "stdoutTo" must be a relative path inside the task\'s working directory; absolute paths and ".." segments are rejected.');
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
|
-
const step = { kind: "agent", use, prompt: options.prompt };
|
|
42
|
+
const step = brandDeclaration({ kind: "agent", use, prompt: options.prompt }, "agent");
|
|
37
43
|
if (options.model !== undefined)
|
|
38
44
|
step.model = options.model;
|
|
39
45
|
if (options.stdoutTo !== undefined)
|
|
@@ -50,138 +56,159 @@ export function task(definition, run) {
|
|
|
50
56
|
if (definition.run !== undefined && run !== undefined) {
|
|
51
57
|
throw pipelineError("ASYNC_PIPELINE_TASK_ARGUMENT_CONFLICT", "Do not pass a second task argument when config.run is defined.");
|
|
52
58
|
}
|
|
53
|
-
return run === undefined ? definition : { ...definition, run };
|
|
59
|
+
return brandDeclaration(run === undefined ? definition : { ...definition, run }, "task");
|
|
54
60
|
}
|
|
55
61
|
export function job(definition) {
|
|
56
|
-
return definition;
|
|
62
|
+
return brandDeclaration(definition, "job");
|
|
57
63
|
}
|
|
58
64
|
function envVar(name, valuesOrOptions, options = {}) {
|
|
59
65
|
if (!valuesOrOptions)
|
|
60
|
-
return { kind: "async-pipeline.env.var", name };
|
|
66
|
+
return brandDeclaration({ kind: "async-pipeline.env.var", name }, "env.var");
|
|
61
67
|
if (isDefaultOnlyEnvOptions(valuesOrOptions) && Object.keys(valuesOrOptions).length === 1) {
|
|
62
|
-
return { kind: "async-pipeline.env.var", name, default: valuesOrOptions.default };
|
|
68
|
+
return brandDeclaration({ kind: "async-pipeline.env.var", name, default: valuesOrOptions.default }, "env.var");
|
|
63
69
|
}
|
|
64
|
-
return {
|
|
70
|
+
return brandDeclaration({
|
|
65
71
|
kind: "async-pipeline.env.var",
|
|
66
72
|
name,
|
|
67
73
|
values: { ...valuesOrOptions },
|
|
68
74
|
default: options.default
|
|
69
|
-
};
|
|
75
|
+
}, "env.var");
|
|
70
76
|
}
|
|
71
77
|
export const env = {
|
|
72
78
|
secret(name) {
|
|
73
|
-
return { kind: "async-pipeline.env.secret", name };
|
|
79
|
+
return brandDeclaration({ kind: "async-pipeline.env.secret", name }, "env.secret");
|
|
74
80
|
},
|
|
75
81
|
var: envVar
|
|
76
82
|
};
|
|
77
83
|
export const sandbox = {
|
|
78
84
|
host() {
|
|
79
|
-
return { kind: "host" };
|
|
85
|
+
return brandDeclaration({ kind: "host" }, "sandbox.host");
|
|
80
86
|
},
|
|
81
87
|
lima(options = {}) {
|
|
82
|
-
return { kind: "lima", vm: options.vm };
|
|
88
|
+
return brandDeclaration({ kind: "lima", vm: options.vm }, "sandbox.lima");
|
|
83
89
|
},
|
|
84
90
|
docker(options) {
|
|
85
|
-
return {
|
|
91
|
+
return brandDeclaration({
|
|
86
92
|
kind: "docker",
|
|
87
93
|
image: options.image,
|
|
88
94
|
workdir: options.workdir,
|
|
89
95
|
volumes: options.volumes ? options.volumes.map((volume) => ({ ...volume })) : undefined
|
|
90
|
-
};
|
|
96
|
+
}, "sandbox.docker");
|
|
91
97
|
},
|
|
92
98
|
container(options) {
|
|
93
|
-
return {
|
|
99
|
+
return brandDeclaration({
|
|
94
100
|
kind: "container",
|
|
95
101
|
image: options.image,
|
|
96
102
|
workdir: options.workdir,
|
|
97
103
|
volumes: options.volumes ? options.volumes.map((volume) => ({ ...volume })) : undefined
|
|
98
|
-
};
|
|
104
|
+
}, "sandbox.container");
|
|
99
105
|
}
|
|
100
106
|
};
|
|
101
107
|
export const execution = {
|
|
102
108
|
local(options = {}) {
|
|
103
|
-
return {
|
|
109
|
+
return brandDeclaration({
|
|
104
110
|
kind: "local",
|
|
105
111
|
sandbox: options.sandbox,
|
|
106
112
|
provider: options.provider
|
|
107
|
-
};
|
|
113
|
+
}, "execution.local");
|
|
108
114
|
},
|
|
109
115
|
github(options) {
|
|
110
|
-
return {
|
|
116
|
+
return brandDeclaration({
|
|
111
117
|
kind: "github",
|
|
112
118
|
sandbox: options.sandbox,
|
|
113
119
|
provider: options.provider,
|
|
114
120
|
runsOn: options.runsOn ? cloneRunsOnEntry(options.runsOn) : undefined,
|
|
115
121
|
runsOnMatrix: options.runsOnMatrix ? options.runsOnMatrix.map(cloneRunsOnEntry) : undefined
|
|
116
|
-
};
|
|
122
|
+
}, "execution.github");
|
|
117
123
|
}
|
|
118
124
|
};
|
|
119
125
|
export const command = {
|
|
120
126
|
policy(options = {}) {
|
|
121
|
-
return {
|
|
127
|
+
return brandDeclaration({
|
|
122
128
|
rules: options.rules ? options.rules.map(cloneCommandRule) : [],
|
|
123
129
|
fallback: options.fallback ? cloneCommandAction(options.fallback) : undefined,
|
|
124
130
|
record: options.record,
|
|
125
131
|
output: options.output ? { ...options.output } : undefined,
|
|
126
132
|
shims: options.shims ? [...options.shims] : undefined
|
|
127
|
-
};
|
|
133
|
+
}, "command.policy");
|
|
128
134
|
},
|
|
129
135
|
rule(options) {
|
|
130
|
-
return cloneCommandRule(options);
|
|
136
|
+
return brandDeclaration(cloneCommandRule(options), "command.rule");
|
|
131
137
|
},
|
|
132
138
|
allow(options = {}) {
|
|
133
|
-
return { kind: "async-pipeline.command.allow", output: options.output ? { ...options.output } : undefined };
|
|
139
|
+
return brandDeclaration({ kind: "async-pipeline.command.allow", output: options.output ? { ...options.output } : undefined }, "command.allow");
|
|
134
140
|
},
|
|
135
141
|
deny(options = {}) {
|
|
136
|
-
return { kind: "async-pipeline.command.deny", message: options.message, output: options.output ? { ...options.output } : undefined };
|
|
142
|
+
return brandDeclaration({ kind: "async-pipeline.command.deny", message: options.message, output: options.output ? { ...options.output } : undefined }, "command.deny");
|
|
137
143
|
},
|
|
138
144
|
mock(options = {}) {
|
|
139
|
-
return {
|
|
145
|
+
return brandDeclaration({
|
|
140
146
|
kind: "async-pipeline.command.mock",
|
|
141
147
|
code: options.code,
|
|
142
148
|
stdout: options.stdout,
|
|
143
149
|
stderr: options.stderr,
|
|
144
150
|
output: options.output ? { ...options.output } : undefined
|
|
145
|
-
};
|
|
151
|
+
}, "command.mock");
|
|
146
152
|
},
|
|
147
153
|
requireApproval(options = {}) {
|
|
148
|
-
return { kind: "async-pipeline.command.requireApproval", message: options.message, output: options.output ? { ...options.output } : undefined };
|
|
154
|
+
return brandDeclaration({ kind: "async-pipeline.command.requireApproval", message: options.message, output: options.output ? { ...options.output } : undefined }, "command.requireApproval");
|
|
149
155
|
},
|
|
150
156
|
requireEnvironment(options) {
|
|
151
|
-
return { kind: "async-pipeline.command.requireEnvironment", name: options.name, output: options.output ? { ...options.output } : undefined };
|
|
157
|
+
return brandDeclaration({ kind: "async-pipeline.command.requireEnvironment", name: options.name, output: options.output ? { ...options.output } : undefined }, "command.requireEnvironment");
|
|
152
158
|
}
|
|
153
159
|
};
|
|
154
160
|
export const trigger = {
|
|
155
161
|
manual() {
|
|
156
|
-
return { type: "manual" };
|
|
162
|
+
return brandDeclaration({ type: "manual" }, "trigger.manual");
|
|
157
163
|
},
|
|
158
164
|
github(options) {
|
|
159
|
-
return {
|
|
165
|
+
return brandDeclaration({
|
|
160
166
|
type: "github",
|
|
161
167
|
events: [...options.events],
|
|
162
168
|
branches: options.branches ? [...options.branches] : undefined,
|
|
163
169
|
paths: options.paths ? [...options.paths] : undefined,
|
|
164
170
|
tags: options.tags ? [...options.tags] : undefined
|
|
165
|
-
};
|
|
171
|
+
}, "trigger.github");
|
|
166
172
|
},
|
|
167
173
|
cron(cron, options = {}) {
|
|
168
|
-
return { type: "schedule", cron, timezone: options.timezone };
|
|
174
|
+
return brandDeclaration({ type: "schedule", cron, timezone: options.timezone }, "trigger.cron");
|
|
169
175
|
},
|
|
170
176
|
schedule(cron) {
|
|
171
|
-
return { type: "schedule", cron };
|
|
177
|
+
return brandDeclaration({ type: "schedule", cron }, "trigger.schedule");
|
|
172
178
|
}
|
|
173
179
|
};
|
|
174
180
|
export function dependsOn(...taskIds) {
|
|
175
|
-
return { kind: "async-pipeline.directive.dependsOn", taskIds };
|
|
181
|
+
return brandDeclaration({ kind: "async-pipeline.directive.dependsOn", taskIds }, "directive.dependsOn");
|
|
176
182
|
}
|
|
177
183
|
export const source = {
|
|
178
184
|
git(definition) {
|
|
179
|
-
return { ...definition, type: "git" };
|
|
185
|
+
return brandDeclaration({ ...definition, type: "git" }, "source.git");
|
|
180
186
|
},
|
|
181
187
|
path(definition) {
|
|
182
|
-
return { ...definition, type: "path" };
|
|
188
|
+
return brandDeclaration({ ...definition, type: "path" }, "source.path");
|
|
183
189
|
}
|
|
184
190
|
};
|
|
191
|
+
export function tasks(definitions) {
|
|
192
|
+
return brandDeclaration(definitions, "section.tasks");
|
|
193
|
+
}
|
|
194
|
+
export function jobs(definitions) {
|
|
195
|
+
return brandDeclaration(definitions, "section.jobs");
|
|
196
|
+
}
|
|
197
|
+
export function triggers(definitions) {
|
|
198
|
+
return brandDeclaration(definitions, "section.triggers");
|
|
199
|
+
}
|
|
200
|
+
export function sources(definitions) {
|
|
201
|
+
return brandDeclaration(definitions, "section.sources");
|
|
202
|
+
}
|
|
203
|
+
export function taskDefaults(definitions) {
|
|
204
|
+
return brandDeclaration(definitions, "section.taskDefaults");
|
|
205
|
+
}
|
|
206
|
+
export function agents(definitions) {
|
|
207
|
+
return brandDeclaration(definitions, "section.agents");
|
|
208
|
+
}
|
|
209
|
+
export function sandboxes(definitions) {
|
|
210
|
+
return brandDeclaration(definitions, "section.sandboxes");
|
|
211
|
+
}
|
|
185
212
|
const PIPELINE_FIELDS = new Set(["name", "env", "commands", "agents", "sandboxes", "execution", "cache", "namedInputs", "taskDefaults", "triggers", "sync", "sources", "tasks", "jobs"]);
|
|
186
213
|
const AGENT_PROFILE_FIELDS = new Set(["command", "model"]);
|
|
187
214
|
const TASK_FIELDS = new Set(["description", "dependsOn", "inputs", "outputs", "cache", "retry", "timeout", "requires", "run", "steps"]);
|
|
@@ -190,6 +217,15 @@ const EXECUTION_PROFILE_FIELDS = new Set(["kind", "sandbox", "provider", "runsOn
|
|
|
190
217
|
const GITHUB_JOB_FIELDS = new Set(["environment", "permissions", "runsOn", "runsOnMatrix"]);
|
|
191
218
|
const GITHUB_PERMISSION_FIELDS = new Set(["contents", "idToken", "issues", "packages", "pullRequests"]);
|
|
192
219
|
const CONTAINER_PROVIDERS = new Set(["auto", "docker", "apple-container", "lima"]);
|
|
220
|
+
const SECTION_KINDS = {
|
|
221
|
+
agents: "section.agents",
|
|
222
|
+
sandboxes: "section.sandboxes",
|
|
223
|
+
taskDefaults: "section.taskDefaults",
|
|
224
|
+
triggers: "section.triggers",
|
|
225
|
+
sources: "section.sources",
|
|
226
|
+
tasks: "section.tasks",
|
|
227
|
+
jobs: "section.jobs"
|
|
228
|
+
};
|
|
193
229
|
function rejectUnknownFields(known, value, where) {
|
|
194
230
|
for (const key of Object.keys(value)) {
|
|
195
231
|
if (!known.has(key)) {
|
|
@@ -215,9 +251,6 @@ function validateDefinitionShape(definition) {
|
|
|
215
251
|
throw pipelineError("ASYNC_PIPELINE_AGENT_INVALID", `Agent profile "${id}" requires "model": a non-empty string or env.var(...). The model is the profile's cache identity; the command is deliberately not.`);
|
|
216
252
|
}
|
|
217
253
|
}
|
|
218
|
-
for (const [id, taskDefinition] of Object.entries(definition.tasks ?? {})) {
|
|
219
|
-
rejectUnknownFields(TASK_FIELDS, taskDefinition, `Task "${id}"`);
|
|
220
|
-
}
|
|
221
254
|
for (const [id, defaults] of Object.entries(definition.taskDefaults ?? {})) {
|
|
222
255
|
rejectUnknownFields(TASK_FIELDS, defaults, `taskDefaults["${id}"]`);
|
|
223
256
|
}
|
|
@@ -243,10 +276,112 @@ function validateDefinitionShape(definition) {
|
|
|
243
276
|
}
|
|
244
277
|
}
|
|
245
278
|
}
|
|
279
|
+
function normalizePipelineSections(definition) {
|
|
280
|
+
return {
|
|
281
|
+
...definition,
|
|
282
|
+
agents: normalizeSection("agents", definition.agents),
|
|
283
|
+
sandboxes: normalizeSection("sandboxes", definition.sandboxes),
|
|
284
|
+
taskDefaults: normalizeSection("taskDefaults", definition.taskDefaults),
|
|
285
|
+
triggers: normalizeSection("triggers", definition.triggers),
|
|
286
|
+
sources: normalizeSection("sources", definition.sources),
|
|
287
|
+
tasks: normalizeSection("tasks", definition.tasks),
|
|
288
|
+
jobs: normalizeSection("jobs", definition.jobs)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function normalizeSection(key, value) {
|
|
292
|
+
if (value === undefined)
|
|
293
|
+
return value;
|
|
294
|
+
if (!isObjectRecord(value)) {
|
|
295
|
+
throw pipelineError("ASYNC_PIPELINE_SECTION_INVALID", `Pipeline section "${key}" must be an object.`);
|
|
296
|
+
}
|
|
297
|
+
const expectedKind = SECTION_KINDS[key];
|
|
298
|
+
const metadata = assertSupportedDeclaration(value);
|
|
299
|
+
if (metadata) {
|
|
300
|
+
if (metadata.kind !== expectedKind) {
|
|
301
|
+
throw pipelineError("ASYNC_PIPELINE_SECTION_KIND_MISMATCH", `Pipeline section "${key}" expected declaration kind "${expectedKind}", received "${metadata.kind}".`);
|
|
302
|
+
}
|
|
303
|
+
return value;
|
|
304
|
+
}
|
|
305
|
+
return brandDeclaration(value, expectedKind);
|
|
306
|
+
}
|
|
307
|
+
function flattenTaskDefinitions(definitions) {
|
|
308
|
+
const tasks = [];
|
|
309
|
+
const seen = new Set();
|
|
310
|
+
function visit(node, path) {
|
|
311
|
+
for (const [key, value] of Object.entries(node)) {
|
|
312
|
+
validateTaskTreeKey(key, path);
|
|
313
|
+
if (!isObjectRecord(value)) {
|
|
314
|
+
throw pipelineError("ASYNC_PIPELINE_TASK_TREE_INVALID", `Task tree entry "${[...path, key].join(".")}" must be an object.`);
|
|
315
|
+
}
|
|
316
|
+
const isTask = isTaskDefinitionNode(value, path);
|
|
317
|
+
if (isTask) {
|
|
318
|
+
const id = taskTreeId(path, key);
|
|
319
|
+
validateLocalTaskId(id);
|
|
320
|
+
if (seen.has(id)) {
|
|
321
|
+
throw pipelineError("ASYNC_PIPELINE_TASK_ID_COLLISION", `Task group entry "${[...path, key].join(".")}" normalizes to duplicate task id "${id}".`);
|
|
322
|
+
}
|
|
323
|
+
seen.add(id);
|
|
324
|
+
rejectUnknownFields(TASK_FIELDS, value, `Task "${id}"`);
|
|
325
|
+
tasks.push({ id, definition: value, groupPath: path });
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (key.includes(".")) {
|
|
329
|
+
throw pipelineError("ASYNC_PIPELINE_TASK_GROUP_INVALID_KEY", `Task group key "${key}" cannot contain ".". Use nested objects instead.`);
|
|
330
|
+
}
|
|
331
|
+
visit(value, [...path, key]);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
visit(definitions, []);
|
|
335
|
+
return tasks;
|
|
336
|
+
}
|
|
337
|
+
function validateTaskTreeKey(key, path) {
|
|
338
|
+
if (!key.trim()) {
|
|
339
|
+
throw pipelineError("ASYNC_PIPELINE_TASK_GROUP_INVALID_KEY", "Task group key cannot be empty.");
|
|
340
|
+
}
|
|
341
|
+
if (key.includes(":")) {
|
|
342
|
+
throw pipelineError("ASYNC_PIPELINE_TASK_GROUP_INVALID_KEY", `Task group key "${key}" cannot contain ":". Use source namespaces through dependsOn instead.`);
|
|
343
|
+
}
|
|
344
|
+
if (path.length > 0 && key.includes(".")) {
|
|
345
|
+
throw pipelineError("ASYNC_PIPELINE_TASK_GROUP_INVALID_KEY", `Nested task group key "${key}" cannot contain ".". Use nested objects instead.`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function taskTreeId(path, key) {
|
|
349
|
+
const segments = path.length > 0 && (key === "default" || key === "index") ? path : [...path, key];
|
|
350
|
+
return segments.join(".");
|
|
351
|
+
}
|
|
352
|
+
function isTaskDefinitionNode(value, path) {
|
|
353
|
+
const metadata = assertSupportedDeclaration(value);
|
|
354
|
+
if (metadata?.kind === "task")
|
|
355
|
+
return true;
|
|
356
|
+
if (metadata?.kind.startsWith("section."))
|
|
357
|
+
return false;
|
|
358
|
+
if (metadata)
|
|
359
|
+
return false;
|
|
360
|
+
if (Object.keys(value).some((key) => TASK_FIELDS.has(key)))
|
|
361
|
+
return true;
|
|
362
|
+
return path.length === 0 && Object.keys(value).length === 0;
|
|
363
|
+
}
|
|
364
|
+
function resolveTaskDependencies(taskId, groupPath, dependencies, knownTaskIds) {
|
|
365
|
+
return dependencies.map((dependency) => resolveTaskDependency(taskId, groupPath, dependency, knownTaskIds));
|
|
366
|
+
}
|
|
367
|
+
function resolveTaskDependency(taskId, groupPath, dependency, knownTaskIds) {
|
|
368
|
+
if (isNamespacedTaskRef(dependency) || groupPath.length === 0)
|
|
369
|
+
return dependency;
|
|
370
|
+
const groupCandidate = [...groupPath, dependency].join(".");
|
|
371
|
+
const groupMatch = knownTaskIds.has(groupCandidate);
|
|
372
|
+
const rootMatch = knownTaskIds.has(dependency);
|
|
373
|
+
if (groupMatch && rootMatch && groupCandidate !== dependency) {
|
|
374
|
+
throw pipelineError("ASYNC_PIPELINE_TASK_DEPENDENCY_AMBIGUOUS", `Task "${taskId}" depends on ambiguous local task "${dependency}". Use "${groupCandidate}" or "${dependency}" explicitly.`);
|
|
375
|
+
}
|
|
376
|
+
if (groupMatch)
|
|
377
|
+
return groupCandidate;
|
|
378
|
+
return dependency;
|
|
379
|
+
}
|
|
246
380
|
export function definePipeline(definition) {
|
|
247
381
|
return normalizePipeline(definition);
|
|
248
382
|
}
|
|
249
383
|
export function normalizePipeline(definition) {
|
|
384
|
+
definition = normalizePipelineSections(definition);
|
|
250
385
|
validateDefinitionShape(definition);
|
|
251
386
|
const namedInputs = definition.namedInputs ?? {};
|
|
252
387
|
const cacheRegistry = normalizeCacheRegistry(definition.cache);
|
|
@@ -256,16 +391,18 @@ export function normalizePipeline(definition) {
|
|
|
256
391
|
sources[id] = normalizeSource(id, sourceDefinition);
|
|
257
392
|
}
|
|
258
393
|
const tasks = {};
|
|
259
|
-
|
|
394
|
+
const flattenedTaskDefinitions = flattenTaskDefinitions(definition.tasks);
|
|
395
|
+
const knownTaskIds = new Set(flattenedTaskDefinitions.map((entry) => entry.id));
|
|
396
|
+
for (const { id, definition: taskDefinition, groupPath } of flattenedTaskDefinitions) {
|
|
260
397
|
validateLocalTaskId(id);
|
|
261
398
|
const defaults = definition.taskDefaults?.[id] ?? definition.taskDefaults?.[taskName(id)] ?? {};
|
|
262
399
|
const merged = { ...defaults, ...taskDefinition };
|
|
263
400
|
const runItems = merged.steps ? [...merged.steps] : runItemsFromDefinition(merged.run);
|
|
264
401
|
const { steps, cacheDirectives, dependsOnDirectives } = partitionRunItems(runItems);
|
|
265
|
-
const liftedDependsOn = uniqueTaskIds([
|
|
402
|
+
const liftedDependsOn = resolveTaskDependencies(id, groupPath, uniqueTaskIds([
|
|
266
403
|
...(merged.dependsOn ?? []),
|
|
267
404
|
...dependsOnDirectives.flatMap((directive) => directive.taskIds)
|
|
268
|
-
]);
|
|
405
|
+
]), knownTaskIds);
|
|
269
406
|
const cache = normalizeCache(merged.cache ?? cacheDirectives[0], cacheRegistry);
|
|
270
407
|
const retry = normalizeRetry(merged.retry);
|
|
271
408
|
const timeoutMs = normalizeTimeout(merged.timeout);
|
|
@@ -311,6 +448,12 @@ export function normalizePipeline(definition) {
|
|
|
311
448
|
function isDefaultOnlyEnvOptions(value) {
|
|
312
449
|
return typeof value.default === "string";
|
|
313
450
|
}
|
|
451
|
+
function isObjectRecord(value) {
|
|
452
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
453
|
+
}
|
|
454
|
+
function isEnvVarRef(value) {
|
|
455
|
+
return isObjectRecord(value) && value.kind === "async-pipeline.env.var";
|
|
456
|
+
}
|
|
314
457
|
function normalizeAgents(definitions = {}) {
|
|
315
458
|
const normalized = {};
|
|
316
459
|
for (const [id, profile] of Object.entries(definitions)) {
|
|
@@ -655,21 +798,31 @@ function collectRequiredTasks(pipeline, targets) {
|
|
|
655
798
|
}
|
|
656
799
|
function normalizeSource(id, sourceDefinition) {
|
|
657
800
|
const prepare = [...(sourceDefinition.prepare ?? [])];
|
|
658
|
-
const pipeline = sourceDefinition.pipeline ??
|
|
659
|
-
|
|
660
|
-
|
|
801
|
+
const pipeline = sourceDefinition.pipeline ?? DEFAULT_PIPELINE_CONFIG_FILES[0];
|
|
802
|
+
const defaulted = !sourceDefinition.pipeline;
|
|
803
|
+
const normalized = sourceDefinition.type === "git"
|
|
804
|
+
? {
|
|
805
|
+
...sourceDefinition,
|
|
806
|
+
id,
|
|
807
|
+
pipeline,
|
|
808
|
+
prepare
|
|
809
|
+
}
|
|
810
|
+
: {
|
|
661
811
|
...sourceDefinition,
|
|
662
812
|
id,
|
|
663
813
|
pipeline,
|
|
664
814
|
prepare
|
|
665
815
|
};
|
|
816
|
+
if (defaulted) {
|
|
817
|
+
Object.defineProperty(normalized, SOURCE_PIPELINE_DEFAULTED, {
|
|
818
|
+
value: true,
|
|
819
|
+
enumerable: false
|
|
820
|
+
});
|
|
666
821
|
}
|
|
667
|
-
return
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
prepare
|
|
672
|
-
};
|
|
822
|
+
return normalized;
|
|
823
|
+
}
|
|
824
|
+
export function sourceUsesDefaultPipelineConfig(sourceDefinition) {
|
|
825
|
+
return sourceDefinition[SOURCE_PIPELINE_DEFAULTED] === true;
|
|
673
826
|
}
|
|
674
827
|
function validateComposedPipeline(pipeline, loadedSources) {
|
|
675
828
|
for (const taskDefinition of Object.values(pipeline.tasks)) {
|
|
@@ -908,7 +1061,8 @@ function partitionRunItems(items) {
|
|
|
908
1061
|
const steps = [];
|
|
909
1062
|
const cacheDirectives = [];
|
|
910
1063
|
const dependsOnDirectives = [];
|
|
911
|
-
for (const
|
|
1064
|
+
for (const rawItem of items) {
|
|
1065
|
+
const item = normalizeDeclaredRunItem(rawItem);
|
|
912
1066
|
if (isCacheDirective(item)) {
|
|
913
1067
|
cacheDirectives.push(item);
|
|
914
1068
|
continue;
|
|
@@ -924,10 +1078,49 @@ function partitionRunItems(items) {
|
|
|
924
1078
|
}
|
|
925
1079
|
return { steps, cacheDirectives, dependsOnDirectives };
|
|
926
1080
|
}
|
|
1081
|
+
function normalizeDeclaredRunItem(item) {
|
|
1082
|
+
const metadata = assertSupportedDeclaration(item);
|
|
1083
|
+
if (!metadata || !isObjectRecord(item))
|
|
1084
|
+
return item;
|
|
1085
|
+
if (metadata.kind === "shell") {
|
|
1086
|
+
rejectUnknownFields(SHELL_STEP_FIELDS, item, "shell declaration");
|
|
1087
|
+
const command = item.command;
|
|
1088
|
+
if (typeof command !== "string") {
|
|
1089
|
+
throw pipelineError("ASYNC_PIPELINE_DECLARATION_INVALID", "Shell declaration requires a string command.");
|
|
1090
|
+
}
|
|
1091
|
+
return brandDeclaration({ kind: "shell", command }, "shell");
|
|
1092
|
+
}
|
|
1093
|
+
if (metadata.kind === "deferred-shell") {
|
|
1094
|
+
rejectUnknownFields(SHELL_STEP_FIELDS, item, "deferred shell declaration");
|
|
1095
|
+
const command = item.command;
|
|
1096
|
+
if (typeof command !== "function") {
|
|
1097
|
+
throw pipelineError("ASYNC_PIPELINE_DECLARATION_INVALID", "Deferred shell declaration requires a command function.");
|
|
1098
|
+
}
|
|
1099
|
+
return brandDeclaration({ kind: "deferred-shell", command: command }, "deferred-shell");
|
|
1100
|
+
}
|
|
1101
|
+
if (metadata.kind === "agent") {
|
|
1102
|
+
rejectUnknownFields(DECLARED_AGENT_STEP_FIELDS, item, "agent declaration");
|
|
1103
|
+
const use = item.use;
|
|
1104
|
+
const prompt = item.prompt;
|
|
1105
|
+
if ((typeof use !== "string" && !isEnvVarRef(use)) || (typeof use === "string" && use.length === 0)) {
|
|
1106
|
+
throw pipelineError("ASYNC_PIPELINE_AGENT_INVALID", 'agent declaration requires "use": a non-empty profile id or env.var(...).');
|
|
1107
|
+
}
|
|
1108
|
+
if (typeof prompt !== "string" || prompt.length === 0) {
|
|
1109
|
+
throw pipelineError("ASYNC_PIPELINE_AGENT_INVALID", 'agent declaration requires a non-empty "prompt" string.');
|
|
1110
|
+
}
|
|
1111
|
+
const step = brandDeclaration({ kind: "agent", use: use, prompt }, "agent");
|
|
1112
|
+
if (item.model !== undefined)
|
|
1113
|
+
step.model = item.model;
|
|
1114
|
+
if (item.stdoutTo !== undefined)
|
|
1115
|
+
step.stdoutTo = item.stdoutTo;
|
|
1116
|
+
return step;
|
|
1117
|
+
}
|
|
1118
|
+
return item;
|
|
1119
|
+
}
|
|
927
1120
|
function isDependsOnDirective(value) {
|
|
928
1121
|
return Boolean(value)
|
|
929
1122
|
&& typeof value === "object"
|
|
930
|
-
&& value.kind === "async-pipeline.directive.dependsOn";
|
|
1123
|
+
&& (value.kind === "async-pipeline.directive.dependsOn" || hasDeclarationKind(value, "directive.dependsOn"));
|
|
931
1124
|
}
|
|
932
1125
|
function uniqueTaskIds(taskIds) {
|
|
933
1126
|
return [...new Set(taskIds)];
|