@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,554 @@
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.findOverride = findOverride;
37
+ const child_process_1 = require("child_process");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const stream_1 = require("stream");
41
+ const artifacts_1 = require("../ci/artifacts");
42
+ const workflow_1 = require("../ci/workflow");
43
+ function git(args, cwd) {
44
+ return new Promise((resolve, reject) => {
45
+ (0, child_process_1.execFile)('git', args, { cwd, maxBuffer: 64 * 1024 * 1024 }, (err, stdout, stderr) => {
46
+ if (err)
47
+ reject(new Error((stderr || err.message).toString().trim()));
48
+ else
49
+ resolve(stdout.toString());
50
+ });
51
+ });
52
+ }
53
+ // ---- actions/checkout ----
54
+ // mochi checks the repository out before the job starts, so this is usually
55
+ // a no-op that reports what is already there. It does real work when the step
56
+ // asks for something different: another repository, another ref, another
57
+ // directory, full history, or submodules.
58
+ const checkout = {
59
+ name: 'checkout',
60
+ async run({ ctx, inputs }) {
61
+ const wanted = (inputs.repository ?? '').trim();
62
+ const current = String(ctx.spec.github.repository ?? '');
63
+ const ref = (inputs.ref ?? '').trim();
64
+ const target = (inputs.path ?? '').trim();
65
+ const depthRaw = (inputs['fetch-depth'] ?? '1').trim();
66
+ const depth = Number.isFinite(Number(depthRaw)) ? Number(depthRaw) : 1;
67
+ const submodules = (inputs.submodules ?? '').trim().toLowerCase();
68
+ const sameRepo = wanted === '' || wanted === current;
69
+ const sameRef = ref === '' || ref === ctx.spec.sha || ref === ctx.spec.ref || ref === ctx.spec.refName;
70
+ const samePath = target === '' || target === '.' || target === './';
71
+ if (sameRepo && sameRef && samePath) {
72
+ ctx.log(`${current} is already checked out at ${ctx.spec.sha.slice(0, 8)} (mochi checks out before the job starts)`);
73
+ if (depth === 0) {
74
+ ctx.log('fetch-depth: 0 requested; fetching the full history');
75
+ try {
76
+ await git(['fetch', '--quiet', '--unshallow', 'origin'], ctx.hostPaths.workspace);
77
+ }
78
+ catch {
79
+ // Not a shallow clone; nothing to deepen.
80
+ }
81
+ try {
82
+ await git(['fetch', '--quiet', '--tags', '--force', 'origin', '+refs/heads/*:refs/remotes/origin/*'], ctx.hostPaths.workspace);
83
+ }
84
+ catch (e) {
85
+ ctx.log(`could not fetch the full history: ${e instanceof Error ? e.message : e}`);
86
+ return { ok: false };
87
+ }
88
+ }
89
+ if (submodules !== '' && submodules !== 'false') {
90
+ const args = ['submodule', 'update', '--init'];
91
+ if (submodules === 'recursive')
92
+ args.push('--recursive');
93
+ try {
94
+ await git(args, ctx.hostPaths.workspace);
95
+ }
96
+ catch (e) {
97
+ ctx.log(`could not initialize submodules: ${e instanceof Error ? e.message : e}`);
98
+ return { ok: false };
99
+ }
100
+ }
101
+ return { ok: true, outputs: { ref: ctx.spec.ref, commit: ctx.spec.sha } };
102
+ }
103
+ // A different repository, ref, or directory: a real checkout. Note that
104
+ // `repository:` names a repository in this vault, since github.server_url
105
+ // is the vault; a workflow copied from GitHub that pulls in a second
106
+ // repository from github.com needs its own git commands.
107
+ const repoSpec = sameRepo ? current : wanted;
108
+ const [collection, name] = repoSpec.split('/');
109
+ if (!collection || !name) {
110
+ ctx.log(`repository: ${repoSpec} is not collection/repo`);
111
+ return { ok: false };
112
+ }
113
+ const dest = samePath ? ctx.hostPaths.workspace : path.join(ctx.hostPaths.workspace, target);
114
+ const real = path.resolve(dest);
115
+ if (real !== path.resolve(ctx.hostPaths.workspace) && !real.startsWith(path.resolve(ctx.hostPaths.workspace) + path.sep)) {
116
+ ctx.log(`path: ${target} resolves outside the workspace`);
117
+ return { ok: false };
118
+ }
119
+ const url = ctx.cloneUrl(collection, name);
120
+ ctx.log(`Checking out ${repoSpec}${ref ? `@${ref}` : ''} into ${samePath ? '.' : target}`);
121
+ try {
122
+ fs.rmSync(dest, { recursive: true, force: true });
123
+ fs.mkdirSync(dest, { recursive: true });
124
+ await git(['init', '--quiet', dest]);
125
+ await git(['remote', 'add', 'origin', url], dest);
126
+ const fetchArgs = ['fetch', '--quiet'];
127
+ if (depth > 0)
128
+ fetchArgs.push('--depth', String(depth));
129
+ fetchArgs.push('origin', ref === '' ? ctx.spec.sha : ref);
130
+ await git(fetchArgs, dest);
131
+ await git(['checkout', '--quiet', 'FETCH_HEAD'], dest);
132
+ if (submodules !== '' && submodules !== 'false') {
133
+ const args = ['submodule', 'update', '--init'];
134
+ if (submodules === 'recursive')
135
+ args.push('--recursive');
136
+ await git(args, dest);
137
+ }
138
+ const sha = (await git(['rev-parse', 'HEAD'], dest)).trim();
139
+ return { ok: true, outputs: { ref: ref || ctx.spec.ref, commit: sha } };
140
+ }
141
+ catch (e) {
142
+ const msg = e instanceof Error ? e.message : String(e);
143
+ ctx.log(`checkout failed: ${msg}`);
144
+ // A workflow copied from GitHub is the common way to arrive here, and
145
+ // its author read `repository:` as naming something on github.com. The
146
+ // vault cannot know which they meant, but "not found" plus a repository
147
+ // this vault does not have is worth explaining rather than leaving as a
148
+ // bare git error.
149
+ if (/not found|does not exist|could not read/i.test(msg) && !sameRepo) {
150
+ ctx.log(`Note: on mochi, repository: ${repoSpec} names a repository in this vault, not on github.com.`);
151
+ ctx.log(`If you meant github.com, check it out with git instead: run: git clone --depth 1${ref ? ` --branch ${ref}` : ''} https://github.com/${repoSpec}.git ${target || '.'}`);
152
+ }
153
+ return { ok: false };
154
+ }
155
+ },
156
+ };
157
+ // ---- artifacts ----
158
+ // GitHub's upload-artifact and download-artifact are clients for its
159
+ // artifact service. These do the same job against the vault: the runner
160
+ // builds a tar of the requested paths and PUTs it to the run's artifact
161
+ // endpoint, authorized by the job's lease.
162
+ function splitPaths(input) {
163
+ const include = [];
164
+ const exclude = [];
165
+ for (const raw of (input ?? '').split('\n')) {
166
+ const line = raw.trim();
167
+ if (line === '')
168
+ continue;
169
+ if (line.startsWith('!'))
170
+ exclude.push(line.slice(1));
171
+ else
172
+ include.push(line);
173
+ }
174
+ return { include, exclude };
175
+ }
176
+ // Expand one path, which may contain * and **, against the workspace. The
177
+ // glob dialect is the one workflow filters use, so there is one answer in the
178
+ // codebase to what a pattern means.
179
+ function expandPattern(rootDir, pattern, unreadable) {
180
+ const normalized = pattern.replace(/^\.\//, '').replace(/\/+$/, '');
181
+ const absolute = path.isAbsolute(normalized);
182
+ const full = absolute ? normalized : path.join(rootDir, normalized);
183
+ if (!/[*?[]/.test(normalized)) {
184
+ return fs.existsSync(full) ? [full] : [];
185
+ }
186
+ // Start from the deepest directory the pattern names literally. Walking
187
+ // from the filesystem root instead would be both slow and wrong.
188
+ const segments = full.split('/');
189
+ const firstGlob = segments.findIndex((s) => /[*?[]/.test(s));
190
+ const start = segments.slice(0, firstGlob).join('/') || '/';
191
+ const rx = (0, workflow_1.filterPatternToRegExp)(absolute ? full : normalized);
192
+ const out = [];
193
+ const walk = (dir) => {
194
+ let entries;
195
+ try {
196
+ entries = fs.readdirSync(dir, { withFileTypes: true });
197
+ }
198
+ catch (e) {
199
+ // A directory that is there but closed to us is not the same as one
200
+ // that is not there, and the caller says so rather than reporting an
201
+ // empty match.
202
+ if (e.code === 'EACCES')
203
+ unreadable?.push(dir);
204
+ return;
205
+ }
206
+ for (const e of entries) {
207
+ const child = path.join(dir, e.name);
208
+ const candidate = absolute ? child : path.relative(rootDir, child);
209
+ if (rx.test(candidate))
210
+ out.push(child);
211
+ if (e.isDirectory())
212
+ walk(child);
213
+ }
214
+ };
215
+ walk(start);
216
+ return out;
217
+ }
218
+ // Every file the pattern set selects, with the directory their paths are
219
+ // relative to. GitHub uses the least common ancestor of the matched paths as
220
+ // that root, so `path: dist` uploads dist's contents rather than a dist/
221
+ // prefix; this reproduces that.
222
+ function collectArtifactFiles(ctx, input) {
223
+ const unreadable = [];
224
+ const workspace = ctx.hostPaths.workspace;
225
+ const { include, exclude } = splitPaths(input);
226
+ // A path in a workflow or an action is written as the step sees it inside
227
+ // the container, so it is translated before touching the host filesystem.
228
+ const onHost = (p) => (path.isAbsolute(p) ? ctx.hostPath(p) : p);
229
+ const matched = new Set();
230
+ for (const p of include) {
231
+ const mapped = onHost(p);
232
+ if (mapped === null) {
233
+ ctx.log(`WARNING: ${p} is not inside the workspace or the runner temp directory; ignoring it`);
234
+ continue;
235
+ }
236
+ for (const m of expandPattern(workspace, mapped, unreadable))
237
+ matched.add(m);
238
+ }
239
+ const excluded = new Set();
240
+ for (const p of exclude) {
241
+ const mapped = onHost(p);
242
+ if (mapped === null)
243
+ continue;
244
+ for (const m of expandPattern(workspace, mapped))
245
+ excluded.add(m);
246
+ }
247
+ const files = [];
248
+ const addFile = (p) => {
249
+ if (excluded.has(p))
250
+ return;
251
+ if ([...excluded].some((e) => p.startsWith(e + path.sep)))
252
+ return;
253
+ files.push(p);
254
+ };
255
+ const walk = (dir) => {
256
+ let entries;
257
+ try {
258
+ entries = fs.readdirSync(dir, { withFileTypes: true });
259
+ }
260
+ catch (e) {
261
+ if (e.code === 'EACCES')
262
+ unreadable.push(dir);
263
+ return;
264
+ }
265
+ for (const e of entries) {
266
+ const full = path.join(dir, e.name);
267
+ if (e.isDirectory())
268
+ walk(full);
269
+ else
270
+ addFile(full);
271
+ }
272
+ };
273
+ for (const m of matched) {
274
+ try {
275
+ if (fs.statSync(m).isDirectory())
276
+ walk(m);
277
+ else
278
+ addFile(m);
279
+ }
280
+ catch (e) {
281
+ // A file that vanished between matching and reading is not worth
282
+ // reporting; one we are not allowed to read is.
283
+ if (e.code === 'EACCES')
284
+ unreadable.push(m);
285
+ }
286
+ }
287
+ if (files.length === 0)
288
+ return { root: workspace, files: [], unreadable };
289
+ let root = path.dirname(files[0]);
290
+ for (const f of files.slice(1)) {
291
+ const dir = path.dirname(f);
292
+ while (root !== dir && !dir.startsWith(root + path.sep)) {
293
+ const parent = path.dirname(root);
294
+ if (parent === root)
295
+ break;
296
+ root = parent;
297
+ }
298
+ }
299
+ return { root, files, unreadable };
300
+ }
301
+ function tar(args, cwd) {
302
+ return new Promise((resolve, reject) => {
303
+ (0, child_process_1.execFile)('tar', args, { cwd, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
304
+ if (err)
305
+ reject(new Error((stderr || err.message).toString().trim()));
306
+ else
307
+ resolve(stdout.toString());
308
+ });
309
+ });
310
+ }
311
+ function artifactUrl(ctx, name) {
312
+ const a = ctx.spec.address;
313
+ const base = `${ctx.serverUrl}/api/runner/jobs/${encodeURIComponent(a.collection)}/${encodeURIComponent(a.repo)}/${a.run}/${encodeURIComponent(a.job)}/artifacts`;
314
+ return name === undefined ? base : `${base}/${encodeURIComponent(name)}`;
315
+ }
316
+ const uploadArtifact = {
317
+ name: 'upload-artifact',
318
+ async run({ ctx, inputs }) {
319
+ const name = (inputs.name ?? 'artifact').trim() || 'artifact';
320
+ if (!(0, artifacts_1.isValidArtifactName)(name)) {
321
+ ctx.log(`artifact name "${name}" is not usable as a file name`);
322
+ return { ok: false };
323
+ }
324
+ const { root, files, unreadable } = collectArtifactFiles(ctx, inputs.path);
325
+ // Nothing collected because nothing could be read is a different answer
326
+ // from nothing collected because nothing is there, and it is not one
327
+ // `if-no-files-found` is about: the step asked for files that exist. It
328
+ // happens when the job's container writes as root onto a filesystem whose
329
+ // permissions leave the runner's own user unable to walk back in, and it
330
+ // would otherwise be a green build that shipped an empty artifact.
331
+ if (files.length === 0 && unreadable.length > 0) {
332
+ ctx.log(`ERROR: cannot read ${unreadable[0]}: permission denied`);
333
+ ctx.log('The job wrote it as another user; nothing was uploaded.');
334
+ return { ok: false };
335
+ }
336
+ if (unreadable.length > 0) {
337
+ ctx.log(`WARNING: leaving out ${unreadable.length} path(s) this runner may not read, starting with ${unreadable[0]}`);
338
+ }
339
+ if (files.length === 0) {
340
+ const behavior = (inputs['if-no-files-found'] ?? 'warn').trim().toLowerCase();
341
+ const message = `no files matched ${JSON.stringify((inputs.path ?? '').trim())}`;
342
+ if (behavior === 'error') {
343
+ ctx.log(`ERROR: ${message}`);
344
+ return { ok: false };
345
+ }
346
+ if (behavior !== 'ignore')
347
+ ctx.log(`WARNING: ${message}`);
348
+ return { ok: true, outputs: { 'artifact-id': '', 'artifact-url': '' } };
349
+ }
350
+ const listFile = path.join(ctx.hostPaths.temp, `artifact-${name}.list`);
351
+ fs.writeFileSync(listFile, files.map((f) => path.relative(root, f)).join('\n') + '\n');
352
+ const tarFile = path.join(ctx.hostPaths.temp, `artifact-${name}.tar`);
353
+ try {
354
+ await tar(['-cf', tarFile, '-C', root, '--files-from', listFile]);
355
+ const size = fs.statSync(tarFile).size;
356
+ ctx.log(`Uploading artifact ${name} (${files.length} file${files.length === 1 ? '' : 's'}, ${size} bytes)`);
357
+ // Streamed rather than read into memory: an artifact may be hundreds of
358
+ // megabytes, and the runner is somebody's laptop.
359
+ const res = await fetch(artifactUrl(ctx, name), {
360
+ method: 'PUT',
361
+ headers: {
362
+ authorization: `Bearer ${ctx.runnerToken}`,
363
+ 'x-mochi-lease': ctx.spec.lease,
364
+ 'content-type': 'application/x-tar',
365
+ 'content-length': String(size),
366
+ },
367
+ body: stream_1.Readable.toWeb(fs.createReadStream(tarFile)),
368
+ duplex: 'half',
369
+ });
370
+ if (!res.ok) {
371
+ const err = (await res.json().catch(() => ({})));
372
+ ctx.log(`uploading the artifact failed: ${err.error ?? res.status}`);
373
+ return { ok: false };
374
+ }
375
+ return {
376
+ ok: true,
377
+ outputs: { 'artifact-id': name, 'artifact-url': artifactUrl(ctx, name) },
378
+ };
379
+ }
380
+ catch (e) {
381
+ ctx.log(`uploading the artifact failed: ${e instanceof Error ? e.message : e}`);
382
+ return { ok: false };
383
+ }
384
+ finally {
385
+ fs.rmSync(listFile, { force: true });
386
+ fs.rmSync(tarFile, { force: true });
387
+ }
388
+ },
389
+ };
390
+ const downloadArtifact = {
391
+ name: 'download-artifact',
392
+ async run({ ctx, inputs }) {
393
+ const name = (inputs.name ?? '').trim();
394
+ const target = (inputs.path ?? '').trim();
395
+ const mapped = target === '' ? ctx.hostPaths.workspace : ctx.hostPath(target);
396
+ if (mapped === null) {
397
+ ctx.log(`path: ${target} is not inside the workspace or the runner temp directory`);
398
+ return { ok: false };
399
+ }
400
+ const dest = path.resolve(mapped);
401
+ const wsReal = path.resolve(ctx.hostPaths.workspace);
402
+ const tmpReal = path.resolve(ctx.hostPaths.temp);
403
+ const inside = (base) => dest === base || dest.startsWith(base + path.sep);
404
+ if (!inside(wsReal) && !inside(tmpReal)) {
405
+ ctx.log(`path: ${target} resolves outside the workspace`);
406
+ return { ok: false };
407
+ }
408
+ const headers = { authorization: `Bearer ${ctx.runnerToken}`, 'x-mochi-lease': ctx.spec.lease };
409
+ let names;
410
+ if (name !== '') {
411
+ names = [name];
412
+ }
413
+ else {
414
+ const res = await fetch(artifactUrl(ctx), { headers });
415
+ if (!res.ok) {
416
+ ctx.log(`could not list the run's artifacts: ${res.status}`);
417
+ return { ok: false };
418
+ }
419
+ const body = (await res.json());
420
+ names = (body.artifacts ?? []).map((a) => a.name);
421
+ if (names.length === 0) {
422
+ ctx.log('this run has no artifacts to download');
423
+ return { ok: true };
424
+ }
425
+ }
426
+ for (const one of names) {
427
+ const res = await fetch(artifactUrl(ctx, one), { headers });
428
+ if (!res.ok) {
429
+ const err = (await res.json().catch(() => ({})));
430
+ ctx.log(`could not download artifact ${one}: ${err.error ?? res.status}`);
431
+ return { ok: false };
432
+ }
433
+ // With no name given, each artifact lands in its own subdirectory, as
434
+ // download-artifact does.
435
+ const into = name === '' ? path.join(dest, one) : dest;
436
+ fs.mkdirSync(into, { recursive: true });
437
+ const tmp = path.join(ctx.hostPaths.temp, `download-${one}.tar`);
438
+ if (res.body) {
439
+ await new Promise((resolve, reject) => {
440
+ const out = fs.createWriteStream(tmp);
441
+ out.on('error', reject);
442
+ out.on('finish', () => resolve());
443
+ stream_1.Readable.fromWeb(res.body).on('error', reject).pipe(out);
444
+ });
445
+ }
446
+ else {
447
+ fs.writeFileSync(tmp, Buffer.from(await res.arrayBuffer()));
448
+ }
449
+ try {
450
+ await tar(['-xf', tmp, '-C', into, '--no-same-owner']);
451
+ ctx.log(`Downloaded artifact ${one} into ${path.relative(wsReal, into) || '.'}`);
452
+ }
453
+ catch (e) {
454
+ ctx.log(`could not unpack artifact ${one}: ${e instanceof Error ? e.message : e}`);
455
+ return { ok: false };
456
+ }
457
+ finally {
458
+ fs.rmSync(tmp, { force: true });
459
+ }
460
+ }
461
+ return { ok: true, outputs: { 'download-path': dest } };
462
+ },
463
+ };
464
+ // ---- the site ----
465
+ //
466
+ // These two keep GitHub's spelling in their `uses:` keys and in the
467
+ // `github-pages` artifact name, because those are somebody else's interface.
468
+ // mochi's own noun for what they publish is a site.
469
+ function siteUrls(ctx) {
470
+ const a = ctx.spec.address;
471
+ // The vault says where the site will be, because only it knows whether a
472
+ // sites hostname is configured; a site served on one has an origin of its
473
+ // own and sits at its root, which no amount of arithmetic on the server URL
474
+ // would reveal. The fallback is the path shape, which is what a vault older
475
+ // than this field serves.
476
+ const baseUrl = (ctx.spec.site?.url ?? `${ctx.serverUrl.replace(/\/+$/, '')}/${a.collection}/${a.repo}/site/`).replace(/\/+$/, '');
477
+ const basePath = ctx.spec.site?.basePath ?? `/${a.collection}/${a.repo}/site`;
478
+ let origin = baseUrl;
479
+ let host = baseUrl;
480
+ try {
481
+ const u = new URL(baseUrl);
482
+ origin = u.origin;
483
+ host = u.host;
484
+ }
485
+ catch {
486
+ // keep the whole string if it is not a URL
487
+ }
488
+ return { origin, basePath, baseUrl, host };
489
+ }
490
+ // configure-pages exists to tell the rest of a workflow where the site will
491
+ // live. The real one asks GitHub's API; this answers with what the vault said
492
+ // when it handed out the job. Note the shape differs from GitHub's, and
493
+ // differs between vaults: a site is served at /<collection>/<repo>/site/, or
494
+ // at the root of an origin of its own where a sites hostname is configured,
495
+ // but never at <owner>.github.io/<repo>/. A generator that computes its own
496
+ // base path instead of reading base_path will be wrong on both, and needs the
497
+ // path passed to it explicitly.
498
+ const configurePages = {
499
+ name: 'configure-pages',
500
+ async run({ ctx }) {
501
+ const u = siteUrls(ctx);
502
+ ctx.log(`This site will be served at ${u.baseUrl}/`);
503
+ ctx.rt.env.MOCHI_SITE_BASE_PATH = u.basePath;
504
+ // The former name of the same variable, kept so workflows written against
505
+ // it keep working; remove it once they have moved.
506
+ ctx.rt.env.MOCHI_PAGES_BASE_PATH = u.basePath;
507
+ return {
508
+ ok: true,
509
+ outputs: { base_url: u.baseUrl, origin: u.origin, host: u.host, base_path: u.basePath },
510
+ };
511
+ },
512
+ };
513
+ // deploy-pages publishes the artifact that upload-pages-artifact produced.
514
+ // The server unpacks it, since the site directory is vault state.
515
+ const deployPages = {
516
+ name: 'deploy-pages',
517
+ async run({ ctx, inputs }) {
518
+ const artifact = (inputs.artifact_name ?? 'github-pages').trim() || 'github-pages';
519
+ const a = ctx.spec.address;
520
+ const url = `${ctx.serverUrl}/api/runner/jobs/${encodeURIComponent(a.collection)}/${encodeURIComponent(a.repo)}/${a.run}/${encodeURIComponent(a.job)}/site`;
521
+ const res = await fetch(url, {
522
+ method: 'POST',
523
+ headers: {
524
+ authorization: `Bearer ${ctx.runnerToken}`,
525
+ 'x-mochi-lease': ctx.spec.lease,
526
+ 'content-type': 'application/json',
527
+ },
528
+ body: JSON.stringify({ artifact }),
529
+ });
530
+ const body = (await res.json().catch(() => ({})));
531
+ if (!res.ok) {
532
+ ctx.log(`deploying the site failed: ${body.error ?? res.status}`);
533
+ return { ok: false };
534
+ }
535
+ ctx.log(`Deployed ${body.files} file${body.files === 1 ? '' : 's'} to ${body.url}`);
536
+ return { ok: true, outputs: { page_url: body.url ?? siteUrls(ctx).baseUrl + '/' } };
537
+ },
538
+ };
539
+ // ---- the registry ----
540
+ // Matched on owner/repo, ignoring the ref, since the point is to substitute
541
+ // every version of these actions.
542
+ const OVERRIDES = {
543
+ 'actions/checkout': checkout,
544
+ 'actions/upload-artifact': uploadArtifact,
545
+ 'actions/download-artifact': downloadArtifact,
546
+ 'actions/configure-pages': configurePages,
547
+ 'actions/deploy-pages': deployPages,
548
+ };
549
+ function findOverride(ref) {
550
+ if (ref.kind !== 'repo')
551
+ return null;
552
+ const key = `${ref.owner}/${ref.repo}${ref.subpath ? '/' + ref.subpath : ''}`.toLowerCase();
553
+ return OVERRIDES[key] ?? null;
554
+ }