@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.
Files changed (116) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +108 -0
  3. package/dist/ansi.js +174 -0
  4. package/dist/api/admin.js +416 -0
  5. package/dist/api/auth.js +166 -0
  6. package/dist/api/backup.js +598 -0
  7. package/dist/api/ci.js +336 -0
  8. package/dist/api/contents.js +339 -0
  9. package/dist/api/issues.js +165 -0
  10. package/dist/api/pulls.js +244 -0
  11. package/dist/api/releases.js +83 -0
  12. package/dist/api/repos.js +156 -0
  13. package/dist/api/write.js +518 -0
  14. package/dist/api.js +326 -0
  15. package/dist/assets.js +29 -0
  16. package/dist/atom.js +32 -0
  17. package/dist/atomic.js +171 -0
  18. package/dist/avatar.js +81 -0
  19. package/dist/browse.js +630 -0
  20. package/dist/build-info.json +4 -0
  21. package/dist/ci/actionref.js +86 -0
  22. package/dist/ci/api.js +829 -0
  23. package/dist/ci/artifacts.js +201 -0
  24. package/dist/ci/dispatch.js +30 -0
  25. package/dist/ci/engine.js +1321 -0
  26. package/dist/ci/expr.js +526 -0
  27. package/dist/ci/manual.js +199 -0
  28. package/dist/ci/present.js +82 -0
  29. package/dist/ci/protocol.js +6 -0
  30. package/dist/ci/runners.js +256 -0
  31. package/dist/ci/runs.js +208 -0
  32. package/dist/ci/trigger.js +28 -0
  33. package/dist/ci/views.js +441 -0
  34. package/dist/ci/wake.js +194 -0
  35. package/dist/ci/web.js +617 -0
  36. package/dist/ci/workflow.js +436 -0
  37. package/dist/cli/admin-cmd.js +324 -0
  38. package/dist/cli/api-cmd.js +128 -0
  39. package/dist/cli/backup-cmd.js +1500 -0
  40. package/dist/cli/exit.js +69 -0
  41. package/dist/cli/input.js +64 -0
  42. package/dist/cli/issue-cmd.js +243 -0
  43. package/dist/cli/output.js +93 -0
  44. package/dist/cli/parse.js +317 -0
  45. package/dist/cli/pr-cmd.js +289 -0
  46. package/dist/cli/release-cmd.js +171 -0
  47. package/dist/cli/repo-cmd.js +763 -0
  48. package/dist/cli/repo.js +101 -0
  49. package/dist/cli/run-cmd.js +438 -0
  50. package/dist/cli/target.js +54 -0
  51. package/dist/cli-api.js +84 -0
  52. package/dist/compare.js +111 -0
  53. package/dist/config.js +212 -0
  54. package/dist/credentials.js +235 -0
  55. package/dist/deploy-cli.js +859 -0
  56. package/dist/deploy-runner-cli.js +592 -0
  57. package/dist/diff.js +171 -0
  58. package/dist/discussion.js +253 -0
  59. package/dist/egress.js +559 -0
  60. package/dist/filecache.js +68 -0
  61. package/dist/find.js +162 -0
  62. package/dist/forms.js +737 -0
  63. package/dist/git.js +547 -0
  64. package/dist/githttp.js +428 -0
  65. package/dist/html.js +87 -0
  66. package/dist/icons.js +101 -0
  67. package/dist/import-cli.js +316 -0
  68. package/dist/index.js +752 -0
  69. package/dist/issues.js +308 -0
  70. package/dist/issueweb.js +447 -0
  71. package/dist/job-cli.js +197 -0
  72. package/dist/jobtoken.js +96 -0
  73. package/dist/languages.js +383 -0
  74. package/dist/layout.js +100 -0
  75. package/dist/lfs.js +438 -0
  76. package/dist/lfsstore.js +425 -0
  77. package/dist/limit.js +259 -0
  78. package/dist/logo.js +61 -0
  79. package/dist/markdown.js +382 -0
  80. package/dist/migrate.js +334 -0
  81. package/dist/multipart.js +90 -0
  82. package/dist/ops.js +869 -0
  83. package/dist/pagescript.js +465 -0
  84. package/dist/perms.js +370 -0
  85. package/dist/pointer.js +55 -0
  86. package/dist/profile.js +106 -0
  87. package/dist/pulls.js +320 -0
  88. package/dist/pullweb.js +461 -0
  89. package/dist/redirects.js +455 -0
  90. package/dist/releases.js +435 -0
  91. package/dist/render.js +233 -0
  92. package/dist/runner/actions.js +448 -0
  93. package/dist/runner/client.js +428 -0
  94. package/dist/runner/context.js +247 -0
  95. package/dist/runner/docker.js +197 -0
  96. package/dist/runner/externals.js +175 -0
  97. package/dist/runner/job.js +290 -0
  98. package/dist/runner/manual-run.js +272 -0
  99. package/dist/runner/overrides.js +554 -0
  100. package/dist/runner/steps.js +571 -0
  101. package/dist/runner/wake.js +84 -0
  102. package/dist/runner-cli.js +405 -0
  103. package/dist/scan.js +231 -0
  104. package/dist/server.js +424 -0
  105. package/dist/session.js +267 -0
  106. package/dist/site.js +259 -0
  107. package/dist/siteshost.js +94 -0
  108. package/dist/source.js +90 -0
  109. package/dist/style.js +1295 -0
  110. package/dist/themes.js +369 -0
  111. package/dist/vault.js +442 -0
  112. package/dist/version.js +88 -0
  113. package/dist/views.js +1007 -0
  114. package/dist/web.js +182 -0
  115. package/dist/webops.js +1402 -0
  116. 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; } });