willfire 0.1.26 → 0.1.28
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/dist/execute.js +66 -68
- package/dist/expr/cursor.js +1 -1
- package/dist/expr/evaluateValue.js +1 -1
- package/dist/expr/indexVal.js +1 -1
- package/dist/expr/lookup.js +2 -2
- package/dist/expr/parseAtom.js +1 -1
- package/dist/expr/parseCall.js +5 -6
- package/dist/expr/tokenize.js +44 -37
- package/dist/jobs/evalIf.js +1 -1
- package/dist/jobs/expandJobs.js +95 -86
- package/dist/matrix/expandMatrix.js +1 -1
- package/dist/matrix/expandMatrixDetailed.js +7 -8
- package/dist/matrix/formatMatrixValue.js +1 -1
- package/dist/names/jobDisplayName.js +1 -1
- package/dist/names/skippedDisplayName.js +1 -1
- package/dist/predict/predict.js +21 -7
- package/dist/triggers/workflowDispatches.js +2 -2
- package/dist/verify.js +2 -2
- package/package.json +1 -1
package/dist/execute.js
CHANGED
|
@@ -33,7 +33,7 @@ export function renderTemplate(text, scope) {
|
|
|
33
33
|
}
|
|
34
34
|
/** An `env:` block rendered to concrete strings, every key or nothing. */
|
|
35
35
|
function renderEnvLayer(layer, scope) {
|
|
36
|
-
if (layer
|
|
36
|
+
if (layer === null || layer === undefined) {
|
|
37
37
|
return { ok: true, v: {} };
|
|
38
38
|
}
|
|
39
39
|
if (typeof layer !== "object" || Array.isArray(layer)) {
|
|
@@ -42,7 +42,7 @@ function renderEnvLayer(layer, scope) {
|
|
|
42
42
|
const out = {};
|
|
43
43
|
for (const [k, raw] of Object.entries(layer)) {
|
|
44
44
|
const rendered = renderTemplate(String(raw ?? ""), scope);
|
|
45
|
-
if (rendered
|
|
45
|
+
if (rendered === null) {
|
|
46
46
|
return err(`cannot resolve env '${k}'`);
|
|
47
47
|
}
|
|
48
48
|
out[k] = rendered;
|
|
@@ -60,32 +60,25 @@ export function parseGithubOutput(text) {
|
|
|
60
60
|
while (i < lines.length) {
|
|
61
61
|
const line = lines[i];
|
|
62
62
|
i++;
|
|
63
|
-
if (line
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const buf = [];
|
|
70
|
-
for (;;) {
|
|
71
|
-
if (i >= lines.length) {
|
|
63
|
+
if (line !== "") {
|
|
64
|
+
const heredoc = /^([^=<]+)<<(.+)$/.exec(line);
|
|
65
|
+
if (heredoc !== null) {
|
|
66
|
+
const [, name, delim] = heredoc;
|
|
67
|
+
const end = lines.indexOf(delim, i);
|
|
68
|
+
if (end === -1) {
|
|
72
69
|
return null; // unterminated heredoc
|
|
73
70
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
out[name] = lines.slice(i, end).join("\n");
|
|
72
|
+
i = end + 1;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const eq = line.indexOf("=");
|
|
76
|
+
if (eq <= 0) {
|
|
77
|
+
return null;
|
|
77
78
|
}
|
|
78
|
-
|
|
79
|
-
i++;
|
|
79
|
+
out[line.slice(0, eq)] = line.slice(eq + 1);
|
|
80
80
|
}
|
|
81
|
-
out[name] = buf.join("\n");
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
const eq = line.indexOf("=");
|
|
85
|
-
if (eq <= 0) {
|
|
86
|
-
return null;
|
|
87
81
|
}
|
|
88
|
-
out[line.slice(0, eq)] = line.slice(eq + 1);
|
|
89
82
|
}
|
|
90
83
|
return out;
|
|
91
84
|
}
|
|
@@ -130,23 +123,22 @@ async function readActionManifest(dir) {
|
|
|
130
123
|
*/
|
|
131
124
|
function bindActionInputs(action, withBlock, scope) {
|
|
132
125
|
const bind = (raw) => {
|
|
133
|
-
if (raw
|
|
126
|
+
if (raw === null || raw === undefined) {
|
|
134
127
|
return { kind: "value", v: "" };
|
|
135
128
|
}
|
|
136
129
|
if (typeof raw === "boolean" || typeof raw === "number") {
|
|
137
130
|
return { kind: "value", v: String(raw) };
|
|
138
131
|
}
|
|
139
132
|
const rendered = renderTemplate(String(raw), scope);
|
|
140
|
-
return rendered
|
|
133
|
+
return rendered === null ? UNKNOWN : { kind: "value", v: rendered };
|
|
141
134
|
};
|
|
142
135
|
const out = {};
|
|
143
136
|
for (const [name, decl] of Object.entries(action?.inputs ?? {})) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
: { kind: "value", v: "" };
|
|
137
|
+
// An input declared without a `default:` binds the same empty string a
|
|
138
|
+
// missing declaration does, so the two cases share one path.
|
|
139
|
+
out[name] = bind(decl?.["default"]);
|
|
148
140
|
}
|
|
149
|
-
if (withBlock
|
|
141
|
+
if (withBlock !== null && typeof withBlock === "object") {
|
|
150
142
|
for (const [name, raw] of Object.entries(withBlock)) {
|
|
151
143
|
out[name] = bind(raw);
|
|
152
144
|
}
|
|
@@ -164,34 +156,37 @@ async function runSteps(steps, scope, ctx) {
|
|
|
164
156
|
const step = steps[i] ?? {};
|
|
165
157
|
const label = `step '${step.id ?? step.name ?? `#${i + 1}`}'`;
|
|
166
158
|
const stepScope = { ...scope, steps: stepsCtx };
|
|
167
|
-
|
|
159
|
+
let skipped = false;
|
|
160
|
+
if (step.if !== undefined && step.if !== null) {
|
|
168
161
|
const verdict = evaluate(String(step.if), stepScope);
|
|
169
|
-
if (verdict
|
|
162
|
+
if (verdict === null) {
|
|
170
163
|
return err(`cannot decide if: for ${label}`);
|
|
171
164
|
}
|
|
172
|
-
|
|
173
|
-
// A skipped step still occupies its id, with no outputs.
|
|
174
|
-
if (typeof step.id === "string") {
|
|
175
|
-
stepsCtx[step.id] = { outputs: {} };
|
|
176
|
-
}
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
165
|
+
skipped = !verdict;
|
|
179
166
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
res = await runRun(step, label, stepScope, ctx);
|
|
167
|
+
if (skipped) {
|
|
168
|
+
// A skipped step still occupies its id, with no outputs.
|
|
169
|
+
if (typeof step.id === "string") {
|
|
170
|
+
stepsCtx[step.id] = { outputs: {} };
|
|
171
|
+
}
|
|
186
172
|
}
|
|
187
173
|
else {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
174
|
+
let res;
|
|
175
|
+
if (typeof step.uses === "string") {
|
|
176
|
+
res = await runUses(step, label, stepScope, ctx);
|
|
177
|
+
}
|
|
178
|
+
else if (step.run !== undefined && step.run !== null) {
|
|
179
|
+
res = await runRun(step, label, stepScope, ctx);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
return err(`${label} has neither uses nor run`);
|
|
183
|
+
}
|
|
184
|
+
if (!res.ok) {
|
|
185
|
+
return res;
|
|
186
|
+
}
|
|
187
|
+
if (typeof step.id === "string") {
|
|
188
|
+
stepsCtx[step.id] = { outputs: res.v };
|
|
189
|
+
}
|
|
195
190
|
}
|
|
196
191
|
}
|
|
197
192
|
return { ok: true, v: stepsCtx };
|
|
@@ -247,24 +242,24 @@ async function runUses(step, label, scope, ctx) {
|
|
|
247
242
|
}
|
|
248
243
|
else {
|
|
249
244
|
const target = parseActionUses(uses);
|
|
250
|
-
if (target
|
|
245
|
+
if (target === null) {
|
|
251
246
|
return err(`${label}: unresolvable uses: ${uses}`);
|
|
252
247
|
}
|
|
253
248
|
const { ref } = target.source;
|
|
254
249
|
const sha = SHA_RE.test(ref) ? ref : await ctx.deps.resolveRef(target.source);
|
|
255
|
-
if (sha
|
|
250
|
+
if (sha === null) {
|
|
256
251
|
return err(`${label}: cannot resolve ref for ${uses}`);
|
|
257
252
|
}
|
|
258
253
|
const source = { ...target.source, sha };
|
|
259
254
|
const root = await ctx.deps.provideTree(source);
|
|
260
|
-
if (root
|
|
255
|
+
if (root === null) {
|
|
261
256
|
return err(`${label}: cannot materialize ${source.owner}/${source.repo}@${sha}`);
|
|
262
257
|
}
|
|
263
|
-
actionDir =
|
|
258
|
+
actionDir = join(root, target.path);
|
|
264
259
|
actionRoot = root;
|
|
265
260
|
}
|
|
266
261
|
const manifest = await readActionManifest(actionDir);
|
|
267
|
-
if (manifest
|
|
262
|
+
if (manifest === null) {
|
|
268
263
|
return err(`${label}: no action.yml under ${uses}`);
|
|
269
264
|
}
|
|
270
265
|
let action;
|
|
@@ -300,11 +295,11 @@ async function runUses(step, label, scope, ctx) {
|
|
|
300
295
|
const outputs = {};
|
|
301
296
|
for (const [name, decl] of Object.entries(action.outputs ?? {})) {
|
|
302
297
|
const raw = decl?.["value"];
|
|
303
|
-
if (raw
|
|
298
|
+
if (raw === null || raw === undefined) {
|
|
304
299
|
return err(`${label}: output '${name}' of ${uses} has no value`);
|
|
305
300
|
}
|
|
306
301
|
const rendered = renderTemplate(String(raw), outScope);
|
|
307
|
-
if (rendered
|
|
302
|
+
if (rendered === null) {
|
|
308
303
|
return err(`${label}: cannot resolve output '${name}' of ${uses}`);
|
|
309
304
|
}
|
|
310
305
|
outputs[name] = rendered;
|
|
@@ -384,12 +379,12 @@ async function runNodeAction(step, label, uses, action, actionDir, actionRoot, u
|
|
|
384
379
|
}
|
|
385
380
|
/** A `run:` step, executed under its declared shell with its declared env. */
|
|
386
381
|
async function runRun(step, label, scope, ctx) {
|
|
387
|
-
const shell = step.shell
|
|
382
|
+
const shell = step.shell === null || step.shell === undefined ? "bash" : String(step.shell);
|
|
388
383
|
if (shell !== "bash" && shell !== "sh") {
|
|
389
384
|
return err(`${label}: shell '${shell}' is not modelled`);
|
|
390
385
|
}
|
|
391
386
|
const script = renderTemplate(String(step.run), scope);
|
|
392
|
-
if (script
|
|
387
|
+
if (script === null) {
|
|
393
388
|
return err(`${label}: cannot resolve \${{ }} in run`);
|
|
394
389
|
}
|
|
395
390
|
const env = {
|
|
@@ -416,9 +411,9 @@ async function runRun(step, label, scope, ctx) {
|
|
|
416
411
|
Object.assign(env, rendered.v);
|
|
417
412
|
}
|
|
418
413
|
let cwd = ctx.tree;
|
|
419
|
-
if (step["working-directory"]
|
|
414
|
+
if (step["working-directory"] !== undefined && step["working-directory"] !== null) {
|
|
420
415
|
const wd = renderTemplate(String(step["working-directory"]), scope);
|
|
421
|
-
if (wd
|
|
416
|
+
if (wd === null) {
|
|
422
417
|
return err(`${label}: cannot resolve working-directory`);
|
|
423
418
|
}
|
|
424
419
|
cwd = resolve(ctx.tree, wd);
|
|
@@ -445,7 +440,7 @@ async function runRun(step, label, scope, ctx) {
|
|
|
445
440
|
return err(`${label}: exited ${r.code}${tail === "" ? "" : ` (${tail})`}`);
|
|
446
441
|
}
|
|
447
442
|
const outputs = parseGithubOutput(await readFile(outFile, "utf8"));
|
|
448
|
-
if (outputs
|
|
443
|
+
if (outputs === null) {
|
|
449
444
|
return err(`${label}: malformed GITHUB_OUTPUT`);
|
|
450
445
|
}
|
|
451
446
|
return { ok: true, v: outputs };
|
|
@@ -463,7 +458,10 @@ export function makeExecutor(opts) {
|
|
|
463
458
|
if (job.strategy !== undefined && job.strategy !== null) {
|
|
464
459
|
return fail(`job '${jobId}' has a strategy; not modelled`);
|
|
465
460
|
}
|
|
466
|
-
if (job.container
|
|
461
|
+
if (job.container !== null && job.container !== undefined) {
|
|
462
|
+
return fail(`job '${jobId}' uses a container or services; not modelled`);
|
|
463
|
+
}
|
|
464
|
+
if (job.services !== null && job.services !== undefined) {
|
|
467
465
|
return fail(`job '${jobId}' uses a container or services; not modelled`);
|
|
468
466
|
}
|
|
469
467
|
if (!Array.isArray(job.steps)) {
|
|
@@ -477,7 +475,7 @@ export function makeExecutor(opts) {
|
|
|
477
475
|
CHECKOUT_RE.test(s.uses) &&
|
|
478
476
|
Object.keys(s.with ?? {}).length > 0);
|
|
479
477
|
const tree = await deps.provideTree(workspace, { history: needsHistory });
|
|
480
|
-
if (tree
|
|
478
|
+
if (tree === null) {
|
|
481
479
|
return fail(`cannot materialize workspace ${workspace.owner}/${workspace.repo}@${workspace.sha}`);
|
|
482
480
|
}
|
|
483
481
|
const jobScope = { ...scope, github: { ...github, ...scope.github } };
|
|
@@ -496,7 +494,7 @@ export function makeExecutor(opts) {
|
|
|
496
494
|
const outputs = {};
|
|
497
495
|
for (const [name, raw] of Object.entries(job.outputs ?? {})) {
|
|
498
496
|
const rendered = renderTemplate(String(raw), outScope);
|
|
499
|
-
if (rendered
|
|
497
|
+
if (rendered === null) {
|
|
500
498
|
return fail(`cannot resolve job output '${name}'`);
|
|
501
499
|
}
|
|
502
500
|
outputs[name] = rendered;
|
|
@@ -609,7 +607,7 @@ async function cloneAt(source, remote, token, runCommand) {
|
|
|
609
607
|
}
|
|
610
608
|
async function materialize(source, download, runCommand) {
|
|
611
609
|
const bytes = await download(source);
|
|
612
|
-
if (bytes
|
|
610
|
+
if (bytes === null) {
|
|
613
611
|
return null;
|
|
614
612
|
}
|
|
615
613
|
const dir = await mkdtemp(join(tmpdir(), "willfire-tree-"));
|
package/dist/expr/cursor.js
CHANGED
package/dist/expr/indexVal.js
CHANGED
package/dist/expr/lookup.js
CHANGED
|
@@ -22,7 +22,7 @@ export function lookup(scope, path) {
|
|
|
22
22
|
return UNKNOWN;
|
|
23
23
|
}
|
|
24
24
|
const job = scope.needs?.[parts[0]];
|
|
25
|
-
if (job
|
|
25
|
+
if (job === undefined) {
|
|
26
26
|
return UNKNOWN;
|
|
27
27
|
}
|
|
28
28
|
// A known job's missing output is the empty string, not a hole: the
|
|
@@ -40,7 +40,7 @@ export function lookup(scope, path) {
|
|
|
40
40
|
return UNKNOWN;
|
|
41
41
|
}
|
|
42
42
|
const step = scope.steps?.[parts[0]];
|
|
43
|
-
if (step
|
|
43
|
+
if (step === undefined) {
|
|
44
44
|
return UNKNOWN;
|
|
45
45
|
}
|
|
46
46
|
return { kind: "value", v: step.outputs[parts[2]] ?? "" };
|
package/dist/expr/parseAtom.js
CHANGED
package/dist/expr/parseCall.js
CHANGED
|
@@ -12,13 +12,12 @@ export function parseCall(cur, scope, name) {
|
|
|
12
12
|
if (!cur.eatOp(")")) {
|
|
13
13
|
for (;;) {
|
|
14
14
|
args.push(parseOr(cur, scope));
|
|
15
|
-
if (cur.eatOp(",")) {
|
|
16
|
-
|
|
15
|
+
if (!cur.eatOp(",")) {
|
|
16
|
+
if (cur.eatOp(")")) {
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
return UNKNOWN; // malformed argument list
|
|
17
20
|
}
|
|
18
|
-
if (cur.eatOp(")")) {
|
|
19
|
-
break;
|
|
20
|
-
}
|
|
21
|
-
return UNKNOWN; // malformed argument list
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
23
|
return applyFunction(name.toLowerCase(), args);
|
package/dist/expr/tokenize.js
CHANGED
|
@@ -11,13 +11,13 @@ export function tokenize(src) {
|
|
|
11
11
|
const c = src[i];
|
|
12
12
|
if (c === " " || c === "\t" || c === "\n" || c === "\r") {
|
|
13
13
|
i++;
|
|
14
|
-
continue;
|
|
15
14
|
}
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
else if (c === "'") {
|
|
16
|
+
// Single-quoted string. GitHub escapes an inner quote by doubling it.
|
|
18
17
|
let j = i + 1;
|
|
19
18
|
let s = "";
|
|
20
|
-
|
|
19
|
+
let closed = false;
|
|
20
|
+
while (!closed) {
|
|
21
21
|
if (j >= src.length) {
|
|
22
22
|
return null; // unterminated
|
|
23
23
|
}
|
|
@@ -25,50 +25,57 @@ export function tokenize(src) {
|
|
|
25
25
|
if (src[j + 1] === "'") {
|
|
26
26
|
s += "'";
|
|
27
27
|
j += 2;
|
|
28
|
-
continue;
|
|
29
28
|
}
|
|
29
|
+
else {
|
|
30
|
+
j++;
|
|
31
|
+
closed = true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
s += src[j];
|
|
30
36
|
j++;
|
|
31
|
-
break;
|
|
32
37
|
}
|
|
33
|
-
s += src[j];
|
|
34
|
-
j++;
|
|
35
38
|
}
|
|
36
39
|
out.push({ t: "str", v: s });
|
|
37
40
|
i = j;
|
|
38
|
-
continue;
|
|
39
41
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
const word = /^[A-Za-z_][A-Za-z0-9_.\-]*/.exec(src.slice(i));
|
|
47
|
-
if (word != null) {
|
|
48
|
-
const w = word[0];
|
|
49
|
-
i += w.length;
|
|
50
|
-
const lower = w.toLowerCase();
|
|
51
|
-
if (lower === "true") {
|
|
52
|
-
out.push({ t: "bool", v: true });
|
|
53
|
-
}
|
|
54
|
-
else if (lower === "false") {
|
|
55
|
-
out.push({ t: "bool", v: false });
|
|
56
|
-
}
|
|
57
|
-
else if (lower === "null") {
|
|
58
|
-
out.push({ t: "null" });
|
|
42
|
+
else {
|
|
43
|
+
const op = OPS.find((o) => src.startsWith(o, i));
|
|
44
|
+
if (op !== undefined) {
|
|
45
|
+
out.push({ t: "op", v: op });
|
|
46
|
+
i += op.length;
|
|
59
47
|
}
|
|
60
48
|
else {
|
|
61
|
-
|
|
49
|
+
const word = /^[A-Za-z_][A-Za-z0-9_.\-]*/.exec(src.slice(i));
|
|
50
|
+
if (word !== null) {
|
|
51
|
+
const w = word[0];
|
|
52
|
+
i += w.length;
|
|
53
|
+
const lower = w.toLowerCase();
|
|
54
|
+
if (lower === "true") {
|
|
55
|
+
out.push({ t: "bool", v: true });
|
|
56
|
+
}
|
|
57
|
+
else if (lower === "false") {
|
|
58
|
+
out.push({ t: "bool", v: false });
|
|
59
|
+
}
|
|
60
|
+
else if (lower === "null") {
|
|
61
|
+
out.push({ t: "null" });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
out.push({ t: "path", v: w });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const num = /^-?\d+(\.\d+)?/.exec(src.slice(i));
|
|
69
|
+
if (num !== null) {
|
|
70
|
+
out.push({ t: "num", v: Number(num[0]) });
|
|
71
|
+
i += num[0].length;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
return null; // a character we have no token for
|
|
75
|
+
}
|
|
76
|
+
}
|
|
62
77
|
}
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
const num = /^-?\d+(\.\d+)?/.exec(src.slice(i));
|
|
66
|
-
if (num != null) {
|
|
67
|
-
out.push({ t: "num", v: Number(num[0]) });
|
|
68
|
-
i += num[0].length;
|
|
69
|
-
continue;
|
|
70
78
|
}
|
|
71
|
-
return null; // a character we have no token for
|
|
72
79
|
}
|
|
73
80
|
return out;
|
|
74
81
|
}
|
package/dist/jobs/evalIf.js
CHANGED
package/dist/jobs/expandJobs.js
CHANGED
|
@@ -37,15 +37,15 @@ function inputValue(raw, scope) {
|
|
|
37
37
|
/** The `on.workflow_call.inputs` block, tolerating the YAML 1.1 `on` -> true key. */
|
|
38
38
|
function workflowCallInputs(wf) {
|
|
39
39
|
const on = wf?.["on"] ?? wf?.["true"];
|
|
40
|
-
if (on
|
|
40
|
+
if (on === null || typeof on !== "object") {
|
|
41
41
|
return {};
|
|
42
42
|
}
|
|
43
43
|
const call = on["workflow_call"];
|
|
44
|
-
if (call
|
|
44
|
+
if (call === null || call === undefined) {
|
|
45
45
|
return {};
|
|
46
46
|
}
|
|
47
47
|
const inputs = call["inputs"];
|
|
48
|
-
return inputs
|
|
48
|
+
return inputs !== null && typeof inputs === "object" ? inputs : {};
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
51
|
* What `inputs.*` resolves to inside a called workflow: what the caller passed,
|
|
@@ -60,12 +60,12 @@ function calleeInputs(withBlock, subWf, scope) {
|
|
|
60
60
|
const out = {};
|
|
61
61
|
for (const [name, decl] of Object.entries(workflowCallInputs(subWf))) {
|
|
62
62
|
out[name] =
|
|
63
|
-
decl
|
|
63
|
+
decl !== null && typeof decl === "object" && "default" in decl
|
|
64
64
|
? // Defaults live in the callee, out of the caller's context's reach.
|
|
65
65
|
inputValue(decl["default"], {})
|
|
66
66
|
: UNKNOWN;
|
|
67
67
|
}
|
|
68
|
-
if (withBlock
|
|
68
|
+
if (withBlock !== null && typeof withBlock === "object") {
|
|
69
69
|
for (const [name, raw] of Object.entries(withBlock)) {
|
|
70
70
|
out[name] = inputValue(raw, scope);
|
|
71
71
|
}
|
|
@@ -108,7 +108,7 @@ export async function expandJobs(wf, ctx, reader, source, depth = 0, prefix = ""
|
|
|
108
108
|
// verdict the main loop applies.
|
|
109
109
|
let scoped = scope;
|
|
110
110
|
const execFailures = {};
|
|
111
|
-
if (executor
|
|
111
|
+
if (executor !== undefined) {
|
|
112
112
|
const needed = neededJobIds(jobs);
|
|
113
113
|
for (const [jobId, jobRaw] of Object.entries(jobs)) {
|
|
114
114
|
const job = jobRaw ?? {};
|
|
@@ -127,12 +127,12 @@ export async function expandJobs(wf, ctx, reader, source, depth = 0, prefix = ""
|
|
|
127
127
|
}
|
|
128
128
|
const execNote = (needs) => {
|
|
129
129
|
const failed = needs.find((n) => n in execFailures);
|
|
130
|
-
return failed
|
|
130
|
+
return failed === undefined ? "" : `; executing '${failed}' failed: ${execFailures[failed]}`;
|
|
131
131
|
};
|
|
132
132
|
for (const [jobId, jobRaw] of Object.entries(jobs)) {
|
|
133
133
|
const job = jobRaw ?? {};
|
|
134
134
|
let status = evalIf(job.if, scoped);
|
|
135
|
-
let reason = job.if
|
|
135
|
+
let reason = job.if !== undefined && job.if !== null ? `if: ${JSON.stringify(job.if)}` : "";
|
|
136
136
|
let needs = job.needs ?? [];
|
|
137
137
|
if (typeof needs === "string") {
|
|
138
138
|
needs = [needs];
|
|
@@ -162,113 +162,122 @@ export async function expandJobs(wf, ctx, reader, source, depth = 0, prefix = ""
|
|
|
162
162
|
status,
|
|
163
163
|
reason,
|
|
164
164
|
});
|
|
165
|
-
continue;
|
|
166
165
|
}
|
|
167
|
-
|
|
168
|
-
if ("uses" in job) {
|
|
166
|
+
else if ("uses" in job) {
|
|
169
167
|
// Reusable workflow call. The calling job produces no check of its own;
|
|
170
168
|
// each called job becomes `<calling job name> / <called job name>`, and
|
|
171
169
|
// a matrix on the *caller* multiplies the whole callee set. A cross-repo
|
|
172
170
|
// call names its checks exactly the same way a local one does — probe
|
|
173
171
|
// PR #9, `call-remote-tag / r-inner` alongside `call-plain / inner`.
|
|
172
|
+
const combos = expandMatrixDetailed(job.strategy, prScope(scoped));
|
|
174
173
|
const uses = job.uses;
|
|
175
|
-
if (combos
|
|
174
|
+
if (combos === null) {
|
|
176
175
|
entries.push({
|
|
177
176
|
job: prefix + jobId,
|
|
178
177
|
checkName: null,
|
|
179
178
|
status: "unknown",
|
|
180
179
|
reason: "dynamic matrix on reusable workflow call" + execNote(needs),
|
|
181
180
|
});
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
// Resolve the called workflow once, not once per matrix combination.
|
|
185
|
-
let subWf = null;
|
|
186
|
-
let failure = null;
|
|
187
|
-
// Where the callee's own `./` calls will resolve. A remote `uses:` moves
|
|
188
|
-
// this to the callee's repo and pinned ref; a local one leaves it alone.
|
|
189
|
-
let subSource = source;
|
|
190
|
-
// What `inputs.*` means on the other side of the call.
|
|
191
|
-
let subScope = {};
|
|
192
|
-
const target = parseUses(uses);
|
|
193
|
-
if (depth + 1 > MAX_REUSABLE_DEPTH) {
|
|
194
|
-
failure = `reusable workflow nested deeper than ${MAX_REUSABLE_DEPTH} levels`;
|
|
195
|
-
}
|
|
196
|
-
else if (target == null) {
|
|
197
|
-
failure = `unresolvable reusable reference: ${uses}`;
|
|
198
181
|
}
|
|
199
182
|
else {
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
183
|
+
// Resolve the called workflow once, not once per matrix combination.
|
|
184
|
+
let subWf = null;
|
|
185
|
+
let failure = null;
|
|
186
|
+
// Where the callee's own `./` calls will resolve. A remote `uses:`
|
|
187
|
+
// moves this to the callee's repo and pinned ref; a local one leaves
|
|
188
|
+
// it alone.
|
|
189
|
+
let subSource = source;
|
|
190
|
+
// What `inputs.*` means on the other side of the call.
|
|
191
|
+
let subScope = {};
|
|
192
|
+
const target = parseUses(uses);
|
|
193
|
+
if (depth + 1 > MAX_REUSABLE_DEPTH) {
|
|
194
|
+
failure = `reusable workflow nested deeper than ${MAX_REUSABLE_DEPTH} levels`;
|
|
210
195
|
}
|
|
211
|
-
if (
|
|
212
|
-
failure = `
|
|
196
|
+
else if (target === null) {
|
|
197
|
+
failure = `unresolvable reusable reference: ${uses}`;
|
|
213
198
|
}
|
|
214
199
|
else {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
200
|
+
// A local `./` call stays on the caller's source, which is already
|
|
201
|
+
// pinned to a commit. A cross-repo one arrives as whatever the
|
|
202
|
+
// `uses:` string spelled — `@v0` — and has to be resolved before
|
|
203
|
+
// anything is read from it, so the file that gets read and the
|
|
204
|
+
// commit the prediction names are the same one.
|
|
205
|
+
let resolved = source;
|
|
206
|
+
if (target.source !== null) {
|
|
207
|
+
const { ref } = target.source;
|
|
208
|
+
const sha = isSha(ref) ? ref : await reader.resolveRef(target.source);
|
|
209
|
+
resolved = sha === null ? null : { ...target.source, sha };
|
|
210
|
+
}
|
|
211
|
+
if (resolved === null) {
|
|
212
|
+
failure = `cannot resolve ref for ${uses}`;
|
|
219
213
|
}
|
|
220
214
|
else {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
// at the top of the prediction stay true all the way down.
|
|
226
|
-
subScope = {
|
|
227
|
-
inputs: calleeInputs(job.with, subWf ?? {}, scoped),
|
|
228
|
-
github: scoped.github,
|
|
229
|
-
};
|
|
215
|
+
subSource = resolved;
|
|
216
|
+
const content = await reader.fetchWorkflow(target.path, subSource);
|
|
217
|
+
if (content === null) {
|
|
218
|
+
failure = `cannot fetch ${uses}`;
|
|
230
219
|
}
|
|
231
|
-
|
|
232
|
-
|
|
220
|
+
else {
|
|
221
|
+
try {
|
|
222
|
+
const parsed = parseYaml(content);
|
|
223
|
+
// `inputs.*` changes at the call boundary; `github.*` does
|
|
224
|
+
// not. A callee's jobs run in the caller's repo, so the facts
|
|
225
|
+
// seeded at the top of the prediction stay true all the way
|
|
226
|
+
// down.
|
|
227
|
+
subScope = {
|
|
228
|
+
inputs: calleeInputs(job.with, parsed ?? {}, scoped),
|
|
229
|
+
github: scoped.github,
|
|
230
|
+
};
|
|
231
|
+
// Assigned last, so a throw above leaves it null and the one
|
|
232
|
+
// check below covers every way the call failed to resolve.
|
|
233
|
+
subWf = parsed;
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
failure = `YAML parse error in ${uses}: ${e}`;
|
|
237
|
+
}
|
|
233
238
|
}
|
|
234
239
|
}
|
|
235
240
|
}
|
|
241
|
+
for (const combo of combos) {
|
|
242
|
+
const disp = jobDisplayName(jobId, job, combo);
|
|
243
|
+
const baseName = prefix + disp.name;
|
|
244
|
+
const nameResolved = prefixResolved && disp.resolved;
|
|
245
|
+
if (subWf === null) {
|
|
246
|
+
entries.push({
|
|
247
|
+
job: baseName,
|
|
248
|
+
checkName: null,
|
|
249
|
+
status: "unknown",
|
|
250
|
+
reason: failure ?? `cannot resolve ${uses}`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
entries.push(...(await expandJobs(subWf, ctx, reader, subSource, depth + 1, `${baseName} / `, nameResolved, subScope, executor)));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const combos = expandMatrixDetailed(job.strategy, prScope(scoped));
|
|
261
|
+
if (combos === null) {
|
|
262
|
+
entries.push({
|
|
263
|
+
job: prefix + jobId,
|
|
264
|
+
checkName: null,
|
|
265
|
+
status: "unknown",
|
|
266
|
+
reason: "dynamic matrix" + execNote(needs),
|
|
267
|
+
});
|
|
236
268
|
}
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (failure != null || subWf == null) {
|
|
269
|
+
else {
|
|
270
|
+
for (const combo of combos) {
|
|
271
|
+
const disp = jobDisplayName(jobId, job, combo);
|
|
272
|
+
const name = prefix + disp.name;
|
|
242
273
|
entries.push({
|
|
243
|
-
job:
|
|
244
|
-
checkName: null,
|
|
245
|
-
status
|
|
246
|
-
reason
|
|
274
|
+
job: name,
|
|
275
|
+
checkName: prefixResolved && disp.resolved ? name : null,
|
|
276
|
+
status,
|
|
277
|
+
reason,
|
|
247
278
|
});
|
|
248
|
-
continue;
|
|
249
279
|
}
|
|
250
|
-
entries.push(...(await expandJobs(subWf, ctx, reader, subSource, depth + 1, `${baseName} / `, nameResolved, subScope, executor)));
|
|
251
280
|
}
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (combos == null) {
|
|
255
|
-
entries.push({
|
|
256
|
-
job: prefix + jobId,
|
|
257
|
-
checkName: null,
|
|
258
|
-
status: "unknown",
|
|
259
|
-
reason: "dynamic matrix" + execNote(needs),
|
|
260
|
-
});
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
for (const combo of combos) {
|
|
264
|
-
const disp = jobDisplayName(jobId, job, combo);
|
|
265
|
-
const name = prefix + disp.name;
|
|
266
|
-
entries.push({
|
|
267
|
-
job: name,
|
|
268
|
-
checkName: prefixResolved && disp.resolved ? name : null,
|
|
269
|
-
status,
|
|
270
|
-
reason,
|
|
271
|
-
});
|
|
272
281
|
}
|
|
273
282
|
}
|
|
274
283
|
return entries;
|
|
@@ -2,5 +2,5 @@ import { expandMatrixDetailed } from "./expandMatrixDetailed.js";
|
|
|
2
2
|
/** Return list of matrix combination dicts, or null if dynamic. */
|
|
3
3
|
export function expandMatrix(strategy, scope = {}) {
|
|
4
4
|
const detailed = expandMatrixDetailed(strategy, scope);
|
|
5
|
-
return detailed
|
|
5
|
+
return detailed === null ? null : detailed.map((c) => (c === null ? null : c.values));
|
|
6
6
|
}
|
|
@@ -43,7 +43,7 @@ function comboList(v, scope) {
|
|
|
43
43
|
}
|
|
44
44
|
export function expandMatrixDetailed(strategy, scope = {}) {
|
|
45
45
|
const matrix = strategy?.matrix;
|
|
46
|
-
if (matrix
|
|
46
|
+
if (matrix === null || matrix === undefined) {
|
|
47
47
|
return [null];
|
|
48
48
|
}
|
|
49
49
|
// `matrix: ${{ ... }}` — the whole matrix as one expression, rather than the
|
|
@@ -59,14 +59,13 @@ export function expandMatrixDetailed(strategy, scope = {}) {
|
|
|
59
59
|
}
|
|
60
60
|
const axes = {};
|
|
61
61
|
for (const [k, v] of Object.entries(matrix)) {
|
|
62
|
-
if (k
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
if (k !== "include" && k !== "exclude") {
|
|
63
|
+
const vals = axisValues(v, scope);
|
|
64
|
+
if (vals === null) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
axes[k] = vals;
|
|
68
68
|
}
|
|
69
|
-
axes[k] = vals;
|
|
70
69
|
}
|
|
71
70
|
const axisKeys = Object.keys(axes);
|
|
72
71
|
let combos = [{ values: {}, displayKeys: axisKeys }];
|
|
@@ -16,7 +16,7 @@ import { renderName } from "./renderName.js";
|
|
|
16
16
|
export const EXPRESSION_RE = /\$\{\{/;
|
|
17
17
|
/** The check name for one job/combination. */
|
|
18
18
|
export function jobDisplayName(jobId, job, combo) {
|
|
19
|
-
const raw = job
|
|
19
|
+
const raw = job.name !== undefined && job.name !== null ? String(job.name) : null;
|
|
20
20
|
if (raw === null) {
|
|
21
21
|
return { name: capDisplayName(jobId + (combo ? matrixSuffix(combo) : "")), resolved: true };
|
|
22
22
|
}
|
|
@@ -12,6 +12,6 @@ import { capDisplayName } from "./capDisplayName.js";
|
|
|
12
12
|
* caller, with no `/ <callee job>` entries.
|
|
13
13
|
*/
|
|
14
14
|
export function skippedDisplayName(jobId, job) {
|
|
15
|
-
const raw = job
|
|
15
|
+
const raw = job.name !== undefined && job.name !== null ? String(job.name) : null;
|
|
16
16
|
return { name: capDisplayName(raw ?? jobId), resolved: true };
|
|
17
17
|
}
|
package/dist/predict/predict.js
CHANGED
|
@@ -35,10 +35,18 @@ export async function predict(github, repo, prNumber, opts = {}) {
|
|
|
35
35
|
};
|
|
36
36
|
const headSha = pr.head.sha;
|
|
37
37
|
/**
|
|
38
|
-
* The PR's own repo at the head commit —
|
|
39
|
-
* a commit id, so its `ref` and `sha` are the same
|
|
38
|
+
* The PR's own repo at the head commit — the surface the skip instruction is
|
|
39
|
+
* read from, and already a commit id, so its `ref` and `sha` are the same
|
|
40
|
+
* string.
|
|
40
41
|
*/
|
|
41
42
|
const headSource = { owner, repo: name, ref: headSha, sha: headSha };
|
|
43
|
+
/**
|
|
44
|
+
* GitHub evaluates a `pull_request` at the test merge, not the head (#105).
|
|
45
|
+
* Null when it has none: a conflict, or a merge not computed yet.
|
|
46
|
+
*/
|
|
47
|
+
const mergeSha = pr.merge_commit_sha;
|
|
48
|
+
const readSource = mergeSha === null ? headSource : { owner, repo: name, ref: mergeSha, sha: mergeSha };
|
|
49
|
+
const readLabel = readSource.sha === headSha ? "head" : "the test merge commit";
|
|
42
50
|
// Provenance for the answer, filled as expansion reaches each source. The head
|
|
43
51
|
// is in from the start: it is read even on the skip path, where the commit
|
|
44
52
|
// message is what decides the verdict.
|
|
@@ -51,6 +59,7 @@ export async function predict(github, repo, prNumber, opts = {}) {
|
|
|
51
59
|
if (SKIP_RE.test(headMsg) || SKIP_TRAILER_RE.test(headMsg)) {
|
|
52
60
|
return finalizePrediction([], "head commit message contains a skip instruction", sources);
|
|
53
61
|
}
|
|
62
|
+
sources.set(sourceKey(readSource), readSource);
|
|
54
63
|
// A `uses:` naming a tag is the same lookup from every caller that writes it,
|
|
55
64
|
// so resolve each `owner/repo@ref` once. Misses are cached too: a ref that
|
|
56
65
|
// cannot be resolved will not start resolving on the second ask.
|
|
@@ -115,7 +124,7 @@ export async function predict(github, repo, prNumber, opts = {}) {
|
|
|
115
124
|
const reader = { fetchWorkflow, resolveRef };
|
|
116
125
|
// Execution is on by default and costs nothing until a workflow needs it.
|
|
117
126
|
const executor = opts.executor === undefined
|
|
118
|
-
? makeLiveExecutor(github,
|
|
127
|
+
? makeLiveExecutor(github, readSource, resolveRef)
|
|
119
128
|
: (opts.executor ?? undefined);
|
|
120
129
|
const workflows = await github.paginate(github.rest.actions.listRepoWorkflows, {
|
|
121
130
|
...base,
|
|
@@ -134,13 +143,18 @@ export async function predict(github, repo, prNumber, opts = {}) {
|
|
|
134
143
|
{ workflow: path, job: "*", status: "no-dispatch", reason: `workflow state: ${state}` },
|
|
135
144
|
];
|
|
136
145
|
}
|
|
137
|
-
const content = await fetchWorkflow(path,
|
|
146
|
+
const content = await fetchWorkflow(path, readSource);
|
|
138
147
|
if (content === null) {
|
|
139
148
|
// The Actions API keeps listing a workflow as `active` after its file is
|
|
140
|
-
// deleted. There is no file
|
|
149
|
+
// deleted. There is no file to evaluate, so there is nothing to dispatch —
|
|
141
150
|
// the same verdict as the disabled case above, reached a different way.
|
|
142
151
|
return [
|
|
143
|
-
{
|
|
152
|
+
{
|
|
153
|
+
workflow: path,
|
|
154
|
+
job: "*",
|
|
155
|
+
status: "no-dispatch",
|
|
156
|
+
reason: `no workflow file at ${readLabel}`,
|
|
157
|
+
},
|
|
144
158
|
];
|
|
145
159
|
}
|
|
146
160
|
let wf;
|
|
@@ -157,7 +171,7 @@ export async function predict(github, repo, prNumber, opts = {}) {
|
|
|
157
171
|
if (!dispatches) {
|
|
158
172
|
return [{ workflow: path, job: "*", status: "no-dispatch", reason }];
|
|
159
173
|
}
|
|
160
|
-
const jobs = await expandJobs(wf, ctx, reader,
|
|
174
|
+
const jobs = await expandJobs(wf, ctx, reader, readSource, 0, "", true, prFacts, executor);
|
|
161
175
|
return jobs.map((j) => ({
|
|
162
176
|
workflow: path,
|
|
163
177
|
job: jobName(j.job),
|
|
@@ -22,13 +22,13 @@ export function workflowDispatches(wf, ctx) {
|
|
|
22
22
|
}
|
|
23
23
|
const branchRef = ctx.stackTarget ?? ctx.baseRef;
|
|
24
24
|
if ("branches" in trig && !matchFilters(branchRef, trig["branches"])) {
|
|
25
|
-
const label = ctx.stackTarget
|
|
25
|
+
const label = ctx.stackTarget === undefined ? "base branch" : "stack target";
|
|
26
26
|
return [false, `${label} '${branchRef}' not in branches`];
|
|
27
27
|
}
|
|
28
28
|
if ("branches-ignore" in trig && matchFilters(branchRef, trig["branches-ignore"])) {
|
|
29
29
|
return [
|
|
30
30
|
false,
|
|
31
|
-
ctx.stackTarget
|
|
31
|
+
ctx.stackTarget === undefined
|
|
32
32
|
? "base branch in branches-ignore"
|
|
33
33
|
: `stack target '${branchRef}' in branches-ignore`,
|
|
34
34
|
];
|
package/dist/verify.js
CHANGED
|
@@ -50,9 +50,9 @@ const { entries: predictedRaw } = await predict(octokit, repo, pr);
|
|
|
50
50
|
// no key to compare and are reported separately below.
|
|
51
51
|
const predicted = new Map(predictedRaw
|
|
52
52
|
.filter(isJobEntry)
|
|
53
|
-
.filter((r) => r.checkName
|
|
53
|
+
.filter((r) => r.checkName !== null)
|
|
54
54
|
.map((r) => [`${r.workflow} :: ${r.checkName}`, r.status]));
|
|
55
|
-
const unresolved = predictedRaw.filter(isJobEntry).filter((r) => r.checkName
|
|
55
|
+
const unresolved = predictedRaw.filter(isJobEntry).filter((r) => r.checkName === null);
|
|
56
56
|
const unknownWfs = new Set(predictedRaw
|
|
57
57
|
.filter((r) => r.status === "unknown")
|
|
58
58
|
.map((r) => r.workflow)
|