@async/pipeline 0.2.3 → 0.3.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 +54 -3
- package/dist/internal/core/index.d.ts.map +1 -1
- package/dist/internal/core/index.js +361 -44
- 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 +39 -9
- package/dist/internal/node/cli.js.map +1 -1
- package/dist/internal/node/doctor.d.ts +2 -1
- package/dist/internal/node/doctor.d.ts.map +1 -1
- package/dist/internal/node/doctor.js +27 -1
- package/dist/internal/node/doctor.js.map +1 -1
- package/dist/internal/node/github.d.ts +2 -1
- package/dist/internal/node/github.d.ts.map +1 -1
- package/dist/internal/node/github.js +4 -3
- package/dist/internal/node/github.js.map +1 -1
- package/dist/internal/node/runner.d.ts +18 -2
- package/dist/internal/node/runner.d.ts.map +1 -1
- package/dist/internal/node/runner.js +109 -5
- package/dist/internal/node/runner.js.map +1 -1
- package/dist/internal/node/store.js +2 -1
- package/dist/internal/node/store.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,12 @@
|
|
|
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";
|
|
5
7
|
export function sh(first, ...values) {
|
|
6
8
|
if (typeof first === "function") {
|
|
7
|
-
return { kind: "deferred-shell", command: first };
|
|
9
|
+
return brandDeclaration({ kind: "deferred-shell", command: first }, "deferred-shell");
|
|
8
10
|
}
|
|
9
11
|
let command = "";
|
|
10
12
|
for (let index = 0; index < first.length; index += 1) {
|
|
@@ -13,9 +15,11 @@ export function sh(first, ...values) {
|
|
|
13
15
|
command += String(values[index]);
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
|
-
return { kind: "shell", command };
|
|
18
|
+
return brandDeclaration({ kind: "shell", command }, "shell");
|
|
17
19
|
}
|
|
18
|
-
const AGENT_STEP_FIELDS = new Set(["use", "prompt", "model"]);
|
|
20
|
+
const AGENT_STEP_FIELDS = new Set(["use", "prompt", "model", "stdoutTo"]);
|
|
21
|
+
const DECLARED_AGENT_STEP_FIELDS = new Set(["kind", ...AGENT_STEP_FIELDS]);
|
|
22
|
+
const SHELL_STEP_FIELDS = new Set(["kind", "command"]);
|
|
19
23
|
export function agent(options) {
|
|
20
24
|
rejectUnknownFields(AGENT_STEP_FIELDS, options, "agent() step");
|
|
21
25
|
const use = options.use;
|
|
@@ -25,9 +29,19 @@ export function agent(options) {
|
|
|
25
29
|
if (typeof options.prompt !== "string" || options.prompt.length === 0) {
|
|
26
30
|
throw pipelineError("ASYNC_PIPELINE_AGENT_INVALID", 'agent() requires a non-empty "prompt" string.');
|
|
27
31
|
}
|
|
28
|
-
|
|
32
|
+
if (options.stdoutTo !== undefined) {
|
|
33
|
+
if (typeof options.stdoutTo !== "string" || options.stdoutTo.length === 0) {
|
|
34
|
+
throw pipelineError("ASYNC_PIPELINE_AGENT_INVALID", 'agent() "stdoutTo" must be a non-empty path string.');
|
|
35
|
+
}
|
|
36
|
+
if (options.stdoutTo.startsWith("/") || /^[A-Za-z]:[\\/]/.test(options.stdoutTo) || options.stdoutTo.split(/[\\/]+/).includes("..")) {
|
|
37
|
+
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.');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const step = brandDeclaration({ kind: "agent", use, prompt: options.prompt }, "agent");
|
|
29
41
|
if (options.model !== undefined)
|
|
30
42
|
step.model = options.model;
|
|
43
|
+
if (options.stdoutTo !== undefined)
|
|
44
|
+
step.stdoutTo = options.stdoutTo;
|
|
31
45
|
return step;
|
|
32
46
|
}
|
|
33
47
|
export function isAgentStep(step) {
|
|
@@ -40,118 +54,176 @@ export function task(definition, run) {
|
|
|
40
54
|
if (definition.run !== undefined && run !== undefined) {
|
|
41
55
|
throw pipelineError("ASYNC_PIPELINE_TASK_ARGUMENT_CONFLICT", "Do not pass a second task argument when config.run is defined.");
|
|
42
56
|
}
|
|
43
|
-
return run === undefined ? definition : { ...definition, run };
|
|
57
|
+
return brandDeclaration(run === undefined ? definition : { ...definition, run }, "task");
|
|
44
58
|
}
|
|
45
59
|
export function job(definition) {
|
|
46
|
-
return definition;
|
|
60
|
+
return brandDeclaration(definition, "job");
|
|
47
61
|
}
|
|
48
62
|
function envVar(name, valuesOrOptions, options = {}) {
|
|
49
63
|
if (!valuesOrOptions)
|
|
50
|
-
return { kind: "async-pipeline.env.var", name };
|
|
64
|
+
return brandDeclaration({ kind: "async-pipeline.env.var", name }, "env.var");
|
|
51
65
|
if (isDefaultOnlyEnvOptions(valuesOrOptions) && Object.keys(valuesOrOptions).length === 1) {
|
|
52
|
-
return { kind: "async-pipeline.env.var", name, default: valuesOrOptions.default };
|
|
66
|
+
return brandDeclaration({ kind: "async-pipeline.env.var", name, default: valuesOrOptions.default }, "env.var");
|
|
53
67
|
}
|
|
54
|
-
return {
|
|
68
|
+
return brandDeclaration({
|
|
55
69
|
kind: "async-pipeline.env.var",
|
|
56
70
|
name,
|
|
57
71
|
values: { ...valuesOrOptions },
|
|
58
72
|
default: options.default
|
|
59
|
-
};
|
|
73
|
+
}, "env.var");
|
|
60
74
|
}
|
|
61
75
|
export const env = {
|
|
62
76
|
secret(name) {
|
|
63
|
-
return { kind: "async-pipeline.env.secret", name };
|
|
77
|
+
return brandDeclaration({ kind: "async-pipeline.env.secret", name }, "env.secret");
|
|
64
78
|
},
|
|
65
79
|
var: envVar
|
|
66
80
|
};
|
|
67
81
|
export const sandbox = {
|
|
68
82
|
host() {
|
|
69
|
-
return { kind: "host" };
|
|
83
|
+
return brandDeclaration({ kind: "host" }, "sandbox.host");
|
|
70
84
|
},
|
|
71
85
|
lima(options = {}) {
|
|
72
|
-
return { kind: "lima", vm: options.vm };
|
|
86
|
+
return brandDeclaration({ kind: "lima", vm: options.vm }, "sandbox.lima");
|
|
73
87
|
},
|
|
74
88
|
docker(options) {
|
|
75
|
-
return {
|
|
89
|
+
return brandDeclaration({
|
|
76
90
|
kind: "docker",
|
|
77
91
|
image: options.image,
|
|
78
92
|
workdir: options.workdir,
|
|
79
93
|
volumes: options.volumes ? options.volumes.map((volume) => ({ ...volume })) : undefined
|
|
80
|
-
};
|
|
94
|
+
}, "sandbox.docker");
|
|
95
|
+
},
|
|
96
|
+
container(options) {
|
|
97
|
+
return brandDeclaration({
|
|
98
|
+
kind: "container",
|
|
99
|
+
image: options.image,
|
|
100
|
+
workdir: options.workdir,
|
|
101
|
+
volumes: options.volumes ? options.volumes.map((volume) => ({ ...volume })) : undefined
|
|
102
|
+
}, "sandbox.container");
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
export const execution = {
|
|
106
|
+
local(options = {}) {
|
|
107
|
+
return brandDeclaration({
|
|
108
|
+
kind: "local",
|
|
109
|
+
sandbox: options.sandbox,
|
|
110
|
+
provider: options.provider
|
|
111
|
+
}, "execution.local");
|
|
112
|
+
},
|
|
113
|
+
github(options) {
|
|
114
|
+
return brandDeclaration({
|
|
115
|
+
kind: "github",
|
|
116
|
+
sandbox: options.sandbox,
|
|
117
|
+
provider: options.provider,
|
|
118
|
+
runsOn: options.runsOn ? cloneRunsOnEntry(options.runsOn) : undefined,
|
|
119
|
+
runsOnMatrix: options.runsOnMatrix ? options.runsOnMatrix.map(cloneRunsOnEntry) : undefined
|
|
120
|
+
}, "execution.github");
|
|
81
121
|
}
|
|
82
122
|
};
|
|
83
123
|
export const command = {
|
|
84
124
|
policy(options = {}) {
|
|
85
|
-
return {
|
|
125
|
+
return brandDeclaration({
|
|
86
126
|
rules: options.rules ? options.rules.map(cloneCommandRule) : [],
|
|
87
127
|
fallback: options.fallback ? cloneCommandAction(options.fallback) : undefined,
|
|
88
128
|
record: options.record,
|
|
89
129
|
output: options.output ? { ...options.output } : undefined,
|
|
90
130
|
shims: options.shims ? [...options.shims] : undefined
|
|
91
|
-
};
|
|
131
|
+
}, "command.policy");
|
|
92
132
|
},
|
|
93
133
|
rule(options) {
|
|
94
|
-
return cloneCommandRule(options);
|
|
134
|
+
return brandDeclaration(cloneCommandRule(options), "command.rule");
|
|
95
135
|
},
|
|
96
136
|
allow(options = {}) {
|
|
97
|
-
return { kind: "async-pipeline.command.allow", output: options.output ? { ...options.output } : undefined };
|
|
137
|
+
return brandDeclaration({ kind: "async-pipeline.command.allow", output: options.output ? { ...options.output } : undefined }, "command.allow");
|
|
98
138
|
},
|
|
99
139
|
deny(options = {}) {
|
|
100
|
-
return { kind: "async-pipeline.command.deny", message: options.message, output: options.output ? { ...options.output } : undefined };
|
|
140
|
+
return brandDeclaration({ kind: "async-pipeline.command.deny", message: options.message, output: options.output ? { ...options.output } : undefined }, "command.deny");
|
|
101
141
|
},
|
|
102
142
|
mock(options = {}) {
|
|
103
|
-
return {
|
|
143
|
+
return brandDeclaration({
|
|
104
144
|
kind: "async-pipeline.command.mock",
|
|
105
145
|
code: options.code,
|
|
106
146
|
stdout: options.stdout,
|
|
107
147
|
stderr: options.stderr,
|
|
108
148
|
output: options.output ? { ...options.output } : undefined
|
|
109
|
-
};
|
|
149
|
+
}, "command.mock");
|
|
110
150
|
},
|
|
111
151
|
requireApproval(options = {}) {
|
|
112
|
-
return { kind: "async-pipeline.command.requireApproval", message: options.message, output: options.output ? { ...options.output } : undefined };
|
|
152
|
+
return brandDeclaration({ kind: "async-pipeline.command.requireApproval", message: options.message, output: options.output ? { ...options.output } : undefined }, "command.requireApproval");
|
|
113
153
|
},
|
|
114
154
|
requireEnvironment(options) {
|
|
115
|
-
return { kind: "async-pipeline.command.requireEnvironment", name: options.name, output: options.output ? { ...options.output } : undefined };
|
|
155
|
+
return brandDeclaration({ kind: "async-pipeline.command.requireEnvironment", name: options.name, output: options.output ? { ...options.output } : undefined }, "command.requireEnvironment");
|
|
116
156
|
}
|
|
117
157
|
};
|
|
118
158
|
export const trigger = {
|
|
119
159
|
manual() {
|
|
120
|
-
return { type: "manual" };
|
|
160
|
+
return brandDeclaration({ type: "manual" }, "trigger.manual");
|
|
121
161
|
},
|
|
122
162
|
github(options) {
|
|
123
|
-
return {
|
|
163
|
+
return brandDeclaration({
|
|
124
164
|
type: "github",
|
|
125
165
|
events: [...options.events],
|
|
126
166
|
branches: options.branches ? [...options.branches] : undefined,
|
|
127
167
|
paths: options.paths ? [...options.paths] : undefined,
|
|
128
168
|
tags: options.tags ? [...options.tags] : undefined
|
|
129
|
-
};
|
|
169
|
+
}, "trigger.github");
|
|
130
170
|
},
|
|
131
171
|
cron(cron, options = {}) {
|
|
132
|
-
return { type: "schedule", cron, timezone: options.timezone };
|
|
172
|
+
return brandDeclaration({ type: "schedule", cron, timezone: options.timezone }, "trigger.cron");
|
|
133
173
|
},
|
|
134
174
|
schedule(cron) {
|
|
135
|
-
return { type: "schedule", cron };
|
|
175
|
+
return brandDeclaration({ type: "schedule", cron }, "trigger.schedule");
|
|
136
176
|
}
|
|
137
177
|
};
|
|
138
178
|
export function dependsOn(...taskIds) {
|
|
139
|
-
return { kind: "async-pipeline.directive.dependsOn", taskIds };
|
|
179
|
+
return brandDeclaration({ kind: "async-pipeline.directive.dependsOn", taskIds }, "directive.dependsOn");
|
|
140
180
|
}
|
|
141
181
|
export const source = {
|
|
142
182
|
git(definition) {
|
|
143
|
-
return { ...definition, type: "git" };
|
|
183
|
+
return brandDeclaration({ ...definition, type: "git" }, "source.git");
|
|
144
184
|
},
|
|
145
185
|
path(definition) {
|
|
146
|
-
return { ...definition, type: "path" };
|
|
186
|
+
return brandDeclaration({ ...definition, type: "path" }, "source.path");
|
|
147
187
|
}
|
|
148
188
|
};
|
|
149
|
-
|
|
189
|
+
export function tasks(definitions) {
|
|
190
|
+
return brandDeclaration(definitions, "section.tasks");
|
|
191
|
+
}
|
|
192
|
+
export function jobs(definitions) {
|
|
193
|
+
return brandDeclaration(definitions, "section.jobs");
|
|
194
|
+
}
|
|
195
|
+
export function triggers(definitions) {
|
|
196
|
+
return brandDeclaration(definitions, "section.triggers");
|
|
197
|
+
}
|
|
198
|
+
export function sources(definitions) {
|
|
199
|
+
return brandDeclaration(definitions, "section.sources");
|
|
200
|
+
}
|
|
201
|
+
export function taskDefaults(definitions) {
|
|
202
|
+
return brandDeclaration(definitions, "section.taskDefaults");
|
|
203
|
+
}
|
|
204
|
+
export function agents(definitions) {
|
|
205
|
+
return brandDeclaration(definitions, "section.agents");
|
|
206
|
+
}
|
|
207
|
+
export function sandboxes(definitions) {
|
|
208
|
+
return brandDeclaration(definitions, "section.sandboxes");
|
|
209
|
+
}
|
|
210
|
+
const PIPELINE_FIELDS = new Set(["name", "env", "commands", "agents", "sandboxes", "execution", "cache", "namedInputs", "taskDefaults", "triggers", "sync", "sources", "tasks", "jobs"]);
|
|
150
211
|
const AGENT_PROFILE_FIELDS = new Set(["command", "model"]);
|
|
151
212
|
const TASK_FIELDS = new Set(["description", "dependsOn", "inputs", "outputs", "cache", "retry", "timeout", "requires", "run", "steps"]);
|
|
152
|
-
const JOB_FIELDS = new Set(["description", "target", "trigger", "environment", "env", "requires", "github"]);
|
|
213
|
+
const JOB_FIELDS = new Set(["description", "target", "trigger", "environment", "env", "requires", "execution", "github"]);
|
|
214
|
+
const EXECUTION_PROFILE_FIELDS = new Set(["kind", "sandbox", "provider", "runsOn", "runsOnMatrix"]);
|
|
153
215
|
const GITHUB_JOB_FIELDS = new Set(["environment", "permissions", "runsOn", "runsOnMatrix"]);
|
|
154
216
|
const GITHUB_PERMISSION_FIELDS = new Set(["contents", "idToken", "issues", "packages", "pullRequests"]);
|
|
217
|
+
const CONTAINER_PROVIDERS = new Set(["auto", "docker", "apple-container", "lima"]);
|
|
218
|
+
const SECTION_KINDS = {
|
|
219
|
+
agents: "section.agents",
|
|
220
|
+
sandboxes: "section.sandboxes",
|
|
221
|
+
taskDefaults: "section.taskDefaults",
|
|
222
|
+
triggers: "section.triggers",
|
|
223
|
+
sources: "section.sources",
|
|
224
|
+
tasks: "section.tasks",
|
|
225
|
+
jobs: "section.jobs"
|
|
226
|
+
};
|
|
155
227
|
function rejectUnknownFields(known, value, where) {
|
|
156
228
|
for (const key of Object.keys(value)) {
|
|
157
229
|
if (!known.has(key)) {
|
|
@@ -177,12 +249,21 @@ function validateDefinitionShape(definition) {
|
|
|
177
249
|
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.`);
|
|
178
250
|
}
|
|
179
251
|
}
|
|
180
|
-
for (const [id, taskDefinition] of Object.entries(definition.tasks ?? {})) {
|
|
181
|
-
rejectUnknownFields(TASK_FIELDS, taskDefinition, `Task "${id}"`);
|
|
182
|
-
}
|
|
183
252
|
for (const [id, defaults] of Object.entries(definition.taskDefaults ?? {})) {
|
|
184
253
|
rejectUnknownFields(TASK_FIELDS, defaults, `taskDefaults["${id}"]`);
|
|
185
254
|
}
|
|
255
|
+
for (const [id, profile] of Object.entries(definition.execution ?? {})) {
|
|
256
|
+
rejectUnknownFields(EXECUTION_PROFILE_FIELDS, profile, `Execution profile "${id}"`);
|
|
257
|
+
if (profile.kind !== "local" && profile.kind !== "github") {
|
|
258
|
+
throw pipelineError("ASYNC_PIPELINE_EXECUTION_INVALID", `Execution profile "${id}" requires kind "local" or "github".`);
|
|
259
|
+
}
|
|
260
|
+
if (profile.provider !== undefined && !CONTAINER_PROVIDERS.has(profile.provider)) {
|
|
261
|
+
throw pipelineError("ASYNC_PIPELINE_EXECUTION_INVALID", `Execution profile "${id}" has unsupported provider "${profile.provider}". Use auto, docker, apple-container, or lima.`);
|
|
262
|
+
}
|
|
263
|
+
if (profile.kind === "local" && ("runsOn" in profile || "runsOnMatrix" in profile)) {
|
|
264
|
+
throw pipelineError("ASYNC_PIPELINE_EXECUTION_INVALID", `Execution profile "${id}" is local and cannot set GitHub runner fields. Use execution.github(...) for runsOn or runsOnMatrix.`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
186
267
|
for (const [id, jobDefinition] of Object.entries(definition.jobs ?? {})) {
|
|
187
268
|
rejectUnknownFields(JOB_FIELDS, jobDefinition, `Job "${id}"`);
|
|
188
269
|
if (jobDefinition.github) {
|
|
@@ -193,10 +274,112 @@ function validateDefinitionShape(definition) {
|
|
|
193
274
|
}
|
|
194
275
|
}
|
|
195
276
|
}
|
|
277
|
+
function normalizePipelineSections(definition) {
|
|
278
|
+
return {
|
|
279
|
+
...definition,
|
|
280
|
+
agents: normalizeSection("agents", definition.agents),
|
|
281
|
+
sandboxes: normalizeSection("sandboxes", definition.sandboxes),
|
|
282
|
+
taskDefaults: normalizeSection("taskDefaults", definition.taskDefaults),
|
|
283
|
+
triggers: normalizeSection("triggers", definition.triggers),
|
|
284
|
+
sources: normalizeSection("sources", definition.sources),
|
|
285
|
+
tasks: normalizeSection("tasks", definition.tasks),
|
|
286
|
+
jobs: normalizeSection("jobs", definition.jobs)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function normalizeSection(key, value) {
|
|
290
|
+
if (value === undefined)
|
|
291
|
+
return value;
|
|
292
|
+
if (!isObjectRecord(value)) {
|
|
293
|
+
throw pipelineError("ASYNC_PIPELINE_SECTION_INVALID", `Pipeline section "${key}" must be an object.`);
|
|
294
|
+
}
|
|
295
|
+
const expectedKind = SECTION_KINDS[key];
|
|
296
|
+
const metadata = assertSupportedDeclaration(value);
|
|
297
|
+
if (metadata) {
|
|
298
|
+
if (metadata.kind !== expectedKind) {
|
|
299
|
+
throw pipelineError("ASYNC_PIPELINE_SECTION_KIND_MISMATCH", `Pipeline section "${key}" expected declaration kind "${expectedKind}", received "${metadata.kind}".`);
|
|
300
|
+
}
|
|
301
|
+
return value;
|
|
302
|
+
}
|
|
303
|
+
return brandDeclaration(value, expectedKind);
|
|
304
|
+
}
|
|
305
|
+
function flattenTaskDefinitions(definitions) {
|
|
306
|
+
const tasks = [];
|
|
307
|
+
const seen = new Set();
|
|
308
|
+
function visit(node, path) {
|
|
309
|
+
for (const [key, value] of Object.entries(node)) {
|
|
310
|
+
validateTaskTreeKey(key, path);
|
|
311
|
+
if (!isObjectRecord(value)) {
|
|
312
|
+
throw pipelineError("ASYNC_PIPELINE_TASK_TREE_INVALID", `Task tree entry "${[...path, key].join(".")}" must be an object.`);
|
|
313
|
+
}
|
|
314
|
+
const isTask = isTaskDefinitionNode(value, path);
|
|
315
|
+
if (isTask) {
|
|
316
|
+
const id = taskTreeId(path, key);
|
|
317
|
+
validateLocalTaskId(id);
|
|
318
|
+
if (seen.has(id)) {
|
|
319
|
+
throw pipelineError("ASYNC_PIPELINE_TASK_ID_COLLISION", `Task group entry "${[...path, key].join(".")}" normalizes to duplicate task id "${id}".`);
|
|
320
|
+
}
|
|
321
|
+
seen.add(id);
|
|
322
|
+
rejectUnknownFields(TASK_FIELDS, value, `Task "${id}"`);
|
|
323
|
+
tasks.push({ id, definition: value, groupPath: path });
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (key.includes(".")) {
|
|
327
|
+
throw pipelineError("ASYNC_PIPELINE_TASK_GROUP_INVALID_KEY", `Task group key "${key}" cannot contain ".". Use nested objects instead.`);
|
|
328
|
+
}
|
|
329
|
+
visit(value, [...path, key]);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
visit(definitions, []);
|
|
333
|
+
return tasks;
|
|
334
|
+
}
|
|
335
|
+
function validateTaskTreeKey(key, path) {
|
|
336
|
+
if (!key.trim()) {
|
|
337
|
+
throw pipelineError("ASYNC_PIPELINE_TASK_GROUP_INVALID_KEY", "Task group key cannot be empty.");
|
|
338
|
+
}
|
|
339
|
+
if (key.includes(":")) {
|
|
340
|
+
throw pipelineError("ASYNC_PIPELINE_TASK_GROUP_INVALID_KEY", `Task group key "${key}" cannot contain ":". Use source namespaces through dependsOn instead.`);
|
|
341
|
+
}
|
|
342
|
+
if (path.length > 0 && key.includes(".")) {
|
|
343
|
+
throw pipelineError("ASYNC_PIPELINE_TASK_GROUP_INVALID_KEY", `Nested task group key "${key}" cannot contain ".". Use nested objects instead.`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function taskTreeId(path, key) {
|
|
347
|
+
const segments = path.length > 0 && (key === "default" || key === "index") ? path : [...path, key];
|
|
348
|
+
return segments.join(".");
|
|
349
|
+
}
|
|
350
|
+
function isTaskDefinitionNode(value, path) {
|
|
351
|
+
const metadata = assertSupportedDeclaration(value);
|
|
352
|
+
if (metadata?.kind === "task")
|
|
353
|
+
return true;
|
|
354
|
+
if (metadata?.kind.startsWith("section."))
|
|
355
|
+
return false;
|
|
356
|
+
if (metadata)
|
|
357
|
+
return false;
|
|
358
|
+
if (Object.keys(value).some((key) => TASK_FIELDS.has(key)))
|
|
359
|
+
return true;
|
|
360
|
+
return path.length === 0 && Object.keys(value).length === 0;
|
|
361
|
+
}
|
|
362
|
+
function resolveTaskDependencies(taskId, groupPath, dependencies, knownTaskIds) {
|
|
363
|
+
return dependencies.map((dependency) => resolveTaskDependency(taskId, groupPath, dependency, knownTaskIds));
|
|
364
|
+
}
|
|
365
|
+
function resolveTaskDependency(taskId, groupPath, dependency, knownTaskIds) {
|
|
366
|
+
if (isNamespacedTaskRef(dependency) || groupPath.length === 0)
|
|
367
|
+
return dependency;
|
|
368
|
+
const groupCandidate = [...groupPath, dependency].join(".");
|
|
369
|
+
const groupMatch = knownTaskIds.has(groupCandidate);
|
|
370
|
+
const rootMatch = knownTaskIds.has(dependency);
|
|
371
|
+
if (groupMatch && rootMatch && groupCandidate !== dependency) {
|
|
372
|
+
throw pipelineError("ASYNC_PIPELINE_TASK_DEPENDENCY_AMBIGUOUS", `Task "${taskId}" depends on ambiguous local task "${dependency}". Use "${groupCandidate}" or "${dependency}" explicitly.`);
|
|
373
|
+
}
|
|
374
|
+
if (groupMatch)
|
|
375
|
+
return groupCandidate;
|
|
376
|
+
return dependency;
|
|
377
|
+
}
|
|
196
378
|
export function definePipeline(definition) {
|
|
197
379
|
return normalizePipeline(definition);
|
|
198
380
|
}
|
|
199
381
|
export function normalizePipeline(definition) {
|
|
382
|
+
definition = normalizePipelineSections(definition);
|
|
200
383
|
validateDefinitionShape(definition);
|
|
201
384
|
const namedInputs = definition.namedInputs ?? {};
|
|
202
385
|
const cacheRegistry = normalizeCacheRegistry(definition.cache);
|
|
@@ -206,16 +389,18 @@ export function normalizePipeline(definition) {
|
|
|
206
389
|
sources[id] = normalizeSource(id, sourceDefinition);
|
|
207
390
|
}
|
|
208
391
|
const tasks = {};
|
|
209
|
-
|
|
392
|
+
const flattenedTaskDefinitions = flattenTaskDefinitions(definition.tasks);
|
|
393
|
+
const knownTaskIds = new Set(flattenedTaskDefinitions.map((entry) => entry.id));
|
|
394
|
+
for (const { id, definition: taskDefinition, groupPath } of flattenedTaskDefinitions) {
|
|
210
395
|
validateLocalTaskId(id);
|
|
211
396
|
const defaults = definition.taskDefaults?.[id] ?? definition.taskDefaults?.[taskName(id)] ?? {};
|
|
212
397
|
const merged = { ...defaults, ...taskDefinition };
|
|
213
398
|
const runItems = merged.steps ? [...merged.steps] : runItemsFromDefinition(merged.run);
|
|
214
399
|
const { steps, cacheDirectives, dependsOnDirectives } = partitionRunItems(runItems);
|
|
215
|
-
const liftedDependsOn = uniqueTaskIds([
|
|
400
|
+
const liftedDependsOn = resolveTaskDependencies(id, groupPath, uniqueTaskIds([
|
|
216
401
|
...(merged.dependsOn ?? []),
|
|
217
402
|
...dependsOnDirectives.flatMap((directive) => directive.taskIds)
|
|
218
|
-
]);
|
|
403
|
+
]), knownTaskIds);
|
|
219
404
|
const cache = normalizeCache(merged.cache ?? cacheDirectives[0], cacheRegistry);
|
|
220
405
|
const retry = normalizeRetry(merged.retry);
|
|
221
406
|
const timeoutMs = normalizeTimeout(merged.timeout);
|
|
@@ -246,6 +431,7 @@ export function normalizePipeline(definition) {
|
|
|
246
431
|
commands: definition.commands ? normalizeCommandPolicy(definition.commands) : undefined,
|
|
247
432
|
agents: normalizeAgents(definition.agents),
|
|
248
433
|
sandboxes: normalizeSandboxes(definition.sandboxes),
|
|
434
|
+
execution: normalizeExecutionProfiles(definition.execution),
|
|
249
435
|
cache: cacheRegistry,
|
|
250
436
|
namedInputs,
|
|
251
437
|
triggers: definition.triggers ?? {},
|
|
@@ -260,6 +446,12 @@ export function normalizePipeline(definition) {
|
|
|
260
446
|
function isDefaultOnlyEnvOptions(value) {
|
|
261
447
|
return typeof value.default === "string";
|
|
262
448
|
}
|
|
449
|
+
function isObjectRecord(value) {
|
|
450
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
451
|
+
}
|
|
452
|
+
function isEnvVarRef(value) {
|
|
453
|
+
return isObjectRecord(value) && value.kind === "async-pipeline.env.var";
|
|
454
|
+
}
|
|
263
455
|
function normalizeAgents(definitions = {}) {
|
|
264
456
|
const normalized = {};
|
|
265
457
|
for (const [id, profile] of Object.entries(definitions)) {
|
|
@@ -277,11 +469,18 @@ function normalizeSandboxes(definitions = {}) {
|
|
|
277
469
|
}
|
|
278
470
|
return normalized;
|
|
279
471
|
}
|
|
472
|
+
function normalizeExecutionProfiles(definitions = {}) {
|
|
473
|
+
const normalized = {};
|
|
474
|
+
for (const [id, definition] of Object.entries(definitions)) {
|
|
475
|
+
normalized[id] = cloneExecutionProfile(definition);
|
|
476
|
+
}
|
|
477
|
+
return normalized;
|
|
478
|
+
}
|
|
280
479
|
function normalizeCommandPolicy(policy) {
|
|
281
480
|
return command.policy(policy);
|
|
282
481
|
}
|
|
283
482
|
function cloneSandboxDefinition(definition) {
|
|
284
|
-
if (definition.kind === "docker") {
|
|
483
|
+
if (definition.kind === "docker" || definition.kind === "container") {
|
|
285
484
|
return {
|
|
286
485
|
...definition,
|
|
287
486
|
volumes: definition.volumes ? definition.volumes.map((volume) => ({ ...volume })) : undefined
|
|
@@ -289,6 +488,19 @@ function cloneSandboxDefinition(definition) {
|
|
|
289
488
|
}
|
|
290
489
|
return { ...definition };
|
|
291
490
|
}
|
|
491
|
+
function cloneExecutionProfile(definition) {
|
|
492
|
+
if (definition.kind === "github") {
|
|
493
|
+
return {
|
|
494
|
+
...definition,
|
|
495
|
+
runsOn: definition.runsOn ? cloneRunsOnEntry(definition.runsOn) : undefined,
|
|
496
|
+
runsOnMatrix: definition.runsOnMatrix ? definition.runsOnMatrix.map(cloneRunsOnEntry) : undefined
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
return { ...definition };
|
|
500
|
+
}
|
|
501
|
+
function cloneRunsOnEntry(entry) {
|
|
502
|
+
return Array.isArray(entry) ? [...entry] : entry;
|
|
503
|
+
}
|
|
292
504
|
function cloneCommandRule(rule) {
|
|
293
505
|
return {
|
|
294
506
|
exact: rule.exact ? [...rule.exact] : undefined,
|
|
@@ -323,7 +535,70 @@ function validateJobRunsOn(jobId, github) {
|
|
|
323
535
|
}
|
|
324
536
|
}
|
|
325
537
|
}
|
|
538
|
+
function githubConfigFromExecution(profile) {
|
|
539
|
+
if (!profile || profile.kind !== "github")
|
|
540
|
+
return undefined;
|
|
541
|
+
return {
|
|
542
|
+
runsOn: profile.runsOn ? cloneRunsOnEntry(profile.runsOn) : undefined,
|
|
543
|
+
runsOnMatrix: profile.runsOnMatrix ? profile.runsOnMatrix.map(cloneRunsOnEntry) : undefined
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
export function githubConfigForJob(pipeline, jobDefinition) {
|
|
547
|
+
const profileGithub = githubConfigFromExecution(jobDefinition.execution ? pipeline.execution[jobDefinition.execution] : undefined);
|
|
548
|
+
if (!profileGithub && !jobDefinition.github)
|
|
549
|
+
return undefined;
|
|
550
|
+
return {
|
|
551
|
+
...profileGithub,
|
|
552
|
+
...jobDefinition.github
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function validateExecutionProfiles(pipeline) {
|
|
556
|
+
for (const [id, profile] of Object.entries(pipeline.execution)) {
|
|
557
|
+
if (profile.sandbox !== undefined) {
|
|
558
|
+
const sandboxDefinition = pipeline.sandboxes[profile.sandbox];
|
|
559
|
+
if (!sandboxDefinition) {
|
|
560
|
+
throw pipelineError("ASYNC_PIPELINE_EXECUTION_UNKNOWN_SANDBOX", `Execution profile "${id}" references unknown sandbox "${profile.sandbox}". Declare it under sandboxes.`);
|
|
561
|
+
}
|
|
562
|
+
if (profile.provider !== undefined && sandboxDefinition.kind !== "container") {
|
|
563
|
+
throw pipelineError("ASYNC_PIPELINE_EXECUTION_PROVIDER_MISMATCH", `Execution profile "${id}" sets provider "${profile.provider}", but providers only apply to sandbox.container(...) definitions.`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
else if (profile.provider !== undefined) {
|
|
567
|
+
throw pipelineError("ASYNC_PIPELINE_EXECUTION_PROVIDER_MISMATCH", `Execution profile "${id}" sets provider "${profile.provider}" without a sandbox.`);
|
|
568
|
+
}
|
|
569
|
+
if (profile.kind === "github") {
|
|
570
|
+
validateJobRunsOn(`execution:${id}`, githubConfigFromExecution(profile));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function validateJobExecution(pipeline, jobDefinition) {
|
|
575
|
+
if (!jobDefinition.execution)
|
|
576
|
+
return;
|
|
577
|
+
const profile = pipeline.execution[jobDefinition.execution];
|
|
578
|
+
if (!profile) {
|
|
579
|
+
throw pipelineError("ASYNC_PIPELINE_EXECUTION_UNKNOWN", `Job "${jobDefinition.id}" references unknown execution profile "${jobDefinition.execution}". Declare it under execution.`);
|
|
580
|
+
}
|
|
581
|
+
const effectiveGithub = githubConfigForJob(pipeline, jobDefinition);
|
|
582
|
+
validateJobRunsOn(jobDefinition.id, effectiveGithub);
|
|
583
|
+
if (profile.kind === "github" && profile.provider === "apple-container") {
|
|
584
|
+
validateAppleContainerRunsOn(jobDefinition.id, effectiveGithub);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
function validateAppleContainerRunsOn(jobId, github) {
|
|
588
|
+
const entries = github?.runsOnMatrix ?? (github?.runsOn ? [github.runsOn] : []);
|
|
589
|
+
if (entries.length === 0) {
|
|
590
|
+
throw pipelineError("ASYNC_PIPELINE_EXECUTION_RUNNER_UNSUPPORTED", `Job "${jobId}" uses provider "apple-container", which requires an explicit self-hosted macOS runner label set.`);
|
|
591
|
+
}
|
|
592
|
+
for (const entry of entries) {
|
|
593
|
+
const labels = Array.isArray(entry) ? entry : [entry];
|
|
594
|
+
const normalized = labels.map((label) => label.toLowerCase());
|
|
595
|
+
if (!Array.isArray(entry) || !normalized.includes("self-hosted") || !normalized.includes("macos")) {
|
|
596
|
+
throw pipelineError("ASYNC_PIPELINE_EXECUTION_RUNNER_UNSUPPORTED", `Job "${jobId}" uses provider "apple-container", which requires a self-hosted macOS runner label set.`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
326
600
|
export function validatePipeline(pipeline) {
|
|
601
|
+
validateExecutionProfiles(pipeline);
|
|
327
602
|
for (const taskDefinition of Object.values(pipeline.tasks)) {
|
|
328
603
|
for (const dependency of taskDefinition.dependsOn) {
|
|
329
604
|
if (!pipeline.tasks[dependency] && !isKnownExternalTaskRef(pipeline, dependency)) {
|
|
@@ -353,7 +628,9 @@ export function validatePipeline(pipeline) {
|
|
|
353
628
|
throw new Error(`Job "${jobDefinition.id}" references missing trigger "${triggerId}".`);
|
|
354
629
|
}
|
|
355
630
|
}
|
|
356
|
-
|
|
631
|
+
validateJobExecution(pipeline, jobDefinition);
|
|
632
|
+
if (!jobDefinition.execution)
|
|
633
|
+
validateJobRunsOn(jobDefinition.id, jobDefinition.github);
|
|
357
634
|
}
|
|
358
635
|
validateSyncConfig(pipeline);
|
|
359
636
|
// Fail fast on named-input cycles at definePipeline time instead of
|
|
@@ -772,7 +1049,8 @@ function partitionRunItems(items) {
|
|
|
772
1049
|
const steps = [];
|
|
773
1050
|
const cacheDirectives = [];
|
|
774
1051
|
const dependsOnDirectives = [];
|
|
775
|
-
for (const
|
|
1052
|
+
for (const rawItem of items) {
|
|
1053
|
+
const item = normalizeDeclaredRunItem(rawItem);
|
|
776
1054
|
if (isCacheDirective(item)) {
|
|
777
1055
|
cacheDirectives.push(item);
|
|
778
1056
|
continue;
|
|
@@ -788,10 +1066,49 @@ function partitionRunItems(items) {
|
|
|
788
1066
|
}
|
|
789
1067
|
return { steps, cacheDirectives, dependsOnDirectives };
|
|
790
1068
|
}
|
|
1069
|
+
function normalizeDeclaredRunItem(item) {
|
|
1070
|
+
const metadata = assertSupportedDeclaration(item);
|
|
1071
|
+
if (!metadata || !isObjectRecord(item))
|
|
1072
|
+
return item;
|
|
1073
|
+
if (metadata.kind === "shell") {
|
|
1074
|
+
rejectUnknownFields(SHELL_STEP_FIELDS, item, "shell declaration");
|
|
1075
|
+
const command = item.command;
|
|
1076
|
+
if (typeof command !== "string") {
|
|
1077
|
+
throw pipelineError("ASYNC_PIPELINE_DECLARATION_INVALID", "Shell declaration requires a string command.");
|
|
1078
|
+
}
|
|
1079
|
+
return brandDeclaration({ kind: "shell", command }, "shell");
|
|
1080
|
+
}
|
|
1081
|
+
if (metadata.kind === "deferred-shell") {
|
|
1082
|
+
rejectUnknownFields(SHELL_STEP_FIELDS, item, "deferred shell declaration");
|
|
1083
|
+
const command = item.command;
|
|
1084
|
+
if (typeof command !== "function") {
|
|
1085
|
+
throw pipelineError("ASYNC_PIPELINE_DECLARATION_INVALID", "Deferred shell declaration requires a command function.");
|
|
1086
|
+
}
|
|
1087
|
+
return brandDeclaration({ kind: "deferred-shell", command: command }, "deferred-shell");
|
|
1088
|
+
}
|
|
1089
|
+
if (metadata.kind === "agent") {
|
|
1090
|
+
rejectUnknownFields(DECLARED_AGENT_STEP_FIELDS, item, "agent declaration");
|
|
1091
|
+
const use = item.use;
|
|
1092
|
+
const prompt = item.prompt;
|
|
1093
|
+
if ((typeof use !== "string" && !isEnvVarRef(use)) || (typeof use === "string" && use.length === 0)) {
|
|
1094
|
+
throw pipelineError("ASYNC_PIPELINE_AGENT_INVALID", 'agent declaration requires "use": a non-empty profile id or env.var(...).');
|
|
1095
|
+
}
|
|
1096
|
+
if (typeof prompt !== "string" || prompt.length === 0) {
|
|
1097
|
+
throw pipelineError("ASYNC_PIPELINE_AGENT_INVALID", 'agent declaration requires a non-empty "prompt" string.');
|
|
1098
|
+
}
|
|
1099
|
+
const step = brandDeclaration({ kind: "agent", use: use, prompt }, "agent");
|
|
1100
|
+
if (item.model !== undefined)
|
|
1101
|
+
step.model = item.model;
|
|
1102
|
+
if (item.stdoutTo !== undefined)
|
|
1103
|
+
step.stdoutTo = item.stdoutTo;
|
|
1104
|
+
return step;
|
|
1105
|
+
}
|
|
1106
|
+
return item;
|
|
1107
|
+
}
|
|
791
1108
|
function isDependsOnDirective(value) {
|
|
792
1109
|
return Boolean(value)
|
|
793
1110
|
&& typeof value === "object"
|
|
794
|
-
&& value.kind === "async-pipeline.directive.dependsOn";
|
|
1111
|
+
&& (value.kind === "async-pipeline.directive.dependsOn" || hasDeclarationKind(value, "directive.dependsOn"));
|
|
795
1112
|
}
|
|
796
1113
|
function uniqueTaskIds(taskIds) {
|
|
797
1114
|
return [...new Set(taskIds)];
|