willfire 0.1.11 → 0.1.12
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/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/parseArgs.d.ts +9 -0
- package/dist/cli/parseArgs.js +42 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +41 -0
- package/dist/entries/index.d.ts +3 -0
- package/dist/entries/index.js +3 -0
- package/dist/entries/isJobEntry.d.ts +3 -0
- package/dist/entries/isJobEntry.js +2 -0
- package/dist/entries/isWorkflowEntry.d.ts +3 -0
- package/dist/entries/isWorkflowEntry.js +2 -0
- package/dist/entries/jobName.d.ts +3 -0
- package/dist/entries/jobName.js +2 -0
- package/dist/execute.d.ts +1 -1
- package/dist/filters/index.d.ts +2 -0
- package/dist/filters/index.js +2 -0
- package/dist/filters/matchFilters.d.ts +2 -0
- package/dist/filters/matchFilters.js +12 -0
- package/dist/filters/patternToRegex.d.ts +1 -0
- package/dist/filters/patternToRegex.js +35 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/jobs/evalIf.d.ts +10 -0
- package/dist/jobs/evalIf.js +18 -0
- package/dist/jobs/expandJobs.d.ts +4 -0
- package/dist/jobs/expandJobs.js +247 -0
- package/dist/jobs/expandWorkflowJobs.d.ts +14 -0
- package/dist/jobs/expandWorkflowJobs.js +14 -0
- package/dist/jobs/index.d.ts +4 -0
- package/dist/jobs/index.js +4 -0
- package/dist/jobs/prScope.d.ts +10 -0
- package/dist/jobs/prScope.js +19 -0
- package/dist/matrix/expandMatrix.d.ts +4 -0
- package/dist/matrix/expandMatrix.js +6 -0
- package/dist/matrix/expandMatrixDetailed.d.ts +3 -0
- package/dist/matrix/expandMatrixDetailed.js +74 -0
- package/dist/matrix/formatMatrixValue.d.ts +8 -0
- package/dist/matrix/formatMatrixValue.js +16 -0
- package/dist/matrix/index.d.ts +4 -0
- package/dist/matrix/index.js +4 -0
- package/dist/matrix/matrixSuffix.d.ts +3 -0
- package/dist/matrix/matrixSuffix.js +8 -0
- package/dist/names/index.d.ts +4 -0
- package/dist/names/index.js +4 -0
- package/dist/names/jobDisplayName.d.ts +16 -0
- package/dist/names/jobDisplayName.js +25 -0
- package/dist/names/lookupPath.d.ts +1 -0
- package/dist/names/lookupPath.js +9 -0
- package/dist/names/renderName.d.ts +2 -0
- package/dist/names/renderName.js +26 -0
- package/dist/names/skippedDisplayName.d.ts +14 -0
- package/dist/names/skippedDisplayName.js +16 -0
- package/dist/predict/finalizePrediction.d.ts +2 -0
- package/dist/predict/finalizePrediction.js +19 -0
- package/dist/predict/index.d.ts +5 -0
- package/dist/predict/index.js +5 -0
- package/dist/predict/makeOctokit.d.ts +2 -0
- package/dist/predict/makeOctokit.js +7 -0
- package/dist/predict/predict.d.ts +3 -0
- package/dist/predict/predict.js +214 -0
- package/dist/predict/sourceKey.d.ts +3 -0
- package/dist/predict/sourceKey.js +2 -0
- package/dist/predict/stackTargetRef.d.ts +14 -0
- package/dist/predict/stackTargetRef.js +58 -0
- package/dist/triggers/getPrTrigger.d.ts +3 -0
- package/dist/triggers/getPrTrigger.js +18 -0
- package/dist/triggers/index.d.ts +2 -0
- package/dist/triggers/index.js +2 -0
- package/dist/triggers/workflowDispatches.d.ts +2 -0
- package/dist/triggers/workflowDispatches.js +45 -0
- package/dist/{predict.d.ts → types.d.ts} +64 -53
- package/dist/types.js +1 -0
- package/dist/uses/index.d.ts +1 -0
- package/dist/uses/index.js +1 -0
- package/dist/uses/parseUses.d.ts +15 -0
- package/dist/uses/parseUses.js +33 -0
- package/dist/verify.js +1 -1
- package/package.json +7 -7
- package/dist/predict.js +0 -912
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { parseArgs } from "./parseArgs.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { parseArgs } from "./parseArgs.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ExecutionGrant } from "../execute.js";
|
|
2
|
+
import type { PrEventAction } from "../types.js";
|
|
3
|
+
export declare function parseArgs(argv: string[]): {
|
|
4
|
+
repo: string;
|
|
5
|
+
pr: number;
|
|
6
|
+
json: boolean;
|
|
7
|
+
action?: PrEventAction;
|
|
8
|
+
execute: ExecutionGrant[];
|
|
9
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { parseGrant } from "../execute.js";
|
|
2
|
+
const USAGE = "usage: predict --repo owner/name --pr N [--action opened|synchronize|reopened]" +
|
|
3
|
+
" [--execute owner/repo:job1,job2]... [--json]";
|
|
4
|
+
const isPrEventAction = (v) => v === "opened" || v === "synchronize" || v === "reopened";
|
|
5
|
+
export function parseArgs(argv) {
|
|
6
|
+
const get = (flag) => {
|
|
7
|
+
const i = argv.indexOf(flag);
|
|
8
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
9
|
+
};
|
|
10
|
+
const repo = get("--repo");
|
|
11
|
+
const pr = get("--pr");
|
|
12
|
+
if (!repo || !pr) {
|
|
13
|
+
console.error(USAGE);
|
|
14
|
+
process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
// An unrecognised action is refused rather than ignored. Silently falling
|
|
17
|
+
// back to the guess would turn a typo into a wrong prediction, which is the
|
|
18
|
+
// failure this flag exists to remove.
|
|
19
|
+
const action = get("--action");
|
|
20
|
+
if (action !== undefined && !isPrEventAction(action)) {
|
|
21
|
+
console.error(`unknown --action: ${action}`);
|
|
22
|
+
console.error(USAGE);
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
// Repeatable, one grant per flag. A malformed grant is refused for the same
|
|
26
|
+
// reason a bad --action is: silently dropping it would predict without the
|
|
27
|
+
// execution the caller thought they asked for.
|
|
28
|
+
const execute = [];
|
|
29
|
+
for (let i = 0; i < argv.length; i++) {
|
|
30
|
+
if (argv[i] !== "--execute")
|
|
31
|
+
continue;
|
|
32
|
+
const spec = argv[i + 1];
|
|
33
|
+
const grant = spec == null ? null : parseGrant(spec);
|
|
34
|
+
if (grant == null) {
|
|
35
|
+
console.error(`bad --execute: ${spec}`);
|
|
36
|
+
console.error(USAGE);
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
execute.push(grant);
|
|
40
|
+
}
|
|
41
|
+
return { repo, pr: Number(pr), json: argv.includes("--json"), action, execute };
|
|
42
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Usage: pnpm predict --repo owner/name --pr N [--json]
|
|
3
|
+
//
|
|
4
|
+
// Auth: GH_TOKEN or GITHUB_TOKEN env var (any token with
|
|
5
|
+
// contents/actions/pull-requests read). Inside an action, pass the workflow's
|
|
6
|
+
// GITHUB_TOKEN.
|
|
7
|
+
import { parseArgs } from "./cli/parseArgs.js";
|
|
8
|
+
import { isWorkflowEntry } from "./entries/isWorkflowEntry.js";
|
|
9
|
+
import { makeOctokit } from "./predict/makeOctokit.js";
|
|
10
|
+
import { predict } from "./predict/predict.js";
|
|
11
|
+
const isMain = /cli\.(ts|js)$|\/willfire$/.test(process.argv[1] ?? "");
|
|
12
|
+
if (isMain) {
|
|
13
|
+
const args = parseArgs(process.argv.slice(2));
|
|
14
|
+
const prediction = await predict(makeOctokit(), args.repo, args.pr, {
|
|
15
|
+
action: args.action,
|
|
16
|
+
execute: args.execute,
|
|
17
|
+
});
|
|
18
|
+
const { entries, skip, sources } = prediction;
|
|
19
|
+
if (args.json) {
|
|
20
|
+
console.log(JSON.stringify(prediction, null, 2));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
if (skip) {
|
|
24
|
+
console.log(`# ${skip} -> nothing dispatches`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
for (const e of entries) {
|
|
28
|
+
if (isWorkflowEntry(e))
|
|
29
|
+
console.log(`# ${e.workflow} :: ${e.status} (${e.reason})`);
|
|
30
|
+
else {
|
|
31
|
+
const name = e.checkName ?? `${e.job} (name unresolved)`;
|
|
32
|
+
console.log(`${e.workflow} :: ${name} :: ${e.status}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Last, and on the skip path too, so a red gate's first question — which
|
|
37
|
+
// commits was this read from? — is answered wherever the reader lands.
|
|
38
|
+
for (const s of sources)
|
|
39
|
+
console.log(`# read ${s.owner}/${s.repo}@${s.ref} -> ${s.sha}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
package/dist/execute.d.ts
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* recorded as done; a checkout *with inputs* is not modelled and stops.
|
|
31
31
|
*/
|
|
32
32
|
import { type Scope } from "./expr.js";
|
|
33
|
-
import type { ResolveRef, WorkflowSource } from "./
|
|
33
|
+
import type { ResolveRef, WorkflowSource } from "./types.js";
|
|
34
34
|
/**
|
|
35
35
|
* Permission to execute named jobs from one repo's workflows.
|
|
36
36
|
*
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { patternToRegex } from "./patternToRegex.js";
|
|
2
|
+
/** Order-sensitive match: last matching pattern wins; ! negates. */
|
|
3
|
+
export function matchFilters(value, patterns) {
|
|
4
|
+
let matched = false;
|
|
5
|
+
for (const pat of patterns) {
|
|
6
|
+
const neg = pat.startsWith("!");
|
|
7
|
+
const p = neg ? pat.slice(1) : pat;
|
|
8
|
+
if (patternToRegex(p).test(value))
|
|
9
|
+
matched = !neg;
|
|
10
|
+
}
|
|
11
|
+
return matched;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function patternToRegex(pat: string): RegExp;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// GitHub filter pattern glob. Grammar per docs: * (any chars except /),
|
|
2
|
+
// ** (any chars), ? (zero or one of preceding char), + (one or more of
|
|
3
|
+
// preceding char), [ranges], leading ! negates.
|
|
4
|
+
const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5
|
+
export function patternToRegex(pat) {
|
|
6
|
+
let out = "";
|
|
7
|
+
for (let i = 0; i < pat.length; i++) {
|
|
8
|
+
const c = pat[i];
|
|
9
|
+
if (c === "*") {
|
|
10
|
+
if (pat[i + 1] === "*") {
|
|
11
|
+
out += ".*";
|
|
12
|
+
i++;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
out += "[^/]*";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
else if (c === "?" || c === "+") {
|
|
19
|
+
out += c;
|
|
20
|
+
}
|
|
21
|
+
else if (c === "[") {
|
|
22
|
+
const j = pat.indexOf("]", i + 1);
|
|
23
|
+
out += pat.slice(i, j + 1);
|
|
24
|
+
i = j;
|
|
25
|
+
}
|
|
26
|
+
else if (c === "\\") {
|
|
27
|
+
i++;
|
|
28
|
+
out += escapeRegex(pat[i]);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
out += escapeRegex(c);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return new RegExp(`^${out}$`);
|
|
35
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { jobName, isWorkflowEntry, isJobEntry } from "./entries/index.js";
|
|
2
|
+
export { patternToRegex, matchFilters } from "./filters/index.js";
|
|
3
|
+
export { expandMatrix } from "./matrix/index.js";
|
|
4
|
+
export { evalIf, expandWorkflowJobs } from "./jobs/index.js";
|
|
5
|
+
export { parseUses } from "./uses/index.js";
|
|
6
|
+
export { makeOctokit, predict } from "./predict/index.js";
|
|
7
|
+
export type { JobName, WorkflowEntry, JobEntry, Entry, Prediction, PrEventAction, PredictOptions, Ctx, ExpandedJob, SourceRef, WorkflowSource, FetchWorkflow, ResolveRef, WorkflowReader, UsesTarget, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { jobName, isWorkflowEntry, isJobEntry } from "./entries/index.js";
|
|
2
|
+
export { patternToRegex, matchFilters } from "./filters/index.js";
|
|
3
|
+
export { expandMatrix } from "./matrix/index.js";
|
|
4
|
+
export { evalIf, expandWorkflowJobs } from "./jobs/index.js";
|
|
5
|
+
export { parseUses } from "./uses/index.js";
|
|
6
|
+
export { makeOctokit, predict } from "./predict/index.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Scope } from "../expr.js";
|
|
2
|
+
/**
|
|
3
|
+
* Return run|skipped|unknown for a job-level `if:`.
|
|
4
|
+
*
|
|
5
|
+
* `scope` carries the inputs the calling workflow passed down, and any job
|
|
6
|
+
* outputs the caller knows. Without it a reusable workflow's guards are all
|
|
7
|
+
* unknown, because every one of them is written against `inputs.*` or
|
|
8
|
+
* `needs.*`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function evalIf(cond: any, scope?: Scope): "run" | "skipped" | "unknown";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { evaluate } from "../expr.js";
|
|
2
|
+
import { prScope } from "./prScope.js";
|
|
3
|
+
/**
|
|
4
|
+
* Return run|skipped|unknown for a job-level `if:`.
|
|
5
|
+
*
|
|
6
|
+
* `scope` carries the inputs the calling workflow passed down, and any job
|
|
7
|
+
* outputs the caller knows. Without it a reusable workflow's guards are all
|
|
8
|
+
* unknown, because every one of them is written against `inputs.*` or
|
|
9
|
+
* `needs.*`.
|
|
10
|
+
*/
|
|
11
|
+
export function evalIf(cond, scope = {}) {
|
|
12
|
+
if (cond == null)
|
|
13
|
+
return "run";
|
|
14
|
+
const verdict = evaluate(String(cond), prScope(scope));
|
|
15
|
+
if (verdict === null)
|
|
16
|
+
return "unknown";
|
|
17
|
+
return verdict ? "run" : "skipped";
|
|
18
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type Scope } from "../expr.js";
|
|
2
|
+
import type { JobExecutor } from "../execute.js";
|
|
3
|
+
import type { Ctx, ExpandedJob, Workflow, WorkflowReader, WorkflowSource } from "../types.js";
|
|
4
|
+
export declare function expandJobs(wf: Workflow, ctx: Ctx, reader: WorkflowReader, source: WorkflowSource, depth?: number, prefix?: string, prefixResolved?: boolean, scope?: Scope, executor?: JobExecutor): Promise<ExpandedJob[]>;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { parse as parseYaml } from "yaml";
|
|
2
|
+
import { UNKNOWN } from "../expr.js";
|
|
3
|
+
import { expandMatrixDetailed } from "../matrix/expandMatrixDetailed.js";
|
|
4
|
+
import { jobDisplayName } from "../names/jobDisplayName.js";
|
|
5
|
+
import { skippedDisplayName } from "../names/skippedDisplayName.js";
|
|
6
|
+
import { parseUses } from "../uses/parseUses.js";
|
|
7
|
+
import { evalIf } from "./evalIf.js";
|
|
8
|
+
import { prScope } from "./prScope.js";
|
|
9
|
+
/**
|
|
10
|
+
* A `with:` value as the callee will see it.
|
|
11
|
+
*
|
|
12
|
+
* A value carrying `${{ }}` is left unknown rather than evaluated. Resolving
|
|
13
|
+
* it would mean evaluating the caller's own expression context, and every
|
|
14
|
+
* caller in practice passes plain literals — so the reach that would buy is
|
|
15
|
+
* not worth the surface. Unknown here is the same unknown as before this
|
|
16
|
+
* existed; nothing regresses.
|
|
17
|
+
*/
|
|
18
|
+
function inputLiteral(raw) {
|
|
19
|
+
if (raw == null)
|
|
20
|
+
return { kind: "value", v: "" };
|
|
21
|
+
if (typeof raw === "boolean" || typeof raw === "number")
|
|
22
|
+
return { kind: "value", v: raw };
|
|
23
|
+
if (typeof raw === "string")
|
|
24
|
+
return raw.includes("${{") ? UNKNOWN : { kind: "value", v: raw };
|
|
25
|
+
return UNKNOWN;
|
|
26
|
+
}
|
|
27
|
+
/** The `on.workflow_call.inputs` block, tolerating the YAML 1.1 `on` -> true key. */
|
|
28
|
+
function workflowCallInputs(wf) {
|
|
29
|
+
const on = wf?.["on"] ?? wf?.["true"];
|
|
30
|
+
if (on == null || typeof on !== "object")
|
|
31
|
+
return {};
|
|
32
|
+
const call = on["workflow_call"];
|
|
33
|
+
if (call == null || typeof call !== "object")
|
|
34
|
+
return {};
|
|
35
|
+
const inputs = call["inputs"];
|
|
36
|
+
return inputs != null && typeof inputs === "object" ? inputs : {};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* What `inputs.*` resolves to inside a called workflow: what the caller passed,
|
|
40
|
+
* over the defaults the callee declares.
|
|
41
|
+
*
|
|
42
|
+
* A declared input the caller omits falls back to its `default`. A declared
|
|
43
|
+
* input with no default and no caller value is unknown rather than empty — the
|
|
44
|
+
* workflow would be invalid if it were required, and guessing empty would
|
|
45
|
+
* silently decide guards that are not decided.
|
|
46
|
+
*/
|
|
47
|
+
function calleeInputs(withBlock, subWf) {
|
|
48
|
+
const out = {};
|
|
49
|
+
for (const [name, decl] of Object.entries(workflowCallInputs(subWf))) {
|
|
50
|
+
out[name] =
|
|
51
|
+
decl != null && typeof decl === "object" && "default" in decl
|
|
52
|
+
? inputLiteral(decl["default"])
|
|
53
|
+
: UNKNOWN;
|
|
54
|
+
}
|
|
55
|
+
if (withBlock != null && typeof withBlock === "object") {
|
|
56
|
+
for (const [name, raw] of Object.entries(withBlock)) {
|
|
57
|
+
out[name] = inputLiteral(raw);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* GitHub allows a reusable-workflow call chain four levels deep. Past that the
|
|
64
|
+
* run itself fails, so anything deeper is not a name we could predict anyway.
|
|
65
|
+
* Probe-verified to three levels: `call-nested / Mid Call / inner`.
|
|
66
|
+
*
|
|
67
|
+
* A cross-repo hop costs the same one level as a local one, so a chain that
|
|
68
|
+
* mixes the two is counted the same way.
|
|
69
|
+
*/
|
|
70
|
+
const MAX_REUSABLE_DEPTH = 4;
|
|
71
|
+
/** `ref` is already a commit id, so resolving it is a no-op. */
|
|
72
|
+
const SHA_RE = /^[0-9a-f]{40}$/i;
|
|
73
|
+
const isSha = (ref) => SHA_RE.test(ref);
|
|
74
|
+
export async function expandJobs(wf, ctx, reader, source, depth = 0, prefix = "", prefixResolved = true, scope = {}, executor) {
|
|
75
|
+
const entries = [];
|
|
76
|
+
const jobs = wf.jobs ?? {};
|
|
77
|
+
const statuses = {};
|
|
78
|
+
// Execute what the caller granted, before anything reads `needs`. The
|
|
79
|
+
// grant names the repo the workflow *file* lives in, so a granted callee
|
|
80
|
+
// job fires here in the recursion, where `source` is that repo. The guard
|
|
81
|
+
// is the same `evalIf` the main loop applies — same scope, same verdict —
|
|
82
|
+
// so a job is executed exactly when it is predicted to run. A job that
|
|
83
|
+
// would not run is not executed; a job that fails to execute contributes
|
|
84
|
+
// nothing but its reason, which `execNote` threads into the entries that
|
|
85
|
+
// needed it.
|
|
86
|
+
let scoped = scope;
|
|
87
|
+
const execFailures = {};
|
|
88
|
+
if (executor != null) {
|
|
89
|
+
for (const [jobId, jobRaw] of Object.entries(jobs)) {
|
|
90
|
+
if (!executor.granted(source, jobId))
|
|
91
|
+
continue;
|
|
92
|
+
const job = jobRaw ?? {};
|
|
93
|
+
if (evalIf(job.if, scoped) !== "run")
|
|
94
|
+
continue;
|
|
95
|
+
const res = await executor.executeJob(jobId, job, wf, scoped);
|
|
96
|
+
if (res.ok) {
|
|
97
|
+
scoped = { ...scoped, needs: { ...scoped.needs, [jobId]: { outputs: res.outputs } } };
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
execFailures[jobId] = res.reason;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const execNote = (needs) => {
|
|
105
|
+
const failed = needs.find((n) => n in execFailures);
|
|
106
|
+
return failed == null ? "" : `; executing '${failed}' failed: ${execFailures[failed]}`;
|
|
107
|
+
};
|
|
108
|
+
for (const [jobId, jobRaw] of Object.entries(jobs)) {
|
|
109
|
+
const job = jobRaw ?? {};
|
|
110
|
+
let status = evalIf(job.if, scoped);
|
|
111
|
+
let reason = job.if != null ? `if: ${JSON.stringify(job.if)}` : "";
|
|
112
|
+
let needs = job.needs ?? [];
|
|
113
|
+
if (typeof needs === "string")
|
|
114
|
+
needs = [needs];
|
|
115
|
+
const cond = String(job.if ?? "");
|
|
116
|
+
if (status !== "skipped" && !cond.includes("always()")) {
|
|
117
|
+
for (const n of needs) {
|
|
118
|
+
if (statuses[n] === "skipped") {
|
|
119
|
+
status = "skipped";
|
|
120
|
+
reason = `needs '${n}' which is skipped`;
|
|
121
|
+
}
|
|
122
|
+
else if (statuses[n] === "unknown" && status === "run") {
|
|
123
|
+
status = "unknown";
|
|
124
|
+
reason = `needs '${n}' whose status is unknown`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
statuses[jobId] = status;
|
|
129
|
+
// A skipped job never expands its matrix and never dispatches a called
|
|
130
|
+
// workflow: it collapses to a single check under the bare job name.
|
|
131
|
+
if (status === "skipped") {
|
|
132
|
+
const disp = skippedDisplayName(jobId, job);
|
|
133
|
+
const name = prefix + disp.name;
|
|
134
|
+
entries.push({
|
|
135
|
+
job: name,
|
|
136
|
+
checkName: prefixResolved && disp.resolved ? name : null,
|
|
137
|
+
status,
|
|
138
|
+
reason,
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const combos = expandMatrixDetailed(job.strategy, prScope(scoped));
|
|
143
|
+
if ("uses" in job) {
|
|
144
|
+
// Reusable workflow call. The calling job produces no check of its own;
|
|
145
|
+
// each called job becomes `<calling job name> / <called job name>`, and
|
|
146
|
+
// a matrix on the *caller* multiplies the whole callee set. A cross-repo
|
|
147
|
+
// call names its checks exactly the same way a local one does — probe
|
|
148
|
+
// PR #9, `call-remote-tag / r-inner` alongside `call-plain / inner`.
|
|
149
|
+
const uses = job.uses;
|
|
150
|
+
if (combos == null) {
|
|
151
|
+
entries.push({
|
|
152
|
+
job: prefix + jobId,
|
|
153
|
+
checkName: null,
|
|
154
|
+
status: "unknown",
|
|
155
|
+
reason: "dynamic matrix on reusable workflow call" + execNote(needs),
|
|
156
|
+
});
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
// Resolve the called workflow once, not once per matrix combination.
|
|
160
|
+
let subWf = null;
|
|
161
|
+
let failure = null;
|
|
162
|
+
// Where the callee's own `./` calls will resolve. A remote `uses:` moves
|
|
163
|
+
// this to the callee's repo and pinned ref; a local one leaves it alone.
|
|
164
|
+
let subSource = source;
|
|
165
|
+
// What `inputs.*` means on the other side of the call.
|
|
166
|
+
let subScope = {};
|
|
167
|
+
const target = parseUses(uses);
|
|
168
|
+
if (depth + 1 > MAX_REUSABLE_DEPTH) {
|
|
169
|
+
failure = `reusable workflow nested deeper than ${MAX_REUSABLE_DEPTH} levels`;
|
|
170
|
+
}
|
|
171
|
+
else if (target == null) {
|
|
172
|
+
failure = `unresolvable reusable reference: ${uses}`;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// A local `./` call stays on the caller's source, which is already
|
|
176
|
+
// pinned to a commit. A cross-repo one arrives as whatever the `uses:`
|
|
177
|
+
// string spelled — `@v0` — and has to be resolved before anything is
|
|
178
|
+
// read from it, so the file that gets read and the commit the
|
|
179
|
+
// prediction names are the same one.
|
|
180
|
+
let resolved = source;
|
|
181
|
+
if (target.source != null) {
|
|
182
|
+
const { ref } = target.source;
|
|
183
|
+
const sha = isSha(ref) ? ref : await reader.resolveRef(target.source);
|
|
184
|
+
resolved = sha == null ? null : { ...target.source, sha };
|
|
185
|
+
}
|
|
186
|
+
if (resolved == null) {
|
|
187
|
+
failure = `cannot resolve ref for ${uses}`;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
subSource = resolved;
|
|
191
|
+
const content = await reader.fetchWorkflow(target.path, subSource);
|
|
192
|
+
if (content == null) {
|
|
193
|
+
failure = `cannot fetch ${uses}`;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
try {
|
|
197
|
+
subWf = parseYaml(content);
|
|
198
|
+
// `inputs.*` changes at the call boundary; `github.*` does not.
|
|
199
|
+
// A callee's jobs run in the caller's repo, so the facts seeded
|
|
200
|
+
// at the top of the prediction stay true all the way down.
|
|
201
|
+
subScope = { inputs: calleeInputs(job.with, subWf ?? {}), github: scoped.github };
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
failure = `YAML parse error in ${uses}: ${e}`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (const combo of combos) {
|
|
210
|
+
const disp = jobDisplayName(jobId, job, combo);
|
|
211
|
+
const baseName = prefix + disp.name;
|
|
212
|
+
const nameResolved = prefixResolved && disp.resolved;
|
|
213
|
+
if (failure != null || subWf == null) {
|
|
214
|
+
entries.push({
|
|
215
|
+
job: baseName,
|
|
216
|
+
checkName: null,
|
|
217
|
+
status: "unknown",
|
|
218
|
+
reason: failure ?? `cannot resolve ${uses}`,
|
|
219
|
+
});
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
entries.push(...(await expandJobs(subWf, ctx, reader, subSource, depth + 1, `${baseName} / `, nameResolved, subScope, executor)));
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (combos == null) {
|
|
227
|
+
entries.push({
|
|
228
|
+
job: prefix + jobId,
|
|
229
|
+
checkName: null,
|
|
230
|
+
status: "unknown",
|
|
231
|
+
reason: "dynamic matrix" + execNote(needs),
|
|
232
|
+
});
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
for (const combo of combos) {
|
|
236
|
+
const disp = jobDisplayName(jobId, job, combo);
|
|
237
|
+
const name = prefix + disp.name;
|
|
238
|
+
entries.push({
|
|
239
|
+
job: name,
|
|
240
|
+
checkName: prefixResolved && disp.resolved ? name : null,
|
|
241
|
+
status,
|
|
242
|
+
reason,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return entries;
|
|
247
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Scope } from "../expr.js";
|
|
2
|
+
import type { JobExecutor } from "../execute.js";
|
|
3
|
+
import type { Ctx, ExpandedJob, Workflow, WorkflowReader, WorkflowSource } from "../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Expand one already-parsed workflow into its job entries. Exported so check
|
|
6
|
+
* names can be tested against recorded GitHub behaviour without a network
|
|
7
|
+
* round-trip; `predict` is the API you want.
|
|
8
|
+
*
|
|
9
|
+
* `scope` seeds what this workflow's own `${{ }}` resolve against — notably
|
|
10
|
+
* `needs`, the outputs of jobs that have not run. Nothing here works out what
|
|
11
|
+
* those are; a caller that knows hands them in, and a caller that does not
|
|
12
|
+
* leaves them out and gets `unknown` where they would have been used.
|
|
13
|
+
*/
|
|
14
|
+
export declare function expandWorkflowJobs(wf: Workflow, ctx: Ctx, reader: WorkflowReader, source: WorkflowSource, scope?: Scope, executor?: JobExecutor): Promise<ExpandedJob[]>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { expandJobs } from "./expandJobs.js";
|
|
2
|
+
/**
|
|
3
|
+
* Expand one already-parsed workflow into its job entries. Exported so check
|
|
4
|
+
* names can be tested against recorded GitHub behaviour without a network
|
|
5
|
+
* round-trip; `predict` is the API you want.
|
|
6
|
+
*
|
|
7
|
+
* `scope` seeds what this workflow's own `${{ }}` resolve against — notably
|
|
8
|
+
* `needs`, the outputs of jobs that have not run. Nothing here works out what
|
|
9
|
+
* those are; a caller that knows hands them in, and a caller that does not
|
|
10
|
+
* leaves them out and gets `unknown` where they would have been used.
|
|
11
|
+
*/
|
|
12
|
+
export function expandWorkflowJobs(wf, ctx, reader, source, scope = {}, executor) {
|
|
13
|
+
return expandJobs(wf, ctx, reader, source, 0, "", true, scope, executor);
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Scope } from "../expr.js";
|
|
2
|
+
/**
|
|
3
|
+
* A scope with the fixed pull-request facts filled in.
|
|
4
|
+
*
|
|
5
|
+
* Every `${{ }}` this module evaluates — a job `if:`, a matrix axis — is
|
|
6
|
+
* evaluated for the same event, so they all get the same `github.*`. Anything
|
|
7
|
+
* the caller states wins; there is nothing here worth overriding, but a scope
|
|
8
|
+
* that silently ignored what it was handed would be the wrong shape.
|
|
9
|
+
*/
|
|
10
|
+
export declare const prScope: (scope: Scope) => Scope;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `github.*` values that are fixed for everything this module predicts.
|
|
3
|
+
* `predict` only ever answers for a pull request, so `event_name` is not a
|
|
4
|
+
* variable — which is what lets a `github.event_name == 'pull_request'` guard
|
|
5
|
+
* resolve instead of hanging the job on an unknown.
|
|
6
|
+
*/
|
|
7
|
+
const PR_GITHUB_CONTEXT = { event_name: "pull_request" };
|
|
8
|
+
/**
|
|
9
|
+
* A scope with the fixed pull-request facts filled in.
|
|
10
|
+
*
|
|
11
|
+
* Every `${{ }}` this module evaluates — a job `if:`, a matrix axis — is
|
|
12
|
+
* evaluated for the same event, so they all get the same `github.*`. Anything
|
|
13
|
+
* the caller states wins; there is nothing here worth overriding, but a scope
|
|
14
|
+
* that silently ignored what it was handed would be the wrong shape.
|
|
15
|
+
*/
|
|
16
|
+
export const prScope = (scope) => ({
|
|
17
|
+
...scope,
|
|
18
|
+
github: { ...PR_GITHUB_CONTEXT, ...scope.github },
|
|
19
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { expandMatrixDetailed } from "./expandMatrixDetailed.js";
|
|
2
|
+
/** Return list of matrix combination dicts, or null if dynamic. */
|
|
3
|
+
export function expandMatrix(strategy, scope = {}) {
|
|
4
|
+
const detailed = expandMatrixDetailed(strategy, scope);
|
|
5
|
+
return detailed == null ? null : detailed.map((c) => (c == null ? null : c.values));
|
|
6
|
+
}
|