@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,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
|
+
}
|