@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,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REPO_OPTION = void 0;
4
+ exports.resolveRepo = resolveRepo;
5
+ exports.repoPath = repoPath;
6
+ const child_process_1 = require("child_process");
7
+ const exit_1 = require("./exit");
8
+ // Which repository a command is about.
9
+ //
10
+ // This matters more than it looks, because a caller working inside a clone should
11
+ // not have to be told twice where it is: an agent that has just cloned a
12
+ // repository and is standing in it knows the answer, and so does git.
13
+ exports.REPO_OPTION = {
14
+ name: 'repo',
15
+ type: 'string',
16
+ value: '<c/r>',
17
+ summary: 'Repository as collection/repo; otherwise the git remote in this directory',
18
+ };
19
+ /** collection/repo, and nothing else: not a URL, not a bare name. */
20
+ function parseRef(given, where) {
21
+ const parts = given.replace(/^\/+|\/+$/g, '').split('/');
22
+ if (parts.length !== 2 || parts[0] === '' || parts[1] === '') {
23
+ throw new exit_1.CliError(`${where} should be <collection>/<repo>, not '${given}'`, exit_1.EXIT_USAGE);
24
+ }
25
+ return { collection: parts[0], repo: parts[1].replace(/\.git$/, '') };
26
+ }
27
+ function git(args) {
28
+ return new Promise((resolve) => {
29
+ (0, child_process_1.execFile)('git', args, { maxBuffer: 1024 * 1024 }, (err, stdout) => {
30
+ resolve(err ? null : String(stdout));
31
+ });
32
+ });
33
+ }
34
+ /**
35
+ * The repository named by the git remotes here that points at the vault we are
36
+ * talking to, preferring origin. A remote for some other host is not an answer:
37
+ * a clone of a GitHub repository standing in this directory must not be read as
38
+ * naming a repository in this vault.
39
+ */
40
+ async function fromGitRemotes(host) {
41
+ let vaultHost;
42
+ try {
43
+ vaultHost = new URL(host).host.toLowerCase();
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ const config = await git(['config', '--get-regexp', '^remote\\..*\\.url']);
49
+ if (!config)
50
+ return null;
51
+ const candidates = [];
52
+ for (const line of config.split('\n')) {
53
+ const space = line.indexOf(' ');
54
+ if (space === -1)
55
+ continue;
56
+ const key = line.slice(0, space);
57
+ const url = line.slice(space + 1).trim();
58
+ const name = key.slice('remote.'.length, -'.url'.length);
59
+ let parsed;
60
+ try {
61
+ parsed = new URL(url);
62
+ }
63
+ catch {
64
+ continue;
65
+ }
66
+ if (parsed.host.toLowerCase() !== vaultHost)
67
+ continue;
68
+ const segments = parsed.pathname.replace(/^\/+|\/+$/g, '').split('/');
69
+ if (segments.length < 2)
70
+ continue;
71
+ const tail = segments.slice(-2);
72
+ candidates.push({ name, ref: { collection: tail[0], repo: tail[1].replace(/\.git$/, '') } });
73
+ }
74
+ if (candidates.length === 0)
75
+ return null;
76
+ return (candidates.find((c) => c.name === 'origin') ?? candidates[0]).ref;
77
+ }
78
+ /**
79
+ * Resolve the repository a command is about, in order: the argument or --repo,
80
+ * then MOCHI_REPO, then the git remote here that points at this vault.
81
+ * Failing all three, say so and name all three.
82
+ */
83
+ async function resolveRepo(inv, target, positional = null) {
84
+ if (positional)
85
+ return parseRef(positional, 'a repository');
86
+ const flag = inv.str('repo');
87
+ if (flag)
88
+ return parseRef(flag, '--repo');
89
+ const env = process.env.MOCHI_REPO?.trim();
90
+ if (env)
91
+ return parseRef(env, 'MOCHI_REPO');
92
+ const remote = await fromGitRemotes(target.host);
93
+ if (remote)
94
+ return remote;
95
+ throw new exit_1.CliError(`Which repository? Name it as <collection>/<repo>, pass --repo <collection>/<repo>, set MOCHI_REPO,\n` +
96
+ `or run this inside a clone whose remote points at ${target.host}.`, exit_1.EXIT_USAGE);
97
+ }
98
+ /** The path prefix for a repository's API routes. */
99
+ function repoPath(ref) {
100
+ return `/api/repos/${encodeURIComponent(ref.collection)}/${encodeURIComponent(ref.repo)}`;
101
+ }
@@ -0,0 +1,438 @@
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.runCommands = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const cli_api_1 = require("../cli-api");
40
+ const exit_1 = require("./exit");
41
+ const output_1 = require("./output");
42
+ const repo_1 = require("./repo");
43
+ const target_1 = require("./target");
44
+ // Workflows and their runs. `run watch` is the command that turns "an agent can
45
+ // start a workflow" into "an agent can act on the result", and `run view --log`
46
+ // is what it reads when the result is a failure.
47
+ const COMMON = [repo_1.REPO_OPTION, ...target_1.TARGET_OPTIONS];
48
+ function runNumber(inv) {
49
+ const raw = inv.args[0] ?? '';
50
+ if (!/^[1-9][0-9]*$/.test(raw))
51
+ throw new exit_1.CliError(`'${raw}' is not a run number`, exit_1.EXIT_USAGE);
52
+ return parseInt(raw, 10);
53
+ }
54
+ function runRow(r) {
55
+ return [
56
+ `#${r.number}`,
57
+ String(r.status),
58
+ String(r.conclusion ?? ''),
59
+ String(r.workflowName ?? ''),
60
+ String(r.refName ?? r.ref ?? ''),
61
+ String(r.sha ?? '').slice(0, 10),
62
+ (0, output_1.shortDate)(r.createdAt),
63
+ ];
64
+ }
65
+ /** Print a job's log, which is what a caller looking at a failure came for. */
66
+ async function printLog(target, repo, n, jobId, tail) {
67
+ const query = tail === null ? '' : `?tail=${tail}`;
68
+ const data = await (0, cli_api_1.api)(target, 'GET', `${(0, repo_1.repoPath)(repo)}/runs/${n}/jobs/${encodeURIComponent(jobId)}/log${query}`);
69
+ if (data.error) {
70
+ console.error(`job ${jobId} failed before it ran: ${data.error}`);
71
+ return;
72
+ }
73
+ for (const line of (data.lines ?? []))
74
+ console.log(line);
75
+ // Diagnostics go to stderr so that piping the log somewhere gets the log.
76
+ if (data.truncated) {
77
+ console.error(`(the last ${data.lines.length} of ${data.total} lines; --tail 0 for all of it)`);
78
+ }
79
+ if (data.capped)
80
+ console.error('(the server capped this log while it was being written)');
81
+ }
82
+ exports.runCommands = [
83
+ {
84
+ path: ['workflow', 'list'],
85
+ summary: 'List the workflows at a ref, with the inputs each takes',
86
+ options: [
87
+ { name: 'ref', type: 'string', value: '<r>', summary: 'Branch or tag; the default branch otherwise' },
88
+ output_1.JSON_OPTION,
89
+ ...COMMON,
90
+ ],
91
+ async run(inv) {
92
+ const target = await (0, target_1.targetFrom)(inv);
93
+ const repo = await (0, repo_1.resolveRepo)(inv, target);
94
+ const ref = inv.str('ref');
95
+ const data = await (0, cli_api_1.api)(target, 'GET', `${(0, repo_1.repoPath)(repo)}/workflows${ref ? `?ref=${encodeURIComponent(ref)}` : ''}`);
96
+ const workflows = (data.workflows ?? []);
97
+ const json = (0, output_1.jsonMode)(inv);
98
+ if (json.enabled) {
99
+ (0, output_1.printJson)({ ref: data.ref, workflows: (0, output_1.pickFields)(workflows, json.fields) });
100
+ return;
101
+ }
102
+ if (workflows.length === 0) {
103
+ console.log(`No workflows at ${data.ref}`);
104
+ return;
105
+ }
106
+ (0, output_1.printTable)(workflows.map((w) => [
107
+ String(w.name),
108
+ String(w.path),
109
+ w.dispatch ? `dispatch: ${Object.keys(w.dispatch).join(',') || 'no inputs'}` : '',
110
+ w.error ? `error: ${w.error}` : '',
111
+ ]));
112
+ },
113
+ },
114
+ {
115
+ path: ['workflow', 'run'],
116
+ summary: 'Dispatch a workflow by hand',
117
+ description: `The workflow is named by its path, as 'workflow list' reports it. --field k=v
118
+ supplies a workflow_dispatch input; the inputs a workflow takes are in the same
119
+ listing.`,
120
+ args: [{ name: 'workflow', required: true }],
121
+ options: [
122
+ { name: 'ref', type: 'string', value: '<r>', summary: 'Branch to run on; the default branch otherwise' },
123
+ { name: 'field', type: 'string[]', value: '<k=v>', summary: 'A workflow_dispatch input' },
124
+ output_1.JSON_OPTION,
125
+ ...COMMON,
126
+ ],
127
+ async run(inv) {
128
+ const target = await (0, target_1.targetFrom)(inv);
129
+ const repo = await (0, repo_1.resolveRepo)(inv, target);
130
+ const inputs = {};
131
+ for (const pair of inv.list('field')) {
132
+ const i = pair.indexOf('=');
133
+ if (i <= 0)
134
+ throw new exit_1.CliError(`--field takes key=value, not '${pair}'`, exit_1.EXIT_USAGE);
135
+ inputs[pair.slice(0, i)] = pair.slice(i + 1);
136
+ }
137
+ const data = await (0, cli_api_1.api)(target, 'POST', `${(0, repo_1.repoPath)(repo)}/dispatches`, {
138
+ workflow: inv.args[0],
139
+ ref: inv.str('ref') ?? undefined,
140
+ inputs: Object.keys(inputs).length ? inputs : undefined,
141
+ });
142
+ const json = (0, output_1.jsonMode)(inv);
143
+ if (json.enabled) {
144
+ (0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
145
+ return;
146
+ }
147
+ console.log(`Started run #${data.number} of ${data.workflowName}`);
148
+ console.log(` mochi run watch ${data.number} --repo ${repo.collection}/${repo.repo}`);
149
+ },
150
+ },
151
+ {
152
+ path: ['run', 'list'],
153
+ summary: 'List workflow runs, newest first',
154
+ options: [
155
+ { name: 'status', type: 'string', value: '<s>', summary: 'queued, running, or completed' },
156
+ { name: 'limit', type: 'int', value: '<n>', summary: 'At most this many' },
157
+ output_1.JSON_OPTION,
158
+ ...COMMON,
159
+ ],
160
+ async run(inv) {
161
+ const target = await (0, target_1.targetFrom)(inv);
162
+ const repo = await (0, repo_1.resolveRepo)(inv, target);
163
+ const query = new URLSearchParams();
164
+ const status = inv.str('status');
165
+ if (status !== null)
166
+ query.set('status', status);
167
+ const limit = inv.int('limit');
168
+ if (limit !== null)
169
+ query.set('limit', String(limit));
170
+ const data = await (0, cli_api_1.api)(target, 'GET', `${(0, repo_1.repoPath)(repo)}/runs?${query.toString()}`);
171
+ const runs = (data.runs ?? []);
172
+ const json = (0, output_1.jsonMode)(inv);
173
+ if (json.enabled) {
174
+ (0, output_1.printJson)({ runs: (0, output_1.pickFields)(runs, json.fields) });
175
+ return;
176
+ }
177
+ if (runs.length === 0) {
178
+ console.log('No runs');
179
+ return;
180
+ }
181
+ (0, output_1.printTable)(runs.map(runRow));
182
+ },
183
+ },
184
+ {
185
+ path: ['run', 'view'],
186
+ summary: 'Show one run, its jobs, and optionally a job log',
187
+ description: `--log prints the log of the failed job, or of the only job, without needing to be
188
+ told which. --job names one explicitly. The log comes back as its last 200 lines
189
+ by default, because handing a caller the whole of a large log wastes its attention;
190
+ --tail 0 asks for all of it.`,
191
+ args: [{ name: 'number', required: true }],
192
+ options: [
193
+ { name: 'job', type: 'string', value: '<j>', summary: 'A job id, as run view reports it' },
194
+ { name: 'log', type: 'boolean', summary: 'Print a job log as well' },
195
+ { name: 'tail', type: 'int', value: '<n>', summary: 'Log lines to keep from the end; 0 for all' },
196
+ output_1.JSON_OPTION,
197
+ ...COMMON,
198
+ ],
199
+ async run(inv) {
200
+ const target = await (0, target_1.targetFrom)(inv);
201
+ const repo = await (0, repo_1.resolveRepo)(inv, target);
202
+ const n = runNumber(inv);
203
+ const data = await (0, cli_api_1.api)(target, 'GET', `${(0, repo_1.repoPath)(repo)}/runs/${n}`);
204
+ const jobs = (data.jobs ?? []);
205
+ const json = (0, output_1.jsonMode)(inv);
206
+ if (json.enabled) {
207
+ (0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
208
+ return;
209
+ }
210
+ console.log(`#${data.number} ${data.workflowName} (${data.event}) on ${data.refName ?? data.ref}`);
211
+ console.log(`${data.status}${data.conclusion ? ` - ${data.conclusion}` : ''} at ${String(data.sha).slice(0, 10)}`);
212
+ if (data.error)
213
+ console.log(`workflow error: ${data.error}`);
214
+ if (jobs.length) {
215
+ console.log('');
216
+ (0, output_1.printTable)(jobs.map((j) => [String(j.id), String(j.status), String(j.conclusion ?? ''), String(j.name)]));
217
+ }
218
+ if (!inv.bool('log') && inv.str('job') === null)
219
+ return;
220
+ // The failed job is the one being asked about, when there is one; a run
221
+ // with a single job needs no naming either.
222
+ const named = inv.str('job');
223
+ const chosen = named ??
224
+ String((jobs.find((j) => j.conclusion === 'failure') ?? (jobs.length === 1 ? jobs[0] : undefined))?.id ?? '');
225
+ if (!chosen) {
226
+ console.error('Which job? Name one with --job; the ids are in the table above.');
227
+ process.exit(exit_1.EXIT_USAGE);
228
+ }
229
+ console.log('');
230
+ console.log(`--- ${chosen}`);
231
+ await printLog(target, repo, n, chosen, inv.int('tail'));
232
+ },
233
+ },
234
+ {
235
+ path: ['run', 'watch'],
236
+ summary: 'Wait for a run to finish, then report how it went',
237
+ description: `Polls until the run completes. --exit-status makes a failed run a non-zero exit,
238
+ which is what a script wants; without it a finished run is a success whatever it
239
+ concluded.
240
+
241
+ Polling rather than streaming: the engine has no event channel and the vault reads
242
+ its state off disk per request, so a five-second poll against a local process costs
243
+ nothing and needs no protocol.`,
244
+ args: [{ name: 'number', required: true }],
245
+ options: [
246
+ { name: 'interval', type: 'int', value: '<s>', summary: 'Seconds between polls (default 5)' },
247
+ { name: 'timeout', type: 'int', value: '<s>', summary: 'Give up after this long (default 1800)' },
248
+ { name: 'exit-status', type: 'boolean', summary: 'Exit non-zero when the run did not succeed' },
249
+ { name: 'log', type: 'boolean', summary: 'Print the failed job log when it finishes' },
250
+ output_1.JSON_OPTION,
251
+ ...COMMON,
252
+ ],
253
+ async run(inv) {
254
+ const target = await (0, target_1.targetFrom)(inv);
255
+ const repo = await (0, repo_1.resolveRepo)(inv, target);
256
+ const n = runNumber(inv);
257
+ const interval = Math.max(1, inv.int('interval') ?? 5) * 1000;
258
+ const timeout = Math.max(1, inv.int('timeout') ?? 1800) * 1000;
259
+ const json = (0, output_1.jsonMode)(inv);
260
+ const started = Date.now();
261
+ // A watch outlives any single request, so one that cannot reach the
262
+ // vault is not an answer about the run: a restart, a redeploy, or a
263
+ // dropped connection would otherwise abandon a run that is still going
264
+ // and report it as a failure. Keep polling and let the timeout, which is
265
+ // the caller's actual patience, decide when to stop; a vault that is
266
+ // still unreachable when it expires is reported then.
267
+ let unreachable = null;
268
+ // Null means the vault could not be reached this time, and the caller
269
+ // keeps whatever it last knew rather than treating silence as a change
270
+ // of state.
271
+ const poll = async () => {
272
+ try {
273
+ const answer = await (0, cli_api_1.api)(target, 'GET', `${(0, repo_1.repoPath)(repo)}/runs/${n}`);
274
+ if (unreachable !== null && !json.enabled)
275
+ console.error(`${target.host} is answering again`);
276
+ unreachable = null;
277
+ return answer;
278
+ }
279
+ catch (e) {
280
+ if (!(e instanceof exit_1.UnreachableError))
281
+ throw e;
282
+ if (unreachable === null && !json.enabled)
283
+ console.error(`${e.message}; still watching run #${n}`);
284
+ unreachable = e.message;
285
+ return null;
286
+ }
287
+ };
288
+ let data = (await poll()) ?? { status: 'unknown' };
289
+ let last = '';
290
+ while (data.status !== 'completed') {
291
+ if (Date.now() - started > timeout) {
292
+ const why = unreachable ?? `run #${n} is still ${data.status}`;
293
+ const failure = JSON.stringify({ error: `timed out waiting for the run: ${why}` });
294
+ if (json.enabled)
295
+ process.stderr.write(failure + '\n');
296
+ else
297
+ console.error(`Gave up after ${Math.round(timeout / 1000)}s; ${why}.`);
298
+ process.exit(exit_1.EXIT_FAIL);
299
+ }
300
+ // Progress on stderr, so that --json still puts one value on stdout.
301
+ const now = `${data.status}`;
302
+ if (!json.enabled && unreachable === null && now !== last) {
303
+ console.error(`run #${n} is ${now}...`);
304
+ last = now;
305
+ }
306
+ await new Promise((r) => setTimeout(r, interval));
307
+ data = (await poll()) ?? data;
308
+ }
309
+ if (json.enabled) {
310
+ (0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
311
+ }
312
+ else {
313
+ console.log(`#${data.number} ${data.workflowName}: ${data.conclusion ?? 'completed'}`);
314
+ const jobs = (data.jobs ?? []);
315
+ if (jobs.length) {
316
+ (0, output_1.printTable)(jobs.map((j) => [String(j.id), String(j.status), String(j.conclusion ?? ''), String(j.name)]));
317
+ }
318
+ if (inv.bool('log')) {
319
+ const failed = jobs.find((j) => j.conclusion === 'failure') ?? (jobs.length === 1 ? jobs[0] : undefined);
320
+ if (failed) {
321
+ console.log('');
322
+ console.log(`--- ${failed.id}`);
323
+ await printLog(target, repo, n, String(failed.id), null);
324
+ }
325
+ }
326
+ }
327
+ if (inv.bool('exit-status') && data.conclusion !== 'success')
328
+ process.exit(exit_1.EXIT_FAIL);
329
+ },
330
+ },
331
+ {
332
+ path: ['run', 'cancel'],
333
+ summary: 'Cancel a run that is queued or running',
334
+ args: [{ name: 'number', required: true }],
335
+ options: [output_1.JSON_OPTION, ...COMMON],
336
+ async run(inv) {
337
+ const target = await (0, target_1.targetFrom)(inv);
338
+ const repo = await (0, repo_1.resolveRepo)(inv, target);
339
+ const n = runNumber(inv);
340
+ const data = await (0, cli_api_1.api)(target, 'POST', `${(0, repo_1.repoPath)(repo)}/runs/${n}/cancel`);
341
+ const json = (0, output_1.jsonMode)(inv);
342
+ if (json.enabled) {
343
+ (0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
344
+ return;
345
+ }
346
+ console.log(`Cancelled run #${n}`);
347
+ },
348
+ },
349
+ {
350
+ path: ['run', 'rerun'],
351
+ summary: 'Run the same workflow at the same commit again',
352
+ args: [{ name: 'number', required: true }],
353
+ options: [output_1.JSON_OPTION, ...COMMON],
354
+ async run(inv) {
355
+ const target = await (0, target_1.targetFrom)(inv);
356
+ const repo = await (0, repo_1.resolveRepo)(inv, target);
357
+ const n = runNumber(inv);
358
+ const data = await (0, cli_api_1.api)(target, 'POST', `${(0, repo_1.repoPath)(repo)}/runs/${n}/rerun`);
359
+ const json = (0, output_1.jsonMode)(inv);
360
+ if (json.enabled) {
361
+ (0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
362
+ return;
363
+ }
364
+ console.log(`Started run #${data.number} of ${data.workflowName}`);
365
+ },
366
+ },
367
+ {
368
+ path: ['run', 'exec-command'],
369
+ summary: "Mint the command that runs a run's manual jobs on a machine of yours",
370
+ description: `A job with 'manual' in runs-on waits for a person rather than a runner. This
371
+ mints the command to paste on the machine that should execute it, the same one
372
+ the run page's "Run it yourself" button mints. The token in it is shown once,
373
+ must be pasted within fifteen minutes, and works once.`,
374
+ args: [{ name: 'number', required: true }],
375
+ options: [output_1.JSON_OPTION, ...COMMON],
376
+ async run(inv) {
377
+ const target = await (0, target_1.targetFrom)(inv);
378
+ const repo = await (0, repo_1.resolveRepo)(inv, target);
379
+ const n = runNumber(inv);
380
+ const data = await (0, cli_api_1.api)(target, 'POST', `${(0, repo_1.repoPath)(repo)}/runs/${n}/exec-command`);
381
+ const json = (0, output_1.jsonMode)(inv);
382
+ if (json.enabled) {
383
+ (0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
384
+ return;
385
+ }
386
+ console.log('Paste this on the machine that should run the manual jobs (shown once):');
387
+ console.log('');
388
+ console.log(` ${data.command}`);
389
+ console.log('');
390
+ console.log(`It expires in ${data.expiresInMinutes} minutes and works once.`);
391
+ },
392
+ },
393
+ {
394
+ path: ['run', 'download'],
395
+ summary: "Download a run's artifacts",
396
+ description: `An artifact is a tar. Without --artifact every artifact of the run is downloaded;
397
+ with it, just that one. They land in the current directory unless --dir says
398
+ otherwise.`,
399
+ args: [{ name: 'number', required: true }],
400
+ options: [
401
+ { name: 'artifact', type: 'string', value: '<name>', summary: 'Only this artifact' },
402
+ { name: 'dir', type: 'string', value: '<d>', summary: 'Where to write them (default: here)' },
403
+ output_1.JSON_OPTION,
404
+ ...COMMON,
405
+ ],
406
+ async run(inv) {
407
+ const target = await (0, target_1.targetFrom)(inv);
408
+ const repo = await (0, repo_1.resolveRepo)(inv, target);
409
+ const n = runNumber(inv);
410
+ const dir = inv.str('dir') ?? '.';
411
+ const only = inv.str('artifact');
412
+ const listed = await (0, cli_api_1.api)(target, 'GET', `${(0, repo_1.repoPath)(repo)}/runs/${n}/artifacts`);
413
+ const artifacts = (listed.artifacts ?? []).filter((a) => only === null || a.name === only);
414
+ if (artifacts.length === 0) {
415
+ throw new exit_1.CliError(only ? `run #${n} has no artifact named ${only}` : `run #${n} has no artifacts`, exit_1.EXIT_FAIL);
416
+ }
417
+ fs.mkdirSync(dir, { recursive: true });
418
+ const written = [];
419
+ for (const a of artifacts) {
420
+ const name = String(a.name);
421
+ const r = await (0, cli_api_1.requestBytes)(target, 'GET', `${(0, repo_1.repoPath)(repo)}/runs/${n}/artifacts/${encodeURIComponent(name)}`);
422
+ if (!r.ok) {
423
+ process.stderr.write(r.body.toString('utf8').trimEnd() + '\n');
424
+ process.exit((0, exit_1.exitCodeForStatus)(r.status));
425
+ }
426
+ const file = path.join(dir, `${name}.tar`);
427
+ fs.writeFileSync(file, r.body);
428
+ written.push({ name, file, bytes: r.body.length });
429
+ }
430
+ const json = (0, output_1.jsonMode)(inv);
431
+ if (json.enabled) {
432
+ (0, output_1.printJson)({ artifacts: written });
433
+ return;
434
+ }
435
+ (0, output_1.printTable)(written.map((w) => [w.name, w.file, String(w.bytes)]));
436
+ },
437
+ },
438
+ ];
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TARGET_OPTIONS = void 0;
4
+ exports.targetFrom = targetFrom;
5
+ const cli_api_1 = require("../cli-api");
6
+ const exit_1 = require("./exit");
7
+ const input_1 = require("./input");
8
+ // Which vault a command talks to, and with what token. Every command that
9
+ // reaches a vault takes the same three options, declared once here so that they
10
+ // are spelled and documented identically everywhere.
11
+ exports.TARGET_OPTIONS = [
12
+ {
13
+ name: 'host',
14
+ type: 'string',
15
+ value: '<url>',
16
+ summary: 'Vault URL, ahead of MOCHI_HOST and the last login',
17
+ },
18
+ {
19
+ name: 'token',
20
+ type: 'string',
21
+ value: '<t>',
22
+ summary: "Token, ahead of MOCHI_TOKEN and git's credential store",
23
+ },
24
+ {
25
+ name: 'token-stdin',
26
+ type: 'boolean',
27
+ summary: 'Read the token from stdin, so it is in neither argv nor shell history',
28
+ },
29
+ ];
30
+ /**
31
+ * The vault and token a command should use, from its options, the environment, or
32
+ * the login.
33
+ *
34
+ * `opts.host` is a fallback a command can supply from state of its own, and sits
35
+ * between the environment and the login: `mochi backup` records the vault a
36
+ * backup directory is a backup of, and that has to outrank the last login,
37
+ * because a machine that has since logged in to another vault must not quietly
38
+ * start filling one backup directory from a different vault.
39
+ */
40
+ async function targetFrom(inv, opts = {}) {
41
+ let token = inv.str('token');
42
+ if (inv.bool('token-stdin')) {
43
+ // Two ways of saying where the token comes from is a usage error, as every
44
+ // other "not both" in the CLI is; an empty stdin below is not, because
45
+ // there the invocation was fine and the token is what is missing.
46
+ if (token)
47
+ throw new exit_1.CliError('Pass either --token or --token-stdin, not both.', exit_1.EXIT_USAGE);
48
+ token = (await (0, input_1.readStdin)()).trim();
49
+ if (!token)
50
+ throw new exit_1.CliError('--token-stdin was given but stdin was empty.', exit_1.EXIT_AUTH);
51
+ }
52
+ const host = inv.str('host') ?? process.env.MOCHI_HOST?.trim() ?? opts.host ?? null;
53
+ return await (0, cli_api_1.remoteTarget)({ host, token });
54
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.remoteTarget = remoteTarget;
4
+ exports.request = request;
5
+ exports.requestBytes = requestBytes;
6
+ exports.apiTry = apiTry;
7
+ exports.apiFailed = apiFailed;
8
+ exports.api = api;
9
+ const credentials_1 = require("./credentials");
10
+ const exit_1 = require("./cli/exit");
11
+ /** The vault a command works against: --host/--token, then the environment, then the last login. */
12
+ async function remoteTarget(args) {
13
+ try {
14
+ return await (0, credentials_1.vaultTarget)(args);
15
+ }
16
+ catch (e) {
17
+ throw new exit_1.CliError(e instanceof Error ? e.message : String(e), exit_1.EXIT_AUTH);
18
+ }
19
+ }
20
+ /**
21
+ * One request, with the body left as text. `mochi api` prints it verbatim,
22
+ * and the routes that answer with a patch, a log, or an archive are not JSON at
23
+ * all.
24
+ */
25
+ async function request(target, method, pathname, opts = {}) {
26
+ let resp;
27
+ try {
28
+ resp = await fetch(`${target.host}${pathname}`, {
29
+ method,
30
+ headers: {
31
+ authorization: `Bearer ${target.token}`,
32
+ ...(opts.body !== undefined ? { 'content-type': opts.contentType ?? 'application/json' } : {}),
33
+ },
34
+ body: opts.body,
35
+ });
36
+ }
37
+ catch (e) {
38
+ throw new exit_1.UnreachableError(`Could not reach ${target.host}: ${e instanceof Error ? e.message : e}`);
39
+ }
40
+ return {
41
+ ok: resp.ok,
42
+ status: resp.status,
43
+ contentType: resp.headers.get('content-type') ?? '',
44
+ body: await resp.text(),
45
+ };
46
+ }
47
+ /**
48
+ * The same, with the body left as bytes. A tar or an archive is not text, and
49
+ * decoding it as UTF-8 to hand it back would corrupt it beyond recognition.
50
+ */
51
+ async function requestBytes(target, method, pathname) {
52
+ let resp;
53
+ try {
54
+ resp = await fetch(`${target.host}${pathname}`, { method, headers: { authorization: `Bearer ${target.token}` } });
55
+ }
56
+ catch (e) {
57
+ throw new exit_1.UnreachableError(`Could not reach ${target.host}: ${e instanceof Error ? e.message : e}`);
58
+ }
59
+ return { ok: resp.ok, status: resp.status, body: Buffer.from(await resp.arrayBuffer()) };
60
+ }
61
+ /** One API call, with the status left to the caller: for the calls where a 404 is an answer rather than a failure. */
62
+ async function apiTry(target, method, pathname, body) {
63
+ const r = await request(target, method, pathname, {
64
+ body: body !== undefined ? JSON.stringify(body) : undefined,
65
+ });
66
+ let data = null;
67
+ try {
68
+ data = JSON.parse(r.body);
69
+ }
70
+ catch {
71
+ data = null;
72
+ }
73
+ return { ok: r.ok, status: r.status, data: data ?? {} };
74
+ }
75
+ /** The vault refused: report what it said, with the exit code its status maps to. */
76
+ function apiFailed(target, pathname, r) {
77
+ throw new exit_1.CliError(r.data.error ? String(r.data.error) : `HTTP ${r.status} from ${target.host}${pathname}`, (0, exit_1.exitCodeForStatus)(r.status));
78
+ }
79
+ async function api(target, method, pathname, body) {
80
+ const r = await apiTry(target, method, pathname, body);
81
+ if (!r.ok)
82
+ apiFailed(target, pathname, r);
83
+ return r.data;
84
+ }