@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
package/dist/cli/repo.js
ADDED
|
@@ -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
|
+
}
|
package/dist/cli-api.js
ADDED
|
@@ -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
|
+
}
|