willfire 0.1.18 → 0.1.20
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 +44 -45
- package/dist/cli/parseArgs.d.ts +0 -2
- package/dist/cli/parseArgs.js +2 -21
- package/dist/cli.js +0 -1
- package/dist/execute.d.ts +44 -72
- package/dist/execute.js +232 -121
- package/dist/index.d.ts +1 -0
- package/dist/jobs/expandJobs.d.ts +1 -1
- package/dist/jobs/expandJobs.js +56 -36
- package/dist/matrix/expandMatrixDetailed.js +23 -3
- package/dist/names/lookupPath.js +2 -1
- package/dist/names/renderName.js +2 -1
- package/dist/predict/makeLiveExecutor.d.ts +20 -0
- package/dist/predict/makeLiveExecutor.js +38 -0
- package/dist/predict/predict.js +5 -31
- package/dist/sandbox.d.ts +35 -0
- package/dist/sandbox.js +130 -0
- package/dist/triggers/getPrTrigger.js +8 -4
- package/dist/triggers/workflowDispatches.js +2 -1
- package/dist/types.d.ts +5 -10
- package/dist/uses/parseUses.js +8 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,9 +54,9 @@ workflows all applied. That is the unit required status checks key on, so it
|
|
|
54
54
|
is the one worth comparing against. On a `JobEntry` it is `null` only where no
|
|
55
55
|
single name is knowable ahead of the run:
|
|
56
56
|
|
|
57
|
-
- a matrix computed at runtime (`fromJSON` of another job's output)
|
|
58
|
-
|
|
59
|
-
nothing else — see "
|
|
57
|
+
- a matrix computed at runtime (`fromJSON` of another job's output) that
|
|
58
|
+
execution could not resolve, reported as one `unknown` entry for that job
|
|
59
|
+
and nothing else — see "Executing needed jobs" below;
|
|
60
60
|
- a reusable workflow we cannot read — private, deleted, a ref that does not
|
|
61
61
|
exist, a `uses:` built from an expression, or one nested past GitHub's
|
|
62
62
|
four-level limit;
|
|
@@ -84,8 +84,7 @@ Auth is any token with `contents: read`, `actions: read`, and
|
|
|
84
84
|
|
|
85
85
|
```sh
|
|
86
86
|
GH_TOKEN=... willfire --repo owner/repo --pr 123 \
|
|
87
|
-
[--action opened|synchronize|reopened] [--json]
|
|
88
|
-
[--execute owner/repo:job1,job2]...
|
|
87
|
+
[--action opened|synchronize|reopened] [--json]
|
|
89
88
|
```
|
|
90
89
|
|
|
91
90
|
Plain-text output is one line per entry, then a `# read owner/repo@ref -> sha`
|
|
@@ -102,8 +101,9 @@ workflows — both the local `./.github/workflows/x.yml` form and the cross-repo
|
|
|
102
101
|
commit and the callee then read at that commit. Jobs whose `if` is false are
|
|
103
102
|
predicted as `skipped` entries, matching how they appear in the checks UI.
|
|
104
103
|
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
A matrix computed by another job's outputs is not static; willfire resolves it
|
|
105
|
+
by running that job in a sandbox — see "Executing needed jobs". Everything
|
|
106
|
+
else that cannot be known is reported as `unknown` rather than guessed.
|
|
107
107
|
|
|
108
108
|
## Supplying job outputs
|
|
109
109
|
|
|
@@ -138,47 +138,46 @@ lookup against it then stays unknown.
|
|
|
138
138
|
`needs` is workflow-scoped and is not inherited across a reusable-workflow
|
|
139
139
|
call: a callee's `needs.detect` is the callee's own job.
|
|
140
140
|
|
|
141
|
-
Nothing
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
Nothing here guesses what those outputs are. By default `predict` computes
|
|
142
|
+
them, by executing the jobs that produce them — the section below. With
|
|
143
|
+
execution turned off (`executor: null` in `PredictOptions`) a dynamic matrix
|
|
144
|
+
stays `unknown`.
|
|
144
145
|
|
|
145
|
-
## Executing
|
|
146
|
+
## Executing needed jobs
|
|
146
147
|
|
|
147
148
|
The job those outputs come from is usually a few shell steps over the checked
|
|
148
|
-
out tree — cheap to run for real. `predict`
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
the
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
willfire cannot parse. The failure does not change any verdict — entries that
|
|
181
|
-
needed the outputs stay `unknown`, with the reason threaded through:
|
|
149
|
+
out tree — cheap to run for real. `predict` does that, and works out for
|
|
150
|
+
itself which jobs are worth it: a job is executed exactly when some sibling
|
|
151
|
+
job reads its `needs.<id>.outputs`. That fact is written in the workflow being
|
|
152
|
+
predicted, so there is nothing to configure and no repo knowledge in willfire.
|
|
153
|
+
|
|
154
|
+
Each selected job that is predicted to run is executed before expansion reads
|
|
155
|
+
`needs`: the PR's head tree is materialized from a tarball, the job's steps
|
|
156
|
+
run in order under their declared shell and env, step-level `if:` guards are
|
|
157
|
+
evaluated, composite actions are fetched at their pinned commit and recursed
|
|
158
|
+
into, and a bare `actions/checkout` is satisfied by the tree already present.
|
|
159
|
+
A checkout asking for history (`fetch-depth: 0`) switches the workspace to a
|
|
160
|
+
real clone at the same commit. What the steps write to `$GITHUB_OUTPUT`
|
|
161
|
+
becomes the job's outputs, exactly as if they had been supplied by hand.
|
|
162
|
+
|
|
163
|
+
The steps execute for real — nothing interprets or approximates shell — and
|
|
164
|
+
the code that runs is the PR's own version of itself. What makes that safe to
|
|
165
|
+
do by default is the sandbox: steps run in a docker container with no network,
|
|
166
|
+
none of the host's environment, no credentials, an unprivileged user, and a
|
|
167
|
+
read-only image (`node:24-slim` plus git and python3), with only the workspace tree
|
|
168
|
+
writable. Docker is the one runtime requirement; where it is missing,
|
|
169
|
+
execution fails and the entries that needed it stay `unknown` with the reason
|
|
170
|
+
attached.
|
|
171
|
+
|
|
172
|
+
Node actions (`runs.using: node24`) run under the sandbox's node with their
|
|
173
|
+
`inputs:` bound as `INPUT_*` variables; `actions/setup-node` is satisfied as a
|
|
174
|
+
no-op when it asks for exactly the node the sandbox has. Anything execution
|
|
175
|
+
cannot do faithfully fails rather than guessing: a Docker action, a node
|
|
176
|
+
action wanting another major, a checkout with inputs beyond `fetch-depth`, a
|
|
177
|
+
matrix'd or containerized job, an undecidable step `if:`, a non-zero exit,
|
|
178
|
+
output willfire cannot parse. The failure does not change any verdict —
|
|
179
|
+
entries that needed the outputs stay `unknown`, with the reason threaded
|
|
180
|
+
through:
|
|
182
181
|
|
|
183
182
|
```
|
|
184
183
|
dynamic matrix; executing 'detect' failed: step 'scan': exited 1 (...)
|
package/dist/cli/parseArgs.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { type ExecutionGrant } from "../execute.js";
|
|
2
1
|
import type { PrEventAction } from "../types.js";
|
|
3
2
|
export declare function parseArgs(argv: string[]): {
|
|
4
3
|
repo: string;
|
|
5
4
|
pr: number;
|
|
6
5
|
json: boolean;
|
|
7
6
|
action?: PrEventAction;
|
|
8
|
-
execute: ExecutionGrant[];
|
|
9
7
|
};
|
package/dist/cli/parseArgs.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
const USAGE = "usage: predict --repo owner/name --pr N [--action opened|synchronize|reopened]" +
|
|
3
|
-
" [--execute owner/repo:job1,job2]... [--json]";
|
|
1
|
+
const USAGE = "usage: predict --repo owner/name --pr N [--action opened|synchronize|reopened] [--json]";
|
|
4
2
|
const isPrEventAction = (v) => v === "opened" || v === "synchronize" || v === "reopened";
|
|
5
3
|
export function parseArgs(argv) {
|
|
6
4
|
const get = (flag) => {
|
|
@@ -22,22 +20,5 @@ export function parseArgs(argv) {
|
|
|
22
20
|
console.error(USAGE);
|
|
23
21
|
process.exit(2);
|
|
24
22
|
}
|
|
25
|
-
|
|
26
|
-
// reason a bad --action is: silently dropping it would predict without the
|
|
27
|
-
// execution the caller thought they asked for.
|
|
28
|
-
const execute = [];
|
|
29
|
-
for (let i = 0; i < argv.length; i++) {
|
|
30
|
-
if (argv[i] !== "--execute") {
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
const spec = argv[i + 1];
|
|
34
|
-
const grant = spec == null ? null : parseGrant(spec);
|
|
35
|
-
if (grant == null) {
|
|
36
|
-
console.error(`bad --execute: ${spec}`);
|
|
37
|
-
console.error(USAGE);
|
|
38
|
-
process.exit(2);
|
|
39
|
-
}
|
|
40
|
-
execute.push(grant);
|
|
41
|
-
}
|
|
42
|
-
return { repo, pr: Number(pr), json: argv.includes("--json"), action, execute };
|
|
23
|
+
return { repo, pr: Number(pr), json: argv.includes("--json"), action };
|
|
43
24
|
}
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,6 @@ if (isMain) {
|
|
|
13
13
|
const args = parseArgs(process.argv.slice(2));
|
|
14
14
|
const prediction = await predict(makeOctokit(), args.repo, args.pr, {
|
|
15
15
|
action: args.action,
|
|
16
|
-
execute: args.execute,
|
|
17
16
|
});
|
|
18
17
|
const { entries, skip, sources } = prediction;
|
|
19
18
|
if (args.json) {
|
package/dist/execute.d.ts
CHANGED
|
@@ -1,76 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Execute a job
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* writes what it finds to `$GITHUB_OUTPUT`. So this module runs that job the
|
|
8
|
-
* way the runner would: materialize the tree at the pinned commit, walk the
|
|
9
|
-
* steps in order, execute each `run:` under its declared shell and env, and
|
|
10
|
-
* assemble the job's `outputs:` map from what the steps actually wrote.
|
|
11
|
-
*
|
|
12
|
-
* Three rules keep this honest:
|
|
13
|
-
*
|
|
14
|
-
* 1. **Nothing runs without a grant.** willfire has no opinion about which
|
|
15
|
-
* jobs are safe to execute; the caller names them, one repo and job id at
|
|
16
|
-
* a time, and everything else stays as unresolved as it was.
|
|
17
|
-
* 2. **Run it, never interpret it.** The `run:` script is handed to the shell
|
|
18
|
-
* the step declares, with the env it declares. What lands in
|
|
19
|
-
* `$GITHUB_OUTPUT` is the answer; no shell text is ever parsed for meaning.
|
|
20
|
-
* 3. **Anything off the modelled path is a hard stop with a reason.** A
|
|
21
|
-
* JavaScript action, an undecidable `if:`, a `${{ }}` that will not
|
|
22
|
-
* resolve, a step that exits non-zero — each fails the execution and says
|
|
23
|
-
* what it hit, and the consumers of that job's outputs stay unresolved.
|
|
24
|
-
* Guessing is the one move this module never makes.
|
|
25
|
-
*
|
|
26
|
-
* `actions/checkout` is the deliberate exception to rule 2. It is provided by
|
|
27
|
-
* the runner, not run from its repo, and its whole postcondition — the
|
|
28
|
-
* workspace tree at the commit under test — is something the executor has
|
|
29
|
-
* already satisfied by materializing the tree. A bare checkout is therefore
|
|
30
|
-
* recorded as done; a checkout *with inputs* is not modelled and stops.
|
|
2
|
+
* Execute a job whose outputs another job reads, the way the runner would:
|
|
3
|
+
* materialize the tree, walk the steps, read what they wrote to
|
|
4
|
+
* `$GITHUB_OUTPUT`. Two invariants: run it, never interpret it (no shell text
|
|
5
|
+
* is parsed for meaning), and anything off the modelled path is a hard stop
|
|
6
|
+
* with a reason — never a guess.
|
|
31
7
|
*/
|
|
32
8
|
import { type Scope } from "./expr/val.js";
|
|
33
9
|
import type { ResolveRef, WorkflowSource } from "./types.js";
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* calling `testing-conventions/.github/workflows/testing-conventions.yml@v0`,
|
|
39
|
-
* that is `thekevinscott/testing-conventions`, whatever repo the PR is on.
|
|
40
|
-
* The grant is deliberately this narrow: a job id alone would execute
|
|
41
|
-
* whatever any transitively-reached workflow happens to call by that name.
|
|
42
|
-
*/
|
|
43
|
-
export interface ExecutionGrant {
|
|
44
|
-
/** `owner/name` of the repo whose workflow defines the jobs. */
|
|
45
|
-
repo: string;
|
|
46
|
-
/** Job ids within that repo's workflows that may be executed. */
|
|
47
|
-
jobs: string[];
|
|
10
|
+
/** A host path a sandboxed runner must expose inside, at the same path. */
|
|
11
|
+
export interface Mount {
|
|
12
|
+
path: string;
|
|
13
|
+
writable: boolean;
|
|
48
14
|
}
|
|
49
|
-
/** `owner/repo:job1,job2` as the CLI spells a grant. */
|
|
50
|
-
export declare function parseGrant(spec: string): ExecutionGrant | null;
|
|
51
15
|
/** One shell invocation, fully specified — nothing is inherited implicitly. */
|
|
52
16
|
export interface RunSpec {
|
|
53
17
|
script: string;
|
|
54
18
|
shell: "bash" | "sh";
|
|
55
19
|
cwd: string;
|
|
56
20
|
env: Record<string, string>;
|
|
21
|
+
/** For runners that isolate: what of the host this run may see. A direct
|
|
22
|
+
* shell ignores this — it already sees everything. */
|
|
23
|
+
mounts?: Mount[];
|
|
57
24
|
}
|
|
58
25
|
export interface RunResult {
|
|
59
26
|
code: number;
|
|
60
|
-
/** Captured so a failing step can say *why* in its reason. */
|
|
61
27
|
stderr: string;
|
|
62
28
|
}
|
|
63
29
|
export type RunCommand = (spec: RunSpec) => Promise<RunResult>;
|
|
64
30
|
/**
|
|
65
|
-
* Materialize a repo tree at a commit
|
|
66
|
-
*
|
|
31
|
+
* Materialize a repo tree at a commit, or null when it cannot be had. Must not
|
|
32
|
+
* throw. `history: true` demands full git history (the `fetch-depth: 0`
|
|
33
|
+
* postcondition); a provider that cannot supply it answers null.
|
|
67
34
|
*/
|
|
68
|
-
export type ProvideTree = (source: WorkflowSource
|
|
69
|
-
|
|
35
|
+
export type ProvideTree = (source: WorkflowSource, opts?: {
|
|
36
|
+
history?: boolean;
|
|
37
|
+
}) => Promise<string | null>;
|
|
70
38
|
export interface ExecDeps {
|
|
71
39
|
provideTree: ProvideTree;
|
|
72
40
|
runCommand: RunCommand;
|
|
73
41
|
resolveRef: ResolveRef;
|
|
42
|
+
/** The node major `runCommand`'s world provides; asking for another is refused. */
|
|
43
|
+
nodeMajor: number;
|
|
74
44
|
}
|
|
75
45
|
export type ExecOutcome = {
|
|
76
46
|
ok: true;
|
|
@@ -80,44 +50,46 @@ export type ExecOutcome = {
|
|
|
80
50
|
reason: string;
|
|
81
51
|
};
|
|
82
52
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* yields. Step-level guards inside the job are evaluated here, against the
|
|
86
|
-
* fixed facts of the run (notably `github.repository`, which the fleet's
|
|
87
|
-
* hermetic-vs-published guards are written against).
|
|
53
|
+
* The caller decides *whether* a job runs; the executor only decides what
|
|
54
|
+
* running it yields.
|
|
88
55
|
*/
|
|
89
56
|
export interface JobExecutor {
|
|
90
|
-
granted(source: WorkflowSource, jobId: string): boolean;
|
|
91
57
|
executeJob(jobId: string, job: any, wf: any, scope: Scope): Promise<ExecOutcome>;
|
|
92
58
|
}
|
|
93
59
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
|
|
97
|
-
|
|
60
|
+
* Render every `${{ }}` to literal text, or null when any cannot be settled —
|
|
61
|
+
* a partial render would be a different program.
|
|
62
|
+
*/
|
|
63
|
+
export declare function renderTemplate(text: string, scope: Scope): string | null;
|
|
64
|
+
/**
|
|
65
|
+
* `name=value` lines or `name<<DELIMITER` heredocs. Anything else fails the
|
|
66
|
+
* parse, as the runner fails the step on a malformed line.
|
|
98
67
|
*/
|
|
99
68
|
export declare function parseGithubOutput(text: string): Record<string, string> | null;
|
|
100
69
|
export declare function makeExecutor(opts: {
|
|
101
|
-
grants: ExecutionGrant[];
|
|
102
70
|
/**
|
|
103
|
-
* The PR's own repo at the head commit
|
|
104
|
-
*
|
|
105
|
-
* workflow's jobs run in the caller's workspace; this is that fact.
|
|
71
|
+
* The PR's own repo at the head commit. A reusable workflow's jobs run in
|
|
72
|
+
* the caller's workspace, wherever the workflow file lives.
|
|
106
73
|
*/
|
|
107
74
|
workspace: WorkflowSource;
|
|
108
75
|
deps: ExecDeps;
|
|
109
76
|
}): JobExecutor;
|
|
110
77
|
/**
|
|
111
|
-
* The runner's default shell invocations, faithfully
|
|
112
|
-
*
|
|
113
|
-
* leaks in beyond what the spec names.
|
|
78
|
+
* The runner's default shell invocations, faithfully. Nothing of the parent
|
|
79
|
+
* environment leaks in beyond what the spec names.
|
|
114
80
|
*/
|
|
115
81
|
export declare const runShell: RunCommand;
|
|
116
82
|
/**
|
|
117
|
-
* Materialize repo trees from tarballs, one download per commit
|
|
118
|
-
*
|
|
119
|
-
* `owner-repo-shortsha/` directory, which is unwrapped so callers get the
|
|
120
|
-
* tree root itself. Extraction shells out to `tar` through the same
|
|
121
|
-
* `RunCommand` seam every other subprocess uses.
|
|
83
|
+
* Materialize repo trees from tarballs, one download per commit. GitHub wraps
|
|
84
|
+
* the tree in a single `owner-repo-shortsha/` directory, unwrapped here.
|
|
122
85
|
*/
|
|
123
86
|
export declare function makeTreeProvider(download: (source: WorkflowSource) => Promise<Uint8Array | null>, runCommand: RunCommand): ProvideTree;
|
|
87
|
+
/**
|
|
88
|
+
* Materialize repo trees by full clone, on the host — it needs the network
|
|
89
|
+
* the sandbox denies. The token never touches the URL or persisted git
|
|
90
|
+
* config, because `.git/config` later rides into the sandbox: auth travels
|
|
91
|
+
* as a per-invocation `http.extraheader` and is gone when the command is.
|
|
92
|
+
*/
|
|
93
|
+
export declare function makeCloneProvider(runCommand: RunCommand, token: string | null, opts?: {
|
|
94
|
+
remoteUrl?: (source: WorkflowSource) => string;
|
|
95
|
+
}): ProvideTree;
|