@magland/mochi 0.3.1 → 0.4.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/README.md +7 -6
- package/dist/agefile.js +47 -0
- package/dist/agescript.js +343 -0
- package/dist/api/admin.js +66 -28
- package/dist/api/auth.js +3 -2
- package/dist/api/backup.js +227 -192
- package/dist/api/contents.js +46 -16
- package/dist/api/repos.js +60 -4
- package/dist/api/write.js +138 -8
- package/dist/api.js +34 -1
- package/dist/browse.js +171 -33
- package/dist/build-info.json +2 -2
- package/dist/ci/api.js +138 -4
- package/dist/ci/artifacts.js +82 -2
- package/dist/ci/engine.js +30 -17
- package/dist/ci/expr.js +14 -0
- package/dist/ci/runners.js +60 -1
- package/dist/ci/runs.js +21 -3
- package/dist/ci/trigger.js +6 -4
- package/dist/ci/views.js +24 -3
- package/dist/ci/web.js +48 -0
- package/dist/cli/admin-cmd.js +40 -3
- package/dist/cli/backup-cmd.js +166 -120
- package/dist/cli/gitrun.js +50 -0
- package/dist/cli/pr-cmd.js +155 -0
- package/dist/cli/repo-cmd.js +134 -6
- package/dist/cli/sync-cmd.js +128 -0
- package/dist/cli/target.js +7 -3
- package/dist/config.js +10 -0
- package/dist/credentials.js +9 -8
- package/dist/deploy-cli.js +214 -109
- package/dist/deploy-runner-cli.js +125 -49
- package/dist/domains.js +259 -0
- package/dist/egress.js +23 -6
- package/dist/forms.js +703 -32
- package/dist/git.js +124 -13
- package/dist/githttp.js +80 -23
- package/dist/githubauth.js +185 -0
- package/dist/icons.js +14 -0
- package/dist/import-cli.js +40 -25
- package/dist/index.js +110 -4
- package/dist/issueweb.js +10 -6
- package/dist/lfs.js +9 -2
- package/dist/limit.js +2 -2
- package/dist/logincodes.js +31 -0
- package/dist/maintenance.js +297 -0
- package/dist/markdown.js +193 -3
- package/dist/mdedit.js +29 -0
- package/dist/migrate.js +48 -0
- package/dist/naming.js +25 -0
- package/dist/onetime.js +112 -0
- package/dist/ops.js +293 -16
- package/dist/pagescript.js +355 -24
- package/dist/perms.js +1 -0
- package/dist/profile.js +2 -0
- package/dist/pulls.js +21 -1
- package/dist/pullweb.js +25 -6
- package/dist/releases.js +10 -1
- package/dist/runner/actions.js +12 -1
- package/dist/runner/client.js +40 -2
- package/dist/runner/docker.js +40 -28
- package/dist/runner/download.js +77 -0
- package/dist/runner/externals.js +12 -1
- package/dist/runner/job.js +26 -11
- package/dist/runner/steps.js +42 -7
- package/dist/runner-cli.js +89 -0
- package/dist/scan.js +58 -2
- package/dist/server.js +107 -26
- package/dist/session.js +82 -40
- package/dist/site.js +110 -27
- package/dist/sitepublish.js +177 -0
- package/dist/sitesettings.js +420 -0
- package/dist/siteshost.js +116 -28
- package/dist/source.js +20 -0
- package/dist/style.js +162 -3
- package/dist/topics.js +165 -0
- package/dist/vault.js +399 -6
- package/dist/vendor-age.js +15 -0
- package/dist/views.js +242 -57
- package/dist/web.js +11 -1
- package/dist/webauthn.js +347 -0
- package/dist/webops.js +1263 -33
- package/package.json +4 -4
package/dist/import-cli.js
CHANGED
|
@@ -36,11 +36,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.importCmd = importCmd;
|
|
37
37
|
exports.collectionAddCmd = collectionAddCmd;
|
|
38
38
|
exports.collectionListCmd = collectionListCmd;
|
|
39
|
-
|
|
39
|
+
exports.forkCmd = forkCmd;
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const os = __importStar(require("os"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
43
|
const cli_api_1 = require("./cli-api");
|
|
44
|
+
const gitrun_1 = require("./cli/gitrun");
|
|
44
45
|
const scan_1 = require("./scan");
|
|
45
46
|
const source_1 = require("./source");
|
|
46
47
|
function parseImportArgs(args, usage) {
|
|
@@ -100,24 +101,11 @@ function parseImportArgs(args, usage) {
|
|
|
100
101
|
}
|
|
101
102
|
return out;
|
|
102
103
|
}
|
|
103
|
-
/** Run git with its output going straight to the terminal, so a long clone shows progress. */
|
|
104
104
|
function git(args, env) {
|
|
105
|
-
return
|
|
106
|
-
const child = (0, child_process_1.spawn)('git', args, { stdio: 'inherit', env: env ?? process.env });
|
|
107
|
-
child.on('error', (e) => reject(new Error(e.code === 'ENOENT' ? 'git is not on PATH' : String(e.message))));
|
|
108
|
-
child.on('close', (code) => resolve(code ?? 1));
|
|
109
|
-
});
|
|
105
|
+
return (0, gitrun_1.run)('git', args, env);
|
|
110
106
|
}
|
|
111
|
-
// The token is handed to the push through a helper that reads it from the
|
|
112
|
-
// environment rather than through the command line, where every process on the
|
|
113
|
-
// machine could read it. The empty helper first clears the list, so the answer
|
|
114
|
-
// comes from here and nowhere else, and no prompt can appear.
|
|
115
|
-
function pushEnv(username, token) {
|
|
116
|
-
return { ...process.env, MOCHI_USER: username, MOCHI_TOKEN: token, GIT_TERMINAL_PROMPT: '0' };
|
|
117
|
-
}
|
|
118
|
-
const CREDENTIAL_HELPER = `!f() { test "$1" = get && printf 'username=%s\\npassword=%s\\n' "$MOCHI_USER" "$MOCHI_TOKEN"; }; f`;
|
|
119
107
|
function pushArgs(dir) {
|
|
120
|
-
return ['-C', dir,
|
|
108
|
+
return ['-C', dir, ...(0, gitrun_1.mochiCredentialArgs)()];
|
|
121
109
|
}
|
|
122
110
|
/**
|
|
123
111
|
* A repository's one-line description is not part of its git data, so a clone
|
|
@@ -144,14 +132,20 @@ async function githubDescription(gh) {
|
|
|
144
132
|
return null;
|
|
145
133
|
}
|
|
146
134
|
}
|
|
147
|
-
async function importCmd(args, usage) {
|
|
135
|
+
async function importCmd(args, usage, mode = {}) {
|
|
148
136
|
const a = parseImportArgs(args, usage);
|
|
137
|
+
const cmd = mode.fork ? 'fork' : 'import';
|
|
149
138
|
if (!a.src) {
|
|
150
|
-
console.error(
|
|
151
|
-
console.error(
|
|
139
|
+
console.error(`Usage: mochi ${cmd} <source> <collection>[/<name>]`);
|
|
140
|
+
console.error(` e.g. mochi ${cmd} https://github.com/octocat/Hello-World mycollection`);
|
|
152
141
|
process.exit(1);
|
|
153
142
|
}
|
|
154
143
|
const source = (0, source_1.parseSource)(a.src);
|
|
144
|
+
if (mode.fork && source && path.isAbsolute(source.url)) {
|
|
145
|
+
console.error('A fork records the URL it came from, and a local directory has no URL to record.');
|
|
146
|
+
console.error(`Use mochi import instead: mochi import ${a.src} <collection>`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
155
149
|
if (!source) {
|
|
156
150
|
console.error(`Not a source we can clone: ${a.src}`);
|
|
157
151
|
console.error('Give an https or ssh git URL, owner/repo for GitHub, or the path to a repository here.');
|
|
@@ -161,7 +155,7 @@ async function importCmd(args, usage) {
|
|
|
161
155
|
const name = a.name ?? source.name;
|
|
162
156
|
if (!collection) {
|
|
163
157
|
console.error(`Which collection should ${name} go into?`);
|
|
164
|
-
console.error(` mochi
|
|
158
|
+
console.error(` mochi ${cmd} ${a.src} mycollection`);
|
|
165
159
|
process.exit(1);
|
|
166
160
|
}
|
|
167
161
|
if (!(0, scan_1.isValidName)(collection) || !(0, scan_1.isValidName)(name)) {
|
|
@@ -186,7 +180,7 @@ async function importCmd(args, usage) {
|
|
|
186
180
|
console.error(`${collection}/${name} already exists on ${target.host}.`);
|
|
187
181
|
console.error('A mirror push would replace its branches and tags, so this stops here. Import');
|
|
188
182
|
console.error('under another name,');
|
|
189
|
-
console.error(` mochi
|
|
183
|
+
console.error(` mochi ${cmd} ${a.src} ${collection}/another-name`);
|
|
190
184
|
console.error('or delete the existing repository first, from its settings page.');
|
|
191
185
|
process.exit(1);
|
|
192
186
|
}
|
|
@@ -217,7 +211,7 @@ async function importCmd(args, usage) {
|
|
|
217
211
|
}
|
|
218
212
|
}
|
|
219
213
|
console.log(`Pushing to ${dest} as ${username}`);
|
|
220
|
-
const env =
|
|
214
|
+
const env = (0, gitrun_1.mochiCredentialEnv)(username, target.token);
|
|
221
215
|
if ((await git([...pushArgs(tmp), 'push', '--mirror', dest], env)) !== 0) {
|
|
222
216
|
throw new Error(`Could not push to ${dest}.\nIf it refused the token, check that you may create in ${collection}.`);
|
|
223
217
|
}
|
|
@@ -233,8 +227,23 @@ async function importCmd(args, usage) {
|
|
|
233
227
|
// here would skip this and leave the bare clone behind.
|
|
234
228
|
cleanup();
|
|
235
229
|
}
|
|
236
|
-
//
|
|
237
|
-
// repository
|
|
230
|
+
// A fork remembers where it came from: the source URL lands in the
|
|
231
|
+
// repository's config as mochi.upstream, where the repo header, `mochi
|
|
232
|
+
// sync`, and `mochi pr export` read it back. Recorded after the push, since
|
|
233
|
+
// the repository does not exist until the push creates it.
|
|
234
|
+
let upstream = null;
|
|
235
|
+
if (mode.fork) {
|
|
236
|
+
const patch = await (0, cli_api_1.apiTry)(target, 'PATCH', `/api/repos/${encodeURIComponent(collection)}/${encodeURIComponent(name)}`, {
|
|
237
|
+
upstream: source.url,
|
|
238
|
+
});
|
|
239
|
+
if (patch.ok)
|
|
240
|
+
upstream = source.url;
|
|
241
|
+
else {
|
|
242
|
+
console.error(`The repository arrived, but its upstream could not be recorded: ${patch.data.error ?? `HTTP ${patch.status}`}`);
|
|
243
|
+
console.error(` mochi repo edit ${collection}/${name} --upstream ${source.url}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// The description too is set after the push rather than before it.
|
|
238
247
|
let described = a.description?.trim() || null;
|
|
239
248
|
if (!described && !a.noDescription && source.github)
|
|
240
249
|
described = await githubDescription(source.github);
|
|
@@ -248,8 +257,10 @@ async function importCmd(args, usage) {
|
|
|
248
257
|
}
|
|
249
258
|
}
|
|
250
259
|
console.log('');
|
|
251
|
-
console.log(
|
|
260
|
+
console.log(`${mode.fork ? 'Forked' : 'Imported'} ${collection}/${name}`);
|
|
252
261
|
console.log(` ${dest}`);
|
|
262
|
+
if (upstream)
|
|
263
|
+
console.log(` forked from ${upstream}`);
|
|
253
264
|
// Worth saying rather than leaving to be discovered: a mirror push carries
|
|
254
265
|
// LFS pointer files, so the files look present and read as missing.
|
|
255
266
|
if (described)
|
|
@@ -314,3 +325,7 @@ async function collectionListCmd(args, usage) {
|
|
|
314
325
|
console.log(`${c.name.padEnd(width)} ${c.repoCount} ${c.repoCount === 1 ? 'repository' : 'repositories'}`);
|
|
315
326
|
}
|
|
316
327
|
}
|
|
328
|
+
/** `mochi fork`: `mochi import` that records where the repository came from. */
|
|
329
|
+
function forkCmd(args, usage) {
|
|
330
|
+
return importCmd(args, usage, { fork: true });
|
|
331
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
};
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
40
|
const credentials_1 = require("./credentials");
|
|
@@ -52,6 +53,7 @@ const output_1 = require("./cli/output");
|
|
|
52
53
|
const parse_1 = require("./cli/parse");
|
|
53
54
|
const target_1 = require("./cli/target");
|
|
54
55
|
const import_cli_1 = require("./import-cli");
|
|
56
|
+
const sync_cmd_1 = require("./cli/sync-cmd");
|
|
55
57
|
const deploy_cli_1 = require("./deploy-cli");
|
|
56
58
|
const deploy_runner_cli_1 = require("./deploy-runner-cli");
|
|
57
59
|
const job_cli_1 = require("./job-cli");
|
|
@@ -72,7 +74,7 @@ no keyring and possibly no writable home directory.
|
|
|
72
74
|
|
|
73
75
|
Vault layout, where <repos> is <vault>/collections/<collection>/repos:
|
|
74
76
|
<repos>/<repo>.git bare repositories (the .git suffix is optional)
|
|
75
|
-
<repos>/<repo>.site
|
|
77
|
+
<repos>/<repo>.site static site for a repo, served once enabled in its settings
|
|
76
78
|
<repos>/<repo>.lfs Git LFS objects, when no bucket is configured
|
|
77
79
|
<repos>/<repo>.runs workflow run history and logs
|
|
78
80
|
<vault>/vault.json users and hashed tokens (server-managed)
|
|
@@ -129,6 +131,22 @@ async function serveCmd(args, usage) {
|
|
|
129
131
|
// in a loop should not pay for the server it is not starting.
|
|
130
132
|
const { createApp } = await Promise.resolve().then(() => __importStar(require('./server')));
|
|
131
133
|
const app = createApp(vault);
|
|
134
|
+
// What one request cannot be allowed to do is take the vault down for
|
|
135
|
+
// everyone else. Node's default for an uncaught exception or an unhandled
|
|
136
|
+
// rejection is to exit, on the reasoning that the process may be left in
|
|
137
|
+
// a state nothing can trust. That reasoning is weaker here than usual: the
|
|
138
|
+
// process holds no state that matters -- the vault is on disk, written by
|
|
139
|
+
// rename, and re-read on every request -- so what an escaped error has
|
|
140
|
+
// corrupted is at most one response, which is already lost. So the error is
|
|
141
|
+
// logged with its stack and the process goes on. Anything that recurs will
|
|
142
|
+
// recur in the log, which is where an operator can find it; a crash would
|
|
143
|
+
// have said the same thing once and then stopped serving.
|
|
144
|
+
process.on('uncaughtException', (err) => {
|
|
145
|
+
console.error('uncaught exception (the server continues):', err);
|
|
146
|
+
});
|
|
147
|
+
process.on('unhandledRejection', (reason) => {
|
|
148
|
+
console.error('unhandled rejection (the server continues):', reason);
|
|
149
|
+
});
|
|
132
150
|
app.listen(port, host, () => {
|
|
133
151
|
const url = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`;
|
|
134
152
|
if (boot && boot.preset) {
|
|
@@ -408,6 +426,37 @@ async function loginCmd(inv) {
|
|
|
408
426
|
console.log(`and mochi commands talk to it by default (${(0, credentials_1.loginPath)()}).`);
|
|
409
427
|
console.log('Run `mochi logout` to remove it again.');
|
|
410
428
|
}
|
|
429
|
+
// ---- web ----
|
|
430
|
+
// Best effort and platform-shaped. The URL is printed first, so a machine
|
|
431
|
+
// with no opener, or an SSH session, still leaves the person one paste away.
|
|
432
|
+
function openBrowser(url) {
|
|
433
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
434
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
435
|
+
try {
|
|
436
|
+
const child = (0, child_process_1.spawn)(cmd, args, { stdio: 'ignore', detached: true });
|
|
437
|
+
child.on('error', () => { });
|
|
438
|
+
child.unref();
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// The link is already on the terminal.
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async function webCmd(inv) {
|
|
445
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
446
|
+
const next = inv.args[0] ?? '/';
|
|
447
|
+
const data = await (0, cli_api_1.api)(target, 'POST', '/api/login-url', { next });
|
|
448
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
449
|
+
if (json.enabled) {
|
|
450
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
console.log(`A one-time sign-in link for '${data.username}' at ${target.host}:`);
|
|
454
|
+
console.log('');
|
|
455
|
+
console.log(` ${data.url}`);
|
|
456
|
+
console.log('');
|
|
457
|
+
console.log(`It works once, expires in ${data.expiresInSeconds} seconds, and asks before signing in.`);
|
|
458
|
+
openBrowser(String(data.url));
|
|
459
|
+
}
|
|
411
460
|
async function logoutCmd(inv) {
|
|
412
461
|
if (inv.str('token') || inv.str('helper')) {
|
|
413
462
|
throw new exit_1.CliError('logout takes only --host: it removes a stored credential rather than making one.', exit_1.EXIT_USAGE);
|
|
@@ -463,6 +512,16 @@ Options:
|
|
|
463
512
|
--lfs carry Git LFS objects too
|
|
464
513
|
--description <text> set this description instead of the source's
|
|
465
514
|
--no-description leave the description empty`, import_cli_1.importCmd),
|
|
515
|
+
raw(['fork'], 'Import a repository and remember where it came from', `Usage: mochi fork <source> <collection>[/<name>] [--lfs]
|
|
516
|
+
|
|
517
|
+
Everything mochi import does, plus the source URL is recorded as the
|
|
518
|
+
repository's upstream, which the repository header shows, mochi sync
|
|
519
|
+
fast-forwards from, and mochi pr export sends pull requests back to. The
|
|
520
|
+
source must be a URL (or owner/repo for GitHub): a local directory has no
|
|
521
|
+
upstream to record.
|
|
522
|
+
|
|
523
|
+
Options are those of mochi import.`, import_cli_1.forkCmd),
|
|
524
|
+
sync_cmd_1.syncCommand,
|
|
466
525
|
raw(['collection', 'add'], 'Create an empty collection', `Pushing to a new path creates its collection on the way, so this is for the
|
|
467
526
|
other order: making the collection first and filling it afterwards.`, import_cli_1.collectionAddCmd),
|
|
468
527
|
raw(['collection', 'list'], "Show the vault's collections and how many repositories each holds", '', import_cli_1.collectionListCmd),
|
|
@@ -580,6 +639,20 @@ nothing when that is nothing.`,
|
|
|
580
639
|
],
|
|
581
640
|
run: loginCmd,
|
|
582
641
|
},
|
|
642
|
+
{
|
|
643
|
+
path: ['web'],
|
|
644
|
+
summary: 'Open the vault in a browser, signed in as you',
|
|
645
|
+
description: `Mints a one-time sign-in link from the stored token and opens it in the
|
|
646
|
+
default browser (the link is printed too, for machines without one). The link
|
|
647
|
+
lands on a page that names the account and signs in on a click; it works once
|
|
648
|
+
and expires after two minutes. The browser session is bound to the same token
|
|
649
|
+
this CLI holds, so revoking that token signs the browser out as well.
|
|
650
|
+
|
|
651
|
+
An optional path says where to land: mochi web /alice/myrepo`,
|
|
652
|
+
args: [{ name: 'path' }],
|
|
653
|
+
options: [output_1.JSON_OPTION, ...target_1.TARGET_OPTIONS],
|
|
654
|
+
run: webCmd,
|
|
655
|
+
},
|
|
583
656
|
{
|
|
584
657
|
path: ['logout'],
|
|
585
658
|
summary: "Remove this vault's stored credential and forget the vault",
|
|
@@ -612,9 +685,11 @@ Docker rather than Fly's builder. --image <ref> deploys some other published tag
|
|
|
612
685
|
See also: mochi deploy fly show <app>, mochi deploy fly destroy <app>.
|
|
613
686
|
`, deploy_cli_1.deployFlyCmd),
|
|
614
687
|
raw(['deploy', 'fly', 'runner'], 'Put a workflow runner on Fly.io, which stops when idle', `Usage: mochi deploy fly runner <app> [--allow <glob>...] [--labels <l,...>]
|
|
615
|
-
[--
|
|
688
|
+
[--job-timeout <45m>] [--idle <5m>]
|
|
689
|
+
[--region <r>] [--volume <gb>]
|
|
616
690
|
[--vm-size <s>] [--vm-memory <m>] [--org <o>]
|
|
617
691
|
[--image <ref> | --from-source [--local-build]]
|
|
692
|
+
[--image-only]
|
|
618
693
|
|
|
619
694
|
Needs flyctl, and a login to the vault this runner will serve. Registers the
|
|
620
695
|
runner (named after the app unless --name says otherwise), creates the app and a
|
|
@@ -630,17 +705,42 @@ it costs while stopped is the volume alone.
|
|
|
630
705
|
take jobs for; it executes whatever their workflows contain, on this machine.
|
|
631
706
|
Run the same command again to deploy a new version.
|
|
632
707
|
|
|
708
|
+
--job-timeout is the longest a single job may run on the machine, 20 minutes by
|
|
709
|
+
default, which on hardware billed by the minute is the bound worth setting: it
|
|
710
|
+
caps what a workflow's own timeout-minutes may ask for.
|
|
711
|
+
|
|
712
|
+
--image-only moves an already-deployed runner to a new image and touches
|
|
713
|
+
nothing else: no registration, no token, no wake rewrite, so it needs flyctl
|
|
714
|
+
and no vault login. A runner and the vault it serves speak one protocol, so
|
|
715
|
+
the pipeline that redeploys the vault should redeploy the runner beside it,
|
|
716
|
+
and this flag is what lets a job holding only a Fly credential do that.
|
|
717
|
+
|
|
633
718
|
See also: mochi deploy fly runner show <app>, destroy <app>, mochi runner list.
|
|
634
719
|
`, deploy_runner_cli_1.deployFlyRunnerCmd),
|
|
635
720
|
raw(['deploy', 'fly', 'runner', 'show'], 'What Fly has for this runner app, and which runner it serves', '', deploy_runner_cli_1.deployFlyRunnerShowCmd),
|
|
636
721
|
raw(['deploy', 'fly', 'runner', 'destroy'], 'Destroy the runner app, and offer to remove its registration', 'No undo, though a runner keeps nothing that matters. Pass --yes to skip the confirmation.', deploy_runner_cli_1.deployFlyRunnerDestroyCmd),
|
|
637
722
|
raw(['deploy', 'fly', 'show'], 'What Fly has for this app, and whether the vault answers', '', deploy_cli_1.deployShowCmd),
|
|
638
723
|
raw(['deploy', 'fly', 'destroy'], 'Destroy the app and its volume, and with them the vault', 'No undo. Pass --yes to skip the confirmation.', deploy_cli_1.deployDestroyCmd),
|
|
639
|
-
raw(['runner', 'add'], 'Register a machine that will execute workflow jobs', `Usage: mochi runner add <name> --allow <glob>... [--labels <l,...>]
|
|
724
|
+
raw(['runner', 'add'], 'Register a machine that will execute workflow jobs', `Usage: mochi runner add <name> --allow <glob>... [--labels <l,...>]
|
|
725
|
+
[--job-timeout <45m>] [--save]
|
|
640
726
|
|
|
641
727
|
Prints its token once. --allow says which repositories it may take jobs for, as
|
|
642
728
|
globs over collection/repo; you must own every collection they name (a site admin may name any). Jobs never run on
|
|
643
|
-
the vault's machine, so a vault with no runner queues its runs and waits
|
|
729
|
+
the vault's machine, so a vault with no runner queues its runs and waits.
|
|
730
|
+
|
|
731
|
+
--job-timeout is the longest a single job may run on this machine, 20 minutes by
|
|
732
|
+
default and stated in minutes or as a duration like 2h. It is a ceiling on what
|
|
733
|
+
a job's own timeout-minutes may ask for, not a default it can override; change
|
|
734
|
+
it later with mochi runner edit.`, runner_cli_1.runnerAddCmd),
|
|
735
|
+
raw(['runner', 'edit'], "Change a registered runner's job timeout", `Usage: mochi runner edit <name> --job-timeout <45m|default>
|
|
736
|
+
|
|
737
|
+
The longest a single job may run on that machine. A job asking for less with
|
|
738
|
+
timeout-minutes keeps what it asked for; one asking for more, or asking for
|
|
739
|
+
nothing, is held to this. --job-timeout default puts the runner back on the
|
|
740
|
+
vault's own default of 20 minutes.
|
|
741
|
+
|
|
742
|
+
Applies to the next job the runner takes: one already running keeps the timeout
|
|
743
|
+
it was handed with the job.`, runner_cli_1.runnerEditCmd),
|
|
644
744
|
raw(['runner', 'run'], 'Take jobs and run them, one at a time, each in a Docker container', `Usage: mochi runner run [--host <url>] [--runner-token <t>] [--labels <l,...>]
|
|
645
745
|
|
|
646
746
|
Reads ~/.config/mochi/runner.json when given no arguments. Needs a working
|
|
@@ -653,6 +753,12 @@ on the next run; --no-action-cache downloads every time. --work-dir sets where
|
|
|
653
753
|
job workspaces are made, --network which Docker network the container joins,
|
|
654
754
|
and MOCHI_RUNNER_TOKEN supplies the token instead of --runner-token.
|
|
655
755
|
|
|
756
|
+
Each job runs in a container of its own, held to --job-pids <4096> processes
|
|
757
|
+
(0 for no limit) and, when given, --job-memory <2g> and --job-cpus <2>, as
|
|
758
|
+
docker run takes them. A job over the process limit cannot fork; one over the
|
|
759
|
+
memory limit is killed. There is no default for memory or CPU, since the right
|
|
760
|
+
figure is the machine's to decide.
|
|
761
|
+
|
|
656
762
|
--idle <5m> stops the runner when no job has arrived for that long, which is
|
|
657
763
|
what makes a runner that costs money while it is up affordable: it exits, and
|
|
658
764
|
whatever hosts it stops. Something then has to start it again, so --wake-port
|
package/dist/issueweb.js
CHANGED
|
@@ -40,6 +40,7 @@ const icons_1 = require("./icons");
|
|
|
40
40
|
const issues = __importStar(require("./issues"));
|
|
41
41
|
const html_1 = require("./html");
|
|
42
42
|
const markdown_1 = require("./markdown");
|
|
43
|
+
const mdedit_1 = require("./mdedit");
|
|
43
44
|
const ops_1 = require("./ops");
|
|
44
45
|
const render_1 = require("./render");
|
|
45
46
|
const session_1 = require("./session");
|
|
@@ -69,6 +70,7 @@ function body(ctx, text) {
|
|
|
69
70
|
blobBase: `${base}/blob/${ref}`,
|
|
70
71
|
issueBase: `${base}/issues`,
|
|
71
72
|
commitBase: `${base}/commit`,
|
|
73
|
+
mentions: ctx.hasUser,
|
|
72
74
|
}));
|
|
73
75
|
}
|
|
74
76
|
function stateBadge(state) {
|
|
@@ -117,7 +119,7 @@ function listPage(ctx, list, filter, counts, labels, authors) {
|
|
|
117
119
|
const rows = list.map((i) => (0, html_1.html) `<tr>
|
|
118
120
|
<td class="issue-cell">${(0, icons_1.icon)(i.state === 'open' ? 'issue-opened' : 'issue-closed', i.state === 'open' ? 'issue-open' : 'issue-closed')}<span>
|
|
119
121
|
<a class="issue-link" href="${base}/${i.number}">${i.title}</a>${labelChips(i.labels, base, filter)}
|
|
120
|
-
<div class="muted small">#${i.number} opened ${(0, render_1.timeTag)(i.created)} by ${i.author}${i.state === 'closed' && i.closedAt ? (0, html_1.html) ` · closed ${(0, render_1.timeTag)(i.closedAt)}` : ''}</div></span></td>
|
|
122
|
+
<div class="muted small">#${i.number} opened ${(0, render_1.timeTag)(i.created)} by ${(0, views_1.userLink)(i.author)}${i.state === 'closed' && i.closedAt ? (0, html_1.html) ` · closed ${(0, render_1.timeTag)(i.closedAt)}` : ''}</div></span></td>
|
|
121
123
|
<td class="right muted small">${i.comments > 0
|
|
122
124
|
? (0, html_1.html) `<a class="issue-comments" href="${base}/${i.number}" title="${i.comments} comment${i.comments === 1 ? '' : 's'}">${(0, icons_1.icon)('comment')}<span>${i.comments}</span></a>`
|
|
123
125
|
: ''}</td>
|
|
@@ -183,7 +185,7 @@ ${rows.length
|
|
|
183
185
|
}
|
|
184
186
|
function commentCard(author, when, rendered, note = '') {
|
|
185
187
|
return (0, html_1.html) `<article class="issue-comment">
|
|
186
|
-
<div class="issue-comment-head">${(0,
|
|
188
|
+
<div class="issue-comment-head">${(0, views_1.userLink)(author, { face: 24, bold: true })}<span class="muted small">${note || 'commented'} ${(0, render_1.timeTag)(when)}</span></div>
|
|
187
189
|
<div class="issue-comment-body markdown-body">${rendered}</div>
|
|
188
190
|
</article>`;
|
|
189
191
|
}
|
|
@@ -194,7 +196,7 @@ function issuePage(ctx, issue, canWrite) {
|
|
|
194
196
|
const canClose = canWrite || mine;
|
|
195
197
|
const comments = issue.commentList.map((c) => commentCard(c.author, c.created, body(ctx, c.body)));
|
|
196
198
|
const closedNote = issue.state === 'closed' && issue.closedAt
|
|
197
|
-
? (0, html_1.html) `<div class="issue-event">${(0, icons_1.icon)('issue-closed', 'issue-closed')}<span
|
|
199
|
+
? (0, html_1.html) `<div class="issue-event">${(0, icons_1.icon)('issue-closed', 'issue-closed')}<span>${issue.closedBy ? (0, views_1.userLink)(issue.closedBy, { bold: true }) : (0, html_1.html) `<b>someone</b>`} closed this ${(0, render_1.timeTag)(issue.closedAt)}</span></div>`
|
|
198
200
|
: '';
|
|
199
201
|
let replyBox;
|
|
200
202
|
if (viewer) {
|
|
@@ -203,7 +205,7 @@ function issuePage(ctx, issue, canWrite) {
|
|
|
203
205
|
: '';
|
|
204
206
|
replyBox = (0, html_1.html) `<form class="issue-reply" method="post" action="${base}/${issue.number}/comment">${(0, views_1.csrfField)(viewer)}
|
|
205
207
|
<div class="issue-comment-head">${(0, avatar_1.avatar)(viewer.auth.username, 24)}<b>${viewer.auth.username}</b></div>
|
|
206
|
-
|
|
208
|
+
${(0, mdedit_1.markdownEditor)({ name: 'body', rows: 6, placeholder: 'Leave a comment', preview: (0, mdedit_1.previewUrl)(ctx) })}
|
|
207
209
|
<div class="actions">${toggle}<button class="btn btn-primary" type="submit">Comment</button></div>
|
|
208
210
|
</form>`;
|
|
209
211
|
}
|
|
@@ -219,7 +221,9 @@ function issuePage(ctx, issue, canWrite) {
|
|
|
219
221
|
<h1 class="issue-title">${issue.title} <span class="muted">#${issue.number}</span></h1>
|
|
220
222
|
<span class="right-group">${edit}<a class="btn" href="${base}">Back to issues</a></span>
|
|
221
223
|
</div>
|
|
222
|
-
<div class="issue-sub">${stateBadge(issue.state)}<span class="muted"
|
|
224
|
+
<div class="issue-sub">${stateBadge(issue.state)}<span class="muted">${(0, views_1.userLink)(issue.author, {
|
|
225
|
+
bold: true,
|
|
226
|
+
})} opened this ${(0, render_1.timeTag)(issue.created)} · ${issue.commentList.length} comment${issue.commentList.length === 1 ? '' : 's'}</span>${labelChips(issue.labels)}</div>
|
|
223
227
|
<div class="issue-thread">
|
|
224
228
|
${commentCard(issue.author, issue.created, description.text ? description : (0, html_1.raw)('<p class="muted">No description.</p>'), 'opened this')}
|
|
225
229
|
${comments}
|
|
@@ -239,7 +243,7 @@ ${error ? (0, html_1.html) `<div class="form-error">${error}</div>` : ''}
|
|
|
239
243
|
<div class="field"><label for="title">Title</label>
|
|
240
244
|
<input type="text" id="title" name="title" value="${values.title}" maxlength="${issues.MAX_TITLE}" required autofocus></div>
|
|
241
245
|
<div class="field"><label for="body">Description</label>
|
|
242
|
-
|
|
246
|
+
${(0, mdedit_1.markdownEditor)({ name: 'body', id: 'body', rows: 12, placeholder: 'Markdown is welcome', value: values.body, preview: (0, mdedit_1.previewUrl)(ctx) })}</div>
|
|
243
247
|
${ctx.canPush
|
|
244
248
|
? (0, html_1.html) `<div class="field"><label for="labels">Labels</label>
|
|
245
249
|
<input type="text" id="labels" name="labels" value="${values.labels.join(', ')}" placeholder="bug, ui">
|
package/dist/lfs.js
CHANGED
|
@@ -251,8 +251,15 @@ function registerLfs(app, root, lfs, authLimiter) {
|
|
|
251
251
|
};
|
|
252
252
|
}
|
|
253
253
|
// Already present: omitting actions tells the client to skip the
|
|
254
|
-
// upload; this is the deduplication mechanism.
|
|
255
|
-
|
|
254
|
+
// upload; this is the deduplication mechanism. Present at the size
|
|
255
|
+
// the pointer claims, that is. A bucket takes whatever bytes a
|
|
256
|
+
// presigned PUT delivers, so a client that lied about the size in
|
|
257
|
+
// one batch and uploaded something else left an object under this
|
|
258
|
+
// id that is not this object; it fails verification, but it would
|
|
259
|
+
// otherwise also stand in for every honest upload of the real one
|
|
260
|
+
// from then on. An object of the wrong size is treated as absent
|
|
261
|
+
// and the upload it is given overwrites it.
|
|
262
|
+
if (info && info.size === o.size)
|
|
256
263
|
return { oid: o.oid, size: o.size, authenticated: true };
|
|
257
264
|
if (o.size > maxSize) {
|
|
258
265
|
return {
|
package/dist/limit.js
CHANGED
|
@@ -218,7 +218,7 @@ function createGates(limits) {
|
|
|
218
218
|
const AUTH_WINDOW_MS = 15 * 60 * 1000;
|
|
219
219
|
const AUTH_MAX_KEYS = 20000;
|
|
220
220
|
/** Bearer and runner tokens carry no username, so they share one bucket in the fine-grained window. */
|
|
221
|
-
const NO_USERNAME = '
|
|
221
|
+
const NO_USERNAME = '\x00token';
|
|
222
222
|
/**
|
|
223
223
|
* A limiter charged only on failure, so a working credential is never throttled
|
|
224
224
|
* however often it is used. That matters for /api/runner/*, which a runner calls
|
|
@@ -240,7 +240,7 @@ function createAuthLimiter(authFailures) {
|
|
|
240
240
|
const coarse = createLimiter({ limit: authFailures * 5, windowMs: AUTH_WINDOW_MS, maxKeys: AUTH_MAX_KEYS });
|
|
241
241
|
const keys = (req, username) => {
|
|
242
242
|
const address = clientKey(req);
|
|
243
|
-
return { address, fine: `${address}
|
|
243
|
+
return { address, fine: `${address}\x00${username ?? NO_USERNAME}` };
|
|
244
244
|
};
|
|
245
245
|
return {
|
|
246
246
|
allow(req, username) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HANDOFF_TTL_MS = exports.LOGIN_LINK_TTL_MS = void 0;
|
|
4
|
+
exports.mintLoginLink = mintLoginLink;
|
|
5
|
+
exports.peekLoginLink = peekLoginLink;
|
|
6
|
+
exports.takeLoginLink = takeLoginLink;
|
|
7
|
+
exports.mintHandoff = mintHandoff;
|
|
8
|
+
exports.takeHandoff = takeHandoff;
|
|
9
|
+
const onetime_1 = require("./onetime");
|
|
10
|
+
exports.LOGIN_LINK_TTL_MS = 2 * 60 * 1000;
|
|
11
|
+
exports.HANDOFF_TTL_MS = 5 * 60 * 1000;
|
|
12
|
+
const loginLinks = (0, onetime_1.createOneTimeStore)();
|
|
13
|
+
const handoffCodes = (0, onetime_1.createOneTimeStore)(onetime_1.mintHandoffCode);
|
|
14
|
+
function mintLoginLink(root, username, binding, next) {
|
|
15
|
+
return loginLinks.put({ root, username, binding, next }, exports.LOGIN_LINK_TTL_MS);
|
|
16
|
+
}
|
|
17
|
+
function peekLoginLink(root, code) {
|
|
18
|
+
const p = loginLinks.peek(code);
|
|
19
|
+
return p && p.root === root ? p : null;
|
|
20
|
+
}
|
|
21
|
+
function takeLoginLink(root, code) {
|
|
22
|
+
const p = loginLinks.take(code);
|
|
23
|
+
return p && p.root === root ? p : null;
|
|
24
|
+
}
|
|
25
|
+
function mintHandoff(root, username, binding) {
|
|
26
|
+
return handoffCodes.put({ root, username, binding }, exports.HANDOFF_TTL_MS);
|
|
27
|
+
}
|
|
28
|
+
function takeHandoff(root, typed) {
|
|
29
|
+
const p = handoffCodes.take((0, onetime_1.normalizeHandoffCode)(typed));
|
|
30
|
+
return p && p.root === root ? p : null;
|
|
31
|
+
}
|