npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

willfire

v0.1.12

Published

Predict the set of CI check entries GitHub Actions will create for a pull request

Readme

willfire

Predicts the set of CI check entries GitHub Actions will create for a pull request — before (or without) the runs happening.

GitHub's dispatch decision is server-side and unpublished: no API tells you which workflows will fire for a PR after branch/path/type filters, or which job entries they expand into. willfire evaluates the workflow files statically against the PR's base branch, changed files, and head commit message.

Install

pnpm add willfire

Library

import { predict } from "willfire";
import { getOctokit } from "@actions/github"; // or new Octokit({ auth: token })

const { entries, checkNames, skip, sources } = await predict(
  getOctokit(token),
  "owner/repo",
  123,
  { action: context.payload.action }, // "opened" | "synchronize" | "reopened"
);
// checkNames: sorted, deduped checkName of every entry with status "run"
// sources: every repo read, and the commit each ref resolved to

action is optional but worth passing. Omitted, the event action is inferred from the PR's commit count, which is wrong in both directions — a PR opened from a branch with several commits looks like synchronize, a force-push down to one commit looks like opened — and can never produce reopened. That only matters to a workflow narrowing types:, where it decides whether the workflow dispatches at all.

entries is a union of two variants, both carrying workflow and reason:

| variant | job | checkName | status | | --- | --- | --- | --- | | WorkflowEntry | "*" | always null | "run" \| "skipped" \| "no-dispatch" | | JobEntry | the job id | the check name, or null | "run" \| "skipped" \| "unknown" \| "no-dispatch" |

"unknown" is job-level only: every workflow-level verdict is decidable, so a WorkflowEntry cannot express one. Narrow with the exported isWorkflowEntry and isJobEntry guards rather than testing the "*" sentinel yourself.

checkName is the name GitHub actually puts on the check — name: override, matrix parenthetical, and <caller> / <callee> prefixing for reusable workflows all applied. That is the unit required status checks key on, so it is the one worth comparing against. On a JobEntry it is null only where no single name is knowable ahead of the run:

  • a matrix computed at runtime (fromJSON of another job's output) whose outputs were not supplied, reported as one unknown entry for that job and nothing else — see "Supplying job outputs" below;
  • a reusable workflow we cannot read — private, deleted, a ref that does not exist, a uses: built from an expression, or one nested past GitHub's four-level limit;
  • a name: interpolating something we cannot evaluate statically.

sources is the provenance of the answer: the PR's own repo at the head commit, then one entry per cross-repo uses: that was read, each carrying both the ref the workflow wrote and the sha it resolved to. A ref is resolved before the file behind it is read, so the commit named is the commit used. v0 is a tag someone moves; without the sha, "willfire predicted these checks" is not a claim that can be checked against the run afterwards.

A ref that will not resolve is not a source. Its callee is never read, the jobs behind it come back unknown, and nothing falls back to reading the mutable ref.

Duplicate names in checkNames are not possible (it is a set), but duplicate check names are — GitHub happily creates two identically named checks when a matrix job's name: does not vary per combination. entries shows them.

Auth is any token with contents: read, actions: read, and pull-requests: read — inside an action, the workflow's GITHUB_TOKEN.

CLI

GH_TOKEN=... willfire --repo owner/repo --pr 123 \
  [--action opened|synchronize|reopened] [--json] \
  [--execute owner/repo:job1,job2]...

Plain-text output is one line per entry, then a # read owner/repo@ref -> sha line per source. --json prints the whole Prediction, sources included.

What it handles

Path filters (paths, paths-ignore, order-sensitive ! negation), branch filters, event types, combined filters, [skip ci] and friends, disabled workflows, multi-job workflows, static matrix expansion (including exclude/include), needs skip-propagation, job-level if, and reusable workflows — both the local ./.github/workflows/x.yml form and the cross-repo owner/repo/.github/workflows/x.yml@ref form, whose ref is resolved to a commit and the callee then read at that commit. Jobs whose if is false are predicted as skipped entries, matching how they appear in the checks UI.

Things that cannot be known statically are reported as unknown rather than guessed.

Supplying job outputs

A matrix built from another job's output is the common way a workflow decides its own check names:

strategy:
  matrix:
    language: ${{ fromJSON(needs.detect.outputs.coverage_languages) }}

Given those outputs, willfire expands it. expandWorkflowJobs takes a scope whose needs maps a job id to its outputs:

await expandWorkflowJobs(wf, ctx, fetchWorkflow, source, {
  needs: { detect: { outputs: { coverage_languages: '["typescript"]' } } },
});

Values are raw strings — what a step wrote to $GITHUB_OUTPUT, and what the runner substitutes. Parsing them here would break the guards written against them: != '[]' compares a string to a string, and an array on the left makes it unknown. fromJSON is the only thing that turns one into a structure.

outputs must be the job's complete output set, because a key absent from it reads as the empty string — the same answer the runner gives for an output no step wrote. A job you know nothing about belongs left out entirely; every lookup against it then stays unknown.

needs is workflow-scoped and is not inherited across a reusable-workflow call: a callee's needs.detect is the callee's own job.

Nothing in willfire works out what those outputs are on its own. predict supplies none unless the caller grants execution — see below — so a dynamic matrix stays unknown by default.

Executing granted jobs

The job those outputs come from is usually a few shell steps over the checked out tree — cheap to run for real. predict will do that, but only for jobs the caller names:

await predict(octokit, "owner/repo", 123, {
  execute: [{ repo: "the-org/conventions", jobs: ["detect"] }],
});
willfire --repo owner/repo --pr 123 \
  --execute the-org/conventions:detect

A grant names the repo the workflow file lives in — for a reusable workflow, the callee — and the job ids within it. Before expansion reads needs, each granted job that is predicted to run is executed: the PR's head tree is materialized from a tarball, the job's steps run in order under their declared shell and env, step-level if: guards are evaluated, composite actions are fetched at their pinned commit and recursed into, and a bare actions/checkout is satisfied by the tree already present. What the steps write to $GITHUB_OUTPUT becomes the job's outputs, exactly as if they had been supplied by hand.

Execution is mechanism, not policy: willfire knows nothing about any repo, and with no grants nothing runs. The steps execute for real — nothing interprets or approximates shell — so grant only jobs whose code you trust at the commit being predicted; a granted job runs the PR's version of itself.

Anything execution cannot do faithfully fails the grant rather than guessing: a JavaScript or Docker action, a checkout with inputs, a matrix'd or containerized granted job, an undecidable step if:, a non-zero exit, output willfire cannot parse. The failure does not change any verdict — entries that needed the outputs stay unknown, with the reason threaded through:

dynamic matrix; executing 'detect' failed: step 'scan': exited 1 (...)

Development

pnpm install
pnpm typecheck        # tsc over src/, tests included
pnpm test             # vitest, once
pnpm test:coverage    # the 100% floor CI enforces

Unit tests are colocated with their source (foo.tsfoo.test.ts) at 100% coverage, per the testing-conventions standard. .github/workflows/conventions.yml enforces that in CI; testing-conventions.toml holds the thresholds.

Verification

The unit suite fixes the behavior. What fixes the expectations is the probe: predictions are verified against real GitHub behavior, not just the docs. willrun-probe holds one workflow per dispatch rule, and probe PRs exercise each complication (docs-only diffs, negation edges, [skip ci], a 301-file diff, PRs into non-default branches). The verify script diffs predictions against the check entries GitHub actually created. All probe PRs currently pass exactly.

Check-name resolution is verified the same way, on probe PRs #8 and #9. pnpm test replays those probe workflows through the resolver and asserts the exact job names the live run produced. Several of the rules it pins are ones the docs do not state:

  • a name: containing any ${{ }} expression suppresses the matrix parenthetical, even an expression that never reads the matrix — a literal name: still gets one, so name: Static Label over a: [x, y] yields Static Label (x) and Static Label (y);
  • keys an include entry merges into an existing combination do not appear in the name, but keys of a combination the include created from scratch do;
  • object matrix values flatten to their own values, so {os: linux, arch: x64} renders as (linux, x64);
  • a skipped job is never set up, so its matrix does not expand and its name: is not interpolated: one check, expression text and all;
  • a cross-repo uses: names the same <caller> / <callee> checks a local one does, at whichever tag, branch, or SHA the @ref pinned — and a relative uses: ./... inside that callee resolves against the callee's repo and ref, not the caller's. Probe PR #9 settles the second one: the caller's copy of the callee's file was present at head, under a different job name, and GitHub did not use it.

Scope notes: validated on opened pull_request events; synchronize/labeled live events, branches-ignore, and diffs far beyond 301 files are not yet probe-verified — passing action explicitly is what makes probing the other two possible. action accepts the three default types: only; pull_request fires on around twenty actions, and a workflow narrowing to ready_for_review or edited is out of scope either way. Reusable-workflow name prefixing is probe-verified to three levels; deeper nesting is inferred. The cross-repo probe calls back into the probe repo itself by full owner/repo@ref reference, so it pins ref resolution but not the owner/repo half of the address.