@akanjs/cli 3.0.2 → 3.1.0-rc.1
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/.build-stamp +1 -1
- package/{CodeAgent-szbjv4gv.js → CodeAgent-kyprmy06.js} +2 -2
- package/{CodeTui-5nw86ab6.js → CodeTui-21mtze3j.js} +1 -1
- package/{TunnelShare-cwh9ezsm.js → TunnelShare-q4jq08qg.js} +2 -2
- package/{agent.command-np1dffkb.js → agent.command-f3gsc1gd.js} +5 -5
- package/akanCode.js +2 -2
- package/{application.command-gk69v1xs.js → application.command-5s780vnz.js} +37 -15
- package/{applicationBuildRunner-wx09n9g6.js → applicationBuildRunner-1yaqsrdq.js} +115 -103
- package/buildBatch.proc.js +16 -5
- package/cloud.command-yj6xb2a7.js +192 -0
- package/cloud.script-h6pvcpa2.js +20 -0
- package/{code.command-hmtx14b2.js → code.command-3wkn4ja5.js} +6 -6
- package/commandManifest.json +1 -1
- package/{context.command-6aqvdx43.js → context.command-9jjn3gcw.js} +13 -13
- package/context.runner-atcrzz5b.js +21 -0
- package/{guideline.command-axvf68gc.js → guideline.command-dtczrqpf.js} +2 -2
- package/guidelines/agentRule/agentRule.instruction.md +10 -2
- package/guidelines/conventions/conventions.instruction.md +70 -25
- package/guidelines/cssRule/cssRule.instruction.md +19 -0
- package/guidelines/mcpRule/mcpRule.instruction.md +72 -12
- package/guidelines/queryRule/queryRule.instruction.md +11 -1
- package/guidelines/runtimeRule/runtimeRule.instruction.md +97 -16
- package/guidelines/ssrRule/ssrRule.instruction.md +10 -7
- package/guidelines/transportRule/transportRule.instruction.md +19 -0
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +6 -6
- package/incrementalBuilder.proc.js +24 -3
- package/{index-h9ht6s60.js → index-05g3jayg.js} +1 -1
- package/{index-jk9xnck6.js → index-1w2qaee2.js} +240 -133
- package/{index-g38nhv87.js → index-2etcv0sc.js} +1 -1
- package/{index-zyw622y1.js → index-2mx692qq.js} +3 -3
- package/{index-50wb2a26.js → index-55mynnqs.js} +354 -129
- package/{index-2wc41k5w.js → index-56d2g8ms.js} +2 -2
- package/{index-q6ft9fkc.js → index-6jjtnhkw.js} +8 -3
- package/index-6w0rbr9d.js +68 -0
- package/{index-hxnnfy57.js → index-8ggztp8k.js} +204 -8
- package/{index-zwd84m0w.js → index-8r4d7cmt.js} +4 -4
- package/{index-j38qkvkk.js → index-ae5pwhgs.js} +12 -10
- package/{index-gtjgnv36.js → index-aprt7pkz.js} +6 -9
- package/{index-1bjkfvm9.js → index-c1x0be5j.js} +2 -2
- package/{index-z750gk1j.js → index-c2yrsxdh.js} +13 -7
- package/{index-cb6yc5mb.js → index-c76k723e.js} +2 -2
- package/{index-6keyf4sg.js → index-ctanya41.js} +4 -4
- package/{index-f4e08ptp.js → index-e0cvze1s.js} +4 -4
- package/{index-hp3stk1m.js → index-fm46xrf1.js} +3 -3
- package/{index-3xmzr1p1.js → index-gnyf1fqy.js} +2 -2
- package/{index-nwd6qzvv.js → index-jx4bzvwg.js} +138 -55
- package/{index-jb0hmt3k.js → index-krq7btbt.js} +3 -3
- package/{index-94s080qm.js → index-pgj2rt78.js} +139 -1
- package/index-q958tqqg.js +77 -0
- package/{index-50jjwg8y.js → index-qt22xstj.js} +1 -1
- package/index-rc5rwyps.js +201 -0
- package/{index-xwprn9nr.js → index-rtct7d43.js} +1 -1
- package/{index-bsv901fd.js → index-rwq3ekqc.js} +7 -7
- package/{index-1gs9g12n.js → index-v29746vb.js} +4 -4
- package/index-ww9bhp67.js +62 -0
- package/index.js +22 -21
- package/{library.command-9m2rdwwz.js → library.command-gaga5bmq.js} +3 -3
- package/{localRegistry.command-kda81jyk.js → localRegistry.command-vsreyy42.js} +9 -9
- package/{module.command-q63dffya.js → module.command-7vpf6db4.js} +5 -5
- package/{package.command-y2jh0m99.js → package.command-twmvvf69.js} +3 -3
- package/package.json +2 -2
- package/{page.command-f3e4cwhh.js → page.command-5asq7fdf.js} +3 -3
- package/pipeline.command-d0v38572.js +1334 -0
- package/{primitive.command-r4kq75qj.js → primitive.command-dyrs2fm8.js} +6 -6
- package/projectTypecheck-cvtkrzp2.js +7 -0
- package/{quality.command-f7gzh26z.js → quality.command-6emsqpbg.js} +5 -4
- package/{repair.command-0qmyr6qa.js → repair.command-8t6aegv5.js} +5 -5
- package/{scalar.command-kddn7av4.js → scalar.command-bnggy1x7.js} +4 -4
- package/{subspace.command-ft65qvc4.js → subspace.command-md2teygq.js} +158 -91
- package/templates/app/env/env.client.type.ts +2 -3
- package/templates/app/page/styles.css.template +8 -0
- package/templates/appSample/page/task/[taskId]/_index.tsx +2 -2
- package/templates/appSample/page/task/[taskId]/edit.tsx +2 -2
- package/templates/appSample/page/task/_index.tsx +2 -2
- package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -2
- package/templates/crudPages/[__model__Id]/page.tsx +3 -3
- package/templates/crudPages/page.tsx +2 -2
- package/templates/crudSinglePage/page.tsx +3 -3
- package/templates/lib/cnst.ts +2 -2
- package/templates/lib/sig.ts +3 -3
- package/{tunnel.command-b3ymdk4f.js → tunnel.command-jx3tdm2d.js} +16 -13
- package/{workflow.command-z1srcdfw.js → workflow.command-jx0wag5r.js} +10 -10
- package/{workspace.command-vp6vmx3z.js → workspace.command-yxd7rr7g.js} +57 -27
- package/cloud.command-tw06613r.js +0 -95
- package/cloud.script-64gzba8r.js +0 -20
- package/context.runner-hnkvv3z3.js +0 -21
- package/index-t91a7339.js +0 -104
|
@@ -0,0 +1,1334 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
Workspace,
|
|
4
|
+
runner,
|
|
5
|
+
script,
|
|
6
|
+
command
|
|
7
|
+
} from "./index-6jjtnhkw.js";
|
|
8
|
+
import {
|
|
9
|
+
FileSys2
|
|
10
|
+
} from "./index-52w0fvh8.js";
|
|
11
|
+
import {
|
|
12
|
+
AkanAppConfig,
|
|
13
|
+
AkanWorkspaceConfig,
|
|
14
|
+
AppExecutor
|
|
15
|
+
} from "./index-55mynnqs.js";
|
|
16
|
+
import {
|
|
17
|
+
resolveMobileTargets
|
|
18
|
+
} from "./index-q958tqqg.js";
|
|
19
|
+
|
|
20
|
+
// pkgs/@akanjs/cli/pipeline/pipeline.command.ts
|
|
21
|
+
import { pipelineTriggers as pipelineTriggers2 } from "akanjs/common";
|
|
22
|
+
|
|
23
|
+
// pkgs/@akanjs/devkit/pipeline/PipelineCompiler.ts
|
|
24
|
+
import {
|
|
25
|
+
defaultAppPipeline
|
|
26
|
+
} from "akanjs";
|
|
27
|
+
import {
|
|
28
|
+
PipelinePattern as PipelinePattern2,
|
|
29
|
+
PipelinePlanSchema,
|
|
30
|
+
pipelineActionNames as pipelineActionNames2,
|
|
31
|
+
pipelineActions,
|
|
32
|
+
pipelineMilestones as pipelineMilestones2,
|
|
33
|
+
pipelineOtaPlatforms as pipelineOtaPlatforms2,
|
|
34
|
+
pipelinePlanVersion
|
|
35
|
+
} from "akanjs/common";
|
|
36
|
+
|
|
37
|
+
// pkgs/@akanjs/devkit/pipeline/PipelineInputReader.ts
|
|
38
|
+
import {
|
|
39
|
+
PipelineNames,
|
|
40
|
+
PipelinePattern,
|
|
41
|
+
pipelineActionNames,
|
|
42
|
+
pipelineMilestones,
|
|
43
|
+
pipelineOtaPlatforms,
|
|
44
|
+
pipelinePlanLimits,
|
|
45
|
+
pipelineTriggers
|
|
46
|
+
} from "akanjs/common";
|
|
47
|
+
|
|
48
|
+
class PipelineInputReader {
|
|
49
|
+
static #futureKeys = ["cache", "artifacts", "input", "image", "services", "notify"];
|
|
50
|
+
static #durationPattern = /^(\d+)(s|m|h)$/;
|
|
51
|
+
static #stepNameChars = 80;
|
|
52
|
+
diagnostics = [];
|
|
53
|
+
#file;
|
|
54
|
+
constructor(file) {
|
|
55
|
+
this.#file = file;
|
|
56
|
+
}
|
|
57
|
+
workspace(value) {
|
|
58
|
+
const input = this.#object(value, "pipeline", ["schemaVersion", "env", "timeout", "before", "after", "post"]);
|
|
59
|
+
if (!input)
|
|
60
|
+
return null;
|
|
61
|
+
this.#schemaVersion(input.schemaVersion, "pipeline.schemaVersion");
|
|
62
|
+
const ids = new Set;
|
|
63
|
+
const stages = (key) => this.#list(input[key], `pipeline.${key}`).flatMap((stage, idx) => {
|
|
64
|
+
const rule = this.#workspaceStage(stage, `pipeline.${key}[${idx}]`);
|
|
65
|
+
if (rule)
|
|
66
|
+
this.#claimId(ids, rule.id, rule.path);
|
|
67
|
+
return rule ? [rule] : [];
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
env: this.#env(input.env, "pipeline.env"),
|
|
71
|
+
timeoutSeconds: this.#duration(input.timeout, "pipeline.timeout"),
|
|
72
|
+
before: stages("before"),
|
|
73
|
+
after: stages("after"),
|
|
74
|
+
post: this.#post(input.post, "pipeline.post")
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
app(value) {
|
|
78
|
+
const input = this.#object(value, "pipeline", ["schemaVersion", "stages", "env", "after"]);
|
|
79
|
+
if (!input)
|
|
80
|
+
return null;
|
|
81
|
+
this.#schemaVersion(input.schemaVersion, "pipeline.schemaVersion");
|
|
82
|
+
if (input.stages === undefined)
|
|
83
|
+
this.#report("pipeline.schema.invalid", "lists no `stages`", "pipeline");
|
|
84
|
+
const ids = new Set;
|
|
85
|
+
const stages = this.#list(input.stages, "pipeline.stages").flatMap((stage, idx) => {
|
|
86
|
+
const rule = this.#appStage(stage, `pipeline.stages[${idx}]`);
|
|
87
|
+
if (rule)
|
|
88
|
+
this.#claimId(ids, rule.id, rule.path);
|
|
89
|
+
return rule ? [rule] : [];
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
env: this.#env(input.env, "pipeline.env"),
|
|
93
|
+
after: this.#strings(input.after, "pipeline.after"),
|
|
94
|
+
stages
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
#workspaceStage(value, path) {
|
|
98
|
+
const raw = this.#object(value, path);
|
|
99
|
+
if (!raw)
|
|
100
|
+
return null;
|
|
101
|
+
if ("use" in raw) {
|
|
102
|
+
this.#report("pipeline.schema.invalid", "built-in actions are declared per app, in apps/<app>/akan.config.ts", `${path}.use`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
if ("parallel" in raw)
|
|
106
|
+
return this.#parallelStage(raw, path);
|
|
107
|
+
if ("matrix" in raw)
|
|
108
|
+
return this.#matrixStage(raw, path);
|
|
109
|
+
return this.#shellStage(raw, path, "workspace");
|
|
110
|
+
}
|
|
111
|
+
#appStage(value, path) {
|
|
112
|
+
const raw = this.#object(value, path);
|
|
113
|
+
if (!raw)
|
|
114
|
+
return null;
|
|
115
|
+
if ("parallel" in raw || "matrix" in raw) {
|
|
116
|
+
this.#report("pipeline.schema.invalid", `an app's own stage takes no \`${"parallel" in raw ? "parallel" : "matrix"}\`: it is already that app's, and parallel or per-app copies belong to the workspace root akan.config.ts`, path);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
if ("use" in raw)
|
|
120
|
+
return this.#actionStage(raw, path);
|
|
121
|
+
return this.#shellStage(raw, path, "app");
|
|
122
|
+
}
|
|
123
|
+
#actionStage(raw, path) {
|
|
124
|
+
if (!this.#keys(raw, path, [
|
|
125
|
+
"use",
|
|
126
|
+
"id",
|
|
127
|
+
"name",
|
|
128
|
+
"description",
|
|
129
|
+
"milestone",
|
|
130
|
+
"when",
|
|
131
|
+
"timeout",
|
|
132
|
+
"allowFailure",
|
|
133
|
+
"retry",
|
|
134
|
+
...raw.use === "ota" ? ["targets", "platforms"] : []
|
|
135
|
+
]))
|
|
136
|
+
return null;
|
|
137
|
+
const action = this.#oneOf(raw.use, `${path}.use`, pipelineActionNames);
|
|
138
|
+
if (!action)
|
|
139
|
+
return null;
|
|
140
|
+
const name = raw.name === undefined ? null : this.#text(raw.name, `${path}.name`, pipelinePlanLimits.nameChars);
|
|
141
|
+
const id = raw.id === undefined ? action : this.#id(raw.id, `${path}.id`);
|
|
142
|
+
if (!id)
|
|
143
|
+
return null;
|
|
144
|
+
const attempts = raw.retry === undefined ? null : this.#int(raw.retry, `${path}.retry`, 1, pipelinePlanLimits.retryAttempts);
|
|
145
|
+
return {
|
|
146
|
+
kind: "action",
|
|
147
|
+
action,
|
|
148
|
+
id,
|
|
149
|
+
name: name ?? action,
|
|
150
|
+
named: name !== null,
|
|
151
|
+
description: this.#description(raw.description, `${path}.description`),
|
|
152
|
+
milestone: this.#milestone(raw.milestone, `${path}.milestone`),
|
|
153
|
+
when: this.#when(raw.when, `${path}.when`, false),
|
|
154
|
+
timeoutSeconds: this.#duration(raw.timeout, `${path}.timeout`),
|
|
155
|
+
allowFailure: this.#boolean(raw.allowFailure, `${path}.allowFailure`),
|
|
156
|
+
retry: attempts === null ? null : { attempts, delaySeconds: 0 },
|
|
157
|
+
targets: raw.targets === undefined ? null : this.#nonEmptySet(this.#strings(raw.targets, `${path}.targets`), `${path}.targets`),
|
|
158
|
+
platforms: raw.platforms === undefined ? null : this.#nonEmptySet(this.#list(raw.platforms, `${path}.platforms`).flatMap((platform, idx) => {
|
|
159
|
+
const parsed = this.#oneOf(platform, `${path}.platforms[${idx}]`, pipelineOtaPlatforms);
|
|
160
|
+
return parsed ? [parsed] : [];
|
|
161
|
+
}), `${path}.platforms`),
|
|
162
|
+
path
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
#nonEmptySet(items, path) {
|
|
166
|
+
if (!items.length) {
|
|
167
|
+
this.#report("pipeline.schema.invalid", "lists none: leave it out to take the default", path);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return [...new Set(items)];
|
|
171
|
+
}
|
|
172
|
+
#shellStage(raw, path, scope) {
|
|
173
|
+
const keys = [
|
|
174
|
+
"id",
|
|
175
|
+
"name",
|
|
176
|
+
"description",
|
|
177
|
+
"milestone",
|
|
178
|
+
"when",
|
|
179
|
+
"timeout",
|
|
180
|
+
"allowFailure",
|
|
181
|
+
"sh",
|
|
182
|
+
"steps",
|
|
183
|
+
"env",
|
|
184
|
+
"secrets",
|
|
185
|
+
"retry"
|
|
186
|
+
];
|
|
187
|
+
if (!this.#keys(raw, path, keys))
|
|
188
|
+
return null;
|
|
189
|
+
const head = this.#stageHead(raw, path, scope === "workspace" ? "workspace" : "app");
|
|
190
|
+
if (!head)
|
|
191
|
+
return null;
|
|
192
|
+
const steps = this.#stageSteps(raw, path);
|
|
193
|
+
return steps ? { kind: "sh", ...head, steps } : null;
|
|
194
|
+
}
|
|
195
|
+
#matrixStage(raw, path) {
|
|
196
|
+
const keys = [
|
|
197
|
+
"id",
|
|
198
|
+
"name",
|
|
199
|
+
"description",
|
|
200
|
+
"milestone",
|
|
201
|
+
"when",
|
|
202
|
+
"timeout",
|
|
203
|
+
"allowFailure",
|
|
204
|
+
"sh",
|
|
205
|
+
"steps",
|
|
206
|
+
"env",
|
|
207
|
+
"secrets",
|
|
208
|
+
"retry",
|
|
209
|
+
"matrix",
|
|
210
|
+
"maxParallel"
|
|
211
|
+
];
|
|
212
|
+
if (!this.#keys(raw, path, keys))
|
|
213
|
+
return null;
|
|
214
|
+
if (raw.matrix !== "apps") {
|
|
215
|
+
this.#report("pipeline.schema.invalid", `${JSON.stringify(raw.matrix)} is not a matrix axis: only "apps"`, `${path}.matrix`);
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const head = this.#stageHead(raw, path, "matrix");
|
|
219
|
+
if (!head)
|
|
220
|
+
return null;
|
|
221
|
+
const steps = this.#stageSteps(raw, path);
|
|
222
|
+
if (!steps)
|
|
223
|
+
return null;
|
|
224
|
+
return { kind: "matrix", ...head, maxParallel: this.#maxParallel(raw.maxParallel, `${path}.maxParallel`), steps };
|
|
225
|
+
}
|
|
226
|
+
#parallelStage(raw, path) {
|
|
227
|
+
const keys = [
|
|
228
|
+
"id",
|
|
229
|
+
"name",
|
|
230
|
+
"description",
|
|
231
|
+
"milestone",
|
|
232
|
+
"when",
|
|
233
|
+
"timeout",
|
|
234
|
+
"allowFailure",
|
|
235
|
+
"parallel",
|
|
236
|
+
"maxParallel",
|
|
237
|
+
"failFast",
|
|
238
|
+
"env",
|
|
239
|
+
"secrets"
|
|
240
|
+
];
|
|
241
|
+
if (!this.#keys(raw, path, keys))
|
|
242
|
+
return null;
|
|
243
|
+
const head = this.#stageHead(raw, path, "workspace");
|
|
244
|
+
if (!head)
|
|
245
|
+
return null;
|
|
246
|
+
const env = this.#env(raw.env, `${path}.env`);
|
|
247
|
+
const secrets = this.#secrets(raw.secrets, `${path}.secrets`);
|
|
248
|
+
const ids = new Set;
|
|
249
|
+
const branches = this.#list(raw.parallel, `${path}.parallel`).flatMap((branch, idx) => {
|
|
250
|
+
const branchPath = `${path}.parallel[${idx}]`;
|
|
251
|
+
const branchRaw = this.#object(branch, branchPath);
|
|
252
|
+
if (!branchRaw)
|
|
253
|
+
return [];
|
|
254
|
+
if ("use" in branchRaw || "parallel" in branchRaw || "matrix" in branchRaw) {
|
|
255
|
+
this.#report("pipeline.schema.invalid", "a parallel branch is a shell stage", branchPath);
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
const rule = this.#shellStage(branchRaw, branchPath, "workspace");
|
|
259
|
+
if (!rule)
|
|
260
|
+
return [];
|
|
261
|
+
if (rule.when.changed && head.when.changed) {
|
|
262
|
+
this.#report("pipeline.schema.invalid", "`when.changed` on both a parallel stage and its branch cannot be combined; keep one", `${branchPath}.when.changed`);
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
this.#claimId(ids, rule.id, rule.path);
|
|
266
|
+
const steps = rule.steps.map((step) => ({
|
|
267
|
+
...step,
|
|
268
|
+
env: { ...env, ...step.env },
|
|
269
|
+
secrets: [...new Set([...secrets, ...step.secrets])]
|
|
270
|
+
}));
|
|
271
|
+
return [{ ...rule, steps }];
|
|
272
|
+
});
|
|
273
|
+
if (!branches.length && Array.isArray(raw.parallel))
|
|
274
|
+
this.#report("pipeline.schema.invalid", "a parallel stage holds at least one branch", `${path}.parallel`);
|
|
275
|
+
return {
|
|
276
|
+
kind: "parallel",
|
|
277
|
+
...head,
|
|
278
|
+
maxParallel: this.#maxParallel(raw.maxParallel, `${path}.maxParallel`),
|
|
279
|
+
failFast: this.#boolean(raw.failFast, `${path}.failFast`),
|
|
280
|
+
branches
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
#stageHead(raw, path, whenScope) {
|
|
284
|
+
const name = this.#text(raw.name, `${path}.name`, pipelinePlanLimits.nameChars);
|
|
285
|
+
if (name === null)
|
|
286
|
+
return null;
|
|
287
|
+
const id = raw.id === undefined ? this.#slugId(name, `${path}.name`) : this.#id(raw.id, `${path}.id`);
|
|
288
|
+
if (!id)
|
|
289
|
+
return null;
|
|
290
|
+
return {
|
|
291
|
+
id,
|
|
292
|
+
name,
|
|
293
|
+
description: this.#description(raw.description, `${path}.description`),
|
|
294
|
+
milestone: this.#milestone(raw.milestone, `${path}.milestone`),
|
|
295
|
+
when: this.#when(raw.when, `${path}.when`, whenScope === "matrix"),
|
|
296
|
+
timeoutSeconds: this.#duration(raw.timeout, `${path}.timeout`),
|
|
297
|
+
allowFailure: this.#boolean(raw.allowFailure, `${path}.allowFailure`),
|
|
298
|
+
path
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
#stageSteps(raw, path) {
|
|
302
|
+
if (raw.sh === undefined === (raw.steps === undefined)) {
|
|
303
|
+
this.#report("pipeline.schema.invalid", "takes exactly one of `sh` and `steps`", path);
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
const env = this.#env(raw.env, `${path}.env`);
|
|
307
|
+
const secrets = this.#secrets(raw.secrets, `${path}.secrets`);
|
|
308
|
+
const retry = this.#retry(raw.retry, `${path}.retry`);
|
|
309
|
+
const steps = raw.sh === undefined ? this.#list(raw.steps, `${path}.steps`) : [raw.sh];
|
|
310
|
+
const stepsPath = raw.sh === undefined ? `${path}.steps` : path;
|
|
311
|
+
if (!steps.length) {
|
|
312
|
+
this.#report("pipeline.schema.invalid", "a stage runs at least one step", stepsPath);
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
const ids = new Set;
|
|
316
|
+
return steps.flatMap((step, idx) => {
|
|
317
|
+
const stepPath = raw.sh === undefined ? `${stepsPath}[${idx}]` : `${path}.sh`;
|
|
318
|
+
const rule = this.#step(step, stepPath, idx);
|
|
319
|
+
if (!rule)
|
|
320
|
+
return [];
|
|
321
|
+
this.#claimId(ids, rule.localId, stepPath);
|
|
322
|
+
return [
|
|
323
|
+
{
|
|
324
|
+
...rule,
|
|
325
|
+
retry: rule.retry ?? retry,
|
|
326
|
+
env: { ...env, ...rule.env },
|
|
327
|
+
secrets: [...new Set([...secrets, ...rule.secrets])]
|
|
328
|
+
}
|
|
329
|
+
];
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
#step(value, path, idx) {
|
|
333
|
+
const raw = typeof value === "string" ? { sh: value } : this.#object(value, path);
|
|
334
|
+
if (!raw)
|
|
335
|
+
return null;
|
|
336
|
+
const keys = ["sh", "id", "name", "cwd", "env", "secrets", "timeout", "retry", "allowFailure", "skipExitCodes"];
|
|
337
|
+
if (!this.#keys(raw, path, keys))
|
|
338
|
+
return null;
|
|
339
|
+
const run = this.#text(raw.sh, typeof value === "string" ? path : `${path}.sh`, Number.POSITIVE_INFINITY);
|
|
340
|
+
if (run === null)
|
|
341
|
+
return null;
|
|
342
|
+
if (new TextEncoder().encode(run).length > pipelinePlanLimits.shBytes) {
|
|
343
|
+
this.#report("pipeline.limit.exceeded", `a script is at most ${pipelinePlanLimits.shBytes} bytes`, path);
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
const name = raw.name === undefined ? PipelineInputReader.#nameOf(run) : this.#text(raw.name, `${path}.name`, pipelinePlanLimits.nameChars);
|
|
347
|
+
if (name === null)
|
|
348
|
+
return null;
|
|
349
|
+
const localId = raw.id !== undefined ? this.#id(raw.id, `${path}.id`) : raw.name !== undefined ? this.#slugId(name, `${path}.name`) : String(idx + 1);
|
|
350
|
+
if (!localId)
|
|
351
|
+
return null;
|
|
352
|
+
return {
|
|
353
|
+
localId,
|
|
354
|
+
name,
|
|
355
|
+
timeoutSeconds: this.#duration(raw.timeout, `${path}.timeout`),
|
|
356
|
+
retry: this.#retry(raw.retry, `${path}.retry`),
|
|
357
|
+
allowFailure: this.#boolean(raw.allowFailure, `${path}.allowFailure`),
|
|
358
|
+
run,
|
|
359
|
+
cwd: this.#cwd(raw.cwd, `${path}.cwd`),
|
|
360
|
+
env: this.#env(raw.env, `${path}.env`),
|
|
361
|
+
secrets: this.#secrets(raw.secrets, `${path}.secrets`),
|
|
362
|
+
skipExitCodes: this.#exitCodes(raw.skipExitCodes, `${path}.skipExitCodes`)
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
#post(value, path) {
|
|
366
|
+
const empty = { success: [], failure: [], canceled: [], always: [] };
|
|
367
|
+
if (value === undefined)
|
|
368
|
+
return empty;
|
|
369
|
+
const raw = this.#object(value, path);
|
|
370
|
+
if (!raw)
|
|
371
|
+
return empty;
|
|
372
|
+
for (const key of Object.keys(raw))
|
|
373
|
+
if (!["success", "failure", "canceled", "always"].includes(key))
|
|
374
|
+
this.#report("pipeline.schema.unknownKey", key === "unstable" ? "`unstable` is not a result the framework knows: the executor decides which list an allowed failure runs" : `"${key}" is not a post result: success, failure, canceled or always`, `${path}.${key}`);
|
|
375
|
+
const steps = (key) => {
|
|
376
|
+
const ids = new Set;
|
|
377
|
+
return this.#list(raw[key], `${path}.${key}`).flatMap((step, idx) => {
|
|
378
|
+
const rule = this.#step(step, `${path}.${key}[${idx}]`, idx);
|
|
379
|
+
if (rule)
|
|
380
|
+
this.#claimId(ids, rule.localId, `${path}.${key}[${idx}]`);
|
|
381
|
+
return rule ? [rule] : [];
|
|
382
|
+
});
|
|
383
|
+
};
|
|
384
|
+
return {
|
|
385
|
+
success: steps("success"),
|
|
386
|
+
failure: steps("failure"),
|
|
387
|
+
canceled: steps("canceled"),
|
|
388
|
+
always: steps("always")
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
#when(value, path, allowApp) {
|
|
392
|
+
const none = { branch: null, trigger: null, changed: null, app: null };
|
|
393
|
+
if (value === undefined)
|
|
394
|
+
return none;
|
|
395
|
+
const raw = this.#object(value, path);
|
|
396
|
+
if (!raw)
|
|
397
|
+
return none;
|
|
398
|
+
for (const key of Object.keys(raw)) {
|
|
399
|
+
if (["branch", "trigger", "changed"].includes(key) || allowApp && key === "app")
|
|
400
|
+
continue;
|
|
401
|
+
this.#report("pipeline.schema.unknownKey", key === "app" ? "`when.app` narrows a workspace matrix stage only; an app's own stages are already that app's" : `"${key}" is not a condition: branch, trigger, changed${allowApp ? " or app" : ""}`, `${path}.${key}`);
|
|
402
|
+
}
|
|
403
|
+
const triggers = raw.trigger === undefined ? null : (typeof raw.trigger === "string" ? [raw.trigger] : this.#list(raw.trigger, `${path}.trigger`)).flatMap((trigger, idx) => {
|
|
404
|
+
const parsed = this.#oneOf(trigger, `${path}.trigger[${idx}]`, pipelineTriggers);
|
|
405
|
+
return parsed ? [parsed] : [];
|
|
406
|
+
});
|
|
407
|
+
return {
|
|
408
|
+
branch: this.#patterns(raw.branch, `${path}.branch`),
|
|
409
|
+
trigger: triggers,
|
|
410
|
+
changed: this.#patterns(raw.changed, `${path}.changed`),
|
|
411
|
+
app: allowApp ? this.#patterns(raw.app, `${path}.app`) : null
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
#patterns(value, path) {
|
|
415
|
+
if (value === undefined)
|
|
416
|
+
return null;
|
|
417
|
+
const patterns = typeof value === "string" ? [value] : this.#strings(value, path);
|
|
418
|
+
const problem = PipelinePattern.validate(patterns);
|
|
419
|
+
if (problem) {
|
|
420
|
+
this.#report("pipeline.when.pattern", problem, path);
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
return patterns;
|
|
424
|
+
}
|
|
425
|
+
#schemaVersion(value, path) {
|
|
426
|
+
if (value !== undefined && value !== 1)
|
|
427
|
+
this.#report("pipeline.schema.unsupported", `${JSON.stringify(value)} is not a schemaVersion this CLI reads (1)`, path);
|
|
428
|
+
}
|
|
429
|
+
#env(value, path) {
|
|
430
|
+
if (value === undefined)
|
|
431
|
+
return {};
|
|
432
|
+
const raw = this.#object(value, path);
|
|
433
|
+
if (!raw)
|
|
434
|
+
return {};
|
|
435
|
+
const env = {};
|
|
436
|
+
for (const [name, entry] of Object.entries(raw)) {
|
|
437
|
+
const problem = PipelineNames.envNameError(name);
|
|
438
|
+
if (problem)
|
|
439
|
+
this.#report("pipeline.env.name", problem, `${path}.${name}`);
|
|
440
|
+
else if (typeof entry !== "string")
|
|
441
|
+
this.#report("pipeline.schema.invalid", "must be a string", `${path}.${name}`);
|
|
442
|
+
else
|
|
443
|
+
env[name] = entry;
|
|
444
|
+
}
|
|
445
|
+
return env;
|
|
446
|
+
}
|
|
447
|
+
#secrets(value, path) {
|
|
448
|
+
const names = value === undefined ? [] : this.#strings(value, path);
|
|
449
|
+
const valid = names.filter((name) => {
|
|
450
|
+
const problem = PipelineNames.secretNameError(name);
|
|
451
|
+
if (problem)
|
|
452
|
+
this.#report("pipeline.secret.name", problem, path);
|
|
453
|
+
return !problem;
|
|
454
|
+
});
|
|
455
|
+
return [...new Set(valid)];
|
|
456
|
+
}
|
|
457
|
+
#cwd(value, path) {
|
|
458
|
+
if (value === undefined)
|
|
459
|
+
return ".";
|
|
460
|
+
const cwd = this.#text(value, path, 1024);
|
|
461
|
+
if (cwd === null)
|
|
462
|
+
return ".";
|
|
463
|
+
if (cwd.startsWith("/") || cwd.startsWith("~") || cwd.includes("\\") || cwd.split("/").includes("..")) {
|
|
464
|
+
this.#report("pipeline.cwd.outside", `"${cwd}" is not a path inside the workspace`, path);
|
|
465
|
+
return ".";
|
|
466
|
+
}
|
|
467
|
+
return cwd;
|
|
468
|
+
}
|
|
469
|
+
#exitCodes(value, path) {
|
|
470
|
+
if (value === undefined)
|
|
471
|
+
return [];
|
|
472
|
+
const codes = this.#list(value, path).flatMap((code, idx) => {
|
|
473
|
+
const parsed = this.#int(code, `${path}[${idx}]`, 1, 255);
|
|
474
|
+
return parsed === null ? [] : [parsed];
|
|
475
|
+
});
|
|
476
|
+
return [...new Set(codes)];
|
|
477
|
+
}
|
|
478
|
+
#retry(value, path) {
|
|
479
|
+
if (value === undefined)
|
|
480
|
+
return null;
|
|
481
|
+
if (typeof value === "number") {
|
|
482
|
+
const attempts = this.#int(value, path, 1, pipelinePlanLimits.retryAttempts);
|
|
483
|
+
return attempts === null ? null : { attempts, delaySeconds: 0 };
|
|
484
|
+
}
|
|
485
|
+
const raw = this.#object(value, path);
|
|
486
|
+
if (!raw || !this.#keys(raw, path, ["attempts", "delay"]))
|
|
487
|
+
return null;
|
|
488
|
+
const attempts = this.#int(raw.attempts, `${path}.attempts`, 1, pipelinePlanLimits.retryAttempts);
|
|
489
|
+
const delaySeconds = this.#duration(raw.delay, `${path}.delay`, {
|
|
490
|
+
min: 0,
|
|
491
|
+
max: pipelinePlanLimits.retryDelaySeconds
|
|
492
|
+
});
|
|
493
|
+
return attempts === null ? null : { attempts, delaySeconds: delaySeconds ?? 0 };
|
|
494
|
+
}
|
|
495
|
+
#duration(value, path, { min = 1, max = 86400 } = {}) {
|
|
496
|
+
if (value === undefined)
|
|
497
|
+
return null;
|
|
498
|
+
const match = typeof value === "string" ? PipelineInputReader.#durationPattern.exec(value) : null;
|
|
499
|
+
const seconds = typeof value === "number" && Number.isInteger(value) ? value : match ? Number(match[1]) * { s: 1, m: 60, h: 3600 }[match[2]] : Number.NaN;
|
|
500
|
+
if (!Number.isInteger(seconds) || seconds < min || seconds > max) {
|
|
501
|
+
this.#report("pipeline.duration.invalid", `${JSON.stringify(value)} is not a duration between ${min} and ${max} seconds: whole seconds, or "30s", "20m", "1h"`, path);
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
return seconds;
|
|
505
|
+
}
|
|
506
|
+
#maxParallel(value, path) {
|
|
507
|
+
return value === undefined ? null : this.#int(value, path, 1, pipelinePlanLimits.stages);
|
|
508
|
+
}
|
|
509
|
+
#milestone(value, path) {
|
|
510
|
+
return value === undefined ? null : this.#oneOf(value, path, pipelineMilestones);
|
|
511
|
+
}
|
|
512
|
+
#description(value, path) {
|
|
513
|
+
return value === undefined ? null : this.#text(value, path, pipelinePlanLimits.descriptionChars);
|
|
514
|
+
}
|
|
515
|
+
#id(value, path) {
|
|
516
|
+
const id = this.#text(value, path, 64);
|
|
517
|
+
if (id === null)
|
|
518
|
+
return null;
|
|
519
|
+
const problem = PipelineNames.idError(id);
|
|
520
|
+
if (problem) {
|
|
521
|
+
this.#report("pipeline.id.invalid", problem, path);
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
return id;
|
|
525
|
+
}
|
|
526
|
+
#slugId(name, path) {
|
|
527
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").slice(0, 64).replace(/-+$/, "");
|
|
528
|
+
if (!slug) {
|
|
529
|
+
this.#report("pipeline.id.invalid", `"${name}" has no letter or digit to make an id from: give it an \`id\``, path);
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
return slug;
|
|
533
|
+
}
|
|
534
|
+
#claimId(ids, id, path) {
|
|
535
|
+
if (ids.has(id))
|
|
536
|
+
this.#report("pipeline.id.duplicate", `"${id}" is already the id of another one here`, path);
|
|
537
|
+
ids.add(id);
|
|
538
|
+
}
|
|
539
|
+
static #nameOf(run) {
|
|
540
|
+
const line = run.split(`
|
|
541
|
+
`).map((text) => text.trim()).find(Boolean);
|
|
542
|
+
const name = line ?? run.trim();
|
|
543
|
+
return name.length > PipelineInputReader.#stepNameChars ? `${name.slice(0, PipelineInputReader.#stepNameChars - 1)}\u2026` : name;
|
|
544
|
+
}
|
|
545
|
+
#keys(raw, path, keys) {
|
|
546
|
+
let ok = true;
|
|
547
|
+
for (const key of Object.keys(raw)) {
|
|
548
|
+
if (keys.includes(key))
|
|
549
|
+
continue;
|
|
550
|
+
ok = false;
|
|
551
|
+
if (PipelineInputReader.#futureKeys.includes(key))
|
|
552
|
+
this.#report("pipeline.schema.unsupported", `\`${key}\` is not supported in schemaVersion 1`, `${path}.${key}`);
|
|
553
|
+
else
|
|
554
|
+
this.#report("pipeline.schema.unknownKey", `"${key}" is not a key here: ${keys.join(", ")}`, `${path}.${key}`);
|
|
555
|
+
}
|
|
556
|
+
return ok;
|
|
557
|
+
}
|
|
558
|
+
#object(value, path, keys) {
|
|
559
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
560
|
+
this.#report("pipeline.schema.invalid", "must be an object", path);
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
const raw = value;
|
|
564
|
+
if (keys && !this.#keys(raw, path, keys))
|
|
565
|
+
return null;
|
|
566
|
+
return raw;
|
|
567
|
+
}
|
|
568
|
+
#list(value, path) {
|
|
569
|
+
if (value === undefined)
|
|
570
|
+
return [];
|
|
571
|
+
if (!Array.isArray(value)) {
|
|
572
|
+
this.#report("pipeline.schema.invalid", "must be a list", path);
|
|
573
|
+
return [];
|
|
574
|
+
}
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
#strings(value, path) {
|
|
578
|
+
return this.#list(value, path).flatMap((item, idx) => {
|
|
579
|
+
const text = this.#text(item, `${path}[${idx}]`, 1024);
|
|
580
|
+
return text === null ? [] : [text];
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
#text(value, path, max) {
|
|
584
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
585
|
+
this.#report("pipeline.schema.invalid", "must be a non-empty string", path);
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
if (value.length > max) {
|
|
589
|
+
this.#report("pipeline.schema.invalid", `is longer than ${max} characters`, path);
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
return value;
|
|
593
|
+
}
|
|
594
|
+
#boolean(value, path) {
|
|
595
|
+
if (value === undefined)
|
|
596
|
+
return false;
|
|
597
|
+
if (typeof value === "boolean")
|
|
598
|
+
return value;
|
|
599
|
+
this.#report("pipeline.schema.invalid", "must be true or false", path);
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
#int(value, path, min, max) {
|
|
603
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= min && value <= max)
|
|
604
|
+
return value;
|
|
605
|
+
this.#report("pipeline.schema.invalid", `must be a whole number between ${min} and ${max}`, path);
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
#oneOf(value, path, options) {
|
|
609
|
+
if (typeof value === "string" && options.includes(value))
|
|
610
|
+
return value;
|
|
611
|
+
this.#report("pipeline.schema.invalid", `${JSON.stringify(value)} is not one of ${options.join(", ")}`, path);
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
#report(code, message, path) {
|
|
615
|
+
this.diagnostics.push({ severity: "error", code, message, file: this.#file, path });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// pkgs/@akanjs/devkit/pipeline/PipelineCompiler.ts
|
|
620
|
+
class PipelineCompiler {
|
|
621
|
+
static #slotAfter = {
|
|
622
|
+
build: "after-build",
|
|
623
|
+
docker: "after-docker",
|
|
624
|
+
deploy: "after-deploy",
|
|
625
|
+
ota: "after-ota"
|
|
626
|
+
};
|
|
627
|
+
static #slotMilestones = {
|
|
628
|
+
"before-build": "prepare",
|
|
629
|
+
"after-build": "verify",
|
|
630
|
+
"after-docker": "package",
|
|
631
|
+
"after-deploy": "deploy",
|
|
632
|
+
"after-ota": "deploy"
|
|
633
|
+
};
|
|
634
|
+
static #slotNames = {
|
|
635
|
+
"before-build": "Before build",
|
|
636
|
+
"after-build": "After build",
|
|
637
|
+
"after-docker": "After docker",
|
|
638
|
+
"after-deploy": "After deploy",
|
|
639
|
+
"after-ota": "After OTA"
|
|
640
|
+
};
|
|
641
|
+
static #reservedPhaseId = /^(?:build|docker|deploy|ota|(?:before-build|after-build|after-docker|after-deploy|after-ota)(?:-\d+)?)$/;
|
|
642
|
+
static #actionStepNames = { build: "akan build", docker: "image", deploy: "deploy", ota: "ota" };
|
|
643
|
+
static compile(input) {
|
|
644
|
+
return new PipelineCompiler(input).#compile();
|
|
645
|
+
}
|
|
646
|
+
#input;
|
|
647
|
+
#diagnostics;
|
|
648
|
+
#secrets = new Set;
|
|
649
|
+
constructor(input) {
|
|
650
|
+
this.#input = input;
|
|
651
|
+
this.#diagnostics = [...input.diagnostics ?? []];
|
|
652
|
+
}
|
|
653
|
+
#compile() {
|
|
654
|
+
const { context, workspaceApps } = this.#input;
|
|
655
|
+
this.#checkContext();
|
|
656
|
+
const pipelineContext = {
|
|
657
|
+
branch: context.branch,
|
|
658
|
+
env: context.env,
|
|
659
|
+
apps: [...context.apps],
|
|
660
|
+
workspaceApps: [...workspaceApps],
|
|
661
|
+
trigger: context.trigger,
|
|
662
|
+
buildNum: context.buildNum,
|
|
663
|
+
commit: context.commit
|
|
664
|
+
};
|
|
665
|
+
const workspace = this.#readWorkspace(pipelineContext);
|
|
666
|
+
const lanes = new Map;
|
|
667
|
+
for (const app of new Set(context.apps)) {
|
|
668
|
+
const lane = this.#readApp(app, pipelineContext);
|
|
669
|
+
if (lane)
|
|
670
|
+
lanes.set(app, lane);
|
|
671
|
+
}
|
|
672
|
+
const appOrder = this.#appOrder(lanes);
|
|
673
|
+
if (this.#hasErrors())
|
|
674
|
+
return this.#failed();
|
|
675
|
+
const workspaceEnv = workspace?.env ?? {};
|
|
676
|
+
const phases = this.#phases(workspace, lanes, appOrder, workspaceEnv);
|
|
677
|
+
const post = this.#post(workspace?.post, workspaceEnv);
|
|
678
|
+
if (this.#hasErrors())
|
|
679
|
+
return this.#failed();
|
|
680
|
+
const plan = {
|
|
681
|
+
schemaVersion: pipelinePlanVersion,
|
|
682
|
+
sources: this.#sources(),
|
|
683
|
+
planner: { akanVersion: this.#input.akanVersion },
|
|
684
|
+
context: { ...context, apps: [...context.apps] },
|
|
685
|
+
appOrder,
|
|
686
|
+
timeoutSeconds: workspace?.timeoutSeconds ?? null,
|
|
687
|
+
requiredSecrets: [...this.#secrets].sort(),
|
|
688
|
+
phases,
|
|
689
|
+
post
|
|
690
|
+
};
|
|
691
|
+
try {
|
|
692
|
+
PipelinePlanSchema.parse(plan);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
695
|
+
this.#error(message.includes("at most") ? "pipeline.limit.exceeded" : "pipeline.internal", message, null, null);
|
|
696
|
+
return this.#failed();
|
|
697
|
+
}
|
|
698
|
+
return { ok: true, plan, diagnostics: this.#diagnostics };
|
|
699
|
+
}
|
|
700
|
+
#checkContext() {
|
|
701
|
+
const { context, workspaceApps } = this.#input;
|
|
702
|
+
const seen = new Set;
|
|
703
|
+
for (const app of context.apps) {
|
|
704
|
+
if (seen.has(app))
|
|
705
|
+
this.#error("pipeline.context.duplicateApp", `"${app}" is given twice`, null, "apps");
|
|
706
|
+
seen.add(app);
|
|
707
|
+
if (!workspaceApps.includes(app))
|
|
708
|
+
this.#error("pipeline.context.unknownApp", `"${app}" is not an app of this workspace`, null, "apps");
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
#readWorkspace(context) {
|
|
712
|
+
const source = this.#input.workspace;
|
|
713
|
+
if (!source?.pipeline)
|
|
714
|
+
return null;
|
|
715
|
+
const value = this.#evaluate(source.pipeline, context, source.file);
|
|
716
|
+
if (value === undefined)
|
|
717
|
+
return null;
|
|
718
|
+
const reader = new PipelineInputReader(source.file);
|
|
719
|
+
const rule = reader.workspace(value);
|
|
720
|
+
this.#diagnostics.push(...reader.diagnostics);
|
|
721
|
+
return rule;
|
|
722
|
+
}
|
|
723
|
+
#readApp(app, context) {
|
|
724
|
+
const source = this.#input.apps.get(app);
|
|
725
|
+
if (!source)
|
|
726
|
+
return null;
|
|
727
|
+
const appContext = { ...context, app };
|
|
728
|
+
const value = source.pipeline ? this.#evaluate(source.pipeline, appContext, source.file) : defaultAppPipeline;
|
|
729
|
+
if (value === undefined)
|
|
730
|
+
return null;
|
|
731
|
+
const reader = new PipelineInputReader(source.file);
|
|
732
|
+
const rule = reader.app(value);
|
|
733
|
+
this.#diagnostics.push(...reader.diagnostics);
|
|
734
|
+
return rule ? this.#lane(app, rule, source.file) : null;
|
|
735
|
+
}
|
|
736
|
+
#evaluate(config, context, file) {
|
|
737
|
+
if (typeof config !== "function")
|
|
738
|
+
return config;
|
|
739
|
+
if (this.#input.sampled)
|
|
740
|
+
this.#warning("pipeline.function.sampled", "a function form was checked against one sample context; other branches, triggers or apps may differ", file, "pipeline");
|
|
741
|
+
try {
|
|
742
|
+
return config(context);
|
|
743
|
+
} catch (error) {
|
|
744
|
+
this.#error("pipeline.load.failed", `the pipeline function threw: ${error instanceof Error ? error.message : String(error)}`, file, "pipeline");
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
#lane(app, rule, file) {
|
|
749
|
+
const lane = {
|
|
750
|
+
file,
|
|
751
|
+
env: rule.env,
|
|
752
|
+
after: rule.after,
|
|
753
|
+
actions: new Map,
|
|
754
|
+
skips: new Map,
|
|
755
|
+
slots: { "before-build": [], "after-build": [], "after-docker": [], "after-deploy": [], "after-ota": [] },
|
|
756
|
+
ota: null
|
|
757
|
+
};
|
|
758
|
+
let last = null;
|
|
759
|
+
let lastMilestone = -1;
|
|
760
|
+
for (const stage of rule.stages) {
|
|
761
|
+
const milestone = stage.milestone ?? (stage.kind === "action" ? pipelineActions[stage.action].milestone : PipelineCompiler.#slotMilestones[last ? PipelineCompiler.#slotAfter[last] : "before-build"]);
|
|
762
|
+
const rank = pipelineMilestones2.indexOf(milestone);
|
|
763
|
+
if (stage.milestone && rank < lastMilestone)
|
|
764
|
+
this.#warning("pipeline.milestone.backwards", `"${milestone}" comes after a later milestone, so progress would move backwards`, file, `${stage.path}.milestone`);
|
|
765
|
+
lastMilestone = Math.max(lastMilestone, rank);
|
|
766
|
+
if (stage.kind === "sh") {
|
|
767
|
+
lane.slots[last ? PipelineCompiler.#slotAfter[last] : "before-build"].push(stage);
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (lane.actions.has(stage.action)) {
|
|
771
|
+
this.#error("pipeline.action.duplicate", `\`${stage.action}\` is declared twice`, file, stage.path);
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (last && pipelineActionNames2.indexOf(stage.action) < pipelineActionNames2.indexOf(last))
|
|
775
|
+
this.#error("pipeline.action.order", `\`${stage.action}\` comes after \`${last}\` here, but the actions run ${pipelineActionNames2.join(", ")}`, file, stage.path);
|
|
776
|
+
lane.actions.set(stage.action, stage);
|
|
777
|
+
last = stage.action;
|
|
778
|
+
}
|
|
779
|
+
const docker = lane.actions.get("docker");
|
|
780
|
+
if (docker && !lane.actions.has("build"))
|
|
781
|
+
this.#error("pipeline.action.dockerWithoutBuild", `\`docker\` builds the image from this run's \`akan build\` of ${app}, so it needs a \`build\` before it`, file, docker.path);
|
|
782
|
+
const deploy = lane.actions.get("deploy");
|
|
783
|
+
if (deploy && !lane.actions.has("docker"))
|
|
784
|
+
this.#warning("pipeline.action.deployWithoutDocker", `\`deploy\` without \`docker\` deploys the image last pushed for ${app}`, file, deploy.path);
|
|
785
|
+
const ota = lane.actions.get("ota");
|
|
786
|
+
if (ota)
|
|
787
|
+
lane.ota = this.#otaTargets(app, ota, file);
|
|
788
|
+
if (!lane.actions.size)
|
|
789
|
+
this.#warning("pipeline.app.noActions", `the pipeline of ${app} takes no built-in action, so this run neither builds nor deploys it`, file, "pipeline.stages");
|
|
790
|
+
let previous = null;
|
|
791
|
+
for (const action of pipelineActionNames2) {
|
|
792
|
+
const stage = lane.actions.get(action);
|
|
793
|
+
if (!stage)
|
|
794
|
+
continue;
|
|
795
|
+
const inherited = previous && lane.skips.get(previous) ? { reason: `${previous} of ${app} is skipped` } : null;
|
|
796
|
+
lane.skips.set(action, this.#skipOf(stage.when) ?? inherited);
|
|
797
|
+
previous = action;
|
|
798
|
+
}
|
|
799
|
+
return lane;
|
|
800
|
+
}
|
|
801
|
+
#otaTargets(app, stage, file) {
|
|
802
|
+
const platforms = stage.platforms ?? [...pipelineOtaPlatforms2];
|
|
803
|
+
const native = this.#input.nativeTargets?.get(app);
|
|
804
|
+
if (!native || "error" in native) {
|
|
805
|
+
this.#error("pipeline.ota.targets", `\`ota\` needs the native targets of ${app}, which could not be read${native ? `: ${native.error}` : ""}`, file, stage.path);
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
const known = new Map(native.targets.map((target) => [target.name, target.updates]));
|
|
809
|
+
if (!stage.targets) {
|
|
810
|
+
const targets = native.targets.filter((target) => target.updates).map((target) => target.name);
|
|
811
|
+
if (targets.length)
|
|
812
|
+
return { targets, platforms };
|
|
813
|
+
this.#error("pipeline.ota.noUpdates", `\`ota\` packs updates for targets that declare native.updates, and no native target of ${app} does`, file, stage.path);
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
const unknown = stage.targets.filter((name) => !known.has(name));
|
|
817
|
+
const withoutUpdates = stage.targets.filter((name) => known.get(name) === false);
|
|
818
|
+
if (unknown.length)
|
|
819
|
+
this.#error("pipeline.ota.unknownTarget", `${unknown.map((name) => `"${name}"`).join(", ")} ${unknown.length > 1 ? "are not native targets" : "is not a native target"} of ${app}: ${[...known.keys()].join(", ")}`, file, `${stage.path}.targets`);
|
|
820
|
+
if (withoutUpdates.length)
|
|
821
|
+
this.#error("pipeline.ota.noUpdates", `\`ota\` packs updates for targets that declare native.updates, and ${withoutUpdates.map((name) => `"${name}"`).join(", ")} of ${app} ${withoutUpdates.length > 1 ? "do" : "does"} not`, file, `${stage.path}.targets`);
|
|
822
|
+
return unknown.length || withoutUpdates.length ? null : { targets: stage.targets, platforms };
|
|
823
|
+
}
|
|
824
|
+
#appOrder(lanes) {
|
|
825
|
+
const apps = [...new Set(this.#input.context.apps)];
|
|
826
|
+
const known = new Set(apps);
|
|
827
|
+
const after = new Map(apps.map((app) => [app, new Set((lanes.get(app)?.after ?? []).filter((dep) => known.has(dep) && dep !== app))]));
|
|
828
|
+
const order = [];
|
|
829
|
+
const done = new Set;
|
|
830
|
+
while (order.length < apps.length) {
|
|
831
|
+
const next = apps.find((app) => !done.has(app) && [...after.get(app) ?? []].every((dep) => done.has(dep)));
|
|
832
|
+
if (!next) {
|
|
833
|
+
const waiting = apps.filter((app) => !done.has(app));
|
|
834
|
+
this.#error("pipeline.app.afterCycle", `\`after\` makes a cycle among ${waiting.join(", ")}`, lanes.get(waiting[0] ?? "")?.file ?? null, "pipeline.after");
|
|
835
|
+
return apps;
|
|
836
|
+
}
|
|
837
|
+
order.push(next);
|
|
838
|
+
done.add(next);
|
|
839
|
+
}
|
|
840
|
+
return order;
|
|
841
|
+
}
|
|
842
|
+
#phases(workspace, lanes, appOrder, workspaceEnv) {
|
|
843
|
+
const phases = [];
|
|
844
|
+
for (const stage of workspace?.before ?? [])
|
|
845
|
+
phases.push(...this.#workspacePhase(stage, "before", appOrder, workspaceEnv));
|
|
846
|
+
phases.push(...this.#customPhases("before-build", lanes, appOrder, workspaceEnv));
|
|
847
|
+
for (const action of pipelineActionNames2) {
|
|
848
|
+
const phase = this.#actionPhase(action, lanes, appOrder);
|
|
849
|
+
if (phase)
|
|
850
|
+
phases.push(phase);
|
|
851
|
+
phases.push(...this.#customPhases(PipelineCompiler.#slotAfter[action], lanes, appOrder, workspaceEnv));
|
|
852
|
+
}
|
|
853
|
+
for (const stage of workspace?.after ?? [])
|
|
854
|
+
phases.push(...this.#workspacePhase(stage, "after", appOrder, workspaceEnv));
|
|
855
|
+
return phases;
|
|
856
|
+
}
|
|
857
|
+
#actionPhase(action, lanes, appOrder) {
|
|
858
|
+
const stages = appOrder.flatMap((app) => {
|
|
859
|
+
const lane = lanes.get(app);
|
|
860
|
+
const rule = lane?.actions.get(action);
|
|
861
|
+
if (!lane || !rule)
|
|
862
|
+
return [];
|
|
863
|
+
const id = `${app}:${rule.id}`;
|
|
864
|
+
return [
|
|
865
|
+
{
|
|
866
|
+
id,
|
|
867
|
+
name: rule.named ? rule.name : app,
|
|
868
|
+
description: rule.description,
|
|
869
|
+
app,
|
|
870
|
+
milestone: rule.milestone ?? pipelineActions[action].milestone,
|
|
871
|
+
skip: lane.skips.get(action) ?? null,
|
|
872
|
+
runtimeWhen: rule.when.changed ? { changed: rule.when.changed } : null,
|
|
873
|
+
allowFailure: rule.allowFailure,
|
|
874
|
+
timeoutSeconds: rule.timeoutSeconds,
|
|
875
|
+
steps: [
|
|
876
|
+
{
|
|
877
|
+
id: `${id}/${action}`,
|
|
878
|
+
name: `${PipelineCompiler.#actionStepNames[action]} ${app}`,
|
|
879
|
+
timeoutSeconds: null,
|
|
880
|
+
retry: rule.retry,
|
|
881
|
+
allowFailure: false,
|
|
882
|
+
kind: "action",
|
|
883
|
+
actionVersion: pipelineActions[action].version,
|
|
884
|
+
...action === "ota" ? { action, with: { app, targets: lane.ota?.targets ?? [], platforms: lane.ota?.platforms ?? [] } } : { action, with: { app } }
|
|
885
|
+
}
|
|
886
|
+
]
|
|
887
|
+
}
|
|
888
|
+
];
|
|
889
|
+
});
|
|
890
|
+
if (!stages.length)
|
|
891
|
+
return null;
|
|
892
|
+
return {
|
|
893
|
+
id: action,
|
|
894
|
+
name: pipelineActions[action].name,
|
|
895
|
+
description: null,
|
|
896
|
+
slot: action,
|
|
897
|
+
milestone: pipelineActions[action].milestone,
|
|
898
|
+
action,
|
|
899
|
+
order: action === "deploy" ? "sequential" : "parallel",
|
|
900
|
+
maxParallel: null,
|
|
901
|
+
failFast: false,
|
|
902
|
+
timeoutSeconds: null,
|
|
903
|
+
stages
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
#customPhases(slot, lanes, appOrder, workspaceEnv) {
|
|
907
|
+
const depth = Math.max(0, ...appOrder.map((app) => lanes.get(app)?.slots[slot].length ?? 0));
|
|
908
|
+
const milestone = PipelineCompiler.#slotMilestones[slot];
|
|
909
|
+
return Array.from({ length: depth }, (_, idx) => {
|
|
910
|
+
const stages = appOrder.flatMap((app) => {
|
|
911
|
+
const lane = lanes.get(app);
|
|
912
|
+
const rule = lane?.slots[slot][idx];
|
|
913
|
+
if (!lane || !rule)
|
|
914
|
+
return [];
|
|
915
|
+
return [
|
|
916
|
+
this.#shellStage(rule, {
|
|
917
|
+
id: `${app}:${rule.id}`,
|
|
918
|
+
app,
|
|
919
|
+
milestone: rule.milestone ?? milestone,
|
|
920
|
+
env: { ...workspaceEnv, ...lane.env }
|
|
921
|
+
})
|
|
922
|
+
];
|
|
923
|
+
});
|
|
924
|
+
return {
|
|
925
|
+
id: idx === 0 ? slot : `${slot}-${idx + 1}`,
|
|
926
|
+
name: idx === 0 ? PipelineCompiler.#slotNames[slot] : `${PipelineCompiler.#slotNames[slot]} ${idx + 1}`,
|
|
927
|
+
description: null,
|
|
928
|
+
slot,
|
|
929
|
+
milestone,
|
|
930
|
+
action: null,
|
|
931
|
+
order: "parallel",
|
|
932
|
+
maxParallel: null,
|
|
933
|
+
failFast: false,
|
|
934
|
+
timeoutSeconds: null,
|
|
935
|
+
stages
|
|
936
|
+
};
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
#workspacePhase(stage, position, appOrder, env) {
|
|
940
|
+
if (PipelineCompiler.#reservedPhaseId.test(stage.id))
|
|
941
|
+
this.#error("pipeline.id.reserved", `"${stage.id}" is the id of a phase the planner makes; give this stage another \`id\``, this.#input.workspace?.file ?? null, stage.path);
|
|
942
|
+
const milestone = stage.milestone ?? (position === "before" ? "prepare" : "finish");
|
|
943
|
+
const phase = {
|
|
944
|
+
id: stage.id,
|
|
945
|
+
name: stage.name,
|
|
946
|
+
description: stage.description,
|
|
947
|
+
slot: position,
|
|
948
|
+
milestone,
|
|
949
|
+
action: null,
|
|
950
|
+
order: "parallel"
|
|
951
|
+
};
|
|
952
|
+
if (stage.kind === "sh")
|
|
953
|
+
return [
|
|
954
|
+
{
|
|
955
|
+
...phase,
|
|
956
|
+
maxParallel: null,
|
|
957
|
+
failFast: false,
|
|
958
|
+
timeoutSeconds: null,
|
|
959
|
+
stages: [this.#shellStage(stage, { id: stage.id, milestone, env })]
|
|
960
|
+
}
|
|
961
|
+
];
|
|
962
|
+
const parentSkip = this.#skipOf(stage.when);
|
|
963
|
+
if (stage.kind === "parallel")
|
|
964
|
+
return [
|
|
965
|
+
{
|
|
966
|
+
...phase,
|
|
967
|
+
maxParallel: stage.maxParallel,
|
|
968
|
+
failFast: stage.failFast,
|
|
969
|
+
timeoutSeconds: stage.timeoutSeconds,
|
|
970
|
+
stages: stage.branches.map((branch) => this.#shellStage(branch, {
|
|
971
|
+
id: `${stage.id}.${branch.id}`,
|
|
972
|
+
milestone: branch.milestone ?? milestone,
|
|
973
|
+
env,
|
|
974
|
+
inheritedSkip: parentSkip,
|
|
975
|
+
allowFailure: stage.allowFailure,
|
|
976
|
+
changed: stage.when.changed
|
|
977
|
+
}))
|
|
978
|
+
}
|
|
979
|
+
];
|
|
980
|
+
const apps = appOrder.filter((app) => !stage.when.app || PipelinePattern2.matches(stage.when.app, app));
|
|
981
|
+
if (!apps.length)
|
|
982
|
+
return [];
|
|
983
|
+
const copy = { ...stage, kind: "sh", timeoutSeconds: null };
|
|
984
|
+
return [
|
|
985
|
+
{
|
|
986
|
+
...phase,
|
|
987
|
+
maxParallel: stage.maxParallel,
|
|
988
|
+
failFast: false,
|
|
989
|
+
timeoutSeconds: stage.timeoutSeconds,
|
|
990
|
+
stages: apps.map((app) => this.#shellStage(copy, { id: `${stage.id}:${app}`, name: app, app, milestone, env }))
|
|
991
|
+
}
|
|
992
|
+
];
|
|
993
|
+
}
|
|
994
|
+
#shellStage(rule, options) {
|
|
995
|
+
const changed = options.changed ?? rule.when.changed;
|
|
996
|
+
return {
|
|
997
|
+
id: options.id,
|
|
998
|
+
name: options.name ?? rule.name,
|
|
999
|
+
description: rule.description,
|
|
1000
|
+
app: options.app ?? null,
|
|
1001
|
+
milestone: options.milestone,
|
|
1002
|
+
skip: options.inheritedSkip ?? this.#skipOf(rule.when),
|
|
1003
|
+
runtimeWhen: changed ? { changed } : null,
|
|
1004
|
+
allowFailure: (options.allowFailure ?? false) || rule.allowFailure,
|
|
1005
|
+
timeoutSeconds: options.timeoutSeconds === undefined ? rule.timeoutSeconds : options.timeoutSeconds,
|
|
1006
|
+
steps: rule.steps.map((step) => this.#shellStep(step, options.id, options.env))
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
#shellStep(step, stageId, env) {
|
|
1010
|
+
for (const name of step.secrets)
|
|
1011
|
+
this.#secrets.add(name);
|
|
1012
|
+
return {
|
|
1013
|
+
id: `${stageId}/${step.localId}`,
|
|
1014
|
+
name: step.name,
|
|
1015
|
+
timeoutSeconds: step.timeoutSeconds,
|
|
1016
|
+
retry: step.retry,
|
|
1017
|
+
allowFailure: step.allowFailure,
|
|
1018
|
+
kind: "sh",
|
|
1019
|
+
run: step.run,
|
|
1020
|
+
cwd: step.cwd,
|
|
1021
|
+
env: { ...env, ...step.env },
|
|
1022
|
+
secrets: step.secrets,
|
|
1023
|
+
skipExitCodes: step.skipExitCodes
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
#post(rule, env) {
|
|
1027
|
+
const steps = (result) => (rule?.[result] ?? []).map((step) => this.#shellStep(step, `post.${result}`, env));
|
|
1028
|
+
return {
|
|
1029
|
+
success: steps("success"),
|
|
1030
|
+
failure: steps("failure"),
|
|
1031
|
+
canceled: steps("canceled"),
|
|
1032
|
+
always: steps("always")
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
#skipOf(when) {
|
|
1036
|
+
const { branch, trigger } = this.#input.context;
|
|
1037
|
+
if (when.branch && !PipelinePattern2.matches(when.branch, branch))
|
|
1038
|
+
return { reason: `branch "${branch}" does not match ${when.branch.map((glob) => `"${glob}"`).join(", ")}` };
|
|
1039
|
+
if (when.trigger && !when.trigger.includes(trigger))
|
|
1040
|
+
return { reason: `trigger "${trigger}" is not ${when.trigger.map((name) => `"${name}"`).join(" or ")}` };
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
#sources() {
|
|
1044
|
+
const { workspace, apps, context } = this.#input;
|
|
1045
|
+
const sourceOf = (source) => ({
|
|
1046
|
+
file: source.file,
|
|
1047
|
+
sha256: source.sha256,
|
|
1048
|
+
declared: source.pipeline !== null
|
|
1049
|
+
});
|
|
1050
|
+
return {
|
|
1051
|
+
workspace: workspace ? sourceOf(workspace) : null,
|
|
1052
|
+
apps: Object.fromEntries(context.apps.flatMap((app) => {
|
|
1053
|
+
const source = apps.get(app);
|
|
1054
|
+
return source ? [[app, sourceOf(source)]] : [];
|
|
1055
|
+
}))
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
#hasErrors() {
|
|
1059
|
+
return this.#diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
1060
|
+
}
|
|
1061
|
+
#failed() {
|
|
1062
|
+
return { ok: false, plan: null, diagnostics: this.#diagnostics };
|
|
1063
|
+
}
|
|
1064
|
+
#error(code, message, file, path) {
|
|
1065
|
+
this.#diagnostics.push({ severity: "error", code, message, file, path });
|
|
1066
|
+
}
|
|
1067
|
+
#warning(code, message, file, path) {
|
|
1068
|
+
this.#diagnostics.push({ severity: "warning", code, message, file, path });
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
// pkgs/@akanjs/devkit/pipeline/PipelineDefinition.ts
|
|
1072
|
+
import { readFile } from "fs/promises";
|
|
1073
|
+
import path from "path";
|
|
1074
|
+
class PipelineDefinition {
|
|
1075
|
+
static async load(workspace, apps) {
|
|
1076
|
+
const definition = new PipelineDefinition(workspace);
|
|
1077
|
+
await definition.#load(apps);
|
|
1078
|
+
return definition;
|
|
1079
|
+
}
|
|
1080
|
+
diagnostics = [];
|
|
1081
|
+
appSources = new Map;
|
|
1082
|
+
nativeTargets = new Map;
|
|
1083
|
+
workspaceApps = [];
|
|
1084
|
+
workspaceSource = null;
|
|
1085
|
+
#workspace;
|
|
1086
|
+
constructor(workspace) {
|
|
1087
|
+
this.#workspace = workspace;
|
|
1088
|
+
}
|
|
1089
|
+
compile(context, { sampled = false } = {}) {
|
|
1090
|
+
return PipelineCompiler.compile({
|
|
1091
|
+
context,
|
|
1092
|
+
workspaceApps: this.workspaceApps,
|
|
1093
|
+
workspace: this.workspaceSource,
|
|
1094
|
+
apps: this.appSources,
|
|
1095
|
+
nativeTargets: this.nativeTargets,
|
|
1096
|
+
akanVersion: AkanAppConfig.akanVersion,
|
|
1097
|
+
sampled,
|
|
1098
|
+
diagnostics: this.diagnostics
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
async#load(apps) {
|
|
1102
|
+
this.workspaceApps = (await this.#workspace.getApps()).sort((a, b) => a.localeCompare(b));
|
|
1103
|
+
this.workspaceSource = await this.#loadWorkspace();
|
|
1104
|
+
for (const app of new Set(apps)) {
|
|
1105
|
+
if (!this.workspaceApps.includes(app))
|
|
1106
|
+
continue;
|
|
1107
|
+
const source = await this.#loadApp(app);
|
|
1108
|
+
this.appSources.set(app, source);
|
|
1109
|
+
if (PipelineDefinition.#mayTakeOta(source.pipeline))
|
|
1110
|
+
this.nativeTargets.set(app, await this.#loadNativeTargets(app));
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
static #mayTakeOta(pipeline) {
|
|
1114
|
+
if (typeof pipeline === "function")
|
|
1115
|
+
return true;
|
|
1116
|
+
const stages = pipeline?.stages;
|
|
1117
|
+
return Array.isArray(stages) && stages.some((stage) => stage?.use === "ota");
|
|
1118
|
+
}
|
|
1119
|
+
async#loadNativeTargets(app) {
|
|
1120
|
+
try {
|
|
1121
|
+
const targets = await resolveMobileTargets(AppExecutor.from(this.#workspace, app), "all");
|
|
1122
|
+
return { targets: targets.map(({ name, config }) => ({ name, updates: !!config.updates })) };
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
async#loadWorkspace() {
|
|
1128
|
+
const file = AkanWorkspaceConfig.fileName;
|
|
1129
|
+
const absolute = path.join(this.#workspace.workspaceRoot, file);
|
|
1130
|
+
if (!await FileSys2.fileExists(absolute))
|
|
1131
|
+
return null;
|
|
1132
|
+
const sha256 = await PipelineDefinition.#sha256(absolute);
|
|
1133
|
+
try {
|
|
1134
|
+
return { file, sha256, pipeline: (await this.#workspace.getConfig()).pipeline };
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
this.#failed(file, error);
|
|
1137
|
+
return { file, sha256, pipeline: null };
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
async#loadApp(app) {
|
|
1141
|
+
const executor = AppExecutor.from(this.#workspace, app);
|
|
1142
|
+
const file = `apps/${app}/akan.config.ts`;
|
|
1143
|
+
const sha256 = await PipelineDefinition.#sha256(path.join(executor.cwdPath, "akan.config.ts"));
|
|
1144
|
+
try {
|
|
1145
|
+
const module = await AkanAppConfig.importConfigModule(executor.cwdPath);
|
|
1146
|
+
const config = typeof module === "function" ? module(executor) : module;
|
|
1147
|
+
const pipeline = typeof config === "object" && config !== null ? config.pipeline : null;
|
|
1148
|
+
return { file, sha256, pipeline: pipeline ?? null };
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
this.#failed(file, error);
|
|
1151
|
+
return { file, sha256, pipeline: null };
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
#failed(file, error) {
|
|
1155
|
+
this.diagnostics.push({
|
|
1156
|
+
severity: "error",
|
|
1157
|
+
code: "pipeline.load.failed",
|
|
1158
|
+
message: `could not load: ${error instanceof Error ? error.message : String(error)}`,
|
|
1159
|
+
file,
|
|
1160
|
+
path: null
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
static async#sha256(file) {
|
|
1164
|
+
return new Bun.CryptoHasher("sha256").update(await readFile(file)).digest("hex");
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
// pkgs/@akanjs/devkit/pipeline/PipelineDryRun.ts
|
|
1168
|
+
class PipelineDryRun {
|
|
1169
|
+
static render(result) {
|
|
1170
|
+
const lines = result.ok ? PipelineDryRun.#plan(result.plan) : ["no plan: the definition has errors"];
|
|
1171
|
+
const diagnostics = PipelineDryRun.diagnostics(result.diagnostics);
|
|
1172
|
+
return [...lines, ...diagnostics.length ? ["", ...diagnostics] : []].join(`
|
|
1173
|
+
`);
|
|
1174
|
+
}
|
|
1175
|
+
static #plan(plan) {
|
|
1176
|
+
const { context } = plan;
|
|
1177
|
+
const order = plan.appOrder.join() === context.apps.join() ? "" : ` \xB7 order ${plan.appOrder.join(" \u2192 ")}`;
|
|
1178
|
+
const rows = [
|
|
1179
|
+
["prepare", "(executor)", "", "checkout \xB7 env \xB7 install \xB7 plan"],
|
|
1180
|
+
...plan.phases.map((phase) => [
|
|
1181
|
+
phase.milestone,
|
|
1182
|
+
phase.id,
|
|
1183
|
+
phase.name,
|
|
1184
|
+
PipelineDryRun.#stages(phase)
|
|
1185
|
+
]),
|
|
1186
|
+
...["success", "failure", "canceled", "always"].flatMap((result) => {
|
|
1187
|
+
const steps = plan.post[result];
|
|
1188
|
+
if (!steps.length)
|
|
1189
|
+
return [];
|
|
1190
|
+
return [["finish", `post.${result}`, "", steps.map((step) => step.name).join(" \xB7 ")]];
|
|
1191
|
+
}),
|
|
1192
|
+
["finish", "(executor)", "", "cleanup"]
|
|
1193
|
+
];
|
|
1194
|
+
const widths = [0, 1, 2].map((column) => Math.max(...rows.map((row) => row[column]?.length ?? 0)));
|
|
1195
|
+
const sources = [
|
|
1196
|
+
...plan.sources.workspace ? [plan.sources.workspace] : [],
|
|
1197
|
+
...Object.values(plan.sources.apps)
|
|
1198
|
+
].map((source) => `${source.file}${source.declared ? "" : " (default)"}`);
|
|
1199
|
+
return [
|
|
1200
|
+
`plan v${plan.schemaVersion} \xB7 branch ${context.branch} \xB7 env ${context.env} \xB7 trigger ${context.trigger} \xB7 apps ${context.apps.join(", ") || "(none)"}${order}`,
|
|
1201
|
+
`from ${sources.join(", ") || "(no files)"}`,
|
|
1202
|
+
"",
|
|
1203
|
+
...rows.map((row) => row.map((cell, column) => column < 3 ? cell.padEnd(widths[column] ?? 0) : cell).join(" ").trimEnd()),
|
|
1204
|
+
...plan.requiredSecrets.length ? ["", `secrets required: ${plan.requiredSecrets.join(", ")}`] : []
|
|
1205
|
+
];
|
|
1206
|
+
}
|
|
1207
|
+
static #stages(phase) {
|
|
1208
|
+
const labels = phase.stages.map((stage) => PipelineDryRun.#stage(phase, stage));
|
|
1209
|
+
if (phase.order === "sequential")
|
|
1210
|
+
return `\u2192 ${labels.join(", ")} (one at a time)`;
|
|
1211
|
+
if (phase.stages.length === 1) {
|
|
1212
|
+
const [stage] = phase.stages;
|
|
1213
|
+
const command = stage?.steps.map((step) => step.kind === "sh" ? step.name : `action ${step.action}`).join(" \xB7 ");
|
|
1214
|
+
return [labels[0], command].filter((part) => part && part !== stage?.name).join(" ");
|
|
1215
|
+
}
|
|
1216
|
+
return `\u2016 ${labels.join(" \xB7 ")}`;
|
|
1217
|
+
}
|
|
1218
|
+
static #stage(phase, stage) {
|
|
1219
|
+
const label = stage.app && !phase.action && stage.name !== stage.app ? `${stage.app}:${stage.name}` : stage.name;
|
|
1220
|
+
const ota = stage.steps.find((step) => step.kind === "action" && step.action === "ota");
|
|
1221
|
+
const notes = [
|
|
1222
|
+
ota?.kind === "action" && ota.action === "ota" ? `${ota.with.targets.join(", ")} \xD7 ${ota.with.platforms.join(", ")}` : null,
|
|
1223
|
+
stage.skip ? `skipped: ${stage.skip.reason}` : null,
|
|
1224
|
+
stage.allowFailure ? "may fail" : null,
|
|
1225
|
+
stage.runtimeWhen ? `if changed: ${stage.runtimeWhen.changed.join(", ")}` : null
|
|
1226
|
+
].filter(Boolean);
|
|
1227
|
+
return notes.length ? `${label} (${notes.join("; ")})` : label;
|
|
1228
|
+
}
|
|
1229
|
+
static diagnostics(diagnostics) {
|
|
1230
|
+
return diagnostics.map((diagnostic) => {
|
|
1231
|
+
const where = [diagnostic.file, diagnostic.path].filter(Boolean).join(" \xB7 ");
|
|
1232
|
+
return `${diagnostic.severity} ${diagnostic.code}${where ? ` (${where})` : ""}: ${diagnostic.message}`;
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
// pkgs/@akanjs/cli/pipeline/pipeline.script.ts
|
|
1237
|
+
import { Logger } from "akanjs/common";
|
|
1238
|
+
|
|
1239
|
+
// pkgs/@akanjs/cli/pipeline/pipeline.runner.ts
|
|
1240
|
+
import { mkdir, open, rename } from "fs/promises";
|
|
1241
|
+
import path2 from "path";
|
|
1242
|
+
class PipelineRunner extends runner("pipeline") {
|
|
1243
|
+
async context(workspace, flags) {
|
|
1244
|
+
if (!Number.isInteger(flags.buildNum) || flags.buildNum < 0)
|
|
1245
|
+
throw new Error(`--build-num ${flags.buildNum} is not a whole number of 0 or more`);
|
|
1246
|
+
const branch = flags.branch ?? await PipelineRunner.#git(workspace, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? "main";
|
|
1247
|
+
return {
|
|
1248
|
+
branch,
|
|
1249
|
+
env: flags.env ?? branch,
|
|
1250
|
+
apps: flags.apps === null ? (await workspace.getApps()).sort((a, b) => a.localeCompare(b)) : flags.apps.split(","),
|
|
1251
|
+
trigger: flags.trigger,
|
|
1252
|
+
buildNum: flags.buildNum,
|
|
1253
|
+
commit: flags.commit ?? await PipelineRunner.#git(workspace, ["rev-parse", "HEAD"])
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
async plan(workspace, context, { sampled = false } = {}) {
|
|
1257
|
+
return (await PipelineDefinition.load(workspace, context.apps)).compile(context, { sampled });
|
|
1258
|
+
}
|
|
1259
|
+
async writeResult(out, result) {
|
|
1260
|
+
const target = path2.resolve(out);
|
|
1261
|
+
await mkdir(path2.dirname(target), { recursive: true });
|
|
1262
|
+
const temp = `${target}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
1263
|
+
const handle = await open(temp, "wx");
|
|
1264
|
+
try {
|
|
1265
|
+
await handle.writeFile(`${JSON.stringify(result, null, 2)}
|
|
1266
|
+
`);
|
|
1267
|
+
} finally {
|
|
1268
|
+
await handle.close();
|
|
1269
|
+
}
|
|
1270
|
+
await rename(temp, target);
|
|
1271
|
+
}
|
|
1272
|
+
static async#git(workspace, args) {
|
|
1273
|
+
try {
|
|
1274
|
+
const output = (await workspace.spawn("git", args)).trim();
|
|
1275
|
+
return output && output !== "HEAD" ? output : null;
|
|
1276
|
+
} catch {
|
|
1277
|
+
return null;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// pkgs/@akanjs/cli/pipeline/pipeline.script.ts
|
|
1283
|
+
class PipelineScript extends script("pipeline", [PipelineRunner]) {
|
|
1284
|
+
async run(workspace, action, { flags, out, format }) {
|
|
1285
|
+
let result;
|
|
1286
|
+
try {
|
|
1287
|
+
const context = await this.pipelineRunner.context(workspace, flags);
|
|
1288
|
+
result = await this.pipelineRunner.plan(workspace, context, { sampled: action === "validate" });
|
|
1289
|
+
if (out)
|
|
1290
|
+
await this.pipelineRunner.writeResult(out, result);
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
Logger.rawLog(`akan pipeline ${action}: ${error instanceof Error ? error.message : String(error)}`, undefined, "error");
|
|
1293
|
+
process.exitCode = 2;
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
if (format === "json" || action === "plan" && !out)
|
|
1297
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1298
|
+
`);
|
|
1299
|
+
else
|
|
1300
|
+
Logger.rawLog(action === "dry-run" ? PipelineDryRun.render(result) : PipelineScript.#summary(action, result, out));
|
|
1301
|
+
if (!result.ok)
|
|
1302
|
+
process.exitCode = 1;
|
|
1303
|
+
}
|
|
1304
|
+
static #summary(action, result, out) {
|
|
1305
|
+
const head = result.ok ? `${action === "plan" ? "planned" : "valid"}: ${result.plan.phases.length} phases, ${result.plan.phases.reduce((sum, phase) => sum + phase.stages.length, 0)} stages` : `${action === "plan" ? "no plan" : "invalid"}: ${result.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length} error(s)`;
|
|
1306
|
+
return [`${head}${out ? ` \u2192 ${out}` : ""}`, ...PipelineDryRun.diagnostics(result.diagnostics)].join(`
|
|
1307
|
+
`);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// pkgs/@akanjs/cli/pipeline/pipeline.command.ts
|
|
1312
|
+
class PipelineCommand extends command("pipeline", [PipelineScript], ({ public: target }) => ({
|
|
1313
|
+
pipeline: target({
|
|
1314
|
+
desc: "Plan the build pipeline declared in akan.config.ts (the workspace root's and each app's) without running it",
|
|
1315
|
+
failureExitCode: 2
|
|
1316
|
+
}).arg("action", String, {
|
|
1317
|
+
desc: "plan: the JSON plan an executor runs \xB7 validate: check the definitions \xB7 dry-run: show the plan",
|
|
1318
|
+
default: "dry-run",
|
|
1319
|
+
enum: ["plan", "validate", "dry-run"]
|
|
1320
|
+
}).option("branch", String, { desc: "the branch the run builds (default: the current git branch)", nullable: true }).option("env", String, { desc: "the AKAN_PUBLIC_ENV the run is for (default: the branch)", nullable: true }).option("apps", String, { desc: "the apps the run builds, comma-separated (default: every app)", nullable: true }).option("trigger", String, { desc: "what started the run", default: "manual", enum: [...pipelineTriggers2] }).option("buildNum", Number, { flag: "n", desc: "the run's build number", default: 0 }).option("commit", String, { desc: "the commit the run builds (default: HEAD)", nullable: true }).option("out", String, {
|
|
1321
|
+
desc: "also write the result envelope, { ok, plan, diagnostics }, to this file",
|
|
1322
|
+
nullable: true
|
|
1323
|
+
}).option("format", String, { desc: "output format", default: "text", enum: ["text", "json"] }).with(Workspace).exec(async function(action, branch, env, apps, trigger, buildNum, commit, out, format, workspace) {
|
|
1324
|
+
await this.pipelineScript.run(workspace, action, {
|
|
1325
|
+
flags: { branch, env, apps, trigger, buildNum, commit },
|
|
1326
|
+
out,
|
|
1327
|
+
format
|
|
1328
|
+
});
|
|
1329
|
+
})
|
|
1330
|
+
})) {
|
|
1331
|
+
}
|
|
1332
|
+
export {
|
|
1333
|
+
PipelineCommand
|
|
1334
|
+
};
|