@magland/mochi 0.3.1
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/LICENSE +201 -0
- package/README.md +108 -0
- package/dist/ansi.js +174 -0
- package/dist/api/admin.js +416 -0
- package/dist/api/auth.js +166 -0
- package/dist/api/backup.js +598 -0
- package/dist/api/ci.js +336 -0
- package/dist/api/contents.js +339 -0
- package/dist/api/issues.js +165 -0
- package/dist/api/pulls.js +244 -0
- package/dist/api/releases.js +83 -0
- package/dist/api/repos.js +156 -0
- package/dist/api/write.js +518 -0
- package/dist/api.js +326 -0
- package/dist/assets.js +29 -0
- package/dist/atom.js +32 -0
- package/dist/atomic.js +171 -0
- package/dist/avatar.js +81 -0
- package/dist/browse.js +630 -0
- package/dist/build-info.json +4 -0
- package/dist/ci/actionref.js +86 -0
- package/dist/ci/api.js +829 -0
- package/dist/ci/artifacts.js +201 -0
- package/dist/ci/dispatch.js +30 -0
- package/dist/ci/engine.js +1321 -0
- package/dist/ci/expr.js +526 -0
- package/dist/ci/manual.js +199 -0
- package/dist/ci/present.js +82 -0
- package/dist/ci/protocol.js +6 -0
- package/dist/ci/runners.js +256 -0
- package/dist/ci/runs.js +208 -0
- package/dist/ci/trigger.js +28 -0
- package/dist/ci/views.js +441 -0
- package/dist/ci/wake.js +194 -0
- package/dist/ci/web.js +617 -0
- package/dist/ci/workflow.js +436 -0
- package/dist/cli/admin-cmd.js +324 -0
- package/dist/cli/api-cmd.js +128 -0
- package/dist/cli/backup-cmd.js +1500 -0
- package/dist/cli/exit.js +69 -0
- package/dist/cli/input.js +64 -0
- package/dist/cli/issue-cmd.js +243 -0
- package/dist/cli/output.js +93 -0
- package/dist/cli/parse.js +317 -0
- package/dist/cli/pr-cmd.js +289 -0
- package/dist/cli/release-cmd.js +171 -0
- package/dist/cli/repo-cmd.js +763 -0
- package/dist/cli/repo.js +101 -0
- package/dist/cli/run-cmd.js +438 -0
- package/dist/cli/target.js +54 -0
- package/dist/cli-api.js +84 -0
- package/dist/compare.js +111 -0
- package/dist/config.js +212 -0
- package/dist/credentials.js +235 -0
- package/dist/deploy-cli.js +859 -0
- package/dist/deploy-runner-cli.js +592 -0
- package/dist/diff.js +171 -0
- package/dist/discussion.js +253 -0
- package/dist/egress.js +559 -0
- package/dist/filecache.js +68 -0
- package/dist/find.js +162 -0
- package/dist/forms.js +737 -0
- package/dist/git.js +547 -0
- package/dist/githttp.js +428 -0
- package/dist/html.js +87 -0
- package/dist/icons.js +101 -0
- package/dist/import-cli.js +316 -0
- package/dist/index.js +752 -0
- package/dist/issues.js +308 -0
- package/dist/issueweb.js +447 -0
- package/dist/job-cli.js +197 -0
- package/dist/jobtoken.js +96 -0
- package/dist/languages.js +383 -0
- package/dist/layout.js +100 -0
- package/dist/lfs.js +438 -0
- package/dist/lfsstore.js +425 -0
- package/dist/limit.js +259 -0
- package/dist/logo.js +61 -0
- package/dist/markdown.js +382 -0
- package/dist/migrate.js +334 -0
- package/dist/multipart.js +90 -0
- package/dist/ops.js +869 -0
- package/dist/pagescript.js +465 -0
- package/dist/perms.js +370 -0
- package/dist/pointer.js +55 -0
- package/dist/profile.js +106 -0
- package/dist/pulls.js +320 -0
- package/dist/pullweb.js +461 -0
- package/dist/redirects.js +455 -0
- package/dist/releases.js +435 -0
- package/dist/render.js +233 -0
- package/dist/runner/actions.js +448 -0
- package/dist/runner/client.js +428 -0
- package/dist/runner/context.js +247 -0
- package/dist/runner/docker.js +197 -0
- package/dist/runner/externals.js +175 -0
- package/dist/runner/job.js +290 -0
- package/dist/runner/manual-run.js +272 -0
- package/dist/runner/overrides.js +554 -0
- package/dist/runner/steps.js +571 -0
- package/dist/runner/wake.js +84 -0
- package/dist/runner-cli.js +405 -0
- package/dist/scan.js +231 -0
- package/dist/server.js +424 -0
- package/dist/session.js +267 -0
- package/dist/site.js +259 -0
- package/dist/siteshost.js +94 -0
- package/dist/source.js +90 -0
- package/dist/style.js +1295 -0
- package/dist/themes.js +369 -0
- package/dist/vault.js +442 -0
- package/dist/version.js +88 -0
- package/dist/views.js +1007 -0
- package/dist/web.js +182 -0
- package/dist/webops.js +1402 -0
- package/package.json +71 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DockerError = void 0;
|
|
4
|
+
exports.setContainerEngine = setContainerEngine;
|
|
5
|
+
exports.containerEngine = containerEngine;
|
|
6
|
+
exports.dockerAvailable = dockerAvailable;
|
|
7
|
+
exports.probeEngine = probeEngine;
|
|
8
|
+
exports.imagePresent = imagePresent;
|
|
9
|
+
exports.pullImage = pullImage;
|
|
10
|
+
exports.startContainer = startContainer;
|
|
11
|
+
exports.removeContainer = removeContainer;
|
|
12
|
+
exports.execInContainer = execInContainer;
|
|
13
|
+
exports.writeFileInContainer = writeFileInContainer;
|
|
14
|
+
exports.readFileInContainer = readFileInContainer;
|
|
15
|
+
const child_process_1 = require("child_process");
|
|
16
|
+
// The Docker surface the runner needs, as thin wrappers over the CLI rather
|
|
17
|
+
// than the Engine API socket. Shelling out keeps the runner dependency-free
|
|
18
|
+
// and works with anything that provides the docker command set; `podman`
|
|
19
|
+
// answers the same arguments, so the binary is a setting rather than a fact.
|
|
20
|
+
class DockerError extends Error {
|
|
21
|
+
}
|
|
22
|
+
exports.DockerError = DockerError;
|
|
23
|
+
// Which binary carries the commands below. Set once at startup by whichever
|
|
24
|
+
// CLI is running (see setContainerEngine); every wrapper reads it, so a
|
|
25
|
+
// runner told to use podman uses it for everything or for nothing.
|
|
26
|
+
let engineBin = 'docker';
|
|
27
|
+
function setContainerEngine(bin) {
|
|
28
|
+
engineBin = bin;
|
|
29
|
+
}
|
|
30
|
+
function containerEngine() {
|
|
31
|
+
return engineBin;
|
|
32
|
+
}
|
|
33
|
+
function run(args, input) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const child = (0, child_process_1.execFile)(engineBin, args, { maxBuffer: 64 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
36
|
+
if (err)
|
|
37
|
+
reject(new DockerError(`${engineBin} ${args[0]} failed: ${(stderr || err.message).trim()}`));
|
|
38
|
+
else
|
|
39
|
+
resolve({ stdout, stderr });
|
|
40
|
+
});
|
|
41
|
+
if (input !== undefined && child.stdin) {
|
|
42
|
+
child.stdin.write(input);
|
|
43
|
+
child.stdin.end();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function dockerAvailable() {
|
|
48
|
+
try {
|
|
49
|
+
const { stdout } = await run(['version', '--format', '{{.Server.Version}}']);
|
|
50
|
+
return stdout.trim() || 'unknown';
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* What a given binary would report as its server version, without changing
|
|
58
|
+
* which one this process uses. The CLIs ask this of both `docker` and
|
|
59
|
+
* `podman` to offer a choice; a binary that is missing, or present with no
|
|
60
|
+
* working daemon behind it, answers null either way, since a runner cares
|
|
61
|
+
* only whether containers can actually be started.
|
|
62
|
+
*/
|
|
63
|
+
function probeEngine(bin) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
(0, child_process_1.execFile)(bin, ['version', '--format', '{{.Server.Version}}'], { timeout: 15000 }, (err, stdout) => {
|
|
66
|
+
if (err)
|
|
67
|
+
resolve(null);
|
|
68
|
+
else
|
|
69
|
+
resolve(stdout.toString().trim() || 'unknown');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async function imagePresent(image) {
|
|
74
|
+
try {
|
|
75
|
+
await run(['image', 'inspect', image]);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function pullImage(image, onLine) {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const child = (0, child_process_1.spawn)(engineBin, ['pull', image]);
|
|
85
|
+
const feed = (buf) => {
|
|
86
|
+
for (const line of buf.toString('utf8').split('\n')) {
|
|
87
|
+
if (line.trim() !== '')
|
|
88
|
+
onLine(line.replace(/\r/g, ''));
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
child.stdout.on('data', feed);
|
|
92
|
+
child.stderr.on('data', feed);
|
|
93
|
+
child.on('error', (e) => reject(new DockerError(`docker pull failed: ${e.message}`)));
|
|
94
|
+
child.on('close', (code) => code === 0 ? resolve() : reject(new DockerError(`docker pull ${image} exited ${code}`)));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// One container per job, kept alive by a sleep so each step can exec into
|
|
98
|
+
// it. This is how a container job behaves on GitHub: steps share a
|
|
99
|
+
// filesystem and a process namespace, and anything a step installs is there
|
|
100
|
+
// for the next one.
|
|
101
|
+
async function startContainer(opts) {
|
|
102
|
+
const args = ['run', '--detach', '--name', opts.name, '--workdir', opts.workdir, '--entrypoint', ''];
|
|
103
|
+
for (const b of opts.binds) {
|
|
104
|
+
args.push('--volume', `${b.host}:${b.container}${b.readonly ? ':ro' : ''}`);
|
|
105
|
+
}
|
|
106
|
+
for (const [k, v] of Object.entries(opts.env)) {
|
|
107
|
+
args.push('--env', `${k}=${v}`);
|
|
108
|
+
}
|
|
109
|
+
if (opts.network)
|
|
110
|
+
args.push('--network', opts.network);
|
|
111
|
+
args.push(opts.image, 'sh', '-c', 'while true; do sleep 3600; done');
|
|
112
|
+
const { stdout } = await run(args);
|
|
113
|
+
return stdout.trim();
|
|
114
|
+
}
|
|
115
|
+
async function removeContainer(id) {
|
|
116
|
+
try {
|
|
117
|
+
await run(['rm', '--force', '--volumes', id]);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// a container that is already gone is the desired state
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Exec a command in a running container, streaming combined output. stdout
|
|
124
|
+
// and stderr are interleaved into one callback because that is what a build
|
|
125
|
+
// log is: their relative order matters more than their separation.
|
|
126
|
+
function execInContainer(id, argv, onLine, opts = {}) {
|
|
127
|
+
const args = ['exec'];
|
|
128
|
+
if (opts.workdir)
|
|
129
|
+
args.push('--workdir', opts.workdir);
|
|
130
|
+
if (opts.user)
|
|
131
|
+
args.push('--user', opts.user);
|
|
132
|
+
for (const [k, v] of Object.entries(opts.env ?? {}))
|
|
133
|
+
args.push('--env', `${k}=${v}`);
|
|
134
|
+
args.push(id, ...argv);
|
|
135
|
+
const child = (0, child_process_1.spawn)(engineBin, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
136
|
+
// Split on newlines across chunk boundaries, and cap any single line so a
|
|
137
|
+
// program printing a gigabyte without a newline cannot exhaust memory.
|
|
138
|
+
const makeFeeder = () => {
|
|
139
|
+
let buffer = '';
|
|
140
|
+
return (buf) => {
|
|
141
|
+
buffer += buf.toString('utf8');
|
|
142
|
+
let i;
|
|
143
|
+
while ((i = buffer.indexOf('\n')) !== -1) {
|
|
144
|
+
onLine(buffer.slice(0, i).replace(/\r$/, ''));
|
|
145
|
+
buffer = buffer.slice(i + 1);
|
|
146
|
+
}
|
|
147
|
+
if (buffer.length > 64 * 1024) {
|
|
148
|
+
onLine(buffer.slice(0, 64 * 1024));
|
|
149
|
+
buffer = buffer.slice(64 * 1024);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
const outFeed = makeFeeder();
|
|
154
|
+
const errFeed = makeFeeder();
|
|
155
|
+
child.stdout?.on('data', outFeed);
|
|
156
|
+
child.stderr?.on('data', errFeed);
|
|
157
|
+
const done = new Promise((resolve) => {
|
|
158
|
+
let settled = false;
|
|
159
|
+
const finish = (code) => {
|
|
160
|
+
if (!settled) {
|
|
161
|
+
settled = true;
|
|
162
|
+
resolve(code);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
child.on('error', (e) => {
|
|
166
|
+
onLine(`docker exec failed: ${e.message}`);
|
|
167
|
+
finish(126);
|
|
168
|
+
});
|
|
169
|
+
child.on('close', (code) => finish(code === null ? 143 : code));
|
|
170
|
+
});
|
|
171
|
+
return { child, done };
|
|
172
|
+
}
|
|
173
|
+
// Copy a file into a container. `docker cp` from stdin needs a tar stream, so
|
|
174
|
+
// the simpler route for the small files the runner writes (step scripts, the
|
|
175
|
+
// environment files) is to pipe the bytes through a shell redirect.
|
|
176
|
+
function writeFileInContainer(id, containerPath, content) {
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
const child = (0, child_process_1.spawn)(engineBin, ['exec', '--interactive', id, 'sh', '-c', `cat > "$0"`, containerPath], { stdio: ['pipe', 'ignore', 'pipe'] });
|
|
179
|
+
let stderr = '';
|
|
180
|
+
child.stderr?.on('data', (b) => {
|
|
181
|
+
stderr += b.toString('utf8');
|
|
182
|
+
});
|
|
183
|
+
child.on('error', (e) => reject(new DockerError(e.message)));
|
|
184
|
+
child.on('close', (code) => code === 0 ? resolve() : reject(new DockerError(`writing ${containerPath} failed: ${stderr.trim()}`)));
|
|
185
|
+
child.stdin?.write(content);
|
|
186
|
+
child.stdin?.end();
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
async function readFileInContainer(id, containerPath) {
|
|
190
|
+
try {
|
|
191
|
+
const { stdout } = await run(['exec', id, 'sh', '-c', `cat "$0" 2>/dev/null || true`, containerPath]);
|
|
192
|
+
return stdout;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return '';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.Externals = exports.EXTERNALS_MOUNT = exports.ExternalsError = void 0;
|
|
37
|
+
exports.defaultExternalsDir = defaultExternalsDir;
|
|
38
|
+
const child_process_1 = require("child_process");
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const docker_1 = require("./docker");
|
|
43
|
+
// JavaScript actions need a node interpreter inside the job's container.
|
|
44
|
+
// GitHub's runner ships its own and mounts it in, which is why an image
|
|
45
|
+
// without node still runs `actions/checkout`. We do the same, but only when
|
|
46
|
+
// we have to: if the image already has a new enough node, that one is used,
|
|
47
|
+
// and nothing is downloaded. Images built for CI almost always do.
|
|
48
|
+
//
|
|
49
|
+
// The downloaded builds are the official linux-x64/arm64 ones, which are
|
|
50
|
+
// linked against glibc. An Alpine or other musl image therefore cannot use
|
|
51
|
+
// them, and gets a clear message rather than a confusing loader error.
|
|
52
|
+
class ExternalsError extends Error {
|
|
53
|
+
}
|
|
54
|
+
exports.ExternalsError = ExternalsError;
|
|
55
|
+
// Pinned so a job's behavior does not change under it when upstream releases.
|
|
56
|
+
const NODE_VERSIONS = {
|
|
57
|
+
16: '16.20.2',
|
|
58
|
+
20: '20.19.5',
|
|
59
|
+
24: '24.10.0',
|
|
60
|
+
};
|
|
61
|
+
exports.EXTERNALS_MOUNT = '/mochi/externals';
|
|
62
|
+
function run(cmd, args) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
(0, child_process_1.execFile)(cmd, args, { maxBuffer: 64 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
65
|
+
if (err)
|
|
66
|
+
reject(new ExternalsError((stderr || err.message).toString().trim()));
|
|
67
|
+
else
|
|
68
|
+
resolve(stdout.toString());
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function defaultExternalsDir() {
|
|
73
|
+
const base = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache');
|
|
74
|
+
return path.join(base, 'mochi', 'externals');
|
|
75
|
+
}
|
|
76
|
+
function nodeArch() {
|
|
77
|
+
return process.arch === 'arm64' ? 'arm64' : 'x64';
|
|
78
|
+
}
|
|
79
|
+
class Externals {
|
|
80
|
+
dir;
|
|
81
|
+
// Tagged with the container it describes, not merely remembered: one
|
|
82
|
+
// Externals serves every job a runner takes, and consecutive jobs may ask
|
|
83
|
+
// for different images. An untagged field would answer job two with what was
|
|
84
|
+
// true of job one's image. A runner runs one job at a time, so one entry is
|
|
85
|
+
// all there is to keep.
|
|
86
|
+
containerNode = null;
|
|
87
|
+
constructor(dir = defaultExternalsDir()) {
|
|
88
|
+
this.dir = dir;
|
|
89
|
+
}
|
|
90
|
+
// The node binary to run an action with, as a path inside the container.
|
|
91
|
+
async nodeFor(containerId, major, onLine) {
|
|
92
|
+
const inImage = await this.imageNode(containerId);
|
|
93
|
+
// Action bundles are transpiled to a conservative target, so any node at
|
|
94
|
+
// or above the version the action asks for runs them. Rejecting an image
|
|
95
|
+
// whose node is merely newer would mean downloading for no reason.
|
|
96
|
+
if (inImage !== null && inImage.major >= major)
|
|
97
|
+
return inImage.path;
|
|
98
|
+
return this.provision(containerId, major, onLine);
|
|
99
|
+
}
|
|
100
|
+
async imageNode(containerId) {
|
|
101
|
+
if (this.containerNode === null || this.containerNode.id !== containerId) {
|
|
102
|
+
let out = '';
|
|
103
|
+
const handle = (0, docker_1.execInContainer)(containerId, ['sh', '-c', 'command -v node >/dev/null 2>&1 && node --version || true'], (line) => {
|
|
104
|
+
out += line;
|
|
105
|
+
});
|
|
106
|
+
await handle.done;
|
|
107
|
+
const m = out.match(/v(\d+)\./);
|
|
108
|
+
this.containerNode = { id: containerId, node: m ? m[1] : null };
|
|
109
|
+
}
|
|
110
|
+
const node = this.containerNode.node;
|
|
111
|
+
if (node === null)
|
|
112
|
+
return null;
|
|
113
|
+
return { path: 'node', major: parseInt(node, 10) };
|
|
114
|
+
}
|
|
115
|
+
async provision(containerId, major, onLine) {
|
|
116
|
+
// An action may name any nodeN; only the runtimes above are provisioned.
|
|
117
|
+
// Say so, since an action written for a newer one will otherwise fail
|
|
118
|
+
// somewhere deep inside itself with no hint of why.
|
|
119
|
+
const version = NODE_VERSIONS[major];
|
|
120
|
+
if (version === undefined) {
|
|
121
|
+
onLine(`No node ${major} runtime is available; using node ${NODE_VERSIONS[20]} instead`);
|
|
122
|
+
}
|
|
123
|
+
const resolved = version ?? NODE_VERSIONS[20];
|
|
124
|
+
const arch = nodeArch();
|
|
125
|
+
const name = `node-v${resolved}-linux-${arch}`;
|
|
126
|
+
const target = path.join(this.dir, name);
|
|
127
|
+
const bin = path.join(target, 'bin', 'node');
|
|
128
|
+
if (!fs.existsSync(bin)) {
|
|
129
|
+
onLine(`Providing node ${resolved} for JavaScript actions (the image has none)`);
|
|
130
|
+
await this.download(resolved, name, target);
|
|
131
|
+
}
|
|
132
|
+
const containerPath = `${exports.EXTERNALS_MOUNT}/${name}/bin/node`;
|
|
133
|
+
// Fail here, with an explanation, rather than letting the first action
|
|
134
|
+
// die with "no such file or directory" from a dynamic loader.
|
|
135
|
+
let ok = false;
|
|
136
|
+
const check = (0, docker_1.execInContainer)(containerId, ['sh', '-c', `"${containerPath}" --version >/dev/null 2>&1 && echo yes || true`], (line) => {
|
|
137
|
+
if (line.includes('yes'))
|
|
138
|
+
ok = true;
|
|
139
|
+
});
|
|
140
|
+
await check.done;
|
|
141
|
+
if (!ok) {
|
|
142
|
+
throw new ExternalsError(`the image cannot run the provided node binary, which is a glibc build. ` +
|
|
143
|
+
`Use an image that includes node (most CI images do), or one based on glibc rather than musl.`);
|
|
144
|
+
}
|
|
145
|
+
return containerPath;
|
|
146
|
+
}
|
|
147
|
+
async download(version, name, target) {
|
|
148
|
+
const url = `https://nodejs.org/dist/v${version}/${name}.tar.gz`;
|
|
149
|
+
let res;
|
|
150
|
+
try {
|
|
151
|
+
res = await fetch(url, { redirect: 'follow' });
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
throw new ExternalsError(`could not download node ${version}: ${e instanceof Error ? e.message : e}`);
|
|
155
|
+
}
|
|
156
|
+
if (!res.ok)
|
|
157
|
+
throw new ExternalsError(`could not download node ${version}: ${url} answered ${res.status}`);
|
|
158
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
159
|
+
const tmp = path.join(this.dir, `.tmp-${process.pid}`);
|
|
160
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
161
|
+
fs.mkdirSync(tmp);
|
|
162
|
+
const tarball = path.join(tmp, 'node.tar.gz');
|
|
163
|
+
fs.writeFileSync(tarball, Buffer.from(await res.arrayBuffer()));
|
|
164
|
+
const extracted = path.join(tmp, 'out');
|
|
165
|
+
fs.mkdirSync(extracted);
|
|
166
|
+
await run('tar', ['-xzf', tarball, '-C', extracted, '--strip-components=1']);
|
|
167
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
168
|
+
fs.renameSync(extracted, target);
|
|
169
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
hostDir() {
|
|
172
|
+
return this.dir;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
exports.Externals = Externals;
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parseKeyValueFile = exports.parseWorkflowCommand = void 0;
|
|
37
|
+
exports.runJob = runJob;
|
|
38
|
+
exports.defaultWorkDir = defaultWorkDir;
|
|
39
|
+
const crypto = __importStar(require("crypto"));
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const child_process_1 = require("child_process");
|
|
44
|
+
const expr_1 = require("../ci/expr");
|
|
45
|
+
const context_1 = require("./context");
|
|
46
|
+
const docker_1 = require("./docker");
|
|
47
|
+
const externals_1 = require("./externals");
|
|
48
|
+
const steps_1 = require("./steps");
|
|
49
|
+
// Step index -1 carries the runner's own setup, post-step, and teardown
|
|
50
|
+
// output, so anything happening outside the workflow's steps still has
|
|
51
|
+
// somewhere to be reported.
|
|
52
|
+
const RUNNER_STEP = -1;
|
|
53
|
+
function nowIso() {
|
|
54
|
+
return new Date().toISOString();
|
|
55
|
+
}
|
|
56
|
+
function execFileAsync(cmd, args, cwd) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
(0, child_process_1.execFile)(cmd, args, { cwd, maxBuffer: 64 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
59
|
+
if (err)
|
|
60
|
+
reject(new Error((stderr || err.message).toString().trim()));
|
|
61
|
+
else
|
|
62
|
+
resolve(stdout.toString());
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function runJob(spec, ctx, hooks) {
|
|
67
|
+
const image = ctx.imageFor(spec.runsOn);
|
|
68
|
+
const masker = new context_1.Masker();
|
|
69
|
+
const env = (0, context_1.baseEnv)(spec);
|
|
70
|
+
const stepStates = spec.steps.map((s, i) => ({ name: (0, context_1.stepLabel)(s, i), status: 'pending' }));
|
|
71
|
+
const rt = { env: {}, extraPath: [], summaries: [], postHooks: [] };
|
|
72
|
+
const scope = (0, context_1.newScope)(0);
|
|
73
|
+
let currentStep = RUNNER_STEP;
|
|
74
|
+
let failed = false;
|
|
75
|
+
const log = (line) => hooks.log(currentStep, masker.apply(line));
|
|
76
|
+
// The work directory is made per job rather than once at startup. A runner
|
|
77
|
+
// is a long-lived process and its default work directory is under /tmp,
|
|
78
|
+
// where a tmp cleaner or an operator can remove it between jobs; recreating
|
|
79
|
+
// it here costs nothing and turns a run that failed before its first step
|
|
80
|
+
// with a bare ENOENT into one that simply runs.
|
|
81
|
+
fs.mkdirSync(ctx.workDir, { recursive: true });
|
|
82
|
+
const hostWork = fs.mkdtempSync(path.join(ctx.workDir, 'job-'));
|
|
83
|
+
const hostRepo = path.join(hostWork, 'workspace');
|
|
84
|
+
const hostTemp = path.join(hostWork, 'temp');
|
|
85
|
+
const hostFiles = path.join(hostWork, 'files');
|
|
86
|
+
const hostActions = path.join(hostWork, 'actions');
|
|
87
|
+
for (const d of [hostRepo, hostTemp, hostFiles, hostActions])
|
|
88
|
+
fs.mkdirSync(d, { recursive: true });
|
|
89
|
+
let containerId = null;
|
|
90
|
+
// `timeout-minutes` is enforced here rather than by the server: the lease
|
|
91
|
+
// sweep only notices a runner that has stopped heartbeating, and a job stuck
|
|
92
|
+
// inside a step heartbeats perfectly well. Cancellation is cooperative and
|
|
93
|
+
// checked between steps, so a deadline that only set the flag would never
|
|
94
|
+
// interrupt the step that is actually hanging; removing the container is what
|
|
95
|
+
// makes the in-flight exec fail and the loop unwind.
|
|
96
|
+
let timedOut = false;
|
|
97
|
+
let deadline = null;
|
|
98
|
+
const cancelled = () => hooks.cancelled() || timedOut;
|
|
99
|
+
const finish = async (conclusion) => {
|
|
100
|
+
currentStep = RUNNER_STEP;
|
|
101
|
+
if (deadline)
|
|
102
|
+
clearTimeout(deadline);
|
|
103
|
+
if (containerId) {
|
|
104
|
+
log('Cleaning up the job container');
|
|
105
|
+
await (0, docker_1.removeContainer)(containerId);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
fs.rmSync(hostWork, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// a leftover directory is not worth failing the job over
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
conclusion,
|
|
115
|
+
steps: stepStates,
|
|
116
|
+
outputs: resolveJobOutputs(spec, scope, conclusion === 'failure', conclusion === 'cancelled'),
|
|
117
|
+
summaries: rt.summaries,
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
try {
|
|
121
|
+
// mochi checks the repository out before the job starts, rather than
|
|
122
|
+
// leaving an empty workspace for actions/checkout to fill. See the README:
|
|
123
|
+
// it makes run: steps useful on their own, and it means checkout is a
|
|
124
|
+
// re-sync rather than the first clone.
|
|
125
|
+
log(`Job ${spec.name} on ${spec.runsOn.join(', ')}`);
|
|
126
|
+
log(`Checking out ${spec.github.repository} at ${spec.sha.slice(0, 8)}`);
|
|
127
|
+
// A private repository's job carries an ephemeral token for it, which
|
|
128
|
+
// rides in the remote URL the way actions/checkout persists credentials:
|
|
129
|
+
// steps that fetch or pull from origin then work unchanged. It expires
|
|
130
|
+
// with the job either way.
|
|
131
|
+
const url = ctx.cloneUrl(spec.address.collection, spec.address.repo);
|
|
132
|
+
const remote = spec.cloneToken
|
|
133
|
+
? url.replace('://', `://x-job:${encodeURIComponent(spec.cloneToken)}@`)
|
|
134
|
+
: url;
|
|
135
|
+
await execFileAsync('git', ['init', '--quiet', hostRepo]);
|
|
136
|
+
await execFileAsync('git', ['-C', hostRepo, 'remote', 'add', 'origin', remote]);
|
|
137
|
+
await execFileAsync('git', ['-C', hostRepo, 'fetch', '--quiet', '--depth', '1', 'origin', spec.sha]);
|
|
138
|
+
await execFileAsync('git', ['-C', hostRepo, 'checkout', '--quiet', 'FETCH_HEAD']);
|
|
139
|
+
fs.writeFileSync(path.join(hostTemp, 'event.json'), JSON.stringify(spec.github.event ?? {}, null, 1));
|
|
140
|
+
if (!(await (0, docker_1.imagePresent)(image))) {
|
|
141
|
+
log(`Pulling ${image}`);
|
|
142
|
+
await (0, docker_1.pullImage)(image, log);
|
|
143
|
+
}
|
|
144
|
+
const externalsDir = ctx.externals.hostDir();
|
|
145
|
+
fs.mkdirSync(externalsDir, { recursive: true });
|
|
146
|
+
containerId = await (0, docker_1.startContainer)({
|
|
147
|
+
image,
|
|
148
|
+
name: containerName(spec),
|
|
149
|
+
binds: [
|
|
150
|
+
{ host: hostRepo, container: context_1.WORKSPACE },
|
|
151
|
+
{ host: hostTemp, container: context_1.RUNNER_TEMP },
|
|
152
|
+
{ host: hostFiles, container: context_1.FILE_COMMAND_DIR },
|
|
153
|
+
{ host: hostActions, container: context_1.ACTIONS_MOUNT },
|
|
154
|
+
// Read-only: the provided node binaries are shared by every job on
|
|
155
|
+
// this runner, so nothing a job does may change them.
|
|
156
|
+
{ host: externalsDir, container: externals_1.EXTERNALS_MOUNT, readonly: true },
|
|
157
|
+
],
|
|
158
|
+
env,
|
|
159
|
+
workdir: context_1.WORKSPACE,
|
|
160
|
+
network: ctx.network,
|
|
161
|
+
});
|
|
162
|
+
await (0, docker_1.execInContainer)(containerId, ['mkdir', '-p', context_1.RUNNER_TOOL_CACHE], () => { }).done;
|
|
163
|
+
log(`Container started from ${image}`);
|
|
164
|
+
const id = containerId;
|
|
165
|
+
// A runner and its server are separate long-running processes and need not
|
|
166
|
+
// be the same version, so a spec that carries no usable timeout falls back
|
|
167
|
+
// to GitHub's six hours rather than to NaN, which setTimeout would treat as
|
|
168
|
+
// zero and fire against every job immediately.
|
|
169
|
+
const minutes = Number(spec.timeoutMinutes);
|
|
170
|
+
const limit = Number.isFinite(minutes) && minutes > 0 ? minutes : 360;
|
|
171
|
+
// setTimeout counts in a signed 32-bit millisecond field and fires at once
|
|
172
|
+
// on anything larger, so an absurd timeout-minutes has to be capped rather
|
|
173
|
+
// than passed through: uncapped, it would do the opposite of what it asks.
|
|
174
|
+
const delay = Math.min(limit * 60_000, 2 ** 31 - 1);
|
|
175
|
+
deadline = setTimeout(() => {
|
|
176
|
+
timedOut = true;
|
|
177
|
+
log(`Job exceeded its timeout of ${limit} minute${limit === 1 ? '' : 's'}`);
|
|
178
|
+
void (0, docker_1.removeContainer)(id);
|
|
179
|
+
}, delay);
|
|
180
|
+
const exec = {
|
|
181
|
+
containerId,
|
|
182
|
+
spec,
|
|
183
|
+
env,
|
|
184
|
+
rt,
|
|
185
|
+
masker,
|
|
186
|
+
actions: ctx.actions,
|
|
187
|
+
externals: ctx.externals,
|
|
188
|
+
hostPaths: { workspace: hostRepo, temp: hostTemp, files: hostFiles, actions: hostActions },
|
|
189
|
+
cloneUrl: ctx.cloneUrl,
|
|
190
|
+
serverUrl: ctx.serverUrl,
|
|
191
|
+
runnerToken: ctx.runnerToken,
|
|
192
|
+
hostPath: (p) => {
|
|
193
|
+
if (!path.isAbsolute(p))
|
|
194
|
+
return path.resolve(hostRepo, p);
|
|
195
|
+
for (const [mount, host] of [
|
|
196
|
+
[context_1.WORKSPACE, hostRepo],
|
|
197
|
+
[context_1.RUNNER_TEMP, hostTemp],
|
|
198
|
+
[context_1.FILE_COMMAND_DIR, hostFiles],
|
|
199
|
+
[context_1.ACTIONS_MOUNT, hostActions],
|
|
200
|
+
]) {
|
|
201
|
+
if (p === mount)
|
|
202
|
+
return host;
|
|
203
|
+
if (p.startsWith(mount + '/'))
|
|
204
|
+
return path.join(host, p.slice(mount.length + 1));
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
},
|
|
208
|
+
log: (line) => log(line),
|
|
209
|
+
status: () => ({ failed, cancelled: cancelled() }),
|
|
210
|
+
};
|
|
211
|
+
const result = await (0, steps_1.runSteps)(spec.steps, scope, exec, {
|
|
212
|
+
onStart: (i, label) => {
|
|
213
|
+
currentStep = i;
|
|
214
|
+
stepStates[i].status = 'running';
|
|
215
|
+
stepStates[i].startedAt = nowIso();
|
|
216
|
+
stepStates[i].name = label;
|
|
217
|
+
hooks.progress(stepStates);
|
|
218
|
+
log(`▸ ${label}`);
|
|
219
|
+
},
|
|
220
|
+
onEnd: (i, outcome) => {
|
|
221
|
+
const state = stepStates[i];
|
|
222
|
+
state.status = 'completed';
|
|
223
|
+
state.completedAt = state.completedAt ?? nowIso();
|
|
224
|
+
state.conclusion = outcome.skipped ? 'skipped' : outcome.failed ? 'failure' : 'success';
|
|
225
|
+
if (outcome.failed)
|
|
226
|
+
failed = true;
|
|
227
|
+
currentStep = RUNNER_STEP;
|
|
228
|
+
hooks.progress(stepStates);
|
|
229
|
+
},
|
|
230
|
+
checkCancelled: () => cancelled(),
|
|
231
|
+
});
|
|
232
|
+
failed = failed || result.failed;
|
|
233
|
+
// Post steps run even when the job failed or was cancelled, which is what
|
|
234
|
+
// makes them useful for cleanup. A timeout is the exception: the container
|
|
235
|
+
// they would run in is the thing that was torn down to stop the job, so
|
|
236
|
+
// attempting them would only add a failure for each one to the log.
|
|
237
|
+
if (rt.postHooks.length && !timedOut) {
|
|
238
|
+
currentStep = RUNNER_STEP;
|
|
239
|
+
await (0, steps_1.runPostHooks)(exec, failed);
|
|
240
|
+
}
|
|
241
|
+
// A job that ran out of time is a failure, as it is on GitHub; only the
|
|
242
|
+
// server asking for the job back is a cancellation.
|
|
243
|
+
if (timedOut)
|
|
244
|
+
return await finish('failure');
|
|
245
|
+
if (result.cancelled || hooks.cancelled())
|
|
246
|
+
return await finish('cancelled');
|
|
247
|
+
return await finish(failed ? 'failure' : 'success');
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
currentStep = RUNNER_STEP;
|
|
251
|
+
log(`Job failed to run: ${e instanceof Error ? e.message : String(e)}`);
|
|
252
|
+
return await finish(!timedOut && hooks.cancelled() ? 'cancelled' : 'failure');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function containerName(spec) {
|
|
256
|
+
const raw = `mochi-${spec.address.collection}-${spec.address.repo}-${spec.runNumber}-${spec.address.job}-${crypto
|
|
257
|
+
.randomBytes(4)
|
|
258
|
+
.toString('hex')}`;
|
|
259
|
+
return raw.toLowerCase().replace(/[^a-z0-9_.-]/g, '-');
|
|
260
|
+
}
|
|
261
|
+
function resolveJobOutputs(spec, scope, jobFailed, jobCancelled) {
|
|
262
|
+
const out = {};
|
|
263
|
+
if (Object.keys(spec.outputsTemplate).length === 0)
|
|
264
|
+
return out;
|
|
265
|
+
// The job's own outcome, not a placeholder: an output written as
|
|
266
|
+
// `${{ success() && ... }}` is a common way to hand a downstream job
|
|
267
|
+
// something only when this one worked, and a hardcoded success() would
|
|
268
|
+
// hand it over after a failure too.
|
|
269
|
+
const env = {
|
|
270
|
+
contexts: (0, context_1.contextsFor)({ spec, scope, stepEnv: {}, jobFailed, jobCancelled }),
|
|
271
|
+
functions: (0, context_1.statusFunctions)(jobFailed, jobCancelled),
|
|
272
|
+
};
|
|
273
|
+
for (const [k, template] of Object.entries(spec.outputsTemplate)) {
|
|
274
|
+
try {
|
|
275
|
+
out[k] = (0, expr_1.render)(template, env);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
out[k] = '';
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
function defaultWorkDir() {
|
|
284
|
+
const dir = path.join(os.tmpdir(), 'mochi-runner');
|
|
285
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
286
|
+
return dir;
|
|
287
|
+
}
|
|
288
|
+
var context_2 = require("./context");
|
|
289
|
+
Object.defineProperty(exports, "parseWorkflowCommand", { enumerable: true, get: function () { return context_2.parseWorkflowCommand; } });
|
|
290
|
+
Object.defineProperty(exports, "parseKeyValueFile", { enumerable: true, get: function () { return context_2.parseKeyValueFile; } });
|