willfire 0.1.21 → 0.1.23
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 +4 -3
- package/dist/cli/isPrEventAction.d.ts +2 -0
- package/dist/cli/isPrEventAction.js +1 -0
- package/dist/cli/parseArgs.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/predict/index.d.ts +2 -1
- package/dist/predict/index.js +1 -1
- package/dist/predict/makeGithubClient.d.ts +108 -0
- package/dist/predict/makeGithubClient.js +96 -0
- package/dist/predict/makeLiveExecutor.d.ts +2 -2
- package/dist/predict/makeLiveExecutor.js +4 -3
- package/dist/predict/predict.d.ts +2 -2
- package/dist/predict/predict.js +9 -10
- package/dist/predict/stackTargetRef.d.ts +2 -2
- package/dist/predict/stackTargetRef.js +4 -4
- package/dist/sandbox/imageTag.d.ts +2 -0
- package/dist/sandbox/imageTag.js +6 -0
- package/dist/sandbox/index.d.ts +4 -0
- package/dist/sandbox/index.js +4 -0
- package/dist/sandbox/makeSandboxRunner.d.ts +15 -0
- package/dist/sandbox/makeSandboxRunner.js +44 -0
- package/dist/sandbox/runDocker.d.ts +4 -0
- package/dist/sandbox/runDocker.js +26 -0
- package/dist/sandbox/sandboxArgv.d.ts +8 -0
- package/dist/sandbox/sandboxArgv.js +41 -0
- package/dist/sandbox/sandboxConfig.d.ts +13 -0
- package/dist/sandbox/sandboxConfig.js +18 -0
- package/dist/verify.js +2 -2
- package/package.json +1 -2
- package/dist/predict/makeOctokit.d.ts +0 -2
- package/dist/predict/makeOctokit.js +0 -8
- package/dist/sandbox.d.ts +0 -35
- package/dist/sandbox.js +0 -130
package/README.md
CHANGED
|
@@ -17,11 +17,12 @@ pnpm add willfire
|
|
|
17
17
|
## Library
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
|
-
import { predict } from "willfire";
|
|
21
|
-
import { getOctokit } from "@actions/github"; // or new Octokit({ auth: token })
|
|
20
|
+
import { makeGithubClient, predict } from "willfire";
|
|
22
21
|
|
|
22
|
+
// makeGithubClient reads GH_TOKEN or GITHUB_TOKEN from the environment; any
|
|
23
|
+
// object satisfying the exported GithubClient interface works in its place.
|
|
23
24
|
const { entries, checkNames, skip, sources } = await predict(
|
|
24
|
-
|
|
25
|
+
makeGithubClient(),
|
|
25
26
|
"owner/repo",
|
|
26
27
|
123,
|
|
27
28
|
{ action: context.payload.action }, // "opened" | "synchronize" | "reopened"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const isPrEventAction = (v) => v === "opened" || v === "synchronize" || v === "reopened";
|
package/dist/cli/parseArgs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { isPrEventAction } from "./isPrEventAction.js";
|
|
1
2
|
const USAGE = "usage: predict --repo owner/name --pr N [--action opened|synchronize|reopened] [--json]";
|
|
2
|
-
const isPrEventAction = (v) => v === "opened" || v === "synchronize" || v === "reopened";
|
|
3
3
|
export function parseArgs(argv) {
|
|
4
4
|
const get = (flag) => {
|
|
5
5
|
const i = argv.indexOf(flag);
|
package/dist/cli.js
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
// GITHUB_TOKEN.
|
|
7
7
|
import { parseArgs } from "./cli/parseArgs.js";
|
|
8
8
|
import { isWorkflowEntry } from "./entries/isWorkflowEntry.js";
|
|
9
|
-
import {
|
|
9
|
+
import { makeGithubClient } from "./predict/makeGithubClient.js";
|
|
10
10
|
import { predict } from "./predict/predict.js";
|
|
11
11
|
const isMain = /cli\.(ts|js)$|\/willfire$/.test(process.argv[1] ?? "");
|
|
12
12
|
if (isMain) {
|
|
13
13
|
const args = parseArgs(process.argv.slice(2));
|
|
14
|
-
const prediction = await predict(
|
|
14
|
+
const prediction = await predict(makeGithubClient(), args.repo, args.pr, {
|
|
15
15
|
action: args.action,
|
|
16
16
|
});
|
|
17
17
|
const { entries, skip, sources } = prediction;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { patternToRegex, matchFilters } from "./filters/index.js";
|
|
|
3
3
|
export { expandMatrix } from "./matrix/index.js";
|
|
4
4
|
export { evalIf, expandWorkflowJobs } from "./jobs/index.js";
|
|
5
5
|
export { parseUses } from "./uses/index.js";
|
|
6
|
-
export {
|
|
6
|
+
export { makeGithubClient, predict } from "./predict/index.js";
|
|
7
|
+
export type { GithubClient } from "./predict/index.js";
|
|
7
8
|
export type { ExecOutcome, JobExecutor } from "./execute.js";
|
|
8
9
|
export type { JobName, WorkflowEntry, JobEntry, Entry, Prediction, PrEventAction, PredictOptions, Ctx, ExpandedJob, SourceRef, WorkflowSource, FetchWorkflow, ResolveRef, WorkflowReader, UsesTarget, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -3,4 +3,4 @@ export { patternToRegex, matchFilters } from "./filters/index.js";
|
|
|
3
3
|
export { expandMatrix } from "./matrix/index.js";
|
|
4
4
|
export { evalIf, expandWorkflowJobs } from "./jobs/index.js";
|
|
5
5
|
export { parseUses } from "./uses/index.js";
|
|
6
|
-
export {
|
|
6
|
+
export { makeGithubClient, predict } from "./predict/index.js";
|
package/dist/predict/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { makeGithubClient } from "./makeGithubClient.js";
|
|
2
|
+
export type { GithubClient } from "./makeGithubClient.js";
|
|
2
3
|
export { sourceKey } from "./sourceKey.js";
|
|
3
4
|
export { stackTargetRef } from "./stackTargetRef.js";
|
|
4
5
|
export { finalizePrediction } from "./finalizePrediction.js";
|
package/dist/predict/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { makeGithubClient } from "./makeGithubClient.js";
|
|
2
2
|
export { sourceKey } from "./sourceKey.js";
|
|
3
3
|
export { stackTargetRef } from "./stackTargetRef.js";
|
|
4
4
|
export { finalizePrediction } from "./finalizePrediction.js";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
interface RepoParams {
|
|
2
|
+
owner: string;
|
|
3
|
+
repo: string;
|
|
4
|
+
}
|
|
5
|
+
interface PageParams {
|
|
6
|
+
per_page?: number;
|
|
7
|
+
page?: number;
|
|
8
|
+
}
|
|
9
|
+
/** Response types carry only the fields willfire actually reads. */
|
|
10
|
+
export interface GithubPullSummary {
|
|
11
|
+
base: {
|
|
12
|
+
ref: string;
|
|
13
|
+
};
|
|
14
|
+
merge_commit_sha: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface GithubPull extends GithubPullSummary {
|
|
17
|
+
commits: number;
|
|
18
|
+
head: {
|
|
19
|
+
sha: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export interface GithubPullFile {
|
|
23
|
+
filename: string;
|
|
24
|
+
}
|
|
25
|
+
export interface GithubCommit {
|
|
26
|
+
sha: string;
|
|
27
|
+
commit: {
|
|
28
|
+
message: string;
|
|
29
|
+
};
|
|
30
|
+
parents: {
|
|
31
|
+
sha: string;
|
|
32
|
+
}[];
|
|
33
|
+
}
|
|
34
|
+
export interface GithubWorkflow {
|
|
35
|
+
path: string;
|
|
36
|
+
state: string;
|
|
37
|
+
}
|
|
38
|
+
export interface GithubWorkflowRun {
|
|
39
|
+
id: number;
|
|
40
|
+
path: string;
|
|
41
|
+
status: string | null;
|
|
42
|
+
}
|
|
43
|
+
export interface GithubJob {
|
|
44
|
+
name: string;
|
|
45
|
+
conclusion: string | null;
|
|
46
|
+
}
|
|
47
|
+
export interface GithubClient {
|
|
48
|
+
rest: {
|
|
49
|
+
pulls: {
|
|
50
|
+
get(params: RepoParams & {
|
|
51
|
+
pull_number: number;
|
|
52
|
+
}): Promise<{
|
|
53
|
+
data: GithubPull;
|
|
54
|
+
}>;
|
|
55
|
+
list(params: RepoParams & PageParams & {
|
|
56
|
+
state: string;
|
|
57
|
+
head: string;
|
|
58
|
+
}): Promise<{
|
|
59
|
+
data: GithubPullSummary[];
|
|
60
|
+
}>;
|
|
61
|
+
listFiles(params: RepoParams & PageParams & {
|
|
62
|
+
pull_number: number;
|
|
63
|
+
}): Promise<{
|
|
64
|
+
data: GithubPullFile[];
|
|
65
|
+
}>;
|
|
66
|
+
};
|
|
67
|
+
repos: {
|
|
68
|
+
getCommit(params: RepoParams & {
|
|
69
|
+
ref: string;
|
|
70
|
+
}): Promise<{
|
|
71
|
+
data: GithubCommit;
|
|
72
|
+
}>;
|
|
73
|
+
/** The file at `path` as text — always the `raw` media type. */
|
|
74
|
+
getContent(params: RepoParams & {
|
|
75
|
+
path: string;
|
|
76
|
+
ref: string;
|
|
77
|
+
}): Promise<{
|
|
78
|
+
data: string;
|
|
79
|
+
}>;
|
|
80
|
+
downloadTarballArchive(params: RepoParams & {
|
|
81
|
+
ref: string;
|
|
82
|
+
}): Promise<{
|
|
83
|
+
data: ArrayBuffer;
|
|
84
|
+
}>;
|
|
85
|
+
};
|
|
86
|
+
actions: {
|
|
87
|
+
listRepoWorkflows(params: RepoParams & PageParams): Promise<{
|
|
88
|
+
data: GithubWorkflow[];
|
|
89
|
+
}>;
|
|
90
|
+
listWorkflowRunsForRepo(params: RepoParams & PageParams & {
|
|
91
|
+
head_sha: string;
|
|
92
|
+
event: string;
|
|
93
|
+
}): Promise<{
|
|
94
|
+
data: GithubWorkflowRun[];
|
|
95
|
+
}>;
|
|
96
|
+
listJobsForWorkflowRun(params: RepoParams & PageParams & {
|
|
97
|
+
run_id: number;
|
|
98
|
+
}): Promise<{
|
|
99
|
+
data: GithubJob[];
|
|
100
|
+
}>;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
paginate<P extends PageParams, T>(route: (params: P) => Promise<{
|
|
104
|
+
data: T[];
|
|
105
|
+
}>, params: P): Promise<T[]>;
|
|
106
|
+
}
|
|
107
|
+
export declare function makeGithubClient(): GithubClient;
|
|
108
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// The GitHub REST surface willfire uses: a handful of GET endpoints plus the
|
|
2
|
+
// tarball download, over plain `fetch`. The shape mirrors the octokit subset
|
|
3
|
+
// it replaced (#75) so call sites and their fakes carry over unchanged.
|
|
4
|
+
export function makeGithubClient() {
|
|
5
|
+
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN;
|
|
6
|
+
if (!token) {
|
|
7
|
+
throw new Error("GH_TOKEN or GITHUB_TOKEN must be set");
|
|
8
|
+
}
|
|
9
|
+
const request = async (path, query, accept) => {
|
|
10
|
+
const url = new URL(`https://api.github.com${path}`);
|
|
11
|
+
for (const [key, value] of Object.entries(query)) {
|
|
12
|
+
if (value !== undefined) {
|
|
13
|
+
url.searchParams.set(key, String(value));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const res = await fetch(url, {
|
|
17
|
+
headers: {
|
|
18
|
+
accept,
|
|
19
|
+
authorization: `Bearer ${token}`,
|
|
20
|
+
// GitHub rejects requests without a User-Agent.
|
|
21
|
+
"user-agent": "willfire",
|
|
22
|
+
"x-github-api-version": "2022-11-28",
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
throw new Error(`GitHub API ${res.status} for ${path}`);
|
|
27
|
+
}
|
|
28
|
+
return res;
|
|
29
|
+
};
|
|
30
|
+
const json = async (path, query = {}) => {
|
|
31
|
+
const res = await request(path, query, "application/vnd.github+json");
|
|
32
|
+
return { data: (await res.json()) };
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
rest: {
|
|
36
|
+
pulls: {
|
|
37
|
+
get: ({ owner, repo, pull_number }) => json(`/repos/${owner}/${repo}/pulls/${pull_number}`),
|
|
38
|
+
list: ({ owner, repo, state, head, per_page, page }) => json(`/repos/${owner}/${repo}/pulls`, {
|
|
39
|
+
state,
|
|
40
|
+
head,
|
|
41
|
+
per_page,
|
|
42
|
+
page,
|
|
43
|
+
}),
|
|
44
|
+
listFiles: ({ owner, repo, pull_number, per_page, page }) => json(`/repos/${owner}/${repo}/pulls/${pull_number}/files`, {
|
|
45
|
+
per_page,
|
|
46
|
+
page,
|
|
47
|
+
}),
|
|
48
|
+
},
|
|
49
|
+
repos: {
|
|
50
|
+
getCommit: ({ owner, repo, ref }) => json(`/repos/${owner}/${repo}/commits/${ref}`),
|
|
51
|
+
getContent: async ({ owner, repo, path, ref }) => {
|
|
52
|
+
const res = await request(`/repos/${owner}/${repo}/contents/${path}`, { ref }, "application/vnd.github.raw+json");
|
|
53
|
+
return { data: await res.text() };
|
|
54
|
+
},
|
|
55
|
+
// 302 to a short-lived codeload URL; fetch follows it, and undici
|
|
56
|
+
// drops the authorization header on the cross-origin hop.
|
|
57
|
+
downloadTarballArchive: async ({ owner, repo, ref }) => {
|
|
58
|
+
const res = await request(`/repos/${owner}/${repo}/tarball/${ref}`, {}, "application/vnd.github+json");
|
|
59
|
+
return { data: await res.arrayBuffer() };
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
actions: {
|
|
63
|
+
// The actions list endpoints wrap their arrays in an envelope; unwrap
|
|
64
|
+
// here so `paginate` sees one shape everywhere.
|
|
65
|
+
listRepoWorkflows: async ({ owner, repo, per_page, page }) => {
|
|
66
|
+
const { data } = await json(`/repos/${owner}/${repo}/actions/workflows`, { per_page, page });
|
|
67
|
+
return { data: data.workflows };
|
|
68
|
+
},
|
|
69
|
+
listWorkflowRunsForRepo: async ({ owner, repo, head_sha, event, per_page, page }) => {
|
|
70
|
+
const { data } = await json(`/repos/${owner}/${repo}/actions/runs`, { head_sha, event, per_page, page });
|
|
71
|
+
return { data: data.workflow_runs };
|
|
72
|
+
},
|
|
73
|
+
listJobsForWorkflowRun: async ({ owner, repo, run_id, per_page, page }) => {
|
|
74
|
+
const { data } = await json(`/repos/${owner}/${repo}/actions/runs/${run_id}/jobs`, { per_page, page });
|
|
75
|
+
return { data: data.jobs };
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
// A short page ends the walk. A count landing exactly on a page boundary
|
|
80
|
+
// costs one extra empty-page request, which keeps this free of Link-header
|
|
81
|
+
// parsing.
|
|
82
|
+
paginate: async (route, params) => {
|
|
83
|
+
const perPage = params.per_page ?? 30;
|
|
84
|
+
const all = [];
|
|
85
|
+
let page = 1;
|
|
86
|
+
let full = true;
|
|
87
|
+
while (full) {
|
|
88
|
+
const { data } = await route({ ...params, page });
|
|
89
|
+
all.push(...data);
|
|
90
|
+
full = data.length === perPage;
|
|
91
|
+
page += 1;
|
|
92
|
+
}
|
|
93
|
+
return all;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Octokit } from "@octokit/rest";
|
|
2
1
|
import { type JobExecutor, type RunCommand } from "../execute.js";
|
|
2
|
+
import type { GithubClient } from "./makeGithubClient.js";
|
|
3
3
|
import type { ResolveRef, WorkflowSource } from "../types.js";
|
|
4
4
|
export interface LiveExecutorOpts {
|
|
5
5
|
/** How steps run; the hermetic docker sandbox by default. */
|
|
@@ -17,4 +17,4 @@ export interface LiveExecutorOpts {
|
|
|
17
17
|
* docker sandbox; infrastructure subprocesses (`tar`, `git`) run on the host,
|
|
18
18
|
* since the clone needs the network the sandbox denies.
|
|
19
19
|
*/
|
|
20
|
-
export declare function makeLiveExecutor(
|
|
20
|
+
export declare function makeLiveExecutor(github: GithubClient, workspace: WorkflowSource, resolveRef: ResolveRef, opts?: LiveExecutorOpts): JobExecutor;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { makeCloneProvider, makeExecutor, makeTreeProvider, runShell, } from "../execute.js";
|
|
2
|
-
import { makeSandboxRunner
|
|
2
|
+
import { makeSandboxRunner } from "../sandbox/makeSandboxRunner.js";
|
|
3
|
+
import { SANDBOX_NODE_MAJOR } from "../sandbox/sandboxConfig.js";
|
|
3
4
|
/**
|
|
4
5
|
* The executor `predict` uses by default. Repo-authored steps run in the
|
|
5
6
|
* docker sandbox; infrastructure subprocesses (`tar`, `git`) run on the host,
|
|
6
7
|
* since the clone needs the network the sandbox denies.
|
|
7
8
|
*/
|
|
8
|
-
export function makeLiveExecutor(
|
|
9
|
+
export function makeLiveExecutor(github, workspace, resolveRef, opts = {}) {
|
|
9
10
|
const download = async (src) => {
|
|
10
11
|
try {
|
|
11
|
-
const { data } = await
|
|
12
|
+
const { data } = await github.rest.repos.downloadTarballArchive({
|
|
12
13
|
owner: src.owner,
|
|
13
14
|
repo: src.repo,
|
|
14
15
|
ref: src.sha,
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { GithubClient } from "./makeGithubClient.js";
|
|
2
2
|
import type { Prediction, PredictOptions } from "../types.js";
|
|
3
|
-
export declare function predict(
|
|
3
|
+
export declare function predict(github: GithubClient, repo: string, prNumber: number, opts?: PredictOptions): Promise<Prediction>;
|
package/dist/predict/predict.js
CHANGED
|
@@ -15,16 +15,16 @@ import { sourceKey } from "./sourceKey.js";
|
|
|
15
15
|
import { stackTargetRef } from "./stackTargetRef.js";
|
|
16
16
|
const SKIP_RE = /\[(skip ci|ci skip|no ci|skip actions|actions skip)\]/i;
|
|
17
17
|
const SKIP_TRAILER_RE = /^skip-checks:\s*true/im;
|
|
18
|
-
export async function predict(
|
|
18
|
+
export async function predict(github, repo, prNumber, opts = {}) {
|
|
19
19
|
const [owner, name] = repo.split("/");
|
|
20
20
|
const base = { owner, repo: name };
|
|
21
|
-
const { data: pr } = await
|
|
22
|
-
const files = await
|
|
21
|
+
const { data: pr } = await github.rest.pulls.get({ ...base, pull_number: prNumber });
|
|
22
|
+
const files = await github.paginate(github.rest.pulls.listFiles, {
|
|
23
23
|
...base,
|
|
24
24
|
pull_number: prNumber,
|
|
25
25
|
per_page: 100,
|
|
26
26
|
});
|
|
27
|
-
const stackTarget = await stackTargetRef(
|
|
27
|
+
const stackTarget = await stackTargetRef(github, owner, name, pr);
|
|
28
28
|
const ctx = {
|
|
29
29
|
// The caller's answer wins whenever it has one. The commit-count fallback
|
|
30
30
|
// is a guess kept only so existing callers keep working.
|
|
@@ -43,7 +43,7 @@ export async function predict(octokit, repo, prNumber, opts = {}) {
|
|
|
43
43
|
// is in from the start: it is read even on the skip path, where the commit
|
|
44
44
|
// message is what decides the verdict.
|
|
45
45
|
const sources = new Map([[sourceKey(headSource), headSource]]);
|
|
46
|
-
const { data: headCommit } = await
|
|
46
|
+
const { data: headCommit } = await github.rest.repos.getCommit({
|
|
47
47
|
...base,
|
|
48
48
|
ref: headSha,
|
|
49
49
|
});
|
|
@@ -63,7 +63,7 @@ export async function predict(octokit, repo, prNumber, opts = {}) {
|
|
|
63
63
|
}
|
|
64
64
|
let sha;
|
|
65
65
|
try {
|
|
66
|
-
const { data } = await
|
|
66
|
+
const { data } = await github.rest.repos.getCommit({
|
|
67
67
|
owner: src.owner,
|
|
68
68
|
repo: src.repo,
|
|
69
69
|
ref: src.ref,
|
|
@@ -96,12 +96,11 @@ export async function predict(octokit, repo, prNumber, opts = {}) {
|
|
|
96
96
|
}
|
|
97
97
|
let content;
|
|
98
98
|
try {
|
|
99
|
-
const { data } = await
|
|
99
|
+
const { data } = await github.rest.repos.getContent({
|
|
100
100
|
owner: src.owner,
|
|
101
101
|
repo: src.repo,
|
|
102
102
|
path,
|
|
103
103
|
ref: src.sha,
|
|
104
|
-
mediaType: { format: "raw" },
|
|
105
104
|
});
|
|
106
105
|
content = data;
|
|
107
106
|
}
|
|
@@ -116,9 +115,9 @@ export async function predict(octokit, repo, prNumber, opts = {}) {
|
|
|
116
115
|
const reader = { fetchWorkflow, resolveRef };
|
|
117
116
|
// Execution is on by default and costs nothing until a workflow needs it.
|
|
118
117
|
const executor = opts.executor === undefined
|
|
119
|
-
? makeLiveExecutor(
|
|
118
|
+
? makeLiveExecutor(github, headSource, resolveRef)
|
|
120
119
|
: (opts.executor ?? undefined);
|
|
121
|
-
const workflows = await
|
|
120
|
+
const workflows = await github.paginate(github.rest.actions.listRepoWorkflows, {
|
|
122
121
|
...base,
|
|
123
122
|
per_page: 100,
|
|
124
123
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { GithubClient } from "./makeGithubClient.js";
|
|
2
2
|
import type { StackNode } from "../types.js";
|
|
3
3
|
/**
|
|
4
4
|
* The branch this PR's stack ultimately targets, or null for a plain PR.
|
|
@@ -11,4 +11,4 @@ import type { StackNode } from "../types.js";
|
|
|
11
11
|
* parent PR's own merge sha in stacked mode. Anything undecidable ends the
|
|
12
12
|
* walk at the last proven hop; never throws.
|
|
13
13
|
*/
|
|
14
|
-
export declare function stackTargetRef(
|
|
14
|
+
export declare function stackTargetRef(github: GithubClient, owner: string, repo: string, pr: StackNode): Promise<string | null>;
|
|
@@ -11,7 +11,7 @@ const MAX_STACK_DEPTH = 10;
|
|
|
11
11
|
* parent PR's own merge sha in stacked mode. Anything undecidable ends the
|
|
12
12
|
* walk at the last proven hop; never throws.
|
|
13
13
|
*/
|
|
14
|
-
export async function stackTargetRef(
|
|
14
|
+
export async function stackTargetRef(github, owner, repo, pr) {
|
|
15
15
|
let target = null;
|
|
16
16
|
let cur = pr;
|
|
17
17
|
try {
|
|
@@ -20,7 +20,7 @@ export async function stackTargetRef(octokit, owner, repo, pr) {
|
|
|
20
20
|
if (mergeSha === null) {
|
|
21
21
|
break;
|
|
22
22
|
}
|
|
23
|
-
const { data: preview } = await
|
|
23
|
+
const { data: preview } = await github.rest.repos.getCommit({
|
|
24
24
|
owner,
|
|
25
25
|
repo,
|
|
26
26
|
ref: mergeSha,
|
|
@@ -29,7 +29,7 @@ export async function stackTargetRef(octokit, owner, repo, pr) {
|
|
|
29
29
|
if (previewParent === undefined) {
|
|
30
30
|
break;
|
|
31
31
|
}
|
|
32
|
-
const { data: baseTip } = await
|
|
32
|
+
const { data: baseTip } = await github.rest.repos.getCommit({
|
|
33
33
|
owner,
|
|
34
34
|
repo,
|
|
35
35
|
ref: cur.base.ref,
|
|
@@ -40,7 +40,7 @@ export async function stackTargetRef(octokit, owner, repo, pr) {
|
|
|
40
40
|
}
|
|
41
41
|
// Otherwise only an exact match against an open PR whose head is the
|
|
42
42
|
// base branch proves stacked mode; a stale preview matches nothing.
|
|
43
|
-
const { data: candidates } = await
|
|
43
|
+
const { data: candidates } = await github.rest.pulls.list({
|
|
44
44
|
owner,
|
|
45
45
|
repo,
|
|
46
46
|
state: "open",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/** The tag names the dockerfile that built it, so a change is a new image. */
|
|
3
|
+
export function imageTag(dockerfile) {
|
|
4
|
+
const hash = createHash("sha256").update(dockerfile).digest("hex");
|
|
5
|
+
return `willfire-sandbox:${hash.slice(0, 12)}`;
|
|
6
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `RunCommand` that runs each step inside a hermetic docker container: no
|
|
3
|
+
* network, no capabilities, a read-only root, and only the host paths in
|
|
4
|
+
* `RunSpec.mounts`, bound at their own paths. Code that can reach nothing and
|
|
5
|
+
* keep nothing needs no per-repo grant — this is what lets execution be on by
|
|
6
|
+
* default instead of configured.
|
|
7
|
+
*/
|
|
8
|
+
import type { RunCommand } from "../execute.js";
|
|
9
|
+
import { type SandboxConfig } from "./sandboxConfig.js";
|
|
10
|
+
/**
|
|
11
|
+
* Provisions the image lazily, once, and remembers a failure: every later
|
|
12
|
+
* spec gets 125 (docker's "could not start" band) with the reason rather
|
|
13
|
+
* than retrying a build that already failed.
|
|
14
|
+
*/
|
|
15
|
+
export declare function makeSandboxRunner(opts?: Partial<SandboxConfig>): RunCommand;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `RunCommand` that runs each step inside a hermetic docker container: no
|
|
3
|
+
* network, no capabilities, a read-only root, and only the host paths in
|
|
4
|
+
* `RunSpec.mounts`, bound at their own paths. Code that can reach nothing and
|
|
5
|
+
* keep nothing needs no per-repo grant — this is what lets execution be on by
|
|
6
|
+
* default instead of configured.
|
|
7
|
+
*/
|
|
8
|
+
import { imageTag } from "./imageTag.js";
|
|
9
|
+
import { runDocker } from "./runDocker.js";
|
|
10
|
+
import { sandboxArgv } from "./sandboxArgv.js";
|
|
11
|
+
import { sandboxConfig } from "./sandboxConfig.js";
|
|
12
|
+
/**
|
|
13
|
+
* Provisions the image lazily, once, and remembers a failure: every later
|
|
14
|
+
* spec gets 125 (docker's "could not start" band) with the reason rather
|
|
15
|
+
* than retrying a build that already failed.
|
|
16
|
+
*/
|
|
17
|
+
export function makeSandboxRunner(opts = {}) {
|
|
18
|
+
const cfg = sandboxConfig(opts);
|
|
19
|
+
const tag = imageTag(cfg.dockerfile);
|
|
20
|
+
let ensured = null;
|
|
21
|
+
const ensureImage = () => {
|
|
22
|
+
ensured ??= (async () => {
|
|
23
|
+
const inspect = await runDocker(cfg.dockerBin, ["image", "inspect", tag]);
|
|
24
|
+
if (inspect.code === 0) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const build = await runDocker(cfg.dockerBin, ["build", "-t", tag, "-"], cfg.dockerfile);
|
|
28
|
+
if (build.code === 0) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const trimmed = build.stderr.trim();
|
|
32
|
+
const tail = trimmed.slice(trimmed.lastIndexOf("\n") + 1);
|
|
33
|
+
return `cannot build sandbox image ${tag}${tail === "" ? "" : ` (${tail})`}`;
|
|
34
|
+
})();
|
|
35
|
+
return ensured;
|
|
36
|
+
};
|
|
37
|
+
return async (spec) => {
|
|
38
|
+
const failure = await ensureImage();
|
|
39
|
+
if (failure !== null) {
|
|
40
|
+
return { code: 125, stderr: failure };
|
|
41
|
+
}
|
|
42
|
+
return runDocker(cfg.dockerBin, sandboxArgv(spec, cfg));
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
// The client itself runs with the host environment — it needs the host PATH
|
|
3
|
+
// and any DOCKER_HOST to find the daemon.
|
|
4
|
+
export function runDocker(bin, argv, stdin) {
|
|
5
|
+
return new Promise((resolvePromise) => {
|
|
6
|
+
const child = spawn(bin, argv, {
|
|
7
|
+
env: process.env,
|
|
8
|
+
stdio: [stdin === undefined ? "ignore" : "pipe", "ignore", "pipe"],
|
|
9
|
+
});
|
|
10
|
+
let stderr = "";
|
|
11
|
+
child.stderr.on("data", (d) => {
|
|
12
|
+
stderr += String(d);
|
|
13
|
+
if (stderr.length > 4096) {
|
|
14
|
+
stderr = stderr.slice(-4096);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
child.on("spawn", () => {
|
|
18
|
+
if (stdin !== undefined) {
|
|
19
|
+
child.stdin.write(stdin);
|
|
20
|
+
child.stdin.end();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
child.on("error", () => resolvePromise({ code: 127, stderr }));
|
|
24
|
+
child.on("close", (code) => resolvePromise({ code: code ?? 1, stderr }));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RunSpec } from "../execute.js";
|
|
2
|
+
import type { SandboxConfig } from "./sandboxConfig.js";
|
|
3
|
+
/**
|
|
4
|
+
* The complete `docker run` argv for one step. `PATH` and `HOME` in
|
|
5
|
+
* `spec.env` are host facts; the container gets its image's PATH and a
|
|
6
|
+
* writable `HOME=/tmp` instead.
|
|
7
|
+
*/
|
|
8
|
+
export declare function sandboxArgv(spec: RunSpec, cfg: SandboxConfig): string[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { imageTag } from "./imageTag.js";
|
|
2
|
+
/**
|
|
3
|
+
* The complete `docker run` argv for one step. `PATH` and `HOME` in
|
|
4
|
+
* `spec.env` are host facts; the container gets its image's PATH and a
|
|
5
|
+
* writable `HOME=/tmp` instead.
|
|
6
|
+
*/
|
|
7
|
+
export function sandboxArgv(spec, cfg) {
|
|
8
|
+
const argv = [
|
|
9
|
+
"run",
|
|
10
|
+
"--rm",
|
|
11
|
+
"--network",
|
|
12
|
+
"none",
|
|
13
|
+
"--cap-drop",
|
|
14
|
+
"ALL",
|
|
15
|
+
"--security-opt",
|
|
16
|
+
"no-new-privileges",
|
|
17
|
+
"--read-only",
|
|
18
|
+
"--tmpfs",
|
|
19
|
+
"/tmp",
|
|
20
|
+
"--user",
|
|
21
|
+
`${cfg.uid}:${cfg.gid}`,
|
|
22
|
+
];
|
|
23
|
+
for (const m of spec.mounts ?? []) {
|
|
24
|
+
argv.push("-v", `${m.path}:${m.path}${m.writable ? "" : ":ro"}`);
|
|
25
|
+
}
|
|
26
|
+
argv.push("-w", spec.cwd);
|
|
27
|
+
for (const [k, v] of Object.entries(spec.env)) {
|
|
28
|
+
if (k !== "PATH" && k !== "HOME") {
|
|
29
|
+
argv.push("-e", `${k}=${v}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
argv.push("-e", "HOME=/tmp");
|
|
33
|
+
argv.push(imageTag(cfg.dockerfile));
|
|
34
|
+
if (spec.shell === "bash") {
|
|
35
|
+
argv.push("bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", spec.script);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
argv.push("sh", "-e", "-c", spec.script);
|
|
39
|
+
}
|
|
40
|
+
return argv;
|
|
41
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The node major the image ships — also the refusal boundary for `setup-node`
|
|
3
|
+
* and `node2x` runtimes asking for any other major.
|
|
4
|
+
*/
|
|
5
|
+
export declare const SANDBOX_NODE_MAJOR = 24;
|
|
6
|
+
export declare const DOCKERFILE = "FROM node:24-slim\nRUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates python3 && rm -rf /var/lib/apt/lists/*\n";
|
|
7
|
+
export interface SandboxConfig {
|
|
8
|
+
dockerBin: string;
|
|
9
|
+
uid: number;
|
|
10
|
+
gid: number;
|
|
11
|
+
dockerfile: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function sandboxConfig(opts?: Partial<SandboxConfig>): SandboxConfig;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The node major the image ships — also the refusal boundary for `setup-node`
|
|
3
|
+
* and `node2x` runtimes asking for any other major.
|
|
4
|
+
*/
|
|
5
|
+
export const SANDBOX_NODE_MAJOR = 24;
|
|
6
|
+
// git and python3: checkout's postcondition and the interpreters a script on
|
|
7
|
+
// a GitHub-hosted runner takes for granted.
|
|
8
|
+
export const DOCKERFILE = `FROM node:${SANDBOX_NODE_MAJOR}-slim
|
|
9
|
+
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates python3 && rm -rf /var/lib/apt/lists/*
|
|
10
|
+
`;
|
|
11
|
+
export function sandboxConfig(opts = {}) {
|
|
12
|
+
return {
|
|
13
|
+
dockerBin: opts.dockerBin ?? "docker",
|
|
14
|
+
uid: opts.uid ?? process.getuid(),
|
|
15
|
+
gid: opts.gid ?? process.getgid(),
|
|
16
|
+
dockerfile: opts.dockerfile ?? DOCKERFILE,
|
|
17
|
+
};
|
|
18
|
+
}
|
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 { isJobEntry,
|
|
7
|
+
import { isJobEntry, makeGithubClient, predict } from "./index.js";
|
|
8
8
|
async function actualEntries(octokit, repo, prNumber) {
|
|
9
9
|
const [owner, name] = repo.split("/");
|
|
10
10
|
const base = { owner, repo: name };
|
|
@@ -43,7 +43,7 @@ if (!repo || !prArg) {
|
|
|
43
43
|
process.exit(2);
|
|
44
44
|
}
|
|
45
45
|
const pr = Number(prArg);
|
|
46
|
-
const octokit =
|
|
46
|
+
const octokit = makeGithubClient();
|
|
47
47
|
const { entries: predictedRaw } = await predict(octokit, repo, pr);
|
|
48
48
|
// Compare on the resolved check name — that is the string GitHub actually
|
|
49
49
|
// puts on the job. Entries whose name could not be resolved statically have
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "willfire",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
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",
|
|
@@ -46,7 +46,6 @@
|
|
|
46
46
|
"verify": "tsx src/verify.ts"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@octokit/rest": "^22.0.0",
|
|
50
49
|
"yaml": "^2.6.0"
|
|
51
50
|
},
|
|
52
51
|
"devDependencies": {
|
package/dist/sandbox.d.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A `RunCommand` that runs each step inside a hermetic docker container: no
|
|
3
|
-
* network, no capabilities, a read-only root, and only the host paths in
|
|
4
|
-
* `RunSpec.mounts`, bound at their own paths. Code that can reach nothing and
|
|
5
|
-
* keep nothing needs no per-repo grant — this is what lets execution be on by
|
|
6
|
-
* default instead of configured.
|
|
7
|
-
*/
|
|
8
|
-
import type { RunCommand, RunSpec } from "./execute.js";
|
|
9
|
-
/**
|
|
10
|
-
* The node major the image ships — also the refusal boundary for `setup-node`
|
|
11
|
-
* and `node2x` runtimes asking for any other major.
|
|
12
|
-
*/
|
|
13
|
-
export declare const SANDBOX_NODE_MAJOR = 24;
|
|
14
|
-
export declare const DOCKERFILE = "FROM node:24-slim\nRUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates python3 && rm -rf /var/lib/apt/lists/*\n";
|
|
15
|
-
export interface SandboxConfig {
|
|
16
|
-
dockerBin: string;
|
|
17
|
-
uid: number;
|
|
18
|
-
gid: number;
|
|
19
|
-
dockerfile: string;
|
|
20
|
-
}
|
|
21
|
-
export declare function sandboxConfig(opts?: Partial<SandboxConfig>): SandboxConfig;
|
|
22
|
-
/** The tag names the dockerfile that built it, so a change is a new image. */
|
|
23
|
-
export declare function imageTag(dockerfile: string): string;
|
|
24
|
-
/**
|
|
25
|
-
* The complete `docker run` argv for one step. `PATH` and `HOME` in
|
|
26
|
-
* `spec.env` are host facts; the container gets its image's PATH and a
|
|
27
|
-
* writable `HOME=/tmp` instead.
|
|
28
|
-
*/
|
|
29
|
-
export declare function sandboxArgv(spec: RunSpec, cfg: SandboxConfig): string[];
|
|
30
|
-
/**
|
|
31
|
-
* Provisions the image lazily, once, and remembers a failure: every later
|
|
32
|
-
* spec gets 125 (docker's "could not start" band) with the reason rather
|
|
33
|
-
* than retrying a build that already failed.
|
|
34
|
-
*/
|
|
35
|
-
export declare function makeSandboxRunner(opts?: Partial<SandboxConfig>): RunCommand;
|
package/dist/sandbox.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A `RunCommand` that runs each step inside a hermetic docker container: no
|
|
3
|
-
* network, no capabilities, a read-only root, and only the host paths in
|
|
4
|
-
* `RunSpec.mounts`, bound at their own paths. Code that can reach nothing and
|
|
5
|
-
* keep nothing needs no per-repo grant — this is what lets execution be on by
|
|
6
|
-
* default instead of configured.
|
|
7
|
-
*/
|
|
8
|
-
import { spawn } from "node:child_process";
|
|
9
|
-
import { createHash } from "node:crypto";
|
|
10
|
-
/**
|
|
11
|
-
* The node major the image ships — also the refusal boundary for `setup-node`
|
|
12
|
-
* and `node2x` runtimes asking for any other major.
|
|
13
|
-
*/
|
|
14
|
-
export const SANDBOX_NODE_MAJOR = 24;
|
|
15
|
-
// git and python3: checkout's postcondition and the interpreters a script on
|
|
16
|
-
// a GitHub-hosted runner takes for granted.
|
|
17
|
-
export const DOCKERFILE = `FROM node:${SANDBOX_NODE_MAJOR}-slim
|
|
18
|
-
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates python3 && rm -rf /var/lib/apt/lists/*
|
|
19
|
-
`;
|
|
20
|
-
export function sandboxConfig(opts = {}) {
|
|
21
|
-
return {
|
|
22
|
-
dockerBin: opts.dockerBin ?? "docker",
|
|
23
|
-
uid: opts.uid ?? process.getuid(),
|
|
24
|
-
gid: opts.gid ?? process.getgid(),
|
|
25
|
-
dockerfile: opts.dockerfile ?? DOCKERFILE,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
/** The tag names the dockerfile that built it, so a change is a new image. */
|
|
29
|
-
export function imageTag(dockerfile) {
|
|
30
|
-
const hash = createHash("sha256").update(dockerfile).digest("hex");
|
|
31
|
-
return `willfire-sandbox:${hash.slice(0, 12)}`;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* The complete `docker run` argv for one step. `PATH` and `HOME` in
|
|
35
|
-
* `spec.env` are host facts; the container gets its image's PATH and a
|
|
36
|
-
* writable `HOME=/tmp` instead.
|
|
37
|
-
*/
|
|
38
|
-
export function sandboxArgv(spec, cfg) {
|
|
39
|
-
const argv = [
|
|
40
|
-
"run",
|
|
41
|
-
"--rm",
|
|
42
|
-
"--network",
|
|
43
|
-
"none",
|
|
44
|
-
"--cap-drop",
|
|
45
|
-
"ALL",
|
|
46
|
-
"--security-opt",
|
|
47
|
-
"no-new-privileges",
|
|
48
|
-
"--read-only",
|
|
49
|
-
"--tmpfs",
|
|
50
|
-
"/tmp",
|
|
51
|
-
"--user",
|
|
52
|
-
`${cfg.uid}:${cfg.gid}`,
|
|
53
|
-
];
|
|
54
|
-
for (const m of spec.mounts ?? []) {
|
|
55
|
-
argv.push("-v", `${m.path}:${m.path}${m.writable ? "" : ":ro"}`);
|
|
56
|
-
}
|
|
57
|
-
argv.push("-w", spec.cwd);
|
|
58
|
-
for (const [k, v] of Object.entries(spec.env)) {
|
|
59
|
-
if (k !== "PATH" && k !== "HOME") {
|
|
60
|
-
argv.push("-e", `${k}=${v}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
argv.push("-e", "HOME=/tmp");
|
|
64
|
-
argv.push(imageTag(cfg.dockerfile));
|
|
65
|
-
if (spec.shell === "bash") {
|
|
66
|
-
argv.push("bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", spec.script);
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
argv.push("sh", "-e", "-c", spec.script);
|
|
70
|
-
}
|
|
71
|
-
return argv;
|
|
72
|
-
}
|
|
73
|
-
// The client itself runs with the host environment — it needs the host PATH
|
|
74
|
-
// and any DOCKER_HOST to find the daemon.
|
|
75
|
-
function runDocker(bin, argv, stdin) {
|
|
76
|
-
return new Promise((resolvePromise) => {
|
|
77
|
-
const child = spawn(bin, argv, {
|
|
78
|
-
env: process.env,
|
|
79
|
-
stdio: [stdin === undefined ? "ignore" : "pipe", "ignore", "pipe"],
|
|
80
|
-
});
|
|
81
|
-
let stderr = "";
|
|
82
|
-
child.stderr.on("data", (d) => {
|
|
83
|
-
stderr += String(d);
|
|
84
|
-
if (stderr.length > 4096) {
|
|
85
|
-
stderr = stderr.slice(-4096);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
child.on("spawn", () => {
|
|
89
|
-
if (stdin !== undefined) {
|
|
90
|
-
child.stdin.write(stdin);
|
|
91
|
-
child.stdin.end();
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
child.on("error", () => resolvePromise({ code: 127, stderr }));
|
|
95
|
-
child.on("close", (code) => resolvePromise({ code: code ?? 1, stderr }));
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Provisions the image lazily, once, and remembers a failure: every later
|
|
100
|
-
* spec gets 125 (docker's "could not start" band) with the reason rather
|
|
101
|
-
* than retrying a build that already failed.
|
|
102
|
-
*/
|
|
103
|
-
export function makeSandboxRunner(opts = {}) {
|
|
104
|
-
const cfg = sandboxConfig(opts);
|
|
105
|
-
const tag = imageTag(cfg.dockerfile);
|
|
106
|
-
let ensured = null;
|
|
107
|
-
const ensureImage = () => {
|
|
108
|
-
ensured ??= (async () => {
|
|
109
|
-
const inspect = await runDocker(cfg.dockerBin, ["image", "inspect", tag]);
|
|
110
|
-
if (inspect.code === 0) {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
const build = await runDocker(cfg.dockerBin, ["build", "-t", tag, "-"], cfg.dockerfile);
|
|
114
|
-
if (build.code === 0) {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
const trimmed = build.stderr.trim();
|
|
118
|
-
const tail = trimmed.slice(trimmed.lastIndexOf("\n") + 1);
|
|
119
|
-
return `cannot build sandbox image ${tag}${tail === "" ? "" : ` (${tail})`}`;
|
|
120
|
-
})();
|
|
121
|
-
return ensured;
|
|
122
|
-
};
|
|
123
|
-
return async (spec) => {
|
|
124
|
-
const failure = await ensureImage();
|
|
125
|
-
if (failure !== null) {
|
|
126
|
-
return { code: 125, stderr: failure };
|
|
127
|
-
}
|
|
128
|
-
return runDocker(cfg.dockerBin, sandboxArgv(spec, cfg));
|
|
129
|
-
};
|
|
130
|
-
}
|