@sparkwing/sdk 0.0.0-stage → 0.68.2
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 +183 -2
- package/dist/command.js +272 -0
- package/dist/command.js.map +1 -0
- package/dist/context.js +69 -0
- package/dist/context.js.map +1 -0
- package/dist/describe.js +70 -0
- package/dist/describe.js.map +1 -0
- package/dist/errors.js +88 -0
- package/dist/errors.js.map +1 -0
- package/dist/helpers/docker.js +267 -0
- package/dist/helpers/docker.js.map +1 -0
- package/dist/helpers/files.js +141 -0
- package/dist/helpers/files.js.map +1 -0
- package/dist/helpers/git.js +263 -0
- package/dist/helpers/git.js.map +1 -0
- package/dist/helpers/memo.js +29 -0
- package/dist/helpers/memo.js.map +1 -0
- package/dist/helpers/services.js +160 -0
- package/dist/helpers/services.js.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/json.js +27 -0
- package/dist/json.js.map +1 -0
- package/dist/log.js +42 -0
- package/dist/log.js.map +1 -0
- package/dist/main.js +68 -0
- package/dist/main.js.map +1 -0
- package/dist/plan-wire.js +2 -0
- package/dist/plan-wire.js.map +1 -0
- package/dist/plan.js +811 -0
- package/dist/plan.js.map +1 -0
- package/dist/protocol.js +209 -0
- package/dist/protocol.js.map +1 -0
- package/dist/routes.js +185 -0
- package/dist/routes.js.map +1 -0
- package/dist/runner.js +380 -0
- package/dist/runner.js.map +1 -0
- package/dist/scope.js +27 -0
- package/dist/scope.js.map +1 -0
- package/dist/step-state.js +53 -0
- package/dist/step-state.js.map +1 -0
- package/dist/transport.js +50 -0
- package/dist/transport.js.map +1 -0
- package/index.d.ts +390 -0
- package/native/win32-arm64/protocol.node +0 -0
- package/native/win32-arm64/verification.json +1 -0
- package/native/win32-x64/protocol.node +0 -0
- package/native/win32-x64/verification.json +1 -0
- package/package.json +47 -4
package/dist/plan.js
ADDED
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
import { jsonValue } from "./protocol.js";
|
|
2
|
+
import { argument } from "./describe.js";
|
|
3
|
+
import { posix } from "node:path";
|
|
4
|
+
import { failure, SDKError } from "./errors.js";
|
|
5
|
+
function identifier(kind, value) {
|
|
6
|
+
if (typeof value !== "string" || value.length === 0)
|
|
7
|
+
throw new Error(`${kind} identifier must not be empty`);
|
|
8
|
+
}
|
|
9
|
+
function integer(name, value, min = 0) {
|
|
10
|
+
if (!Number.isSafeInteger(value) || value < min)
|
|
11
|
+
throw new Error(`${name} must be a ${min === 0 ? "nonnegative" : "positive"} integer`);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function unique(values) {
|
|
15
|
+
return [...new Set(values)];
|
|
16
|
+
}
|
|
17
|
+
function names(kind, values) {
|
|
18
|
+
for (const value of values)
|
|
19
|
+
identifier(kind, value);
|
|
20
|
+
return unique(values);
|
|
21
|
+
}
|
|
22
|
+
function optionalField(target, key, value) {
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
delete target[key];
|
|
25
|
+
else
|
|
26
|
+
target[key] = value;
|
|
27
|
+
}
|
|
28
|
+
function dependencyIDs(deps) {
|
|
29
|
+
return unique(deps.flatMap((dep) => (typeof dep === "string" ? [dep] : "members" in dep ? dep.members.map((job) => job.id) : [dep.id])));
|
|
30
|
+
}
|
|
31
|
+
function stepIDs(deps) {
|
|
32
|
+
return unique(deps.flatMap((dep) => (typeof dep === "string" ? [dep] : "members" in dep ? dep.members.map((step) => step.id) : [dep.id])));
|
|
33
|
+
}
|
|
34
|
+
export class ConcurrencyGroup {
|
|
35
|
+
name;
|
|
36
|
+
limit;
|
|
37
|
+
constructor(name, limit = {}) {
|
|
38
|
+
identifier("concurrency group", name);
|
|
39
|
+
if (limit.capacity !== undefined)
|
|
40
|
+
integer("concurrency capacity", limit.capacity, 1);
|
|
41
|
+
if (limit.queueTimeoutMs !== undefined)
|
|
42
|
+
integer("concurrency queue timeout", limit.queueTimeoutMs);
|
|
43
|
+
if (limit.scope !== undefined && !["global", "run", "box"].includes(limit.scope))
|
|
44
|
+
throw new Error("invalid concurrency scope");
|
|
45
|
+
if (limit.onLimit !== undefined && !["queue", "fail", "skip", "cancel_others"].includes(limit.onLimit))
|
|
46
|
+
throw new Error("invalid concurrency onLimit");
|
|
47
|
+
this.name = name;
|
|
48
|
+
this.limit = Object.freeze({ ...limit });
|
|
49
|
+
}
|
|
50
|
+
toString() {
|
|
51
|
+
return this.name;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function concurrency(group, cost) {
|
|
55
|
+
const limit = group.limit;
|
|
56
|
+
if (cost !== undefined)
|
|
57
|
+
integer("concurrency cost", cost, 1);
|
|
58
|
+
if (cost !== undefined && limit.capacity !== undefined && cost > limit.capacity)
|
|
59
|
+
throw new Error(`cost ${cost} exceeds group capacity ${limit.capacity}`);
|
|
60
|
+
const spec = { group: group.name };
|
|
61
|
+
if (limit.scope !== undefined)
|
|
62
|
+
spec.scope = limit.scope;
|
|
63
|
+
if (limit.capacity !== undefined)
|
|
64
|
+
spec.capacity = limit.capacity;
|
|
65
|
+
if (cost !== undefined)
|
|
66
|
+
spec.cost = cost;
|
|
67
|
+
if (limit.onLimit !== undefined)
|
|
68
|
+
spec.on_limit = limit.onLimit;
|
|
69
|
+
if (limit.queueTimeoutMs !== undefined)
|
|
70
|
+
spec.queue_timeout_ms = limit.queueTimeoutMs;
|
|
71
|
+
return spec;
|
|
72
|
+
}
|
|
73
|
+
function addConcurrency(spec, group, cost) {
|
|
74
|
+
const entry = concurrency(group, cost);
|
|
75
|
+
const entries = (spec.concurrency ??= []);
|
|
76
|
+
const i = entries.findIndex((other) => other.group === entry.group && other.scope === entry.scope);
|
|
77
|
+
if (i < 0)
|
|
78
|
+
entries.push(entry);
|
|
79
|
+
else
|
|
80
|
+
entries[i] = entry;
|
|
81
|
+
}
|
|
82
|
+
export function startup() {
|
|
83
|
+
return { phase: "startup" };
|
|
84
|
+
}
|
|
85
|
+
export function goModules() {
|
|
86
|
+
return Object.freeze({ kind: "go_modules" });
|
|
87
|
+
}
|
|
88
|
+
export function npm() {
|
|
89
|
+
return Object.freeze({ kind: "npm" });
|
|
90
|
+
}
|
|
91
|
+
export function dir(path, ...keyFiles) {
|
|
92
|
+
identifier("cache path", path);
|
|
93
|
+
if (!keyFiles.length)
|
|
94
|
+
throw new Error("directory cache needs at least one key file");
|
|
95
|
+
return Object.freeze({ kind: "dir", path, keyFiles: Object.freeze(names("cache key file", keyFiles)) });
|
|
96
|
+
}
|
|
97
|
+
function collect(closures) {
|
|
98
|
+
if (!closures.length)
|
|
99
|
+
return undefined;
|
|
100
|
+
const spec = {};
|
|
101
|
+
for (const closure of closures) {
|
|
102
|
+
if (closure.kind === "verify" || closure.kind === "memo_key")
|
|
103
|
+
spec[closure.kind] = closure.id;
|
|
104
|
+
else
|
|
105
|
+
(spec[closure.kind] ??= []).push(closure.id);
|
|
106
|
+
}
|
|
107
|
+
return spec;
|
|
108
|
+
}
|
|
109
|
+
export class Step {
|
|
110
|
+
id;
|
|
111
|
+
body;
|
|
112
|
+
owner;
|
|
113
|
+
#spec;
|
|
114
|
+
#skip = [];
|
|
115
|
+
#dryRun;
|
|
116
|
+
#result = false;
|
|
117
|
+
constructor(owner, id, body) {
|
|
118
|
+
identifier("step", id);
|
|
119
|
+
this.owner = owner;
|
|
120
|
+
this.id = id;
|
|
121
|
+
this.body = body;
|
|
122
|
+
this.#spec = { id };
|
|
123
|
+
}
|
|
124
|
+
needs(...steps) {
|
|
125
|
+
this.assertMutable();
|
|
126
|
+
this.#spec.needs = unique([...(this.#spec.needs ?? []), ...stepIDs(steps)]);
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
result() {
|
|
130
|
+
this.assertMutable();
|
|
131
|
+
this.#result = true;
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
skipIf(fn) {
|
|
135
|
+
this.assertMutable();
|
|
136
|
+
this.#skip.push(fn);
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
finally() {
|
|
140
|
+
this.assertMutable();
|
|
141
|
+
this.#spec.finally = true;
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
continueOnError() {
|
|
145
|
+
this.assertMutable();
|
|
146
|
+
this.#spec.continue_on_error = true;
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
optional() {
|
|
150
|
+
this.assertMutable();
|
|
151
|
+
this.#spec.optional = true;
|
|
152
|
+
return this.continueOnError();
|
|
153
|
+
}
|
|
154
|
+
dryRun(body) {
|
|
155
|
+
this.assertMutable();
|
|
156
|
+
this.#dryRun = body;
|
|
157
|
+
this.#spec.has_dry_run = true;
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
safeWithoutDryRun() {
|
|
161
|
+
this.assertMutable();
|
|
162
|
+
this.#spec.safe_without_dry_run = true;
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
risk(...labels) {
|
|
166
|
+
this.assertMutable();
|
|
167
|
+
this.#spec.risks = names("risk", [...(this.#spec.risks ?? []), ...labels]);
|
|
168
|
+
return this;
|
|
169
|
+
}
|
|
170
|
+
addGroup(id) {
|
|
171
|
+
this.assertMutable();
|
|
172
|
+
this.#spec.groups = unique([...(this.#spec.groups ?? []), id]);
|
|
173
|
+
}
|
|
174
|
+
assertMutable() {
|
|
175
|
+
this.owner.assertMutable();
|
|
176
|
+
}
|
|
177
|
+
get isResult() {
|
|
178
|
+
return this.#result;
|
|
179
|
+
}
|
|
180
|
+
get dryRunBody() {
|
|
181
|
+
return this.#dryRun;
|
|
182
|
+
}
|
|
183
|
+
closures() {
|
|
184
|
+
return this.#skip.map((fn, i) => ({
|
|
185
|
+
id: `${encodeURIComponent(this.owner.id)}/step/${encodeURIComponent(this.id)}/skip_if/${i}`,
|
|
186
|
+
kind: "skip_if",
|
|
187
|
+
fn,
|
|
188
|
+
job: this.owner,
|
|
189
|
+
step: this,
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
toSpec() {
|
|
193
|
+
const spec = structuredClone(this.#spec);
|
|
194
|
+
optionalField(spec, "closures", collect(this.closures()));
|
|
195
|
+
return spec;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
class StepGroup {
|
|
199
|
+
id;
|
|
200
|
+
members;
|
|
201
|
+
constructor(id, members) {
|
|
202
|
+
this.id = id;
|
|
203
|
+
this.members = Object.freeze([...members]);
|
|
204
|
+
}
|
|
205
|
+
needs(...steps) {
|
|
206
|
+
for (const member of this.members)
|
|
207
|
+
member.needs(...steps);
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
skipIf(fn) {
|
|
211
|
+
for (const member of this.members)
|
|
212
|
+
member.skipIf(fn);
|
|
213
|
+
return this;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export class Declaration {
|
|
217
|
+
id;
|
|
218
|
+
owner;
|
|
219
|
+
spec;
|
|
220
|
+
callbacks = [];
|
|
221
|
+
constructor(owner, id) {
|
|
222
|
+
identifier("job", id);
|
|
223
|
+
this.owner = owner;
|
|
224
|
+
this.id = id;
|
|
225
|
+
this.spec = { id };
|
|
226
|
+
}
|
|
227
|
+
needs(...jobs) {
|
|
228
|
+
this.assertMutable();
|
|
229
|
+
this.spec.deps = unique([...(this.spec.deps ?? []), ...dependencyIDs(jobs)]);
|
|
230
|
+
return this;
|
|
231
|
+
}
|
|
232
|
+
needsIfPresent(...jobs) {
|
|
233
|
+
this.assertMutable();
|
|
234
|
+
this.spec.deps_if_present = unique([...(this.spec.deps_if_present ?? []), ...dependencyIDs(jobs)]);
|
|
235
|
+
return this;
|
|
236
|
+
}
|
|
237
|
+
needsGenerated(...generators) {
|
|
238
|
+
this.assertMutable();
|
|
239
|
+
this.spec.needs_generated = unique([...(this.spec.needs_generated ?? []), ...generators.map((g) => g.id)]);
|
|
240
|
+
return this;
|
|
241
|
+
}
|
|
242
|
+
skipIf(fn, options = {}) {
|
|
243
|
+
this.assertMutable();
|
|
244
|
+
this.callbacks.push({ id: this.closureID("skip_if"), kind: "skip_if", fn, job: this });
|
|
245
|
+
if (options.budgetMs !== undefined)
|
|
246
|
+
this.spec.skip_if_budget_ms = integer("skip budget", options.budgetMs);
|
|
247
|
+
return this;
|
|
248
|
+
}
|
|
249
|
+
optional() {
|
|
250
|
+
this.assertMutable();
|
|
251
|
+
this.spec.optional = true;
|
|
252
|
+
return this.continueOnError();
|
|
253
|
+
}
|
|
254
|
+
continueOnError() {
|
|
255
|
+
this.assertMutable();
|
|
256
|
+
this.spec.continue_on_error = true;
|
|
257
|
+
return this;
|
|
258
|
+
}
|
|
259
|
+
onFailure(id, body) {
|
|
260
|
+
this.assertMutable();
|
|
261
|
+
if (this.owner.jobs().some((job) => job.spec.on_failure_of === this.id))
|
|
262
|
+
throw new Error("job already has a recovery");
|
|
263
|
+
const recovery = this.owner.job(id, body);
|
|
264
|
+
recovery.spec.on_failure_of = this.id;
|
|
265
|
+
return recovery;
|
|
266
|
+
}
|
|
267
|
+
addGroup(id) {
|
|
268
|
+
this.assertMutable();
|
|
269
|
+
this.spec.groups = unique([...(this.spec.groups ?? []), id]);
|
|
270
|
+
}
|
|
271
|
+
assertMutable() {
|
|
272
|
+
this.owner.assertMutable();
|
|
273
|
+
}
|
|
274
|
+
closureID(kind) {
|
|
275
|
+
return `${encodeURIComponent(this.id)}/${kind}/${this.callbacks.filter((c) => c.kind === kind).length}`;
|
|
276
|
+
}
|
|
277
|
+
steps() {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
findStep(id) {
|
|
281
|
+
return this.steps().find((step) => step.id === id);
|
|
282
|
+
}
|
|
283
|
+
argSpecs() {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
closures() {
|
|
287
|
+
return [...this.callbacks, ...this.steps().flatMap((step) => step.closures())];
|
|
288
|
+
}
|
|
289
|
+
toSpec() {
|
|
290
|
+
const spec = structuredClone(this.spec);
|
|
291
|
+
if (this.argSpecs().length)
|
|
292
|
+
spec.arguments = this.argSpecs().map((arg) => argument(arg, this.id));
|
|
293
|
+
optionalField(spec, "closures", collect(this.callbacks));
|
|
294
|
+
const steps = this.steps();
|
|
295
|
+
if (steps.length) {
|
|
296
|
+
spec.steps = steps.map((step) => step.toSpec());
|
|
297
|
+
const results = steps.filter((step) => step.isResult);
|
|
298
|
+
if (results.length > 1)
|
|
299
|
+
throw new Error("job has more than one result step");
|
|
300
|
+
if (results[0])
|
|
301
|
+
spec.result_step = results[0].id;
|
|
302
|
+
}
|
|
303
|
+
return spec;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
class Approval extends Declaration {
|
|
307
|
+
constructor(owner, id, options) {
|
|
308
|
+
super(owner, id);
|
|
309
|
+
this.spec.approval = {};
|
|
310
|
+
if (options.message !== undefined)
|
|
311
|
+
this.spec.approval.message = options.message;
|
|
312
|
+
if (options.timeoutMs !== undefined)
|
|
313
|
+
this.spec.approval.timeout_ms = integer("approval timeout", options.timeoutMs);
|
|
314
|
+
if (options.onTimeout !== undefined) {
|
|
315
|
+
if (!["fail", "deny", "approve"].includes(options.onTimeout))
|
|
316
|
+
throw new Error("invalid approval onTimeout");
|
|
317
|
+
this.spec.approval.on_timeout = options.onTimeout;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const knownTools = new Set([
|
|
322
|
+
"aws",
|
|
323
|
+
"buildx",
|
|
324
|
+
"crane",
|
|
325
|
+
"docker",
|
|
326
|
+
"git",
|
|
327
|
+
"go",
|
|
328
|
+
"golangci-lint",
|
|
329
|
+
"helm",
|
|
330
|
+
"kubectl",
|
|
331
|
+
"node",
|
|
332
|
+
"npm",
|
|
333
|
+
"shellcheck",
|
|
334
|
+
"terraform",
|
|
335
|
+
]);
|
|
336
|
+
export class Job extends Declaration {
|
|
337
|
+
#steps = [];
|
|
338
|
+
#args = [];
|
|
339
|
+
#tools = [];
|
|
340
|
+
#requires = [];
|
|
341
|
+
#hasBody;
|
|
342
|
+
#stepGroups = new Set();
|
|
343
|
+
constructor(owner, id, body) {
|
|
344
|
+
super(owner, id);
|
|
345
|
+
this.#hasBody = body !== undefined;
|
|
346
|
+
if (body)
|
|
347
|
+
this.#steps.push(new Step(this, "run", body).result());
|
|
348
|
+
}
|
|
349
|
+
steps() {
|
|
350
|
+
return this.#steps;
|
|
351
|
+
}
|
|
352
|
+
argSpecs() {
|
|
353
|
+
return structuredClone(this.#args);
|
|
354
|
+
}
|
|
355
|
+
withArgs(...args) {
|
|
356
|
+
this.assertMutable();
|
|
357
|
+
const all = [...this.#args, ...args];
|
|
358
|
+
this.#args = structuredClone(all);
|
|
359
|
+
return this;
|
|
360
|
+
}
|
|
361
|
+
startsChildren(...pipelines) {
|
|
362
|
+
return this.startsChildrenIn("", ...pipelines);
|
|
363
|
+
}
|
|
364
|
+
startsChildrenIn(repository, ...pipelines) {
|
|
365
|
+
this.assertMutable();
|
|
366
|
+
(this.spec.children ??= []).push(...pipelines.map((pipeline) => ({ pipeline, ...(repository ? { repository } : {}) })));
|
|
367
|
+
return this;
|
|
368
|
+
}
|
|
369
|
+
references(...refs) {
|
|
370
|
+
this.assertMutable();
|
|
371
|
+
const entries = (this.spec.pipeline_refs ??= []);
|
|
372
|
+
for (const ref of refs)
|
|
373
|
+
if (!entries.some((entry) => entry.pipeline === ref.pipeline && entry.job === ref.jobId))
|
|
374
|
+
entries.push({ pipeline: ref.pipeline, job: ref.jobId });
|
|
375
|
+
return this;
|
|
376
|
+
}
|
|
377
|
+
step(id, body) {
|
|
378
|
+
this.assertMutable();
|
|
379
|
+
if (this.#hasBody)
|
|
380
|
+
throw new Error("a job with a body cannot also have steps");
|
|
381
|
+
if (this.findStep(id))
|
|
382
|
+
throw new Error(`duplicate step ${id}`);
|
|
383
|
+
const step = new Step(this, id, body);
|
|
384
|
+
this.#steps.push(step);
|
|
385
|
+
return step;
|
|
386
|
+
}
|
|
387
|
+
groupSteps(id, ...steps) {
|
|
388
|
+
this.assertMutable();
|
|
389
|
+
identifier("step group", id);
|
|
390
|
+
if (this.#stepGroups.has(id))
|
|
391
|
+
throw new Error(`duplicate step group ${id}`);
|
|
392
|
+
const members = steps.map((step) => {
|
|
393
|
+
if (!(step instanceof Step) || step.owner !== this)
|
|
394
|
+
throw new Error("step group member belongs to another job");
|
|
395
|
+
return step;
|
|
396
|
+
});
|
|
397
|
+
for (const member of members)
|
|
398
|
+
member.addGroup(id);
|
|
399
|
+
this.#stepGroups.add(id);
|
|
400
|
+
return new StepGroup(id, members);
|
|
401
|
+
}
|
|
402
|
+
failurePolicy(value) {
|
|
403
|
+
this.assertMutable();
|
|
404
|
+
if (!["fail_fast", "collect_all"].includes(value))
|
|
405
|
+
throw new Error("invalid failure policy");
|
|
406
|
+
this.spec.failure_policy = value;
|
|
407
|
+
return this;
|
|
408
|
+
}
|
|
409
|
+
retry(attempts, options = {}) {
|
|
410
|
+
this.assertMutable();
|
|
411
|
+
const retry = { attempts: integer("retry attempts", attempts) };
|
|
412
|
+
if (options.backoffMs !== undefined)
|
|
413
|
+
retry.backoff_ms = integer("retry backoff", options.backoffMs);
|
|
414
|
+
this.spec.retry = retry;
|
|
415
|
+
return this;
|
|
416
|
+
}
|
|
417
|
+
timeout(ms) {
|
|
418
|
+
this.assertMutable();
|
|
419
|
+
this.spec.timeout_ms = integer("timeout", ms);
|
|
420
|
+
return this;
|
|
421
|
+
}
|
|
422
|
+
noProgressTimeout(ms) {
|
|
423
|
+
this.assertMutable();
|
|
424
|
+
this.spec.no_progress_timeout_ms = integer("no-progress timeout", ms);
|
|
425
|
+
return this;
|
|
426
|
+
}
|
|
427
|
+
beforeRun(fn) {
|
|
428
|
+
this.assertMutable();
|
|
429
|
+
this.callbacks.push({ id: this.closureID("before_run"), kind: "before_run", fn, job: this });
|
|
430
|
+
return this;
|
|
431
|
+
}
|
|
432
|
+
afterRun(fn) {
|
|
433
|
+
this.assertMutable();
|
|
434
|
+
this.callbacks.push({ id: this.closureID("after_run"), kind: "after_run", fn, job: this });
|
|
435
|
+
return this;
|
|
436
|
+
}
|
|
437
|
+
verify(fn) {
|
|
438
|
+
this.assertMutable();
|
|
439
|
+
this.callbacks = this.callbacks.filter((c) => c.kind !== "verify");
|
|
440
|
+
this.callbacks.push({ id: this.closureID("verify"), kind: "verify", fn, job: this });
|
|
441
|
+
return this;
|
|
442
|
+
}
|
|
443
|
+
memo(fn, options = {}) {
|
|
444
|
+
this.assertMutable();
|
|
445
|
+
const memo = {};
|
|
446
|
+
if (options.ttlMs !== undefined)
|
|
447
|
+
memo.ttl_ms = integer("memo TTL", options.ttlMs);
|
|
448
|
+
this.spec.memo = memo;
|
|
449
|
+
this.callbacks = this.callbacks.filter((c) => c.kind !== "memo_key");
|
|
450
|
+
if (typeof fn === "function")
|
|
451
|
+
this.callbacks.push({ id: this.closureID("memo_key"), kind: "memo_key", fn, job: this });
|
|
452
|
+
else
|
|
453
|
+
memo.input = jsonValue(fn, true);
|
|
454
|
+
return this;
|
|
455
|
+
}
|
|
456
|
+
cache(...caches) {
|
|
457
|
+
this.assertMutable();
|
|
458
|
+
const entries = caches.map((cache) => {
|
|
459
|
+
if (cache.kind === "go_modules")
|
|
460
|
+
return { name: "go-modules", resolver: "go-modules", key_files: ["go.sum"] };
|
|
461
|
+
if (cache.kind === "npm")
|
|
462
|
+
return { name: "npm", resolver: "npm", key_files: ["package-lock.json"] };
|
|
463
|
+
if (cache.kind !== "dir" || !cache.path || !cache.keyFiles?.length)
|
|
464
|
+
throw new Error("directory cache needs a path and at least one key file");
|
|
465
|
+
const clean = posix.normalize(cache.path).replace(/(.)\/+$/, "$1");
|
|
466
|
+
return { name: posix.basename(clean), path: cache.path, key_scope: clean, key_files: names("cache key file", cache.keyFiles) };
|
|
467
|
+
});
|
|
468
|
+
(this.spec.caches ??= []).push(...entries);
|
|
469
|
+
return this;
|
|
470
|
+
}
|
|
471
|
+
artifacts(...globs) {
|
|
472
|
+
this.assertMutable();
|
|
473
|
+
this.spec.artifacts = names("artifact glob", [...(this.spec.artifacts ?? []), ...globs]);
|
|
474
|
+
return this;
|
|
475
|
+
}
|
|
476
|
+
consumes(producer, options = {}) {
|
|
477
|
+
this.assertMutable();
|
|
478
|
+
const ids = dependencyIDs([producer]);
|
|
479
|
+
(this.spec.consumes ??= []).push(...ids.map((id) => (options.into === undefined ? { producer: id } : { producer: id, into: options.into })));
|
|
480
|
+
return this.needs(...ids);
|
|
481
|
+
}
|
|
482
|
+
concurrency(group, cost) {
|
|
483
|
+
this.assertMutable();
|
|
484
|
+
addConcurrency(this.spec, group, cost);
|
|
485
|
+
return this;
|
|
486
|
+
}
|
|
487
|
+
needsTools(...values) {
|
|
488
|
+
this.assertMutable();
|
|
489
|
+
for (const name of values)
|
|
490
|
+
if (!knownTools.has(name))
|
|
491
|
+
throw new Error(`not a known tool: ${name}`);
|
|
492
|
+
this.#tools = unique([...this.#tools, ...values.map((name) => `tool:${name}`)]);
|
|
493
|
+
return this;
|
|
494
|
+
}
|
|
495
|
+
requires(...labels) {
|
|
496
|
+
this.assertMutable();
|
|
497
|
+
this.#requires = [...labels];
|
|
498
|
+
return this;
|
|
499
|
+
}
|
|
500
|
+
prefers(...labels) {
|
|
501
|
+
this.assertMutable();
|
|
502
|
+
this.spec.prefers = [...labels];
|
|
503
|
+
return this;
|
|
504
|
+
}
|
|
505
|
+
inline() {
|
|
506
|
+
this.assertMutable();
|
|
507
|
+
this.spec.inline = true;
|
|
508
|
+
return this;
|
|
509
|
+
}
|
|
510
|
+
env(name, value, option) {
|
|
511
|
+
this.assertMutable();
|
|
512
|
+
if (!name || name.includes("=") || name.includes("\0") || value.includes("\0"))
|
|
513
|
+
throw new Error("invalid environment variable");
|
|
514
|
+
(this.spec.env ??= Object.create(null))[name] =
|
|
515
|
+
option?.phase === "startup" ? { value, startup: true } : { value };
|
|
516
|
+
return this;
|
|
517
|
+
}
|
|
518
|
+
generate(fn) {
|
|
519
|
+
this.assertMutable();
|
|
520
|
+
const generator = new Generator(this.closureID("generate"), this);
|
|
521
|
+
this.callbacks.push({
|
|
522
|
+
id: generator.id,
|
|
523
|
+
kind: "generate",
|
|
524
|
+
fn: fn,
|
|
525
|
+
generator: generator,
|
|
526
|
+
job: this,
|
|
527
|
+
});
|
|
528
|
+
return generator;
|
|
529
|
+
}
|
|
530
|
+
toSpec() {
|
|
531
|
+
const spec = super.toSpec();
|
|
532
|
+
const requires = [...this.#requires, ...this.#tools];
|
|
533
|
+
if (requires.length)
|
|
534
|
+
spec.requires = requires;
|
|
535
|
+
return spec;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
export class Generator {
|
|
539
|
+
id;
|
|
540
|
+
source;
|
|
541
|
+
constructor(id, source) {
|
|
542
|
+
this.id = id;
|
|
543
|
+
this.source = source;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
class JobGroup {
|
|
547
|
+
id;
|
|
548
|
+
members;
|
|
549
|
+
constructor(id, members) {
|
|
550
|
+
this.id = id;
|
|
551
|
+
this.members = Object.freeze([...members]);
|
|
552
|
+
}
|
|
553
|
+
apply(fn) {
|
|
554
|
+
for (const member of this.members)
|
|
555
|
+
fn(member);
|
|
556
|
+
return this;
|
|
557
|
+
}
|
|
558
|
+
needs(...jobs) {
|
|
559
|
+
return this.apply((job) => job.needs(...jobs));
|
|
560
|
+
}
|
|
561
|
+
needsIfPresent(...jobs) {
|
|
562
|
+
return this.apply((job) => job.needsIfPresent(...jobs));
|
|
563
|
+
}
|
|
564
|
+
needsGenerated(...generators) {
|
|
565
|
+
return this.apply((job) => job.needsGenerated(...generators));
|
|
566
|
+
}
|
|
567
|
+
retry(attempts, options) {
|
|
568
|
+
return this.apply((job) => job.retry(attempts, options));
|
|
569
|
+
}
|
|
570
|
+
timeout(ms) {
|
|
571
|
+
return this.apply((job) => job.timeout(ms));
|
|
572
|
+
}
|
|
573
|
+
noProgressTimeout(ms) {
|
|
574
|
+
return this.apply((job) => job.noProgressTimeout(ms));
|
|
575
|
+
}
|
|
576
|
+
skipIf(fn, options) {
|
|
577
|
+
return this.apply((job) => job.skipIf(fn, options));
|
|
578
|
+
}
|
|
579
|
+
beforeRun(fn) {
|
|
580
|
+
return this.apply((job) => job.beforeRun(fn));
|
|
581
|
+
}
|
|
582
|
+
verify(fn) {
|
|
583
|
+
return this.apply((job) => job.verify(fn));
|
|
584
|
+
}
|
|
585
|
+
afterRun(fn) {
|
|
586
|
+
return this.apply((job) => job.afterRun(fn));
|
|
587
|
+
}
|
|
588
|
+
memo(fn, options) {
|
|
589
|
+
return this.apply((job) => job.memo(fn, options));
|
|
590
|
+
}
|
|
591
|
+
cache(...caches) {
|
|
592
|
+
return this.apply((job) => job.cache(...caches));
|
|
593
|
+
}
|
|
594
|
+
artifacts(...globs) {
|
|
595
|
+
return this.apply((job) => job.artifacts(...globs));
|
|
596
|
+
}
|
|
597
|
+
consumes(producer, options) {
|
|
598
|
+
return this.apply((job) => job.consumes(producer, options));
|
|
599
|
+
}
|
|
600
|
+
concurrency(group, cost) {
|
|
601
|
+
return this.apply((job) => job.concurrency(group, cost));
|
|
602
|
+
}
|
|
603
|
+
needsTools(...names) {
|
|
604
|
+
return this.apply((job) => job.needsTools(...names));
|
|
605
|
+
}
|
|
606
|
+
requires(...labels) {
|
|
607
|
+
return this.apply((job) => job.requires(...labels));
|
|
608
|
+
}
|
|
609
|
+
prefers(...labels) {
|
|
610
|
+
return this.apply((job) => job.prefers(...labels));
|
|
611
|
+
}
|
|
612
|
+
inline() {
|
|
613
|
+
return this.apply((job) => job.inline());
|
|
614
|
+
}
|
|
615
|
+
optional() {
|
|
616
|
+
return this.apply((job) => job.optional());
|
|
617
|
+
}
|
|
618
|
+
continueOnError() {
|
|
619
|
+
return this.apply((job) => job.continueOnError());
|
|
620
|
+
}
|
|
621
|
+
env(name, value, option) {
|
|
622
|
+
return this.apply((job) => job.env(name, value, option));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
export class Plan {
|
|
626
|
+
declarations = [];
|
|
627
|
+
settings = {};
|
|
628
|
+
#groups = new Set();
|
|
629
|
+
#sealed = false;
|
|
630
|
+
assertMutable() {
|
|
631
|
+
if (this.#sealed)
|
|
632
|
+
throw failure("plan_drift", "declarations cannot change after planning");
|
|
633
|
+
}
|
|
634
|
+
seal() {
|
|
635
|
+
this.#sealed = true;
|
|
636
|
+
}
|
|
637
|
+
job(id, body) {
|
|
638
|
+
this.assertMutable();
|
|
639
|
+
return this.add(new Job(this, id, body));
|
|
640
|
+
}
|
|
641
|
+
approval(id, options = {}) {
|
|
642
|
+
this.assertMutable();
|
|
643
|
+
return this.add(new Approval(this, id, options));
|
|
644
|
+
}
|
|
645
|
+
add(job) {
|
|
646
|
+
this.assertMutable();
|
|
647
|
+
if (this.findJob(job.id))
|
|
648
|
+
throw new Error(`duplicate job ${job.id}`);
|
|
649
|
+
this.declarations.push(job);
|
|
650
|
+
return job;
|
|
651
|
+
}
|
|
652
|
+
groupJobs(id, ...jobs) {
|
|
653
|
+
this.assertMutable();
|
|
654
|
+
identifier("job group", id);
|
|
655
|
+
if (this.#groups.has(id))
|
|
656
|
+
throw new Error(`duplicate job group ${id}`);
|
|
657
|
+
const members = jobs.map((job) => {
|
|
658
|
+
if (!(job instanceof Job) || job.owner !== this)
|
|
659
|
+
throw new Error("job group member belongs to another plan");
|
|
660
|
+
return job;
|
|
661
|
+
});
|
|
662
|
+
for (const member of members)
|
|
663
|
+
member.addGroup(id);
|
|
664
|
+
this.#groups.add(id);
|
|
665
|
+
return new JobGroup(id, members);
|
|
666
|
+
}
|
|
667
|
+
concurrency(group, cost) {
|
|
668
|
+
this.assertMutable();
|
|
669
|
+
addConcurrency(this.settings, group, cost);
|
|
670
|
+
return this;
|
|
671
|
+
}
|
|
672
|
+
priority(value) {
|
|
673
|
+
this.assertMutable();
|
|
674
|
+
if (!Number.isSafeInteger(value))
|
|
675
|
+
throw new Error("priority must be an integer");
|
|
676
|
+
this.settings.priority = value;
|
|
677
|
+
return this;
|
|
678
|
+
}
|
|
679
|
+
claimWait(ms) {
|
|
680
|
+
this.assertMutable();
|
|
681
|
+
this.settings.claim_wait_ms = integer("claim wait", ms);
|
|
682
|
+
return this;
|
|
683
|
+
}
|
|
684
|
+
admissionClass(value) {
|
|
685
|
+
this.assertMutable();
|
|
686
|
+
identifier("admission class", value);
|
|
687
|
+
this.settings.admission_class = value;
|
|
688
|
+
return this;
|
|
689
|
+
}
|
|
690
|
+
checkout(options) {
|
|
691
|
+
this.assertMutable();
|
|
692
|
+
const source = {};
|
|
693
|
+
if (options.depth !== undefined)
|
|
694
|
+
source.depth = integer("checkout depth", options.depth);
|
|
695
|
+
if (options.fullHistory !== undefined)
|
|
696
|
+
source.full_history = options.fullHistory;
|
|
697
|
+
if (options.tags !== undefined)
|
|
698
|
+
source.tags = options.tags;
|
|
699
|
+
if (options.submodules !== undefined)
|
|
700
|
+
source.submodules = options.submodules;
|
|
701
|
+
if (options.lfs !== undefined)
|
|
702
|
+
source.lfs = options.lfs;
|
|
703
|
+
this.settings.source = source;
|
|
704
|
+
return this;
|
|
705
|
+
}
|
|
706
|
+
jobs() {
|
|
707
|
+
return this.declarations;
|
|
708
|
+
}
|
|
709
|
+
findJob(id) {
|
|
710
|
+
return this.declarations.find((job) => job.id === id);
|
|
711
|
+
}
|
|
712
|
+
findClosure(id) {
|
|
713
|
+
return this.declarations.flatMap((job) => job.closures()).find((closure) => closure.id === id);
|
|
714
|
+
}
|
|
715
|
+
toDoc() {
|
|
716
|
+
const jobs = this.declarations.map((job) => job.toSpec());
|
|
717
|
+
return { jobs, ...structuredClone(this.settings) };
|
|
718
|
+
}
|
|
719
|
+
insert(generated) {
|
|
720
|
+
if (generated.parent !== this)
|
|
721
|
+
throw new Error("generated jobs belong to another plan");
|
|
722
|
+
const jobs = generated.toDoc().jobs;
|
|
723
|
+
generated.seal();
|
|
724
|
+
this.declarations.push(...generated.jobs());
|
|
725
|
+
return jobs;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
export class Generated extends Plan {
|
|
729
|
+
parent;
|
|
730
|
+
source;
|
|
731
|
+
generatorID;
|
|
732
|
+
constructor(source, generatorID, parent) {
|
|
733
|
+
super();
|
|
734
|
+
this.parent = parent;
|
|
735
|
+
this.source = source;
|
|
736
|
+
this.generatorID = generatorID;
|
|
737
|
+
const closure = parent.findClosure(generatorID);
|
|
738
|
+
if (closure?.kind !== "generate" || closure.job !== source)
|
|
739
|
+
throw new Error("generator does not belong to source");
|
|
740
|
+
}
|
|
741
|
+
add(job) {
|
|
742
|
+
job.needs(this.source).addGroup(this.generatorID);
|
|
743
|
+
for (const group of this.source.toSpec().groups ?? [])
|
|
744
|
+
job.addGroup(group);
|
|
745
|
+
return super.add(job);
|
|
746
|
+
}
|
|
747
|
+
toDoc() {
|
|
748
|
+
if (Object.keys(this.settings).length)
|
|
749
|
+
throw new Error("generated jobs cannot change plan settings");
|
|
750
|
+
const jobs = this.declarations.map((job) => job.toSpec());
|
|
751
|
+
return { jobs };
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
export function refTo(job) {
|
|
755
|
+
return {
|
|
756
|
+
jobId: job.id,
|
|
757
|
+
async get(ctx) {
|
|
758
|
+
const value = await ctx.output(job);
|
|
759
|
+
if (value === undefined)
|
|
760
|
+
throw failure("not_found", "job output not found");
|
|
761
|
+
return value;
|
|
762
|
+
},
|
|
763
|
+
async tryGet(ctx) {
|
|
764
|
+
return tryReference(() => ctx.output(job));
|
|
765
|
+
},
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
export function refToLastRun(pipeline, jobId, options = {}) {
|
|
769
|
+
identifier("pipeline", pipeline);
|
|
770
|
+
identifier("job", jobId);
|
|
771
|
+
const ref = {
|
|
772
|
+
pipeline,
|
|
773
|
+
jobId,
|
|
774
|
+
get(ctx) {
|
|
775
|
+
return ctx.latest(ref);
|
|
776
|
+
},
|
|
777
|
+
tryGet(ctx) {
|
|
778
|
+
return tryReference(() => ctx.latest(ref));
|
|
779
|
+
},
|
|
780
|
+
...(options.maxAgeMs === undefined ? {} : { maxAgeMs: integer("reference max age", options.maxAgeMs) }),
|
|
781
|
+
};
|
|
782
|
+
return Object.freeze(ref);
|
|
783
|
+
}
|
|
784
|
+
async function tryReference(read) {
|
|
785
|
+
try {
|
|
786
|
+
return await read();
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
if (error instanceof SDKError && error.code === "not_found")
|
|
790
|
+
return undefined;
|
|
791
|
+
throw error;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
const registry = new Map();
|
|
795
|
+
export function definePipeline(definition) {
|
|
796
|
+
identifier("pipeline", definition.name);
|
|
797
|
+
if (registry.has(definition.name))
|
|
798
|
+
throw new Error(`pipeline ${definition.name} is already defined`);
|
|
799
|
+
registry.set(definition.name, definition);
|
|
800
|
+
return definition;
|
|
801
|
+
}
|
|
802
|
+
export function pipelines() {
|
|
803
|
+
return [...registry.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
804
|
+
}
|
|
805
|
+
export function lookupPipeline(name) {
|
|
806
|
+
return registry.get(name);
|
|
807
|
+
}
|
|
808
|
+
export function resetPipelines() {
|
|
809
|
+
registry.clear();
|
|
810
|
+
}
|
|
811
|
+
//# sourceMappingURL=plan.js.map
|