willfire 0.1.11 → 0.1.13

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 (129) hide show
  1. package/dist/cli/index.d.ts +1 -0
  2. package/dist/cli/index.js +1 -0
  3. package/dist/cli/parseArgs.d.ts +9 -0
  4. package/dist/cli/parseArgs.js +42 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +41 -0
  7. package/dist/entries/index.d.ts +3 -0
  8. package/dist/entries/index.js +3 -0
  9. package/dist/entries/isJobEntry.d.ts +3 -0
  10. package/dist/entries/isJobEntry.js +2 -0
  11. package/dist/entries/isWorkflowEntry.d.ts +3 -0
  12. package/dist/entries/isWorkflowEntry.js +2 -0
  13. package/dist/entries/jobName.d.ts +3 -0
  14. package/dist/entries/jobName.js +2 -0
  15. package/dist/execute.d.ts +2 -2
  16. package/dist/execute.js +3 -1
  17. package/dist/expr/applyFunction.d.ts +11 -0
  18. package/dist/expr/applyFunction.js +41 -0
  19. package/dist/expr/asBool.d.ts +10 -0
  20. package/dist/expr/asBool.js +12 -0
  21. package/dist/expr/coalesceAnd.d.ts +7 -0
  22. package/dist/expr/coalesceAnd.js +17 -0
  23. package/dist/expr/coalesceOr.d.ts +6 -0
  24. package/dist/expr/coalesceOr.js +16 -0
  25. package/dist/expr/compare.d.ts +12 -0
  26. package/dist/expr/compare.js +54 -0
  27. package/dist/expr/cursor.d.ts +11 -0
  28. package/dist/expr/cursor.js +25 -0
  29. package/dist/expr/evaluate.d.ts +3 -0
  30. package/dist/expr/evaluate.js +6 -0
  31. package/dist/expr/evaluateValue.d.ts +14 -0
  32. package/dist/expr/evaluateValue.js +37 -0
  33. package/dist/expr/fromJson.d.ts +11 -0
  34. package/dist/expr/fromJson.js +29 -0
  35. package/dist/expr/index.d.ts +35 -0
  36. package/dist/expr/index.js +34 -0
  37. package/dist/expr/indexVal.d.ts +7 -0
  38. package/dist/expr/indexVal.js +34 -0
  39. package/dist/expr/lookup.d.ts +2 -0
  40. package/dist/expr/lookup.js +51 -0
  41. package/dist/expr/negate.d.ts +1 -0
  42. package/dist/expr/negate.js +3 -0
  43. package/dist/expr/parseAnd.d.ts +3 -0
  44. package/dist/expr/parseAnd.js +10 -0
  45. package/dist/expr/parseAtom.d.ts +3 -0
  46. package/dist/expr/parseAtom.js +43 -0
  47. package/dist/expr/parseCall.d.ts +9 -0
  48. package/dist/expr/parseCall.js +25 -0
  49. package/dist/expr/parseCmp.d.ts +3 -0
  50. package/dist/expr/parseCmp.js +12 -0
  51. package/dist/expr/parseOr.d.ts +11 -0
  52. package/dist/expr/parseOr.js +18 -0
  53. package/dist/expr/parsePrimary.d.ts +4 -0
  54. package/dist/expr/parsePrimary.js +16 -0
  55. package/dist/expr/parseUnary.d.ts +3 -0
  56. package/dist/expr/parseUnary.js +11 -0
  57. package/dist/expr/tokenize.d.ts +24 -0
  58. package/dist/expr/tokenize.js +74 -0
  59. package/dist/expr/truthy.d.ts +7 -0
  60. package/dist/expr/truthy.js +29 -0
  61. package/dist/expr/val.d.ts +72 -0
  62. package/dist/expr/val.js +1 -0
  63. package/dist/filters/index.d.ts +2 -0
  64. package/dist/filters/index.js +2 -0
  65. package/dist/filters/matchFilters.d.ts +2 -0
  66. package/dist/filters/matchFilters.js +12 -0
  67. package/dist/filters/patternToRegex.d.ts +1 -0
  68. package/dist/filters/patternToRegex.js +35 -0
  69. package/dist/index.d.ts +7 -0
  70. package/dist/index.js +6 -0
  71. package/dist/jobs/evalIf.d.ts +10 -0
  72. package/dist/jobs/evalIf.js +18 -0
  73. package/dist/jobs/expandJobs.d.ts +4 -0
  74. package/dist/jobs/expandJobs.js +247 -0
  75. package/dist/jobs/expandWorkflowJobs.d.ts +14 -0
  76. package/dist/jobs/expandWorkflowJobs.js +14 -0
  77. package/dist/jobs/index.d.ts +4 -0
  78. package/dist/jobs/index.js +4 -0
  79. package/dist/jobs/prScope.d.ts +10 -0
  80. package/dist/jobs/prScope.js +19 -0
  81. package/dist/matrix/expandMatrix.d.ts +4 -0
  82. package/dist/matrix/expandMatrix.js +6 -0
  83. package/dist/matrix/expandMatrixDetailed.d.ts +3 -0
  84. package/dist/matrix/expandMatrixDetailed.js +74 -0
  85. package/dist/matrix/formatMatrixValue.d.ts +8 -0
  86. package/dist/matrix/formatMatrixValue.js +16 -0
  87. package/dist/matrix/index.d.ts +4 -0
  88. package/dist/matrix/index.js +4 -0
  89. package/dist/matrix/matrixSuffix.d.ts +3 -0
  90. package/dist/matrix/matrixSuffix.js +8 -0
  91. package/dist/names/index.d.ts +4 -0
  92. package/dist/names/index.js +4 -0
  93. package/dist/names/jobDisplayName.d.ts +16 -0
  94. package/dist/names/jobDisplayName.js +25 -0
  95. package/dist/names/lookupPath.d.ts +1 -0
  96. package/dist/names/lookupPath.js +9 -0
  97. package/dist/names/renderName.d.ts +2 -0
  98. package/dist/names/renderName.js +26 -0
  99. package/dist/names/skippedDisplayName.d.ts +14 -0
  100. package/dist/names/skippedDisplayName.js +16 -0
  101. package/dist/predict/finalizePrediction.d.ts +2 -0
  102. package/dist/predict/finalizePrediction.js +19 -0
  103. package/dist/predict/index.d.ts +5 -0
  104. package/dist/predict/index.js +5 -0
  105. package/dist/predict/makeOctokit.d.ts +2 -0
  106. package/dist/predict/makeOctokit.js +7 -0
  107. package/dist/predict/predict.d.ts +3 -0
  108. package/dist/predict/predict.js +214 -0
  109. package/dist/predict/sourceKey.d.ts +3 -0
  110. package/dist/predict/sourceKey.js +2 -0
  111. package/dist/predict/stackTargetRef.d.ts +14 -0
  112. package/dist/predict/stackTargetRef.js +58 -0
  113. package/dist/triggers/getPrTrigger.d.ts +3 -0
  114. package/dist/triggers/getPrTrigger.js +18 -0
  115. package/dist/triggers/index.d.ts +2 -0
  116. package/dist/triggers/index.js +2 -0
  117. package/dist/triggers/workflowDispatches.d.ts +2 -0
  118. package/dist/triggers/workflowDispatches.js +45 -0
  119. package/dist/{predict.d.ts → types.d.ts} +64 -53
  120. package/dist/types.js +1 -0
  121. package/dist/uses/index.d.ts +1 -0
  122. package/dist/uses/index.js +1 -0
  123. package/dist/uses/parseUses.d.ts +15 -0
  124. package/dist/uses/parseUses.js +33 -0
  125. package/dist/verify.js +1 -1
  126. package/package.json +7 -7
  127. package/dist/expr.d.ts +0 -117
  128. package/dist/expr.js +0 -455
  129. package/dist/predict.js +0 -912
@@ -0,0 +1,247 @@
1
+ import { parse as parseYaml } from "yaml";
2
+ import { UNKNOWN } from "../expr/val.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/val.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/val.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/val.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/val.js";
2
+ import type { DetailedCombos } from "../types.js";
3
+ export declare function expandMatrixDetailed(strategy: any, scope?: Scope): DetailedCombos;
@@ -0,0 +1,74 @@
1
+ import { evaluateValue } from "../expr/evaluateValue.js";
2
+ /**
3
+ * The values of one matrix axis, or null when they cannot be known.
4
+ *
5
+ * A plain list is itself. An axis written as an expression —
6
+ * `language: ${{ fromJSON(needs.detect.outputs.coverage_languages) }}` — is
7
+ * the values another job computed, and is knowable exactly when the scope
8
+ * carries that job's outputs. Anything else stays null, which is what makes
9
+ * the whole job `unknown` rather than a guess at how many checks it creates.
10
+ */
11
+ function axisValues(v, scope) {
12
+ if (Array.isArray(v))
13
+ return v;
14
+ if (typeof v !== "string")
15
+ return null;
16
+ const val = evaluateValue(v, scope);
17
+ if (val.kind !== "json" || !Array.isArray(val.v))
18
+ return null;
19
+ return val.v;
20
+ }
21
+ export function expandMatrixDetailed(strategy, scope = {}) {
22
+ const matrix = strategy?.matrix;
23
+ if (matrix == null)
24
+ return [null];
25
+ // `matrix: ${{ ... }}` — the whole matrix as one expression, rather than the
26
+ // per-axis form below. It yields include-style entries, not axes, so it is a
27
+ // separate expansion and is not modelled.
28
+ if (typeof matrix === "string")
29
+ return null;
30
+ const include = matrix.include ?? [];
31
+ const exclude = matrix.exclude ?? [];
32
+ if (typeof include === "string" || typeof exclude === "string")
33
+ return null;
34
+ const axes = {};
35
+ for (const [k, v] of Object.entries(matrix)) {
36
+ if (k === "include" || k === "exclude")
37
+ continue;
38
+ const vals = axisValues(v, scope);
39
+ if (vals == null)
40
+ return null;
41
+ axes[k] = vals;
42
+ }
43
+ const axisKeys = Object.keys(axes);
44
+ let combos = [{ values: {}, displayKeys: axisKeys }];
45
+ for (const [k, vals] of Object.entries(axes)) {
46
+ combos = combos.flatMap((c) => vals.map((v) => ({ values: { ...c.values, [k]: v }, displayKeys: axisKeys })));
47
+ }
48
+ if (axisKeys.length === 0)
49
+ combos = [];
50
+ combos = combos.filter((c) => !exclude.some((ex) => Object.entries(ex).every(([k, v]) => c.values[k] === v)));
51
+ const extra = [];
52
+ for (const inc of include) {
53
+ const overlapping = Object.fromEntries(Object.entries(inc).filter(([k]) => k in axes));
54
+ const targets = combos.filter((c) => Object.entries(overlapping).every(([k, v]) => c.values[k] === v));
55
+ if (axisKeys.length > 0 && targets.length > 0) {
56
+ // Merge into the matching combinations. With no overlapping keys this
57
+ // matches every combination, per the docs ("added to each of the matrix
58
+ // combinations if none of the key:value pairs overwrite any of the
59
+ // original matrix values").
60
+ for (const c of targets)
61
+ Object.assign(c.values, inc);
62
+ }
63
+ else {
64
+ // No combination to attach to: the include entry becomes a combination
65
+ // of its own, and every one of its keys shows in the name.
66
+ extra.push({ values: { ...inc }, displayKeys: Object.keys(inc) });
67
+ }
68
+ }
69
+ combos.push(...extra);
70
+ // Zero combinations is a real answer, not a missing one: an empty axis, or an
71
+ // `exclude` that removes everything, schedules no jobs at all. Only an absent
72
+ // `matrix:` key means "one unsuffixed job", and that returned above.
73
+ return combos;
74
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * How a single matrix value is rendered inside a check name.
3
+ *
4
+ * Probe-verified: object values are flattened to their own values, so
5
+ * `cfg: {os: linux, arch: x64}` renders as `linux, x64` — the check is
6
+ * `m-object (linux, x64)`.
7
+ */
8
+ export declare function formatMatrixValue(v: unknown): string;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * How a single matrix value is rendered inside a check name.
3
+ *
4
+ * Probe-verified: object values are flattened to their own values, so
5
+ * `cfg: {os: linux, arch: x64}` renders as `linux, x64` — the check is
6
+ * `m-object (linux, x64)`.
7
+ */
8
+ export function formatMatrixValue(v) {
9
+ if (v == null)
10
+ return "";
11
+ if (Array.isArray(v))
12
+ return v.map(formatMatrixValue).join(", ");
13
+ if (typeof v === "object")
14
+ return Object.values(v).map(formatMatrixValue).join(", ");
15
+ return String(v);
16
+ }
@@ -0,0 +1,4 @@
1
+ export { expandMatrixDetailed } from "./expandMatrixDetailed.js";
2
+ export { expandMatrix } from "./expandMatrix.js";
3
+ export { formatMatrixValue } from "./formatMatrixValue.js";
4
+ export { matrixSuffix } from "./matrixSuffix.js";
@@ -0,0 +1,4 @@
1
+ export { expandMatrixDetailed } from "./expandMatrixDetailed.js";
2
+ export { expandMatrix } from "./expandMatrix.js";
3
+ export { formatMatrixValue } from "./formatMatrixValue.js";
4
+ export { matrixSuffix } from "./matrixSuffix.js";
@@ -0,0 +1,3 @@
1
+ import type { DetailedCombo } from "../types.js";
2
+ /** The ` (v1, v2)` suffix GitHub appends for a matrix combination. */
3
+ export declare function matrixSuffix(combo: DetailedCombo): string;
@@ -0,0 +1,8 @@
1
+ import { formatMatrixValue } from "./formatMatrixValue.js";
2
+ /** The ` (v1, v2)` suffix GitHub appends for a matrix combination. */
3
+ export function matrixSuffix(combo) {
4
+ const keys = combo.displayKeys.filter((k) => k in combo.values);
5
+ if (keys.length === 0)
6
+ return "";
7
+ return ` (${keys.map((k) => formatMatrixValue(combo.values[k])).join(", ")})`;
8
+ }
@@ -0,0 +1,4 @@
1
+ export { lookupPath } from "./lookupPath.js";
2
+ export { renderName } from "./renderName.js";
3
+ export { jobDisplayName, EXPRESSION_RE } from "./jobDisplayName.js";
4
+ export { skippedDisplayName } from "./skippedDisplayName.js";
@@ -0,0 +1,4 @@
1
+ export { lookupPath } from "./lookupPath.js";
2
+ export { renderName } from "./renderName.js";
3
+ export { jobDisplayName, EXPRESSION_RE } from "./jobDisplayName.js";
4
+ export { skippedDisplayName } from "./skippedDisplayName.js";
@@ -0,0 +1,16 @@
1
+ import type { DetailedCombo, DisplayName, Workflow } from "../types.js";
2
+ /**
3
+ * A `name:` that contains any `${{ }}` expression suppresses the matrix
4
+ * parenthetical; a literal one does not.
5
+ *
6
+ * Probe-verified three ways over `a: [x, y]`: `name: Static Label` yields
7
+ * `Static Label (x)` / `Static Label (y)`, `name: ev ${{ github.event_name }}`
8
+ * yields two checks both called `ev pull_request`, and
9
+ * `name: p ${{ matrix.a }}` over `a: [x], b: ["1", "2"]` yields two checks
10
+ * both called `p x`. So the trigger is the presence of an expression, not
11
+ * whether the expression happens to read the matrix — and duplicate check
12
+ * names are a real outcome GitHub allows.
13
+ */
14
+ export declare const EXPRESSION_RE: RegExp;
15
+ /** The check name for one job/combination. */
16
+ export declare function jobDisplayName(jobId: string, job: Workflow, combo: DetailedCombo | null): DisplayName;
@@ -0,0 +1,25 @@
1
+ import { matrixSuffix } from "../matrix/matrixSuffix.js";
2
+ import { renderName } from "./renderName.js";
3
+ /**
4
+ * A `name:` that contains any `${{ }}` expression suppresses the matrix
5
+ * parenthetical; a literal one does not.
6
+ *
7
+ * Probe-verified three ways over `a: [x, y]`: `name: Static Label` yields
8
+ * `Static Label (x)` / `Static Label (y)`, `name: ev ${{ github.event_name }}`
9
+ * yields two checks both called `ev pull_request`, and
10
+ * `name: p ${{ matrix.a }}` over `a: [x], b: ["1", "2"]` yields two checks
11
+ * both called `p x`. So the trigger is the presence of an expression, not
12
+ * whether the expression happens to read the matrix — and duplicate check
13
+ * names are a real outcome GitHub allows.
14
+ */
15
+ export const EXPRESSION_RE = /\$\{\{/;
16
+ /** The check name for one job/combination. */
17
+ export function jobDisplayName(jobId, job, combo) {
18
+ const raw = job != null && job.name != null ? String(job.name) : null;
19
+ if (raw === null) {
20
+ return { name: jobId + (combo ? matrixSuffix(combo) : ""), resolved: true };
21
+ }
22
+ const { text, resolved } = renderName(raw, combo?.values ?? null);
23
+ const suffix = combo && !EXPRESSION_RE.test(raw) ? matrixSuffix(combo) : "";
24
+ return { name: text + suffix, resolved };
25
+ }
@@ -0,0 +1 @@
1
+ export declare function lookupPath(obj: any, path: string): unknown;
@@ -0,0 +1,9 @@
1
+ export function lookupPath(obj, path) {
2
+ let cur = obj;
3
+ for (const seg of path.split(".")) {
4
+ if (cur == null || typeof cur !== "object" || !(seg in cur))
5
+ return undefined;
6
+ cur = cur[seg];
7
+ }
8
+ return cur;
9
+ }
@@ -0,0 +1,2 @@
1
+ import type { Combo, Rendered } from "../types.js";
2
+ export declare function renderName(template: string, combo: Combo): Rendered;
@@ -0,0 +1,26 @@
1
+ import { formatMatrixValue } from "../matrix/formatMatrixValue.js";
2
+ import { lookupPath } from "./lookupPath.js";
3
+ export function renderName(template, combo) {
4
+ let resolved = true;
5
+ const text = template.replace(/\$\{\{(.*?)\}\}/g, (whole, inner) => {
6
+ const expr = String(inner).trim();
7
+ if (expr.startsWith("matrix.")) {
8
+ if (!combo) {
9
+ resolved = false;
10
+ return whole;
11
+ }
12
+ const val = lookupPath(combo, expr.slice("matrix.".length));
13
+ if (val === undefined) {
14
+ resolved = false;
15
+ return whole;
16
+ }
17
+ return formatMatrixValue(val);
18
+ }
19
+ // We only predict pull_request dispatch, so this one is knowable.
20
+ if (expr === "github.event_name")
21
+ return "pull_request";
22
+ resolved = false;
23
+ return whole;
24
+ });
25
+ return { text, resolved };
26
+ }
@@ -0,0 +1,14 @@
1
+ import type { DisplayName, Workflow } from "../types.js";
2
+ /**
3
+ * The check name a job gets when it is skipped.
4
+ *
5
+ * A skipped job is never set up, so nothing about it is evaluated: the matrix
6
+ * does not expand and `name:` is not interpolated. Probe-verified twice over:
7
+ * `if: false` with `a: [x, y]` produces the single check `m-skipped`, not
8
+ * `m-skipped (x)` / `m-skipped (y)`; and `name: sk ${{ github.event_name }}`
9
+ * with `if: false` produces a check literally called
10
+ * `sk ${{ github.event_name }}`, expression text and all. The same collapse
11
+ * applies to a skipped reusable-workflow call: one check named after the
12
+ * caller, with no `/ <callee job>` entries.
13
+ */
14
+ export declare function skippedDisplayName(jobId: string, job: Workflow): DisplayName;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The check name a job gets when it is skipped.
3
+ *
4
+ * A skipped job is never set up, so nothing about it is evaluated: the matrix
5
+ * does not expand and `name:` is not interpolated. Probe-verified twice over:
6
+ * `if: false` with `a: [x, y]` produces the single check `m-skipped`, not
7
+ * `m-skipped (x)` / `m-skipped (y)`; and `name: sk ${{ github.event_name }}`
8
+ * with `if: false` produces a check literally called
9
+ * `sk ${{ github.event_name }}`, expression text and all. The same collapse
10
+ * applies to a skipped reusable-workflow call: one check named after the
11
+ * caller, with no `/ <callee job>` entries.
12
+ */
13
+ export function skippedDisplayName(jobId, job) {
14
+ const raw = job != null && job.name != null ? String(job.name) : null;
15
+ return { name: raw ?? jobId, resolved: true };
16
+ }
@@ -0,0 +1,2 @@
1
+ import type { DraftEntry, Prediction, WorkflowSource } from "../types.js";
2
+ export declare function finalizePrediction(entries: DraftEntry[], skip: string | null, sources: Map<string, WorkflowSource>): Prediction;
@@ -0,0 +1,19 @@
1
+ import { sourceKey } from "./sourceKey.js";
2
+ const isWorkflowDraft = (e) => e.job === "*";
3
+ const finalize = (e) => isWorkflowDraft(e)
4
+ ? { ...e, checkName: null }
5
+ : { ...e, checkName: e.checkName ?? null };
6
+ export function finalizePrediction(entries, skip, sources) {
7
+ const final = entries.map(finalize);
8
+ const names = new Set();
9
+ for (const e of final) {
10
+ if (e.status === "run" && e.checkName != null)
11
+ names.add(e.checkName);
12
+ }
13
+ return {
14
+ entries: final,
15
+ checkNames: [...names].sort(),
16
+ skip,
17
+ sources: [...sources.values()].sort((a, b) => sourceKey(a).localeCompare(sourceKey(b))),
18
+ };
19
+ }
@@ -0,0 +1,5 @@
1
+ export { makeOctokit } from "./makeOctokit.js";
2
+ export { sourceKey } from "./sourceKey.js";
3
+ export { stackTargetRef } from "./stackTargetRef.js";
4
+ export { finalizePrediction } from "./finalizePrediction.js";
5
+ export { predict } from "./predict.js";
@@ -0,0 +1,5 @@
1
+ export { makeOctokit } from "./makeOctokit.js";
2
+ export { sourceKey } from "./sourceKey.js";
3
+ export { stackTargetRef } from "./stackTargetRef.js";
4
+ export { finalizePrediction } from "./finalizePrediction.js";
5
+ export { predict } from "./predict.js";
@@ -0,0 +1,2 @@
1
+ import { Octokit } from "@octokit/rest";
2
+ export declare function makeOctokit(): Octokit;