willfire 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -21,9 +21,19 @@ import { predict } from "willfire";
21
21
  import { getOctokit } from "@actions/github"; // or new Octokit({ auth: token })
22
22
 
23
23
  const { entries, skip } = await predict(getOctokit(token), "owner/repo", 123);
24
- // entries: [{ workflow, job, status: "run" | "skipped" | "unknown" | "no-dispatch", reason }]
25
24
  ```
26
25
 
26
+ `entries` is a union of two variants, both carrying `workflow` and `reason`:
27
+
28
+ | variant | `job` | `status` |
29
+ | --- | --- | --- |
30
+ | `WorkflowEntry` | `"*"` | `"run" \| "skipped" \| "no-dispatch"` |
31
+ | `JobEntry` | the job's display name | `"run" \| "skipped" \| "unknown" \| "no-dispatch"` |
32
+
33
+ `"unknown"` is job-level only: every workflow-level verdict is decidable, so a
34
+ `WorkflowEntry` cannot express one. Narrow with the exported `isWorkflowEntry`
35
+ and `isJobEntry` guards rather than testing the `"*"` sentinel yourself.
36
+
27
37
  Auth is any token with `contents: read`, `actions: read`, and
28
38
  `pull-requests: read` — inside an action, the workflow's `GITHUB_TOKEN`.
29
39
 
package/dist/predict.d.ts CHANGED
@@ -1,11 +1,53 @@
1
1
  #!/usr/bin/env node
2
2
  import { Octokit } from "@octokit/rest";
3
- export interface Entry {
3
+ interface EntryBase {
4
4
  workflow: string;
5
- job: string;
6
- status: "run" | "skipped" | "unknown" | "no-dispatch";
7
5
  reason: string;
8
6
  }
7
+ /**
8
+ * A job's display name, e.g. `test (18)`.
9
+ *
10
+ * Nominally distinct from `string` for one reason: `Entry` is a closed union
11
+ * and `"*"` is the workflow-level sentinel, so a plain `string` here would
12
+ * structurally admit `{ job: "*", status: "unknown" }` as a `JobEntry` — the
13
+ * exact shape this split exists to forbid. TypeScript cannot spell "string
14
+ * but not `"*"`", so the job side is branded instead. Build one with
15
+ * {@link jobName}; reading one is just a string.
16
+ */
17
+ export type JobName = string & {
18
+ readonly __jobName: true;
19
+ };
20
+ /** Tag a job display name. Rejects the workflow-level sentinel. */
21
+ export declare const jobName: <S extends string>(name: S extends "*" ? never : S) => JobName;
22
+ /**
23
+ * A verdict about the workflow as a whole: it produces no run at all, or it
24
+ * produces a run that expands into no job entries.
25
+ *
26
+ * There is no `"unknown"` here, and that is the point. Every workflow-level
27
+ * verdict is decidable, and this type is what enforces it — the previous
28
+ * single-interface shape let `{ job: "*", status: "unknown" }` typecheck, which
29
+ * is what shipped and what pr-monitor#17 had to grow a `tolerated` bucket for.
30
+ */
31
+ export interface WorkflowEntry extends EntryBase {
32
+ job: "*";
33
+ status: "run" | "skipped" | "no-dispatch";
34
+ }
35
+ /**
36
+ * A verdict about one job entry inside a workflow that does dispatch.
37
+ *
38
+ * These can be genuinely undecidable statically — dynamic matrix, non-local
39
+ * reusable workflow, unresolvable `if`, or `needs` on any of those — so
40
+ * `"unknown"` lives here and only here.
41
+ */
42
+ export interface JobEntry extends EntryBase {
43
+ job: JobName;
44
+ status: "run" | "skipped" | "unknown" | "no-dispatch";
45
+ }
46
+ export type Entry = WorkflowEntry | JobEntry;
47
+ /** Narrow to the workflow-level variant without inspecting the sentinel. */
48
+ export declare const isWorkflowEntry: (e: Entry) => e is WorkflowEntry;
49
+ /** Narrow to the job-level variant without inspecting the sentinel. */
50
+ export declare const isJobEntry: (e: Entry) => e is JobEntry;
9
51
  export interface Prediction {
10
52
  entries: Entry[];
11
53
  skip: string | null;
package/dist/predict.js CHANGED
@@ -9,6 +9,12 @@
9
9
  // live dispatches on thekevinbot/willrun-probe (PRs 1-7).
10
10
  import { Octokit } from "@octokit/rest";
11
11
  import { parse as parseYaml } from "yaml";
12
+ /** Tag a job display name. Rejects the workflow-level sentinel. */
13
+ export const jobName = (name) => name;
14
+ /** Narrow to the workflow-level variant without inspecting the sentinel. */
15
+ export const isWorkflowEntry = (e) => e.job === "*";
16
+ /** Narrow to the job-level variant without inspecting the sentinel. */
17
+ export const isJobEntry = (e) => e.job !== "*";
12
18
  // ------------------------------------------------- GitHub filter pattern glob
13
19
  // Grammar per docs: * (any chars except /), ** (any chars), ? (zero or one of
14
20
  // preceding char), + (one or more of preceding char), [ranges], leading ! negates.
@@ -347,8 +353,8 @@ export async function predict(octokit, repo, prNumber) {
347
353
  entries.push({ workflow: path, job: "*", status: "no-dispatch", reason });
348
354
  continue;
349
355
  }
350
- for (const [jobName, status, jreason] of await expandJobs(wf, ctx, fetchFile)) {
351
- entries.push({ workflow: path, job: jobName, status, reason: jreason || reason });
356
+ for (const [name, status, jreason] of await expandJobs(wf, ctx, fetchFile)) {
357
+ entries.push({ workflow: path, job: jobName(name), status, reason: jreason || reason });
352
358
  }
353
359
  }
354
360
  return { entries, skip: null };
@@ -379,7 +385,7 @@ if (isMain) {
379
385
  }
380
386
  else {
381
387
  for (const e of entries) {
382
- if (e.job === "*")
388
+ if (isWorkflowEntry(e))
383
389
  console.log(`# ${e.workflow} :: ${e.status} (${e.reason})`);
384
390
  else
385
391
  console.log(`${e.workflow} :: ${e.job} :: ${e.status}`);
package/dist/verify.js CHANGED
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // Ground truth: workflow runs for the PR head SHA with a pull_request event,
6
6
  // and the job entries inside each run (skipped jobs included).
7
- import { makeOctokit, predict } from "./predict.js";
7
+ import { isJobEntry, makeOctokit, predict } from "./predict.js";
8
8
  async function actualEntries(octokit, repo, prNumber) {
9
9
  const [owner, name] = repo.split("/");
10
10
  const base = { owner, repo: name };
@@ -44,9 +44,7 @@ if (!repo || !prArg) {
44
44
  const pr = Number(prArg);
45
45
  const octokit = makeOctokit();
46
46
  const { entries: predictedRaw } = await predict(octokit, repo, pr);
47
- const predicted = new Map(predictedRaw
48
- .filter((r) => r.job !== "*")
49
- .map((r) => [`${r.workflow} :: ${r.job}`, r.status]));
47
+ const predicted = new Map(predictedRaw.filter(isJobEntry).map((r) => [`${r.workflow} :: ${r.job}`, r.status]));
50
48
  const unknownWfs = new Set(predictedRaw.filter((r) => r.status === "unknown").map((r) => r.workflow));
51
49
  const { entries: actual, incomplete } = await actualEntries(octokit, repo, pr);
52
50
  if (incomplete.length > 0) {
@@ -82,10 +80,5 @@ for (const key of keys) {
82
80
  console.log(`DIFF ${key} :: predicted ${p}, actual ${a}`);
83
81
  }
84
82
  }
85
- for (const r of predictedRaw) {
86
- if (r.job === "*" && r.status === "unknown") {
87
- console.log(` ? ${r.workflow} :: workflow-level unknown: ${r.reason}`);
88
- }
89
- }
90
83
  console.log(ok ? "PASS" : "FAIL");
91
84
  process.exit(ok ? 0 : 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "willfire",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Predict the set of CI check entries GitHub Actions will create for a pull request",
5
5
  "license": "MIT",
6
6
  "author": "Kevin Scott <me@thekevinscott.com>",