@akanjs/devkit 3.1.0-rc.1 → 3.1.0-rc.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/akanConfig/akanConfig.test.ts +51 -5
- package/akanConfig/akanConfig.ts +31 -12
- package/akanConfig/akanWorkspaceConfig.test.ts +6 -6
- package/akanConfig/akanWorkspaceConfig.ts +1 -1
- package/applicationBuildRunner.test.ts +33 -1
- package/applicationBuildRunner.ts +4 -5
- package/cloud/cloudApi.ts +22 -15
- package/cloud/cloudHttpError.test.ts +32 -0
- package/cloud/cloudHttpError.ts +54 -0
- package/cloud/cloudUpdates.ts +138 -0
- package/cloud/globalConfig.test.ts +17 -0
- package/cloud/globalConfig.ts +8 -2
- package/cloud/index.ts +3 -0
- package/cloud/pipelineSecrets.ts +114 -0
- package/cloud/updatePackUpload.test.ts +100 -45
- package/cloud/updatePackUpload.ts +38 -18
- package/commandDecorators/argMeta.ts +11 -2
- package/commandDecorators/command.ts +40 -6
- package/commandDecorators/commandBuilder.ts +6 -1
- package/commandDecorators/commandDecorators.test.ts +205 -4
- package/commandDecorators/exitCodeError.ts +15 -0
- package/commandDecorators/index.ts +1 -0
- package/executors.test.ts +46 -0
- package/executors.ts +26 -23
- package/frontendBuild/csrArtifactBuilder.ts +18 -1
- package/incrementalBuilder/buildBatch.proc.ts +12 -7
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +63 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +7 -5
- package/mobile/desktopServerStage.test.ts +13 -0
- package/mobile/desktopServerStage.ts +5 -7
- package/mobile/index.ts +2 -0
- package/mobile/nativeApi.test.ts +26 -0
- package/mobile/nativeApi.ts +5 -2
- package/mobile/nativeApp.test.ts +135 -33
- package/mobile/nativeApp.ts +109 -73
- package/mobile/nativeConfig.ts +2 -1
- package/mobile/nativePublicEnv.test.ts +131 -0
- package/mobile/nativePublicEnv.ts +116 -0
- package/mobile/serverBundleSecrets.test.ts +79 -0
- package/mobile/serverBundleSecrets.ts +60 -16
- package/mobile/updatePackFolder.test.ts +98 -0
- package/mobile/updatePackFolder.ts +131 -0
- package/mobile/updateUploader.test.ts +114 -22
- package/mobile/updateUploader.ts +75 -8
- package/package.json +2 -2
- package/pipeline/PipelineCompiler.test.ts +157 -1
- package/pipeline/PipelineCompiler.ts +80 -46
- package/pipeline/PipelineDefinition.test.ts +9 -0
- package/pipeline/PipelineDefinition.ts +4 -3
- package/pipeline/PipelineDryRun.test.ts +27 -0
- package/pipeline/PipelineDryRun.ts +2 -2
- package/pipeline/PipelineInputReader.ts +157 -253
- package/pipeline/PipelineRules.ts +92 -0
- package/pipeline/index.ts +1 -0
- package/qualityScanner.test.ts +5 -3
- package/slicePlanner.ts +5 -3
- package/subspace.test.ts +90 -19
- package/subspace.ts +52 -500
- package/subspaceClone.ts +361 -0
- package/subspaceConfig.ts +1 -2
- package/subspaceReport.ts +162 -0
- package/transforms/serverEnvPlugin.test.ts +76 -0
- package/transforms/serverEnvPlugin.ts +9 -25
- package/transforms/serverEnvProfile.test.ts +189 -0
- package/transforms/serverEnvProfile.ts +324 -60
- package/typecheck/projectTypecheck.ts +6 -5
|
@@ -1,122 +1,62 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type PipelineActionName,
|
|
3
2
|
type PipelineDiagnostic,
|
|
4
|
-
type PipelineMilestone,
|
|
5
3
|
PipelineNames,
|
|
6
|
-
type PipelineOtaPlatform,
|
|
7
4
|
PipelinePattern,
|
|
8
5
|
type PipelinePlanRetry,
|
|
9
|
-
type
|
|
6
|
+
type PipelineValueProblem,
|
|
7
|
+
PipelineValueReader,
|
|
10
8
|
pipelineActionNames,
|
|
11
9
|
pipelineMilestones,
|
|
12
10
|
pipelineOtaPlatforms,
|
|
13
11
|
pipelinePlanLimits,
|
|
14
12
|
pipelineTriggers,
|
|
15
13
|
} from "akanjs/common";
|
|
14
|
+
import type {
|
|
15
|
+
PipelineActionStageRule,
|
|
16
|
+
PipelineAppRule,
|
|
17
|
+
PipelineAppStageRule,
|
|
18
|
+
PipelineMatrixStageRule,
|
|
19
|
+
PipelineParallelStageRule,
|
|
20
|
+
PipelinePostRule,
|
|
21
|
+
PipelineShellStageRule,
|
|
22
|
+
PipelineStepRule,
|
|
23
|
+
PipelineWhenRule,
|
|
24
|
+
PipelineWorkspaceRule,
|
|
25
|
+
PipelineWorkspaceStageRule,
|
|
26
|
+
} from "./PipelineRules";
|
|
16
27
|
|
|
17
|
-
export interface PipelineWhenRule {
|
|
18
|
-
branch: string[] | null;
|
|
19
|
-
trigger: PipelineTrigger[] | null;
|
|
20
|
-
changed: string[] | null;
|
|
21
|
-
/** A workspace `matrix: "apps"` stage's narrowing; never set elsewhere. */
|
|
22
|
-
app: string[] | null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface PipelineStepRule {
|
|
26
|
-
localId: string;
|
|
27
|
-
name: string;
|
|
28
|
-
timeoutSeconds: number | null;
|
|
29
|
-
retry: PipelinePlanRetry | null;
|
|
30
|
-
allowFailure: boolean;
|
|
31
|
-
run: string;
|
|
32
|
-
cwd: string;
|
|
33
|
-
/** The stage's env under the step's own. */
|
|
34
|
-
env: Record<string, string>;
|
|
35
|
-
secrets: string[];
|
|
36
|
-
skipExitCodes: number[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface PipelineStageRuleBase {
|
|
40
|
-
id: string;
|
|
41
|
-
name: string;
|
|
42
|
-
description: string | null;
|
|
43
|
-
milestone: PipelineMilestone | null;
|
|
44
|
-
when: PipelineWhenRule;
|
|
45
|
-
timeoutSeconds: number | null;
|
|
46
|
-
allowFailure: boolean;
|
|
47
|
-
path: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface PipelineShellStageRule extends PipelineStageRuleBase {
|
|
51
|
-
kind: "sh";
|
|
52
|
-
steps: PipelineStepRule[];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface PipelineActionStageRule extends PipelineStageRuleBase {
|
|
56
|
-
kind: "action";
|
|
57
|
-
action: PipelineActionName;
|
|
58
|
-
named: boolean;
|
|
59
|
-
retry: PipelinePlanRetry | null;
|
|
60
|
-
/** `ota` only; `null` takes the default. */
|
|
61
|
-
targets: string[] | null;
|
|
62
|
-
platforms: PipelineOtaPlatform[] | null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface PipelineParallelStageRule extends PipelineStageRuleBase {
|
|
66
|
-
kind: "parallel";
|
|
67
|
-
maxParallel: number | null;
|
|
68
|
-
failFast: boolean;
|
|
69
|
-
branches: PipelineShellStageRule[];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface PipelineMatrixStageRule extends PipelineStageRuleBase {
|
|
73
|
-
kind: "matrix";
|
|
74
|
-
maxParallel: number | null;
|
|
75
|
-
steps: PipelineStepRule[];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export type PipelineWorkspaceStageRule = PipelineShellStageRule | PipelineParallelStageRule | PipelineMatrixStageRule;
|
|
79
|
-
export type PipelineAppStageRule = PipelineShellStageRule | PipelineActionStageRule;
|
|
80
|
-
|
|
81
|
-
export interface PipelinePostRule {
|
|
82
|
-
success: PipelineStepRule[];
|
|
83
|
-
failure: PipelineStepRule[];
|
|
84
|
-
canceled: PipelineStepRule[];
|
|
85
|
-
always: PipelineStepRule[];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface PipelineWorkspaceRule {
|
|
89
|
-
env: Record<string, string>;
|
|
90
|
-
timeoutSeconds: number | null;
|
|
91
|
-
before: PipelineWorkspaceStageRule[];
|
|
92
|
-
after: PipelineWorkspaceStageRule[];
|
|
93
|
-
post: PipelinePostRule;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface PipelineAppRule {
|
|
97
|
-
env: Record<string, string>;
|
|
98
|
-
after: string[];
|
|
99
|
-
stages: PipelineAppStageRule[];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
type Scope = "workspace" | "app";
|
|
103
28
|
type JsonObject = Record<string, unknown>;
|
|
104
29
|
|
|
105
30
|
/** Turns one file's `pipeline` value into rules, reporting what is wrong with it rather than throwing. */
|
|
106
31
|
export class PipelineInputReader {
|
|
107
32
|
static readonly #futureKeys = ["cache", "artifacts", "input", "image", "services", "notify"];
|
|
33
|
+
static readonly #stageKeys = ["id", "name", "description", "milestone", "when", "timeout", "allowFailure"];
|
|
34
|
+
static readonly #shellKeys = [...PipelineInputReader.#stageKeys, "sh", "steps", "env", "secrets", "retry"];
|
|
35
|
+
static readonly #postResults: readonly string[] = ["success", "failure", "canceled", "always"];
|
|
108
36
|
static readonly #durationPattern = /^(\d+)(s|m|h)$/;
|
|
37
|
+
static readonly #trailingHighSurrogate = /[\uD800-\uDBFF]$/;
|
|
109
38
|
static readonly #stepNameChars = 80;
|
|
39
|
+
static readonly #shownChars = 80;
|
|
40
|
+
|
|
41
|
+
static clip(text: string, max: number) {
|
|
42
|
+
if (text.length <= max) return text;
|
|
43
|
+
const cut = text.slice(0, max - 1);
|
|
44
|
+
return `${PipelineInputReader.#trailingHighSurrogate.test(cut) ? cut.slice(0, -1) : cut}…`;
|
|
45
|
+
}
|
|
110
46
|
|
|
111
47
|
readonly diagnostics: PipelineDiagnostic[] = [];
|
|
112
48
|
readonly #file: string;
|
|
49
|
+
readonly #read = new PipelineValueReader<null>("config", (path, ...problems) => {
|
|
50
|
+
for (const problem of problems) this.#problem(path, problem);
|
|
51
|
+
return null;
|
|
52
|
+
});
|
|
113
53
|
|
|
114
54
|
constructor(file: string) {
|
|
115
55
|
this.#file = file;
|
|
116
56
|
}
|
|
117
57
|
|
|
118
58
|
workspace(value: unknown): PipelineWorkspaceRule | null {
|
|
119
|
-
const input = this.#object(value, "pipeline", ["schemaVersion", "env", "timeout", "before", "after", "post"]);
|
|
59
|
+
const input = this.#read.object(value, "pipeline", ["schemaVersion", "env", "timeout", "before", "after", "post"]);
|
|
120
60
|
if (!input) return null;
|
|
121
61
|
this.#schemaVersion(input.schemaVersion, "pipeline.schemaVersion");
|
|
122
62
|
const ids = new Set<string>();
|
|
@@ -136,7 +76,7 @@ export class PipelineInputReader {
|
|
|
136
76
|
}
|
|
137
77
|
|
|
138
78
|
app(value: unknown): PipelineAppRule | null {
|
|
139
|
-
const input = this.#object(value, "pipeline", ["schemaVersion", "stages", "env", "after"]);
|
|
79
|
+
const input = this.#read.object(value, "pipeline", ["schemaVersion", "stages", "env", "after"]);
|
|
140
80
|
if (!input) return null;
|
|
141
81
|
this.#schemaVersion(input.schemaVersion, "pipeline.schemaVersion");
|
|
142
82
|
if (input.stages === undefined) this.#report("pipeline.schema.invalid", "lists no `stages`", "pipeline");
|
|
@@ -154,9 +94,9 @@ export class PipelineInputReader {
|
|
|
154
94
|
}
|
|
155
95
|
|
|
156
96
|
#workspaceStage(value: unknown, path: string): PipelineWorkspaceStageRule | null {
|
|
157
|
-
const raw = this.#object(value, path);
|
|
97
|
+
const raw = this.#read.object(value, path);
|
|
158
98
|
if (!raw) return null;
|
|
159
|
-
if ("use"
|
|
99
|
+
if (this.#declares(raw, "use")) {
|
|
160
100
|
this.#report(
|
|
161
101
|
"pipeline.schema.invalid",
|
|
162
102
|
"built-in actions are declared per app, in apps/<app>/akan.config.ts",
|
|
@@ -164,49 +104,50 @@ export class PipelineInputReader {
|
|
|
164
104
|
);
|
|
165
105
|
return null;
|
|
166
106
|
}
|
|
167
|
-
if ("parallel"
|
|
168
|
-
if ("matrix"
|
|
169
|
-
return this.#shellStage(raw, path
|
|
107
|
+
if (this.#declares(raw, "parallel")) return this.#parallelStage(raw, path);
|
|
108
|
+
if (this.#declares(raw, "matrix")) return this.#matrixStage(raw, path);
|
|
109
|
+
return this.#shellStage(raw, path);
|
|
170
110
|
}
|
|
171
111
|
|
|
172
112
|
#appStage(value: unknown, path: string): PipelineAppStageRule | null {
|
|
173
|
-
const raw = this.#object(value, path);
|
|
113
|
+
const raw = this.#read.object(value, path);
|
|
174
114
|
if (!raw) return null;
|
|
175
|
-
|
|
115
|
+
const workspaceKey = this.#declares(raw, "parallel") ? "parallel" : this.#declares(raw, "matrix") ? "matrix" : null;
|
|
116
|
+
if (workspaceKey) {
|
|
176
117
|
this.#report(
|
|
177
118
|
"pipeline.schema.invalid",
|
|
178
|
-
`an app's own stage takes no \`${
|
|
119
|
+
`an app's own stage takes no \`${workspaceKey}\`: it is already that app's, and parallel or per-app copies belong to the workspace root akan.config.ts`,
|
|
179
120
|
path,
|
|
180
121
|
);
|
|
181
122
|
return null;
|
|
182
123
|
}
|
|
183
|
-
if ("use"
|
|
184
|
-
return this.#shellStage(raw, path
|
|
124
|
+
if (this.#declares(raw, "use")) return this.#actionStage(raw, path);
|
|
125
|
+
return this.#shellStage(raw, path);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
//? `key: undefined` is the key left out, but on a stage without `sh` or `steps` it still tells which kind was meant.
|
|
129
|
+
#declares(raw: JsonObject, key: "use" | "parallel" | "matrix") {
|
|
130
|
+
return raw[key] !== undefined || (key in raw && raw.sh === undefined && raw.steps === undefined);
|
|
185
131
|
}
|
|
186
132
|
|
|
187
133
|
#actionStage(raw: JsonObject, path: string): PipelineActionStageRule | null {
|
|
188
134
|
if (
|
|
189
|
-
!this.#keys(raw, path, [
|
|
135
|
+
!this.#read.keys(raw, path, [
|
|
190
136
|
"use",
|
|
191
|
-
|
|
192
|
-
"name",
|
|
193
|
-
"description",
|
|
194
|
-
"milestone",
|
|
195
|
-
"when",
|
|
196
|
-
"timeout",
|
|
197
|
-
"allowFailure",
|
|
137
|
+
...PipelineInputReader.#stageKeys,
|
|
198
138
|
"retry",
|
|
199
139
|
...(raw.use === "ota" ? ["targets", "platforms"] : []),
|
|
200
140
|
])
|
|
201
141
|
)
|
|
202
142
|
return null;
|
|
203
|
-
const action = this.#oneOf(raw.use, `${path}.use`, pipelineActionNames);
|
|
143
|
+
const action = this.#read.oneOf(raw.use, `${path}.use`, pipelineActionNames);
|
|
204
144
|
if (!action) return null;
|
|
205
|
-
const name =
|
|
145
|
+
const name =
|
|
146
|
+
raw.name === undefined ? null : this.#read.string(raw.name, `${path}.name`, pipelinePlanLimits.nameChars);
|
|
206
147
|
const id = raw.id === undefined ? action : this.#id(raw.id, `${path}.id`);
|
|
207
148
|
if (!id) return null;
|
|
208
149
|
const attempts =
|
|
209
|
-
raw.retry === undefined ? null : this.#int(raw.retry, `${path}.retry`, 1, pipelinePlanLimits.retryAttempts);
|
|
150
|
+
raw.retry === undefined ? null : this.#read.int(raw.retry, `${path}.retry`, 1, pipelinePlanLimits.retryAttempts);
|
|
210
151
|
return {
|
|
211
152
|
kind: "action",
|
|
212
153
|
action,
|
|
@@ -215,7 +156,7 @@ export class PipelineInputReader {
|
|
|
215
156
|
named: name !== null,
|
|
216
157
|
description: this.#description(raw.description, `${path}.description`),
|
|
217
158
|
milestone: this.#milestone(raw.milestone, `${path}.milestone`),
|
|
218
|
-
when: this.#when(raw.when, `${path}.when
|
|
159
|
+
when: this.#when(raw.when, `${path}.when`),
|
|
219
160
|
timeoutSeconds: this.#duration(raw.timeout, `${path}.timeout`),
|
|
220
161
|
allowFailure: this.#boolean(raw.allowFailure, `${path}.allowFailure`),
|
|
221
162
|
retry: attempts === null ? null : { attempts, delaySeconds: 0 },
|
|
@@ -228,7 +169,7 @@ export class PipelineInputReader {
|
|
|
228
169
|
? null
|
|
229
170
|
: this.#nonEmptySet(
|
|
230
171
|
this.#list(raw.platforms, `${path}.platforms`).flatMap((platform, idx) => {
|
|
231
|
-
const parsed = this.#oneOf(platform, `${path}.platforms[${idx}]`, pipelineOtaPlatforms);
|
|
172
|
+
const parsed = this.#read.oneOf(platform, `${path}.platforms[${idx}]`, pipelineOtaPlatforms);
|
|
232
173
|
return parsed ? [parsed] : [];
|
|
233
174
|
}),
|
|
234
175
|
`${path}.platforms`,
|
|
@@ -245,55 +186,25 @@ export class PipelineInputReader {
|
|
|
245
186
|
return [...new Set(items)];
|
|
246
187
|
}
|
|
247
188
|
|
|
248
|
-
#shellStage(raw: JsonObject, path: string
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
"name",
|
|
252
|
-
"description",
|
|
253
|
-
"milestone",
|
|
254
|
-
"when",
|
|
255
|
-
"timeout",
|
|
256
|
-
"allowFailure",
|
|
257
|
-
"sh",
|
|
258
|
-
"steps",
|
|
259
|
-
"env",
|
|
260
|
-
"secrets",
|
|
261
|
-
"retry",
|
|
262
|
-
];
|
|
263
|
-
if (!this.#keys(raw, path, keys)) return null;
|
|
264
|
-
const head = this.#stageHead(raw, path, scope === "workspace" ? "workspace" : "app");
|
|
189
|
+
#shellStage(raw: JsonObject, path: string): PipelineShellStageRule | null {
|
|
190
|
+
if (!this.#read.keys(raw, path, PipelineInputReader.#shellKeys)) return null;
|
|
191
|
+
const head = this.#stageHead(raw, path);
|
|
265
192
|
if (!head) return null;
|
|
266
193
|
const steps = this.#stageSteps(raw, path);
|
|
267
194
|
return steps ? { kind: "sh", ...head, steps } : null;
|
|
268
195
|
}
|
|
269
196
|
|
|
270
197
|
#matrixStage(raw: JsonObject, path: string): PipelineMatrixStageRule | null {
|
|
271
|
-
|
|
272
|
-
"id",
|
|
273
|
-
"name",
|
|
274
|
-
"description",
|
|
275
|
-
"milestone",
|
|
276
|
-
"when",
|
|
277
|
-
"timeout",
|
|
278
|
-
"allowFailure",
|
|
279
|
-
"sh",
|
|
280
|
-
"steps",
|
|
281
|
-
"env",
|
|
282
|
-
"secrets",
|
|
283
|
-
"retry",
|
|
284
|
-
"matrix",
|
|
285
|
-
"maxParallel",
|
|
286
|
-
];
|
|
287
|
-
if (!this.#keys(raw, path, keys)) return null;
|
|
198
|
+
if (!this.#read.keys(raw, path, [...PipelineInputReader.#shellKeys, "matrix", "maxParallel"])) return null;
|
|
288
199
|
if (raw.matrix !== "apps") {
|
|
289
200
|
this.#report(
|
|
290
201
|
"pipeline.schema.invalid",
|
|
291
|
-
`${
|
|
202
|
+
`${PipelineInputReader.#show(raw.matrix)} is not a matrix axis: only "apps"`,
|
|
292
203
|
`${path}.matrix`,
|
|
293
204
|
);
|
|
294
205
|
return null;
|
|
295
206
|
}
|
|
296
|
-
const head = this.#stageHead(raw, path,
|
|
207
|
+
const head = this.#stageHead(raw, path, { allowApp: true });
|
|
297
208
|
if (!head) return null;
|
|
298
209
|
const steps = this.#stageSteps(raw, path);
|
|
299
210
|
if (!steps) return null;
|
|
@@ -301,35 +212,22 @@ export class PipelineInputReader {
|
|
|
301
212
|
}
|
|
302
213
|
|
|
303
214
|
#parallelStage(raw: JsonObject, path: string): PipelineParallelStageRule | null {
|
|
304
|
-
const keys = [
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
"description",
|
|
308
|
-
"milestone",
|
|
309
|
-
"when",
|
|
310
|
-
"timeout",
|
|
311
|
-
"allowFailure",
|
|
312
|
-
"parallel",
|
|
313
|
-
"maxParallel",
|
|
314
|
-
"failFast",
|
|
315
|
-
"env",
|
|
316
|
-
"secrets",
|
|
317
|
-
];
|
|
318
|
-
if (!this.#keys(raw, path, keys)) return null;
|
|
319
|
-
const head = this.#stageHead(raw, path, "workspace");
|
|
215
|
+
const keys = [...PipelineInputReader.#stageKeys, "parallel", "maxParallel", "failFast", "env", "secrets"];
|
|
216
|
+
if (!this.#read.keys(raw, path, keys)) return null;
|
|
217
|
+
const head = this.#stageHead(raw, path);
|
|
320
218
|
if (!head) return null;
|
|
321
219
|
const env = this.#env(raw.env, `${path}.env`);
|
|
322
220
|
const secrets = this.#secrets(raw.secrets, `${path}.secrets`);
|
|
323
221
|
const ids = new Set<string>();
|
|
324
222
|
const branches = this.#list(raw.parallel, `${path}.parallel`).flatMap((branch, idx) => {
|
|
325
223
|
const branchPath = `${path}.parallel[${idx}]`;
|
|
326
|
-
const branchRaw = this.#object(branch, branchPath);
|
|
224
|
+
const branchRaw = this.#read.object(branch, branchPath);
|
|
327
225
|
if (!branchRaw) return [];
|
|
328
|
-
if ("use"
|
|
226
|
+
if ((["use", "parallel", "matrix"] as const).some((key) => this.#declares(branchRaw, key))) {
|
|
329
227
|
this.#report("pipeline.schema.invalid", "a parallel branch is a shell stage", branchPath);
|
|
330
228
|
return [];
|
|
331
229
|
}
|
|
332
|
-
const rule = this.#shellStage(branchRaw, branchPath
|
|
230
|
+
const rule = this.#shellStage(branchRaw, branchPath);
|
|
333
231
|
if (!rule) return [];
|
|
334
232
|
if (rule.when.changed && head.when.changed) {
|
|
335
233
|
this.#report(
|
|
@@ -339,6 +237,12 @@ export class PipelineInputReader {
|
|
|
339
237
|
);
|
|
340
238
|
return [];
|
|
341
239
|
}
|
|
240
|
+
if (head.id === "post" && PipelineInputReader.#postResults.includes(rule.id))
|
|
241
|
+
this.#report(
|
|
242
|
+
"pipeline.id.reserved",
|
|
243
|
+
`"post.${rule.id}" is reserved for the steps of post.${rule.id}; give this branch or its parallel stage another \`id\``,
|
|
244
|
+
`${branchPath}.${branchRaw.id === undefined ? "name" : "id"}`,
|
|
245
|
+
);
|
|
342
246
|
this.#claimId(ids, rule.id, rule.path);
|
|
343
247
|
const steps = rule.steps.map((step) => ({
|
|
344
248
|
...step,
|
|
@@ -347,7 +251,7 @@ export class PipelineInputReader {
|
|
|
347
251
|
}));
|
|
348
252
|
return [{ ...rule, steps }];
|
|
349
253
|
});
|
|
350
|
-
if (!branches.length && Array.isArray(raw.parallel))
|
|
254
|
+
if (!branches.length && (raw.parallel === undefined || Array.isArray(raw.parallel)))
|
|
351
255
|
this.#report("pipeline.schema.invalid", "a parallel stage holds at least one branch", `${path}.parallel`);
|
|
352
256
|
return {
|
|
353
257
|
kind: "parallel",
|
|
@@ -358,8 +262,8 @@ export class PipelineInputReader {
|
|
|
358
262
|
};
|
|
359
263
|
}
|
|
360
264
|
|
|
361
|
-
#stageHead(raw: JsonObject, path: string,
|
|
362
|
-
const name = this.#
|
|
265
|
+
#stageHead(raw: JsonObject, path: string, { allowApp = false }: { allowApp?: boolean } = {}) {
|
|
266
|
+
const name = this.#read.string(raw.name, `${path}.name`, pipelinePlanLimits.nameChars);
|
|
363
267
|
if (name === null) return null;
|
|
364
268
|
const id = raw.id === undefined ? this.#slugId(name, `${path}.name`) : this.#id(raw.id, `${path}.id`);
|
|
365
269
|
if (!id) return null;
|
|
@@ -368,7 +272,7 @@ export class PipelineInputReader {
|
|
|
368
272
|
name,
|
|
369
273
|
description: this.#description(raw.description, `${path}.description`),
|
|
370
274
|
milestone: this.#milestone(raw.milestone, `${path}.milestone`),
|
|
371
|
-
when: this.#when(raw.when, `${path}.when`,
|
|
275
|
+
when: this.#when(raw.when, `${path}.when`, { allowApp }),
|
|
372
276
|
timeoutSeconds: this.#duration(raw.timeout, `${path}.timeout`),
|
|
373
277
|
allowFailure: this.#boolean(raw.allowFailure, `${path}.allowFailure`),
|
|
374
278
|
path,
|
|
@@ -407,11 +311,11 @@ export class PipelineInputReader {
|
|
|
407
311
|
}
|
|
408
312
|
|
|
409
313
|
#step(value: unknown, path: string, idx: number): PipelineStepRule | null {
|
|
410
|
-
const raw = typeof value === "string" ? { sh: value } : this.#object(value, path);
|
|
314
|
+
const raw = typeof value === "string" ? { sh: value } : this.#read.object(value, path);
|
|
411
315
|
if (!raw) return null;
|
|
412
316
|
const keys = ["sh", "id", "name", "cwd", "env", "secrets", "timeout", "retry", "allowFailure", "skipExitCodes"];
|
|
413
|
-
if (!this.#keys(raw, path, keys)) return null;
|
|
414
|
-
const run = this.#
|
|
317
|
+
if (!this.#read.keys(raw, path, keys)) return null;
|
|
318
|
+
const run = this.#read.string(raw.sh, typeof value === "string" ? path : `${path}.sh`, Number.POSITIVE_INFINITY);
|
|
415
319
|
if (run === null) return null;
|
|
416
320
|
if (new TextEncoder().encode(run).length > pipelinePlanLimits.shBytes) {
|
|
417
321
|
this.#report("pipeline.limit.exceeded", `a script is at most ${pipelinePlanLimits.shBytes} bytes`, path);
|
|
@@ -420,7 +324,7 @@ export class PipelineInputReader {
|
|
|
420
324
|
const name =
|
|
421
325
|
raw.name === undefined
|
|
422
326
|
? PipelineInputReader.#nameOf(run)
|
|
423
|
-
: this.#
|
|
327
|
+
: this.#read.string(raw.name, `${path}.name`, pipelinePlanLimits.nameChars);
|
|
424
328
|
if (name === null) return null;
|
|
425
329
|
const localId =
|
|
426
330
|
raw.id !== undefined
|
|
@@ -446,10 +350,10 @@ export class PipelineInputReader {
|
|
|
446
350
|
#post(value: unknown, path: string): PipelinePostRule {
|
|
447
351
|
const empty: PipelinePostRule = { success: [], failure: [], canceled: [], always: [] };
|
|
448
352
|
if (value === undefined) return empty;
|
|
449
|
-
const raw = this.#object(value, path);
|
|
353
|
+
const raw = this.#read.object(value, path);
|
|
450
354
|
if (!raw) return empty;
|
|
451
355
|
for (const key of Object.keys(raw))
|
|
452
|
-
if (!
|
|
356
|
+
if (!PipelineInputReader.#postResults.includes(key))
|
|
453
357
|
this.#report(
|
|
454
358
|
"pipeline.schema.unknownKey",
|
|
455
359
|
key === "unstable"
|
|
@@ -473,10 +377,10 @@ export class PipelineInputReader {
|
|
|
473
377
|
};
|
|
474
378
|
}
|
|
475
379
|
|
|
476
|
-
#when(value: unknown, path: string, allowApp: boolean): PipelineWhenRule {
|
|
380
|
+
#when(value: unknown, path: string, { allowApp = false }: { allowApp?: boolean } = {}): PipelineWhenRule {
|
|
477
381
|
const none: PipelineWhenRule = { branch: null, trigger: null, changed: null, app: null };
|
|
478
382
|
if (value === undefined) return none;
|
|
479
|
-
const raw = this.#object(value, path);
|
|
383
|
+
const raw = this.#read.object(value, path);
|
|
480
384
|
if (!raw) return none;
|
|
481
385
|
for (const key of Object.keys(raw)) {
|
|
482
386
|
if (["branch", "trigger", "changed"].includes(key) || (allowApp && key === "app")) continue;
|
|
@@ -493,10 +397,12 @@ export class PipelineInputReader {
|
|
|
493
397
|
? null
|
|
494
398
|
: (typeof raw.trigger === "string" ? [raw.trigger] : this.#list(raw.trigger, `${path}.trigger`)).flatMap(
|
|
495
399
|
(trigger, idx) => {
|
|
496
|
-
const parsed = this.#oneOf(trigger, `${path}.trigger[${idx}]`, pipelineTriggers);
|
|
400
|
+
const parsed = this.#read.oneOf(trigger, `${path}.trigger[${idx}]`, pipelineTriggers);
|
|
497
401
|
return parsed ? [parsed] : [];
|
|
498
402
|
},
|
|
499
403
|
);
|
|
404
|
+
if (Array.isArray(raw.trigger) && !raw.trigger.length)
|
|
405
|
+
this.#report("pipeline.schema.invalid", "a trigger list may not be empty", `${path}.trigger`);
|
|
500
406
|
return {
|
|
501
407
|
branch: this.#patterns(raw.branch, `${path}.branch`),
|
|
502
408
|
trigger: triggers,
|
|
@@ -520,14 +426,14 @@ export class PipelineInputReader {
|
|
|
520
426
|
if (value !== undefined && value !== 1)
|
|
521
427
|
this.#report(
|
|
522
428
|
"pipeline.schema.unsupported",
|
|
523
|
-
`${
|
|
429
|
+
`${PipelineInputReader.#show(value)} is not a schemaVersion this CLI reads (1)`,
|
|
524
430
|
path,
|
|
525
431
|
);
|
|
526
432
|
}
|
|
527
433
|
|
|
528
434
|
#env(value: unknown, path: string): Record<string, string> {
|
|
529
435
|
if (value === undefined) return {};
|
|
530
|
-
const raw = this.#object(value, path);
|
|
436
|
+
const raw = this.#read.object(value, path);
|
|
531
437
|
if (!raw) return {};
|
|
532
438
|
const env: Record<string, string> = {};
|
|
533
439
|
for (const [name, entry] of Object.entries(raw)) {
|
|
@@ -535,6 +441,12 @@ export class PipelineInputReader {
|
|
|
535
441
|
if (problem) this.#report("pipeline.env.name", problem, `${path}.${name}`);
|
|
536
442
|
else if (typeof entry !== "string")
|
|
537
443
|
this.#report("pipeline.schema.invalid", "must be a string", `${path}.${name}`);
|
|
444
|
+
else if (entry.length > pipelinePlanLimits.shBytes)
|
|
445
|
+
this.#report(
|
|
446
|
+
"pipeline.limit.exceeded",
|
|
447
|
+
`an env value is at most ${pipelinePlanLimits.shBytes} characters`,
|
|
448
|
+
`${path}.${name}`,
|
|
449
|
+
);
|
|
538
450
|
else env[name] = entry;
|
|
539
451
|
}
|
|
540
452
|
return env;
|
|
@@ -542,9 +454,9 @@ export class PipelineInputReader {
|
|
|
542
454
|
|
|
543
455
|
#secrets(value: unknown, path: string): string[] {
|
|
544
456
|
const names = value === undefined ? [] : this.#strings(value, path);
|
|
545
|
-
const valid = names.filter((name) => {
|
|
457
|
+
const valid = names.filter((name, idx) => {
|
|
546
458
|
const problem = PipelineNames.secretNameError(name);
|
|
547
|
-
if (problem) this.#report("pipeline.secret.name", problem, path);
|
|
459
|
+
if (problem) this.#report("pipeline.secret.name", problem, `${path}[${idx}]`);
|
|
548
460
|
return !problem;
|
|
549
461
|
});
|
|
550
462
|
return [...new Set(valid)];
|
|
@@ -552,7 +464,7 @@ export class PipelineInputReader {
|
|
|
552
464
|
|
|
553
465
|
#cwd(value: unknown, path: string) {
|
|
554
466
|
if (value === undefined) return ".";
|
|
555
|
-
const cwd = this.#
|
|
467
|
+
const cwd = this.#read.string(value, path, 1024);
|
|
556
468
|
if (cwd === null) return ".";
|
|
557
469
|
if (cwd.startsWith("/") || cwd.startsWith("~") || cwd.includes("\\") || cwd.split("/").includes("..")) {
|
|
558
470
|
this.#report("pipeline.cwd.outside", `"${cwd}" is not a path inside the workspace`, path);
|
|
@@ -564,7 +476,7 @@ export class PipelineInputReader {
|
|
|
564
476
|
#exitCodes(value: unknown, path: string): number[] {
|
|
565
477
|
if (value === undefined) return [];
|
|
566
478
|
const codes = this.#list(value, path).flatMap((code, idx) => {
|
|
567
|
-
const parsed = this.#int(code, `${path}[${idx}]`, 1, 255);
|
|
479
|
+
const parsed = this.#read.int(code, `${path}[${idx}]`, 1, 255);
|
|
568
480
|
return parsed === null ? [] : [parsed];
|
|
569
481
|
});
|
|
570
482
|
return [...new Set(codes)];
|
|
@@ -573,12 +485,12 @@ export class PipelineInputReader {
|
|
|
573
485
|
#retry(value: unknown, path: string): PipelinePlanRetry | null {
|
|
574
486
|
if (value === undefined) return null;
|
|
575
487
|
if (typeof value === "number") {
|
|
576
|
-
const attempts = this.#int(value, path, 1, pipelinePlanLimits.retryAttempts);
|
|
488
|
+
const attempts = this.#read.int(value, path, 1, pipelinePlanLimits.retryAttempts);
|
|
577
489
|
return attempts === null ? null : { attempts, delaySeconds: 0 };
|
|
578
490
|
}
|
|
579
|
-
const raw = this.#object(value, path);
|
|
580
|
-
if (!raw || !this.#keys(raw, path, ["attempts", "delay"])) return null;
|
|
581
|
-
const attempts = this.#int(raw.attempts, `${path}.attempts`, 1, pipelinePlanLimits.retryAttempts);
|
|
491
|
+
const raw = this.#read.object(value, path);
|
|
492
|
+
if (!raw || !this.#read.keys(raw, path, ["attempts", "delay"])) return null;
|
|
493
|
+
const attempts = this.#read.int(raw.attempts, `${path}.attempts`, 1, pipelinePlanLimits.retryAttempts);
|
|
582
494
|
const delaySeconds = this.#duration(raw.delay, `${path}.delay`, {
|
|
583
495
|
min: 0,
|
|
584
496
|
max: pipelinePlanLimits.retryDelaySeconds,
|
|
@@ -598,7 +510,7 @@ export class PipelineInputReader {
|
|
|
598
510
|
if (!Number.isInteger(seconds) || seconds < min || seconds > max) {
|
|
599
511
|
this.#report(
|
|
600
512
|
"pipeline.duration.invalid",
|
|
601
|
-
`${
|
|
513
|
+
`${PipelineInputReader.#show(value)} is not a duration between ${min} and ${max} seconds: whole seconds, or "30s", "20m", "1h"`,
|
|
602
514
|
path,
|
|
603
515
|
);
|
|
604
516
|
return null;
|
|
@@ -607,19 +519,19 @@ export class PipelineInputReader {
|
|
|
607
519
|
}
|
|
608
520
|
|
|
609
521
|
#maxParallel(value: unknown, path: string) {
|
|
610
|
-
return value === undefined ? null : this.#int(value, path, 1, pipelinePlanLimits.stages);
|
|
522
|
+
return value === undefined ? null : this.#read.int(value, path, 1, pipelinePlanLimits.stages);
|
|
611
523
|
}
|
|
612
524
|
|
|
613
525
|
#milestone(value: unknown, path: string) {
|
|
614
|
-
return value === undefined ? null : this.#oneOf(value, path, pipelineMilestones);
|
|
526
|
+
return value === undefined ? null : this.#read.oneOf(value, path, pipelineMilestones);
|
|
615
527
|
}
|
|
616
528
|
|
|
617
529
|
#description(value: unknown, path: string) {
|
|
618
|
-
return value === undefined ? null : this.#
|
|
530
|
+
return value === undefined ? null : this.#read.string(value, path, pipelinePlanLimits.descriptionChars);
|
|
619
531
|
}
|
|
620
532
|
|
|
621
533
|
#id(value: unknown, path: string) {
|
|
622
|
-
const id = this.#
|
|
534
|
+
const id = this.#read.string(value, path, 64);
|
|
623
535
|
if (id === null) return null;
|
|
624
536
|
const problem = PipelineNames.idError(id);
|
|
625
537
|
if (problem) {
|
|
@@ -657,80 +569,72 @@ export class PipelineInputReader {
|
|
|
657
569
|
.split("\n")
|
|
658
570
|
.map((text) => text.trim())
|
|
659
571
|
.find(Boolean);
|
|
660
|
-
|
|
661
|
-
return name.length > PipelineInputReader.#stepNameChars
|
|
662
|
-
? `${name.slice(0, PipelineInputReader.#stepNameChars - 1)}…`
|
|
663
|
-
: name;
|
|
572
|
+
return PipelineInputReader.clip(line ?? run.trim(), PipelineInputReader.#stepNameChars);
|
|
664
573
|
}
|
|
665
574
|
|
|
666
|
-
#
|
|
667
|
-
let
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
this.#report("pipeline.schema.unsupported", `\`${key}\` is not supported in schemaVersion 1`, `${path}.${key}`);
|
|
673
|
-
else
|
|
674
|
-
this.#report("pipeline.schema.unknownKey", `"${key}" is not a key here: ${keys.join(", ")}`, `${path}.${key}`);
|
|
575
|
+
static #show(value: unknown) {
|
|
576
|
+
let text: string;
|
|
577
|
+
try {
|
|
578
|
+
text = JSON.stringify(value) ?? String(value);
|
|
579
|
+
} catch {
|
|
580
|
+
text = typeof value === "bigint" ? `${value}n` : Object.prototype.toString.call(value);
|
|
675
581
|
}
|
|
676
|
-
return
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
#object(value: unknown, path: string, keys?: readonly string[]): JsonObject | null {
|
|
680
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
681
|
-
this.#report("pipeline.schema.invalid", "must be an object", path);
|
|
682
|
-
return null;
|
|
683
|
-
}
|
|
684
|
-
const raw = value as JsonObject;
|
|
685
|
-
if (keys && !this.#keys(raw, path, keys)) return null;
|
|
686
|
-
return raw;
|
|
582
|
+
return PipelineInputReader.clip(text, PipelineInputReader.#shownChars);
|
|
687
583
|
}
|
|
688
584
|
|
|
689
585
|
#list(value: unknown, path: string): unknown[] {
|
|
690
|
-
|
|
691
|
-
if (!Array.isArray(value)) {
|
|
692
|
-
this.#report("pipeline.schema.invalid", "must be a list", path);
|
|
693
|
-
return [];
|
|
694
|
-
}
|
|
695
|
-
return value;
|
|
586
|
+
return value === undefined ? [] : (this.#read.list(value, path) ?? []);
|
|
696
587
|
}
|
|
697
588
|
|
|
698
589
|
#strings(value: unknown, path: string): string[] {
|
|
699
590
|
return this.#list(value, path).flatMap((item, idx) => {
|
|
700
|
-
const text = this.#
|
|
591
|
+
const text = this.#read.string(item, `${path}[${idx}]`);
|
|
701
592
|
return text === null ? [] : [text];
|
|
702
593
|
});
|
|
703
594
|
}
|
|
704
595
|
|
|
705
|
-
#text(value: unknown, path: string, max: number) {
|
|
706
|
-
if (typeof value !== "string" || !value.trim()) {
|
|
707
|
-
this.#report("pipeline.schema.invalid", "must be a non-empty string", path);
|
|
708
|
-
return null;
|
|
709
|
-
}
|
|
710
|
-
if (value.length > max) {
|
|
711
|
-
this.#report("pipeline.schema.invalid", `is longer than ${max} characters`, path);
|
|
712
|
-
return null;
|
|
713
|
-
}
|
|
714
|
-
return value;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
596
|
#boolean(value: unknown, path: string) {
|
|
718
|
-
|
|
719
|
-
if (typeof value === "boolean") return value;
|
|
720
|
-
this.#report("pipeline.schema.invalid", "must be true or false", path);
|
|
721
|
-
return false;
|
|
597
|
+
return value === undefined ? false : (this.#read.boolean(value, path) ?? false);
|
|
722
598
|
}
|
|
723
599
|
|
|
724
|
-
#
|
|
725
|
-
if (
|
|
726
|
-
|
|
727
|
-
|
|
600
|
+
#problem(path: string, problem: PipelineValueProblem) {
|
|
601
|
+
if (problem.kind !== "unknownKey")
|
|
602
|
+
this.#report("pipeline.schema.invalid", PipelineInputReader.#reason(problem), path);
|
|
603
|
+
else if (PipelineInputReader.#futureKeys.includes(problem.key))
|
|
604
|
+
this.#report(
|
|
605
|
+
"pipeline.schema.unsupported",
|
|
606
|
+
`\`${problem.key}\` is not supported in schemaVersion 1`,
|
|
607
|
+
`${path}.${problem.key}`,
|
|
608
|
+
);
|
|
609
|
+
else
|
|
610
|
+
this.#report(
|
|
611
|
+
"pipeline.schema.unknownKey",
|
|
612
|
+
`"${problem.key}" is not a key here: ${problem.keys.join(", ")}`,
|
|
613
|
+
`${path}.${problem.key}`,
|
|
614
|
+
);
|
|
728
615
|
}
|
|
729
616
|
|
|
730
|
-
#
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
617
|
+
static #reason(problem: Exclude<PipelineValueProblem, { kind: "unknownKey" }>): string {
|
|
618
|
+
switch (problem.kind) {
|
|
619
|
+
case "object":
|
|
620
|
+
return "must be an object";
|
|
621
|
+
case "missingKey":
|
|
622
|
+
return `"${problem.key}" is missing`;
|
|
623
|
+
case "list":
|
|
624
|
+
return "must be a list";
|
|
625
|
+
case "string":
|
|
626
|
+
case "empty":
|
|
627
|
+
return "must be a non-empty string";
|
|
628
|
+
case "length":
|
|
629
|
+
return `is longer than ${problem.max} characters`;
|
|
630
|
+
case "boolean":
|
|
631
|
+
return "must be true or false";
|
|
632
|
+
case "int":
|
|
633
|
+
case "range":
|
|
634
|
+
return `must be a whole number between ${problem.min} and ${problem.max}`;
|
|
635
|
+
case "oneOf":
|
|
636
|
+
return `${PipelineInputReader.#show(problem.value)} is not one of ${problem.options.join(", ")}`;
|
|
637
|
+
}
|
|
734
638
|
}
|
|
735
639
|
|
|
736
640
|
#report(code: string, message: string, path: string) {
|