willfire 0.1.10 → 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.
Files changed (84) hide show
  1. package/README.md +86 -5
  2. package/dist/cli/index.d.ts +1 -0
  3. package/dist/cli/index.js +1 -0
  4. package/dist/cli/parseArgs.d.ts +9 -0
  5. package/dist/cli/parseArgs.js +42 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +41 -0
  8. package/dist/entries/index.d.ts +3 -0
  9. package/dist/entries/index.js +3 -0
  10. package/dist/entries/isJobEntry.d.ts +3 -0
  11. package/dist/entries/isJobEntry.js +2 -0
  12. package/dist/entries/isWorkflowEntry.d.ts +3 -0
  13. package/dist/entries/isWorkflowEntry.js +2 -0
  14. package/dist/entries/jobName.d.ts +3 -0
  15. package/dist/entries/jobName.js +2 -0
  16. package/dist/execute.d.ts +123 -0
  17. package/dist/execute.js +490 -0
  18. package/dist/expr.d.ts +51 -3
  19. package/dist/expr.js +85 -18
  20. package/dist/filters/index.d.ts +2 -0
  21. package/dist/filters/index.js +2 -0
  22. package/dist/filters/matchFilters.d.ts +2 -0
  23. package/dist/filters/matchFilters.js +12 -0
  24. package/dist/filters/patternToRegex.d.ts +1 -0
  25. package/dist/filters/patternToRegex.js +35 -0
  26. package/dist/index.d.ts +7 -0
  27. package/dist/index.js +6 -0
  28. package/dist/jobs/evalIf.d.ts +10 -0
  29. package/dist/jobs/evalIf.js +18 -0
  30. package/dist/jobs/expandJobs.d.ts +4 -0
  31. package/dist/jobs/expandJobs.js +247 -0
  32. package/dist/jobs/expandWorkflowJobs.d.ts +14 -0
  33. package/dist/jobs/expandWorkflowJobs.js +14 -0
  34. package/dist/jobs/index.d.ts +4 -0
  35. package/dist/jobs/index.js +4 -0
  36. package/dist/jobs/prScope.d.ts +10 -0
  37. package/dist/jobs/prScope.js +19 -0
  38. package/dist/matrix/expandMatrix.d.ts +4 -0
  39. package/dist/matrix/expandMatrix.js +6 -0
  40. package/dist/matrix/expandMatrixDetailed.d.ts +3 -0
  41. package/dist/matrix/expandMatrixDetailed.js +74 -0
  42. package/dist/matrix/formatMatrixValue.d.ts +8 -0
  43. package/dist/matrix/formatMatrixValue.js +16 -0
  44. package/dist/matrix/index.d.ts +4 -0
  45. package/dist/matrix/index.js +4 -0
  46. package/dist/matrix/matrixSuffix.d.ts +3 -0
  47. package/dist/matrix/matrixSuffix.js +8 -0
  48. package/dist/names/index.d.ts +4 -0
  49. package/dist/names/index.js +4 -0
  50. package/dist/names/jobDisplayName.d.ts +16 -0
  51. package/dist/names/jobDisplayName.js +25 -0
  52. package/dist/names/lookupPath.d.ts +1 -0
  53. package/dist/names/lookupPath.js +9 -0
  54. package/dist/names/renderName.d.ts +2 -0
  55. package/dist/names/renderName.js +26 -0
  56. package/dist/names/skippedDisplayName.d.ts +14 -0
  57. package/dist/names/skippedDisplayName.js +16 -0
  58. package/dist/predict/finalizePrediction.d.ts +2 -0
  59. package/dist/predict/finalizePrediction.js +19 -0
  60. package/dist/predict/index.d.ts +5 -0
  61. package/dist/predict/index.js +5 -0
  62. package/dist/predict/makeOctokit.d.ts +2 -0
  63. package/dist/predict/makeOctokit.js +7 -0
  64. package/dist/predict/predict.d.ts +3 -0
  65. package/dist/predict/predict.js +214 -0
  66. package/dist/predict/sourceKey.d.ts +3 -0
  67. package/dist/predict/sourceKey.js +2 -0
  68. package/dist/predict/stackTargetRef.d.ts +14 -0
  69. package/dist/predict/stackTargetRef.js +58 -0
  70. package/dist/triggers/getPrTrigger.d.ts +3 -0
  71. package/dist/triggers/getPrTrigger.js +18 -0
  72. package/dist/triggers/index.d.ts +2 -0
  73. package/dist/triggers/index.js +2 -0
  74. package/dist/triggers/workflowDispatches.d.ts +2 -0
  75. package/dist/triggers/workflowDispatches.js +45 -0
  76. package/dist/{predict.d.ts → types.d.ts} +75 -46
  77. package/dist/types.js +1 -0
  78. package/dist/uses/index.d.ts +1 -0
  79. package/dist/uses/index.js +1 -0
  80. package/dist/uses/parseUses.d.ts +15 -0
  81. package/dist/uses/parseUses.js +33 -0
  82. package/dist/verify.js +1 -1
  83. package/package.json +7 -7
  84. package/dist/predict.js +0 -785
package/dist/expr.js CHANGED
@@ -43,6 +43,10 @@ function truthy(val) {
43
43
  return false;
44
44
  case "unknown":
45
45
  return null;
46
+ // GitHub does cast an array or an object to a boolean, but no workflow
47
+ // asks it to, and the answer is not worth guessing at to find out.
48
+ case "json":
49
+ return null;
46
50
  case "value": {
47
51
  const v = val.v;
48
52
  if (typeof v === "boolean")
@@ -264,10 +268,36 @@ class Parser {
264
268
  const v = this.scope.github?.[rest];
265
269
  return v === undefined ? UNKNOWN : { kind: "value", v };
266
270
  }
267
- // `needs.*`, `steps.*`, `matrix.*`, `env.*`, `vars.*`, `secrets.*`: all
268
- // require something that has not happened yet at prediction time. This is
269
- // the seam where a `needs` context would attach if willfire ever computes
270
- // a called workflow's outputs ahead of the run.
271
+ if (head === "needs") {
272
+ // Only `needs.<job>.outputs.<name>` is modelled. `needs.<job>.result`
273
+ // is a verdict on a run that has not happened; anything else is not a
274
+ // shape the context has.
275
+ const parts = rest.split(".");
276
+ if (parts.length !== 3 || parts[1] !== "outputs")
277
+ return UNKNOWN;
278
+ const job = this.scope.needs?.[parts[0]];
279
+ if (job == null)
280
+ return UNKNOWN;
281
+ // A known job's missing output is the empty string, not a hole: the
282
+ // caller promised the set is complete, and that is what the runner
283
+ // substitutes for an output no step wrote.
284
+ return { kind: "value", v: job.outputs[parts[2]] ?? "" };
285
+ }
286
+ if (head === "steps") {
287
+ // The same shape as `needs`, for the same reason: only
288
+ // `steps.<id>.outputs.<name>` is modelled. `steps.<id>.outcome` and
289
+ // `.conclusion` are verdicts on how a step ran, which the executor does
290
+ // not track — a failed step fails the whole execution instead.
291
+ const parts = rest.split(".");
292
+ if (parts.length !== 3 || parts[1] !== "outputs")
293
+ return UNKNOWN;
294
+ const step = this.scope.steps?.[parts[0]];
295
+ if (step == null)
296
+ return UNKNOWN;
297
+ return { kind: "value", v: step.outputs[parts[2]] ?? "" };
298
+ }
299
+ // `matrix.*`, `env.*`, `vars.*`, `secrets.*`: all require something that
300
+ // has not happened yet at prediction time.
271
301
  return UNKNOWN;
272
302
  }
273
303
  }
@@ -332,17 +362,45 @@ function compare(op, left, right) {
332
362
  return asBool(a > b);
333
363
  return asBool(a >= b);
334
364
  }
365
+ /**
366
+ * `fromJSON(s)` on a known string.
367
+ *
368
+ * The result is sorted into the lattice rather than dropped in whole: a scalar
369
+ * is an ordinary value and stays comparable, `null` has a known truthiness and
370
+ * no useful value, and only an array or an object needs the `json` point.
371
+ * Anything unparseable is unknown — a workflow that reaches this at runtime
372
+ * fails, and predicting a failure is not this function's job.
373
+ */
374
+ function fromJson(arg) {
375
+ if (arg.kind !== "value" || typeof arg.v !== "string")
376
+ return UNKNOWN;
377
+ let parsed;
378
+ try {
379
+ parsed = JSON.parse(arg.v);
380
+ }
381
+ catch {
382
+ return UNKNOWN;
383
+ }
384
+ if (parsed === null)
385
+ return { kind: "falsy" };
386
+ if (typeof parsed === "object")
387
+ return { kind: "json", v: parsed };
388
+ return { kind: "value", v: parsed };
389
+ }
335
390
  /**
336
391
  * The functions worth modelling.
337
392
  *
338
393
  * `always()` is true by definition. `contains` on two known strings is the one
339
- * that unlocks the fleet's `gates` pattern. `success()`, `failure()` and
340
- * `cancelled()` depend on jobs that have not run, and everything else is
341
- * simply not modelled — all unknown.
394
+ * that unlocks the fleet's `gates` pattern. `fromJSON` is what a dynamic matrix
395
+ * axis is built out of. `success()`, `failure()` and `cancelled()` depend on
396
+ * jobs that have not run, and everything else is simply not modelled — all
397
+ * unknown.
342
398
  */
343
399
  function applyFunction(name, args) {
344
400
  if (name === "always")
345
401
  return { kind: "value", v: true };
402
+ if (name === "fromjson" && args.length === 1)
403
+ return fromJson(args[0]);
346
404
  if (name === "contains" && args.length === 2) {
347
405
  const [hay, needle] = args;
348
406
  if (hay.kind !== "value" || needle.kind !== "value")
@@ -363,26 +421,35 @@ function applyFunction(name, args) {
363
421
  }
364
422
  // ---------------------------------------------------------------------- entry
365
423
  /**
366
- * Evaluate a condition to a truthiness, or null when it cannot be settled.
424
+ * Evaluate an expression to a value, or UNKNOWN when it cannot be settled.
367
425
  *
368
- * The `${{ }}` wrapper is optional in `if:` and stripped when present. A
369
- * condition that is only *partly* wrapped (`foo ${{ bar }} baz`) is a string
426
+ * The `${{ }}` wrapper is optional in `if:` and stripped when present. An
427
+ * expression that is only *partly* wrapped (`foo ${{ bar }} baz`) is a string
370
428
  * interpolation rather than an expression, and is not modelled.
429
+ *
430
+ * A `if:` wants {@link evaluate}, which is this narrowed to truthiness. This
431
+ * one is for the places that need the value itself — a matrix axis written as
432
+ * `${{ fromJSON(...) }}` is an array, and its truthiness says nothing about
433
+ * how many jobs it schedules.
371
434
  */
372
- export function evaluate(cond, scope = {}) {
373
- const stripped = cond.trim().replace(/^\$\{\{(.*)\}\}$/s, "$1").trim();
435
+ export function evaluateValue(expr, scope = {}) {
436
+ const stripped = expr.trim().replace(/^\$\{\{(.*)\}\}$/s, "$1").trim();
374
437
  if (stripped === "")
375
- return null;
438
+ return UNKNOWN;
376
439
  if (stripped.includes("${{"))
377
- return null;
440
+ return UNKNOWN;
378
441
  const toks = tokenize(stripped);
379
442
  if (toks == null || toks.length === 0)
380
- return null;
443
+ return UNKNOWN;
381
444
  const p = new Parser(toks, scope);
382
445
  const val = p.or();
383
- // Trailing tokens mean the grammar did not cover this condition; whatever
446
+ // Trailing tokens mean the grammar did not cover this expression; whatever
384
447
  // was parsed describes only a prefix of it, so it decides nothing.
385
448
  if (!p.done())
386
- return null;
387
- return truthy(val);
449
+ return UNKNOWN;
450
+ return val;
451
+ }
452
+ /** Evaluate a condition to a truthiness, or null when it cannot be settled. */
453
+ export function evaluate(cond, scope = {}) {
454
+ return truthy(evaluateValue(cond, scope));
388
455
  }
@@ -0,0 +1,2 @@
1
+ export { patternToRegex } from "./patternToRegex.js";
2
+ export { matchFilters } from "./matchFilters.js";
@@ -0,0 +1,2 @@
1
+ export { patternToRegex } from "./patternToRegex.js";
2
+ export { matchFilters } from "./matchFilters.js";
@@ -0,0 +1,2 @@
1
+ /** Order-sensitive match: last matching pattern wins; ! negates. */
2
+ export declare function matchFilters(value: string, patterns: string[]): boolean;
@@ -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
+ }
@@ -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,4 @@
1
+ export { prScope } from "./prScope.js";
2
+ export { evalIf } from "./evalIf.js";
3
+ export { expandJobs } from "./expandJobs.js";
4
+ export { expandWorkflowJobs } from "./expandWorkflowJobs.js";
@@ -0,0 +1,4 @@
1
+ export { prScope } from "./prScope.js";
2
+ export { evalIf } from "./evalIf.js";
3
+ export { expandJobs } from "./expandJobs.js";
4
+ export { expandWorkflowJobs } from "./expandWorkflowJobs.js";
@@ -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,4 @@
1
+ import type { Scope } from "../expr.js";
2
+ import type { Combo } from "../types.js";
3
+ /** Return list of matrix combination dicts, or null if dynamic. */
4
+ export declare function expandMatrix(strategy: any, scope?: Scope): Combo[] | null;
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import { type Scope } from "../expr.js";
2
+ import type { DetailedCombos } from "../types.js";
3
+ export declare function expandMatrixDetailed(strategy: any, scope?: Scope): DetailedCombos;