willfire 0.1.7 → 0.1.9

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
@@ -20,10 +20,19 @@ pnpm add willfire
20
20
  import { predict } from "willfire";
21
21
  import { getOctokit } from "@actions/github"; // or new Octokit({ auth: token })
22
22
 
23
- const { entries, checkNames, skip } = await predict(getOctokit(token), "owner/repo", 123);
23
+ const { entries, checkNames, skip } = await predict(getOctokit(token), "owner/repo", 123, {
24
+ action: context.payload.action, // "opened" | "synchronize" | "reopened"
25
+ });
24
26
  // checkNames: sorted, deduped checkName of every entry with status "run"
25
27
  ```
26
28
 
29
+ `action` is optional but worth passing. Omitted, the event action is inferred
30
+ from the PR's commit count, which is wrong in both directions — a PR opened
31
+ from a branch with several commits looks like `synchronize`, a force-push down
32
+ to one commit looks like `opened` — and can never produce `reopened`. That only
33
+ matters to a workflow narrowing `types:`, where it decides whether the workflow
34
+ dispatches at all.
35
+
27
36
  `entries` is a union of two variants, both carrying `workflow` and `reason`:
28
37
 
29
38
  | variant | `job` | `checkName` | `status` |
@@ -58,7 +67,8 @@ Auth is any token with `contents: read`, `actions: read`, and
58
67
  ## CLI
59
68
 
60
69
  ```sh
61
- GH_TOKEN=... willfire --repo owner/repo --pr 123 [--json]
70
+ GH_TOKEN=... willfire --repo owner/repo --pr 123 \
71
+ [--action opened|synchronize|reopened] [--json]
62
72
  ```
63
73
 
64
74
  ## What it handles
@@ -124,7 +134,10 @@ the docs do not state:
124
134
 
125
135
  Scope notes: validated on `opened` pull_request events; `synchronize`/`labeled`
126
136
  live events, `branches-ignore`, and diffs far beyond 301 files are not yet
127
- probe-verified. Reusable-workflow name prefixing is probe-verified to three
137
+ probe-verified passing `action` explicitly is what makes probing the other
138
+ two possible. `action` accepts the three default `types:` only; `pull_request`
139
+ fires on around twenty actions, and a workflow narrowing to `ready_for_review`
140
+ or `edited` is out of scope either way. Reusable-workflow name prefixing is probe-verified to three
128
141
  levels; deeper nesting is inferred. The cross-repo probe calls back into the
129
142
  probe repo itself by full `owner/repo@ref` reference, so it pins ref
130
143
  resolution but not the owner/repo half of the address.
package/dist/predict.d.ts CHANGED
@@ -41,6 +41,11 @@ export interface WorkflowEntry extends EntryBase {
41
41
  * These can be genuinely undecidable statically — dynamic matrix, a reusable
42
42
  * workflow we cannot read, unresolvable `if`, or `needs` on any of those — so
43
43
  * `"unknown"` lives here and only here.
44
+ *
45
+ * `"no-dispatch"` is the mirror image, and lives on {@link WorkflowEntry} and
46
+ * only there. It is a verdict about whether the run happens at all, which is
47
+ * settled before any job is looked at — so by the time there is a job entry to
48
+ * label, the answer is already yes.
44
49
  */
45
50
  export interface JobEntry extends EntryBase {
46
51
  job: JobName;
@@ -54,7 +59,7 @@ export interface JobEntry extends EntryBase {
54
59
  * interpolates something we cannot evaluate ahead of the run.
55
60
  */
56
61
  checkName: string | null;
57
- status: "run" | "skipped" | "unknown" | "no-dispatch";
62
+ status: "run" | "skipped" | "unknown";
58
63
  }
59
64
  export type Entry = WorkflowEntry | JobEntry;
60
65
  /** Narrow to the workflow-level variant without inspecting the sentinel. */
@@ -74,6 +79,30 @@ export interface Prediction {
74
79
  export declare function patternToRegex(pat: string): RegExp;
75
80
  /** Order-sensitive match: last matching pattern wins; ! negates. */
76
81
  export declare function matchFilters(value: string, patterns: string[]): boolean;
82
+ /**
83
+ * The `pull_request` actions a caller can name.
84
+ *
85
+ * Deliberately the three default types and no more. `pull_request` fires on
86
+ * around twenty actions — `ready_for_review`, `edited`, `labeled` — and a
87
+ * workflow that narrows `types:` to one of those is not predictable today
88
+ * regardless of what is passed here. Widening this union later is a
89
+ * non-breaking change; narrowing it would not be, so it starts narrow.
90
+ */
91
+ export type PrEventAction = "opened" | "synchronize" | "reopened";
92
+ /** Options every caller may omit. */
93
+ export interface PredictOptions {
94
+ /**
95
+ * The literal event action the run was triggered by — `github.event.action`
96
+ * inside an Action, or `--action` on the CLI.
97
+ *
98
+ * Omitting it falls back to inferring from the commit count, which is a
99
+ * guess and is wrong in both directions: a PR opened from a branch with
100
+ * several commits looks like `synchronize`, a force-push down to one commit
101
+ * looks like `opened`, and `reopened` is never produced. That only matters
102
+ * to a workflow narrowing `types:`, where it matters completely.
103
+ */
104
+ action?: PrEventAction;
105
+ }
77
106
  export interface Ctx {
78
107
  action: string;
79
108
  baseRef: string;
@@ -148,5 +177,5 @@ export declare function parseUses(uses: string): UsesTarget | null;
148
177
  */
149
178
  export declare function expandWorkflowJobs(wf: Workflow, ctx: Ctx, fetchWorkflow: FetchWorkflow, source: WorkflowSource): Promise<ExpandedJob[]>;
150
179
  export declare function makeOctokit(): Octokit;
151
- export declare function predict(octokit: Octokit, repo: string, prNumber: number): Promise<Prediction>;
180
+ export declare function predict(octokit: Octokit, repo: string, prNumber: number, opts?: PredictOptions): Promise<Prediction>;
152
181
  export {};
package/dist/predict.js CHANGED
@@ -171,7 +171,10 @@ function expandMatrixDetailed(strategy) {
171
171
  }
172
172
  }
173
173
  combos.push(...extra);
174
- return combos.length > 0 ? combos : [null];
174
+ // Zero combinations is a real answer, not a missing one: an empty axis, or an
175
+ // `exclude` that removes everything, schedules no jobs at all. Only an absent
176
+ // `matrix:` key means "one unsuffixed job", and that returned above.
177
+ return combos;
175
178
  }
176
179
  /** Return list of matrix combination dicts, or null if dynamic. */
177
180
  export function expandMatrix(strategy) {
@@ -532,7 +535,7 @@ export function makeOctokit() {
532
535
  throw new Error("GH_TOKEN or GITHUB_TOKEN must be set");
533
536
  return new Octokit({ auth: token });
534
537
  }
535
- export async function predict(octokit, repo, prNumber) {
538
+ export async function predict(octokit, repo, prNumber, opts = {}) {
536
539
  const [owner, name] = repo.split("/");
537
540
  const base = { owner, repo: name };
538
541
  const { data: pr } = await octokit.rest.pulls.get({ ...base, pull_number: prNumber });
@@ -542,7 +545,9 @@ export async function predict(octokit, repo, prNumber) {
542
545
  per_page: 100,
543
546
  });
544
547
  const ctx = {
545
- action: pr.commits > 1 ? "synchronize" : "opened",
548
+ // The caller's answer wins whenever it has one. The commit-count fallback
549
+ // is a guess kept only so existing callers keep working.
550
+ action: opts.action ?? (pr.commits > 1 ? "synchronize" : "opened"),
546
551
  baseRef: pr.base.ref,
547
552
  files: files.map((f) => f.filename),
548
553
  };
@@ -659,6 +664,8 @@ function finalizePrediction(entries, skip) {
659
664
  return { entries: final, checkNames: [...names].sort(), skip };
660
665
  }
661
666
  // ------------------------------------------------------------------------ CLI
667
+ const USAGE = "usage: predict --repo owner/name --pr N [--action opened|synchronize|reopened] [--json]";
668
+ const isPrEventAction = (v) => v === "opened" || v === "synchronize" || v === "reopened";
662
669
  function parseArgs(argv) {
663
670
  const get = (flag) => {
664
671
  const i = argv.indexOf(flag);
@@ -667,15 +674,26 @@ function parseArgs(argv) {
667
674
  const repo = get("--repo");
668
675
  const pr = get("--pr");
669
676
  if (!repo || !pr) {
670
- console.error("usage: predict --repo owner/name --pr N [--json]");
677
+ console.error(USAGE);
671
678
  process.exit(2);
672
679
  }
673
- return { repo, pr: Number(pr), json: argv.includes("--json") };
680
+ // An unrecognised action is refused rather than ignored. Silently falling
681
+ // back to the guess would turn a typo into a wrong prediction, which is the
682
+ // failure this flag exists to remove.
683
+ const action = get("--action");
684
+ if (action !== undefined && !isPrEventAction(action)) {
685
+ console.error(`unknown --action: ${action}`);
686
+ console.error(USAGE);
687
+ process.exit(2);
688
+ }
689
+ return { repo, pr: Number(pr), json: argv.includes("--json"), action };
674
690
  }
675
691
  const isMain = /predict\.(ts|js)$|\/willfire$/.test(process.argv[1] ?? "");
676
692
  if (isMain) {
677
693
  const args = parseArgs(process.argv.slice(2));
678
- const { entries, checkNames, skip } = await predict(makeOctokit(), args.repo, args.pr);
694
+ const { entries, checkNames, skip } = await predict(makeOctokit(), args.repo, args.pr, {
695
+ action: args.action,
696
+ });
679
697
  if (args.json) {
680
698
  console.log(JSON.stringify({ entries, checkNames, skip }, null, 2));
681
699
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "willfire",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Predict the set of CI check entries GitHub Actions will create for a pull request",
5
5
  "license": "MIT",
6
6
  "packageManager": "pnpm@10.33.0",