@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,592 @@
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.deployFlyRunnerCmd = deployFlyRunnerCmd;
37
+ exports.deployFlyRunnerShowCmd = deployFlyRunnerShowCmd;
38
+ exports.deployFlyRunnerDestroyCmd = deployFlyRunnerDestroyCmd;
39
+ const fs = __importStar(require("fs"));
40
+ const os = __importStar(require("os"));
41
+ const path = __importStar(require("path"));
42
+ const cli_api_1 = require("./cli-api");
43
+ const cli_api_2 = require("./cli-api");
44
+ const wake_1 = require("./ci/wake");
45
+ const deploy_cli_1 = require("./deploy-cli");
46
+ const scan_1 = require("./scan");
47
+ // `mochi deploy fly runner`: put a workflow runner on Fly.io, beside a
48
+ // vault that is already there or anywhere else.
49
+ //
50
+ // A runner is not a second vault. It keeps nothing worth keeping, it serves no
51
+ // requests, and it is worth money only while it is executing a job. So the
52
+ // arrangement here is the opposite of the vault's: the machine stops as soon
53
+ // as it has been idle for a few minutes, and the vault starts it again by
54
+ // sending a request to it when a job is waiting. Between runs the app costs
55
+ // nothing but its volume.
56
+ //
57
+ // The volume is mounted at /var/lib/docker, which is what makes stopping
58
+ // affordable: a job image pulled once stays pulled, so a cold start is a boot
59
+ // and not a download.
60
+ const RUNNER_IMAGE_REPO = `${deploy_cli_1.IMAGE_REPO}-runner`;
61
+ const VOLUME_NAME = 'docker';
62
+ const INTERNAL_PORT = 3000;
63
+ const HOST_SECRET = 'MOCHI_HOST';
64
+ const TOKEN_SECRET = 'MOCHI_RUNNER_TOKEN';
65
+ const WAKE_SECRET = 'MOCHI_WAKE_SECRET';
66
+ // Bigger than the vault's defaults, because the work is the other way round: a
67
+ // vault serves small requests from a disk, and a runner compiles things. The
68
+ // volume holds job images rather than anything of the operator's, so it is
69
+ // sized for a few of those and no more.
70
+ const DEFAULTS = {
71
+ region: 'ewr',
72
+ volumeGb: 20,
73
+ cpuKind: 'shared',
74
+ cpus: 2,
75
+ memory: '2gb',
76
+ idle: '5m',
77
+ };
78
+ function parseArgs(args, usage) {
79
+ const out = {
80
+ app: null,
81
+ name: null,
82
+ allow: [],
83
+ labels: [],
84
+ region: null,
85
+ volumeGb: null,
86
+ vmSize: null,
87
+ memory: null,
88
+ idle: null,
89
+ image: null,
90
+ fromSource: false,
91
+ localBuild: false,
92
+ org: null,
93
+ host: null,
94
+ token: null,
95
+ yes: false,
96
+ };
97
+ const list = (v) => v.split(/[\s,]+/).filter((s) => s.length > 0);
98
+ for (let i = 0; i < args.length; i++) {
99
+ const a = args[i];
100
+ if (a === '-h' || a === '--help')
101
+ usage();
102
+ else if (a === '--name')
103
+ out.name = args[++i];
104
+ else if (a === '--allow')
105
+ out.allow.push(...list(args[++i] ?? ''));
106
+ else if (a === '--labels' || a === '--label')
107
+ out.labels.push(...list(args[++i] ?? ''));
108
+ else if (a === '--region')
109
+ out.region = args[++i];
110
+ else if (a === '--volume') {
111
+ const gb = parseInt(args[++i], 10);
112
+ if (!Number.isInteger(gb) || gb < 1)
113
+ (0, deploy_cli_1.die)('--volume takes a size in whole gigabytes, e.g. --volume 20');
114
+ out.volumeGb = gb;
115
+ }
116
+ else if (a === '--vm-size')
117
+ out.vmSize = args[++i];
118
+ else if (a === '--vm-memory')
119
+ out.memory = args[++i];
120
+ else if (a === '--idle')
121
+ out.idle = args[++i];
122
+ else if (a === '--image')
123
+ out.image = args[++i];
124
+ else if (a === '--from-source')
125
+ out.fromSource = true;
126
+ else if (a === '--local-build')
127
+ out.localBuild = true;
128
+ else if (a === '--org')
129
+ out.org = args[++i];
130
+ else if (a === '--host')
131
+ out.host = args[++i];
132
+ else if (a === '--token')
133
+ out.token = args[++i];
134
+ else if (a === '-y' || a === '--yes')
135
+ out.yes = true;
136
+ else if (a.startsWith('-'))
137
+ (0, deploy_cli_1.die)(`Unknown option: ${a}`);
138
+ else if (!out.app)
139
+ out.app = a;
140
+ else
141
+ (0, deploy_cli_1.die)(`Unexpected argument: ${a}`);
142
+ }
143
+ return out;
144
+ }
145
+ /**
146
+ * Refuse the flags that shape a deployment on the commands that deploy nothing.
147
+ *
148
+ * `show` and `destroy` share the parser with the deploy, so accepting
149
+ * `--vm-size` here and ignoring it would look like it had been applied. Only
150
+ * the first one found is named, since fixing it means dropping it and running
151
+ * the command again either way.
152
+ */
153
+ function rejectShapingFlags(a, usage, allowYes) {
154
+ const used = [
155
+ ['--name', a.name !== null],
156
+ ['--allow', a.allow.length > 0],
157
+ ['--labels', a.labels.length > 0],
158
+ ['--region', a.region !== null],
159
+ ['--volume', a.volumeGb !== null],
160
+ ['--vm-size', a.vmSize !== null],
161
+ ['--vm-memory', a.memory !== null],
162
+ ['--idle', a.idle !== null],
163
+ ['--image', a.image !== null],
164
+ ['--from-source', a.fromSource],
165
+ ['--local-build', a.localBuild],
166
+ ['--org', a.org !== null],
167
+ ['--yes', a.yes && !allowYes],
168
+ ];
169
+ const flag = used.find(([, given]) => given);
170
+ if (!flag)
171
+ return;
172
+ if (flag[0] === '--yes')
173
+ (0, deploy_cli_1.die)(`--yes confirms a destroy, and there is nothing here to confirm.\nUsage: ${usage}`);
174
+ (0, deploy_cli_1.die)(`${flag[0]} says how to deploy, and this command deploys nothing.\nUsage: ${usage}`);
175
+ }
176
+ /** Checked here rather than by the runner, since a bad value would only be found after a deploy. */
177
+ function checkIdle(idle) {
178
+ if (!/^\d+\s*[smh]?$/i.test(idle.trim())) {
179
+ (0, deploy_cli_1.die)(`--idle takes a duration like 5m, 90s, or 1h, got: ${idle}`);
180
+ }
181
+ return idle.trim();
182
+ }
183
+ /** What Fly has now, so that a flag-less redeploy changes nothing and one flag changes one thing. */
184
+ async function liveSettings(app) {
185
+ const out = {};
186
+ const vol = await (0, deploy_cli_1.namedVolume)(app, VOLUME_NAME);
187
+ if (vol) {
188
+ out.region = vol.region;
189
+ out.volumeGb = vol.size_gb;
190
+ }
191
+ const guest = (await (0, deploy_cli_1.machines)(app)).find((m) => m.config?.guest)?.config?.guest;
192
+ if (guest) {
193
+ if (guest.cpu_kind)
194
+ out.cpuKind = guest.cpu_kind;
195
+ if (guest.cpus)
196
+ out.cpus = guest.cpus;
197
+ if (guest.memory_mb)
198
+ out.memory = `${guest.memory_mb}mb`;
199
+ }
200
+ return out;
201
+ }
202
+ function resolveSettings(a, live) {
203
+ const base = { ...DEFAULTS, ...live };
204
+ const vm = a.vmSize ? (0, deploy_cli_1.parseVmSize)(a.vmSize) : null;
205
+ return {
206
+ region: a.region ?? base.region,
207
+ volumeGb: a.volumeGb ?? base.volumeGb,
208
+ cpuKind: vm?.cpuKind ?? base.cpuKind,
209
+ cpus: vm?.cpus ?? base.cpus,
210
+ memory: a.memory ? (0, deploy_cli_1.normalizeMemory)(a.memory) : base.memory,
211
+ idle: a.idle ? checkIdle(a.idle) : base.idle,
212
+ };
213
+ }
214
+ /**
215
+ * The generated fly.toml.
216
+ *
217
+ * Two settings carry the whole design. `auto_stop_machines = "off"` because
218
+ * Fly stops a machine that has had no inbound traffic, and a runner's traffic
219
+ * is all outbound: left on, it would stop a machine in the middle of a job.
220
+ * The runner stops itself instead, by exiting when idle, which a restart
221
+ * policy of "never" turns into a stopped machine rather than a restarted one.
222
+ * `auto_start_machines` is what the vault's wake request then acts on.
223
+ */
224
+ function flyToml(app, s) {
225
+ return `# Generated by mochi deploy fly runner for '${app}'. Written to a temporary
226
+ # directory for the length of one deploy; edit the deploy command, not this.
227
+ app = "${app}"
228
+ primary_region = "${s.region}"
229
+
230
+ [mounts]
231
+ source = "${VOLUME_NAME}"
232
+ destination = "/var/lib/docker"
233
+
234
+ [env]
235
+ MOCHI_IDLE = "${s.idle}"
236
+ MOCHI_WAKE_PORT = "${INTERNAL_PORT}"
237
+
238
+ # The runner exits when it has been idle, and a machine whose process exits
239
+ # stops. Restarting it here would defeat that entirely.
240
+ [[restart]]
241
+ policy = "never"
242
+
243
+ # Nothing serves anything here: the only request this app ever receives is the
244
+ # vault's wake request, and the point of receiving it is that Fly starts the
245
+ # machine in order to deliver it.
246
+ [http_service]
247
+ internal_port = ${INTERNAL_PORT}
248
+ force_https = true
249
+ auto_stop_machines = "off"
250
+ auto_start_machines = true
251
+ min_machines_running = 0
252
+
253
+ [[vm]]
254
+ cpu_kind = "${s.cpuKind}"
255
+ cpus = ${s.cpus}
256
+ memory = "${s.memory}"
257
+ `;
258
+ }
259
+ function writeTempConfig(app, s) {
260
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mochi-runner-deploy-'));
261
+ const file = path.join(dir, 'fly.toml');
262
+ fs.writeFileSync(file, flyToml(app, s));
263
+ return file;
264
+ }
265
+ function wakeUrlFor(app) {
266
+ return `${(0, deploy_cli_1.appUrl)(app)}/wake`;
267
+ }
268
+ async function registeredRunners(target) {
269
+ const data = await (0, cli_api_1.api)(target, 'GET', '/api/runners');
270
+ return (data.runners ?? []) ?? [];
271
+ }
272
+ async function deployFlyRunnerCmd(args, usage) {
273
+ const a = parseArgs(args, usage);
274
+ if (!a.app) {
275
+ (0, deploy_cli_1.die)('Which app? Fly app names are globally unique, and this one is the address the vault\n' +
276
+ 'will send its wake request to:\n\n' +
277
+ ' mochi deploy fly runner my-runner --allow "mycollection/*"\n');
278
+ }
279
+ const app = a.app;
280
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(app)) {
281
+ (0, deploy_cli_1.die)(`Not a valid Fly app name: ${app}\nUse lowercase letters, digits, and dashes.`);
282
+ }
283
+ const name = a.name ?? app;
284
+ if (!(0, scan_1.isValidName)(name)) {
285
+ (0, deploy_cli_1.die)(`Not a usable runner name: ${name}\nUse letters, digits, dot, dash, underscore.`);
286
+ }
287
+ if (a.fromSource && a.image !== null) {
288
+ (0, deploy_cli_1.die)('--image names an image to pull and --from-source builds one instead. Pass one or the other.');
289
+ }
290
+ if (a.localBuild && !a.fromSource) {
291
+ (0, deploy_cli_1.die)('--local-build says how to build, and without --from-source there is nothing to build.');
292
+ }
293
+ const buildRoot = a.fromSource ? (0, deploy_cli_1.sourceRoot)() : null;
294
+ const image = buildRoot === null ? a.image ?? `${RUNNER_IMAGE_REPO}:${(0, deploy_cli_1.ownVersion)()}` : null;
295
+ // The vault first: a runner with no vault to serve is nothing, and finding
296
+ // out that the login is missing after creating a Fly app would be a poor
297
+ // trade. This is the same login every other admin command uses.
298
+ const target = await (0, cli_api_2.remoteTarget)(a);
299
+ const existingRunners = await registeredRunners(target);
300
+ const registered = existingRunners.find((r) => r.name === name) ?? null;
301
+ await (0, deploy_cli_1.requireFly)();
302
+ const appIsThere = await (0, deploy_cli_1.appExists)(app);
303
+ const secrets = appIsThere ? await (0, deploy_cli_1.secretNames)(app) : [];
304
+ if (!registered && a.allow.length === 0) {
305
+ (0, deploy_cli_1.die)(`No runner named '${name}' is registered with ${target.host}, so this deploy would\n` +
306
+ 'register one, and a runner needs to be told which repositories it may take jobs\n' +
307
+ 'for. It executes whatever their workflows contain, on the machine deployed here:\n\n' +
308
+ ` mochi deploy fly runner ${app} --allow 'mycollection/*'\n`);
309
+ }
310
+ // An --allow that repeats what the runner already serves is not a request to
311
+ // change anything, and refusing it would be perverse: a first deploy that
312
+ // registers the runner and then fails at the image leaves exactly that
313
+ // situation, and this command's own advice is to run it again unchanged.
314
+ // Only a different set is refused, because quietly ignoring that would leave
315
+ // an operator believing they had widened or narrowed what the runner serves.
316
+ const sameAllow = registered !== null &&
317
+ a.allow.length === registered.allow.length &&
318
+ [...a.allow].sort().join('') === [...registered.allow].sort().join('');
319
+ if (registered && a.allow.length > 0 && !sameAllow) {
320
+ (0, deploy_cli_1.die)(`Runner '${name}' is already registered, serving ${registered.allow.join(', ')}.\n` +
321
+ 'This deploy does not change that. To change what it serves, remove and register it:\n\n' +
322
+ ` mochi runner remove ${name}\n` +
323
+ ` mochi deploy fly runner ${app} --allow '<globs>'\n`);
324
+ }
325
+ const live = appIsThere ? await liveSettings(app) : {};
326
+ const settings = resolveSettings(a, live);
327
+ if (appIsThere && live.region && a.region && a.region !== live.region) {
328
+ (0, deploy_cli_1.die)(`This runner's volume is in ${live.region}, and a volume cannot be moved to ${a.region}.\n` +
329
+ 'Deploy a runner in another region as a second app, or destroy this one first.');
330
+ }
331
+ console.log(appIsThere ? `==> Updating '${app}' (${(0, deploy_cli_1.appUrl)(app)})` : `==> Creating '${app}' in ${settings.region}`);
332
+ if (!appIsThere) {
333
+ const created = await (0, deploy_cli_1.flyStream)(['apps', 'create', app, ...(a.org ? ['--org', a.org] : [])]);
334
+ if (created !== 0) {
335
+ (0, deploy_cli_1.die)(`\nCould not create the Fly app '${app}'.\n` +
336
+ 'App names are globally unique, so a name in use by anyone stops this. Try another.');
337
+ }
338
+ }
339
+ const vol = await (0, deploy_cli_1.namedVolume)(app, VOLUME_NAME);
340
+ if (!vol) {
341
+ console.log(`==> Creating a ${settings.volumeGb}GB volume '${VOLUME_NAME}' in ${settings.region}`);
342
+ console.log(' It holds the images jobs run in, so that stopping when idle costs a boot');
343
+ console.log(' on the next job rather than a download.');
344
+ const code = await (0, deploy_cli_1.flyStream)([
345
+ 'volumes',
346
+ 'create',
347
+ VOLUME_NAME,
348
+ '-a',
349
+ app,
350
+ '--region',
351
+ settings.region,
352
+ '--size',
353
+ String(settings.volumeGb),
354
+ '--yes',
355
+ ]);
356
+ if (code !== 0)
357
+ (0, deploy_cli_1.die)('\nCould not create the volume.');
358
+ }
359
+ else if (settings.volumeGb > vol.size_gb) {
360
+ console.log(`==> Extending volume '${VOLUME_NAME}' from ${vol.size_gb}GB to ${settings.volumeGb}GB`);
361
+ const code = await (0, deploy_cli_1.flyStream)(['volumes', 'extend', vol.id, '-a', app, '--size', String(settings.volumeGb)]);
362
+ if (code !== 0)
363
+ (0, deploy_cli_1.die)('\nCould not extend the volume.');
364
+ }
365
+ else if (a.volumeGb !== null && a.volumeGb < vol.size_gb) {
366
+ (0, deploy_cli_1.die)(`The volume is ${vol.size_gb}GB and Fly volumes cannot be shrunk, so --volume ${a.volumeGb} cannot be applied.\n` +
367
+ 'Leave the flag off to keep the size it has.');
368
+ }
369
+ // The token this machine will hold. A registration issues one; an existing
370
+ // runner whose token is not already a secret on this app needs a new one,
371
+ // because the old one is unrecoverable by design and there is no way to put
372
+ // it back. Rotating it stops any other machine running as this runner,
373
+ // which for a runner named after this app is the intended outcome.
374
+ const wake = { url: wakeUrlFor(app), secret: (0, wake_1.newWakeSecret)() };
375
+ let runnerToken = null;
376
+ if (!registered) {
377
+ console.log(`==> Registering runner '${name}' with ${target.host}`);
378
+ const data = await (0, cli_api_1.api)(target, 'POST', '/api/runners', {
379
+ name,
380
+ labels: a.labels.length ? a.labels : ['ubuntu-latest'],
381
+ allow: a.allow,
382
+ wakeUrl: wake.url,
383
+ wakeSecret: wake.secret,
384
+ });
385
+ runnerToken = data.token;
386
+ }
387
+ else if (!secrets.includes(TOKEN_SECRET)) {
388
+ console.log(`==> Issuing a token for the registered runner '${name}'`);
389
+ console.log(' Its previous token cannot be read back from anywhere, so this deploy gives it');
390
+ console.log(' a new one. Any other machine running as this runner will stop being able to.');
391
+ const data = await (0, cli_api_1.api)(target, 'POST', `/api/runners/${encodeURIComponent(name)}/token`, {});
392
+ runnerToken = data.token;
393
+ }
394
+ // The wake address is rewritten on every deploy, not only on the first. The
395
+ // secret is this run's, and the app it points at is this app; a redeploy
396
+ // that left an older secret in place would leave the vault sending one the
397
+ // machine has never been given.
398
+ console.log(`==> Pointing the vault's wake request at ${wake.url}`);
399
+ await (0, cli_api_1.api)(target, 'PUT', `/api/runners/${encodeURIComponent(name)}/wake`, {
400
+ wakeUrl: wake.url,
401
+ wakeSecret: wake.secret,
402
+ });
403
+ // Staged rather than set, so that no machine restarts before the deploy
404
+ // below, and over stdin so that no secret appears in this machine's `ps`.
405
+ const staged = [`${HOST_SECRET}=${target.host}`, `${WAKE_SECRET}=${wake.secret}`];
406
+ if (runnerToken)
407
+ staged.push(`${TOKEN_SECRET}=${runnerToken}`);
408
+ console.log('==> Setting the vault URL, the runner token, and the wake secret as Fly secrets');
409
+ const set = await (0, deploy_cli_1.fly)(['secrets', 'import', '-a', app, '--stage'], staged.join('\n') + '\n');
410
+ if (set.code !== 0)
411
+ (0, deploy_cli_1.die)(`Could not set the secrets:\n${set.stderr.trim() || set.stdout.trim()}`);
412
+ const config = writeTempConfig(app, settings);
413
+ if (buildRoot !== null) {
414
+ console.log(`==> Building ${buildRoot} ${a.localBuild ? 'with the local Docker' : "on Fly's builder"}`);
415
+ }
416
+ else {
417
+ console.log(`==> Deploying ${image}`);
418
+ }
419
+ const code = await (0, deploy_cli_1.flyStream)([
420
+ 'deploy',
421
+ '--app',
422
+ app,
423
+ '--config',
424
+ config,
425
+ ...(buildRoot !== null
426
+ ? ['--dockerfile', path.join(buildRoot, 'Dockerfile.runner'), ...(a.localBuild ? ['--local-only'] : [])]
427
+ : ['--image', image]),
428
+ '--ha=false',
429
+ '--yes',
430
+ ], buildRoot ?? undefined);
431
+ fs.rmSync(path.dirname(config), { recursive: true, force: true });
432
+ if (code !== 0) {
433
+ console.error('');
434
+ console.error('The deploy failed. The app, the volume, and the registration survive, so fix the');
435
+ console.error('cause and run the same command again.');
436
+ if (buildRoot === null && a.image === null) {
437
+ // Named as the likely cause rather than as one possibility among many,
438
+ // because it is the one every early deploy hits: the runner image is
439
+ // published per release like the vault's, so a version of the CLI newer
440
+ // than the newest release has no image to pull. Fly reports a package
441
+ // that is absent, and one that is private, with the same message about
442
+ // authentication, and a package newly published to GHCR is private
443
+ // until someone says otherwise.
444
+ console.error('');
445
+ console.error(`If Fly could not fetch ${image}:`);
446
+ console.error('');
447
+ console.error(' - It may not be published. The runner image is built per release, so a CLI');
448
+ console.error(' newer than the newest release asks for a tag that does not exist yet.');
449
+ console.error(' - It may be private. A package newly published to GHCR is private until it');
450
+ console.error(' is made public, and Fly reports that as an authentication error too.');
451
+ console.error('');
452
+ console.error('Either way, this deploys the checkout you are running instead:');
453
+ console.error('');
454
+ console.error(` mochi deploy fly runner ${app} --from-source`);
455
+ console.error('');
456
+ console.error('or --image <ref> deploys some other tag.');
457
+ }
458
+ process.exit(1);
459
+ }
460
+ // A machine is started by the deploy and will stop itself once it has been
461
+ // idle for the configured time. Saying so is worth a line, because a
462
+ // `deploy fly` that ends with a stopped machine looks like a failure to
463
+ // anyone who has deployed a vault before.
464
+ console.log('');
465
+ console.log(`==> Deployed: ${(0, deploy_cli_1.appUrl)(app)}`);
466
+ console.log('');
467
+ // Laid out as facts rather than as a sentence broken over three lines: the
468
+ // globs and the labels are lists of unknown length, and wrapping around them
469
+ // put a line break in the middle of a clause.
470
+ const servedLabels = registered?.labels ?? (a.labels.length ? a.labels : ['ubuntu-latest']);
471
+ console.log(` runner ${name}, registered with ${target.host}`);
472
+ console.log(` serving ${(registered?.allow ?? a.allow).join(', ')}`);
473
+ console.log(` labels ${servedLabels.join(', ')}, matched against a job's runs-on`);
474
+ console.log('');
475
+ console.log(`It stops after ${settings.idle} with no job, and the vault starts it again by`);
476
+ console.log('requesting its wake address when a job is waiting. So a machine in the stopped');
477
+ console.log('state is this working, not this broken; the first job after a stop waits about');
478
+ console.log('half a minute for the boot.');
479
+ console.log('');
480
+ console.log(' mochi runner list');
481
+ console.log(` mochi runner wake ${name} # start it now, and time how long that takes`);
482
+ console.log(` fly logs -a ${app}`);
483
+ console.log('');
484
+ console.log(` mochi deploy fly runner ${app} --vm-size shared-cpu-4x --idle 15m`);
485
+ }
486
+ async function deployFlyRunnerShowCmd(args, usage) {
487
+ const a = parseArgs(args, usage);
488
+ if (!a.app)
489
+ (0, deploy_cli_1.die)('Which app? Usage: mochi deploy fly runner show <app>');
490
+ rejectShapingFlags(a, 'mochi deploy fly runner show <app>', false);
491
+ const app = a.app;
492
+ await (0, deploy_cli_1.requireFly)();
493
+ if (!(await (0, deploy_cli_1.appExists)(app))) {
494
+ (0, deploy_cli_1.die)(`No Fly app named '${app}' that you can see. Check the name, or: fly apps list`);
495
+ }
496
+ const vol = await (0, deploy_cli_1.namedVolume)(app, VOLUME_NAME);
497
+ const ms = await (0, deploy_cli_1.machines)(app);
498
+ const secrets = await (0, deploy_cli_1.secretNames)(app);
499
+ console.log(`${app} ${(0, deploy_cli_1.appUrl)(app)}`);
500
+ console.log('');
501
+ if (ms.length === 0) {
502
+ console.log(' machines none');
503
+ }
504
+ else {
505
+ for (const m of ms) {
506
+ const g = m.config?.guest;
507
+ const shape = g ? `${g.cpu_kind}-cpu-${g.cpus}x, ${g.memory_mb}mb` : 'unknown shape';
508
+ // A stopped runner is the normal resting state, so the state is
509
+ // reported with what it means rather than on its own.
510
+ const meaning = m.state === 'stopped' ? ' (idle; the vault starts it when a job is waiting)' : '';
511
+ console.log(` machine ${m.id} ${m.state ?? '?'}${meaning} ${m.region ?? '?'} ${shape}`);
512
+ console.log(` image ${m.config?.image ?? 'unknown'}`);
513
+ }
514
+ }
515
+ console.log(vol ? ` volume ${vol.size_gb}GB in ${vol.region}, holding job images` : ' volume none');
516
+ console.log(` vault ${secrets.includes(HOST_SECRET) ? 'configured as a Fly secret' : 'not configured'}`);
517
+ console.log(` token ${secrets.includes(TOKEN_SECRET) ? 'set' : 'not set, so this runner cannot poll'}`);
518
+ console.log(` wake ${secrets.includes(WAKE_SECRET) ? wakeUrlFor(app) : 'no secret set, so wake requests are refused'}`);
519
+ // Whether the vault agrees, which is the half Fly cannot answer: an app that
520
+ // is perfectly healthy and a registration that was removed look identical
521
+ // from here.
522
+ try {
523
+ const target = await (0, cli_api_2.remoteTarget)(a);
524
+ const runners = await registeredRunners(target);
525
+ const mine = runners.filter((r) => r.wakeUrl === wakeUrlFor(app));
526
+ if (mine.length === 0) {
527
+ console.log(` runner no runner on ${target.host} is woken at this app`);
528
+ }
529
+ else {
530
+ for (const r of mine) {
531
+ console.log(` runner ${r.name} on ${target.host}, serving ${r.allow.join(', ')}`);
532
+ }
533
+ }
534
+ }
535
+ catch (e) {
536
+ console.log(` runner could not ask the vault: ${e instanceof Error ? e.message : e}`);
537
+ }
538
+ console.log('');
539
+ console.log(` fly logs -a ${app}`);
540
+ }
541
+ async function deployFlyRunnerDestroyCmd(args, usage) {
542
+ const a = parseArgs(args, usage);
543
+ if (!a.app)
544
+ (0, deploy_cli_1.die)('Which app? Usage: mochi deploy fly runner destroy <app> [--yes]');
545
+ rejectShapingFlags(a, 'mochi deploy fly runner destroy <app> [--yes]', true);
546
+ const app = a.app;
547
+ await (0, deploy_cli_1.requireFly)();
548
+ if (!(await (0, deploy_cli_1.appExists)(app))) {
549
+ (0, deploy_cli_1.die)(`No Fly app named '${app}' that you can see. Check the name, or: fly apps list`);
550
+ }
551
+ // Which registration this app was serving, asked before the app is gone,
552
+ // since the wake address is the only thing tying the two together.
553
+ let orphan = null;
554
+ let target = null;
555
+ try {
556
+ target = await (0, cli_api_2.remoteTarget)(a);
557
+ const runners = await registeredRunners(target);
558
+ orphan = runners.find((r) => r.wakeUrl === wakeUrlFor(app))?.name ?? null;
559
+ }
560
+ catch {
561
+ // A destroy should not require a working login; it only makes the tidying
562
+ // below unavailable.
563
+ }
564
+ if (!a.yes) {
565
+ console.log(`This destroys the Fly app '${app}' and its volume of cached job images.`);
566
+ console.log('Nothing of yours is on it: a runner keeps no state that matters between jobs.');
567
+ if (orphan)
568
+ console.log(`The runner '${orphan}' stays registered with the vault, and this offers to remove it.`);
569
+ console.log('');
570
+ const answer = await (0, deploy_cli_1.promptLine)('Type the app name to confirm: ');
571
+ if (answer !== app)
572
+ (0, deploy_cli_1.die)('Not destroyed.');
573
+ }
574
+ const code = await (0, deploy_cli_1.flyStream)(['apps', 'destroy', app, '--yes']);
575
+ if (code !== 0)
576
+ (0, deploy_cli_1.die)('\nCould not destroy the app.');
577
+ if (orphan && target) {
578
+ // A registration whose machine is gone is worse than useless: the vault
579
+ // goes on trying to wake an app that no longer exists, and a queued job
580
+ // waits for a runner that will never poll.
581
+ await (0, cli_api_1.api)(target, 'DELETE', `/api/runners/${encodeURIComponent(orphan)}`);
582
+ console.log(`Removed the runner registration '${orphan}' from ${target.host}.`);
583
+ }
584
+ else if (orphan === null) {
585
+ console.log('');
586
+ console.log('No runner registration was found pointing at this app. If one is left behind,');
587
+ console.log('it will keep a queued job waiting for a runner that cannot start:');
588
+ console.log('');
589
+ console.log(' mochi runner list');
590
+ console.log(' mochi runner remove <name>');
591
+ }
592
+ }