gcb-host 0.7.0 → 0.8.0

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 (94) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/bin/gcb.js +3 -3
  3. package/lib/audit.d.ts +4 -4
  4. package/lib/audit.d.ts.map +1 -1
  5. package/lib/audit.js +15 -14
  6. package/lib/audit.js.map +1 -1
  7. package/lib/broker.d.ts +12 -13
  8. package/lib/broker.d.ts.map +1 -1
  9. package/lib/broker.js +77 -53
  10. package/lib/broker.js.map +1 -1
  11. package/lib/commands/authorize.d.ts +43 -13
  12. package/lib/commands/authorize.d.ts.map +1 -1
  13. package/lib/commands/authorize.js +146 -53
  14. package/lib/commands/authorize.js.map +1 -1
  15. package/lib/commands/compose.d.ts +7 -7
  16. package/lib/commands/compose.d.ts.map +1 -1
  17. package/lib/commands/compose.js +21 -22
  18. package/lib/commands/compose.js.map +1 -1
  19. package/lib/commands/config.d.ts.map +1 -1
  20. package/lib/commands/config.js +59 -41
  21. package/lib/commands/config.js.map +1 -1
  22. package/lib/commands/deployment.d.ts +11 -11
  23. package/lib/commands/deployment.d.ts.map +1 -1
  24. package/lib/commands/deployment.js +19 -18
  25. package/lib/commands/deployment.js.map +1 -1
  26. package/lib/commands/diagnose.d.ts.map +1 -1
  27. package/lib/commands/diagnose.js +51 -33
  28. package/lib/commands/diagnose.js.map +1 -1
  29. package/lib/commands/doctor.d.ts +5 -5
  30. package/lib/commands/doctor.d.ts.map +1 -1
  31. package/lib/commands/doctor.js +53 -32
  32. package/lib/commands/doctor.js.map +1 -1
  33. package/lib/commands/host.d.ts +7 -7
  34. package/lib/commands/host.d.ts.map +1 -1
  35. package/lib/commands/host.js +147 -92
  36. package/lib/commands/host.js.map +1 -1
  37. package/lib/commands/init.d.ts +33 -33
  38. package/lib/commands/init.d.ts.map +1 -1
  39. package/lib/commands/init.js +93 -75
  40. package/lib/commands/init.js.map +1 -1
  41. package/lib/commands/stage.d.ts +6 -6
  42. package/lib/commands/stage.d.ts.map +1 -1
  43. package/lib/commands/stage.js +21 -15
  44. package/lib/commands/stage.js.map +1 -1
  45. package/lib/config.d.ts +29 -28
  46. package/lib/config.d.ts.map +1 -1
  47. package/lib/config.js +101 -85
  48. package/lib/config.js.map +1 -1
  49. package/lib/daemon.d.ts +9 -9
  50. package/lib/daemon.d.ts.map +1 -1
  51. package/lib/daemon.js +21 -18
  52. package/lib/daemon.js.map +1 -1
  53. package/lib/device-flow.d.ts +23 -22
  54. package/lib/device-flow.d.ts.map +1 -1
  55. package/lib/device-flow.js +33 -28
  56. package/lib/device-flow.js.map +1 -1
  57. package/lib/gh.d.ts +2 -2
  58. package/lib/gh.d.ts.map +1 -1
  59. package/lib/gh.js +74 -46
  60. package/lib/gh.js.map +1 -1
  61. package/lib/http.d.ts.map +1 -1
  62. package/lib/http.js +35 -18
  63. package/lib/http.js.map +1 -1
  64. package/lib/index.d.ts.map +1 -1
  65. package/lib/index.js.map +1 -1
  66. package/lib/policy.d.ts +25 -25
  67. package/lib/policy.d.ts.map +1 -1
  68. package/lib/policy.js +57 -36
  69. package/lib/policy.js.map +1 -1
  70. package/lib/providers/github-app.d.ts +12 -12
  71. package/lib/providers/github-app.d.ts.map +1 -1
  72. package/lib/providers/github-app.js +194 -125
  73. package/lib/providers/github-app.js.map +1 -1
  74. package/lib/providers/index.d.ts +8 -9
  75. package/lib/providers/index.d.ts.map +1 -1
  76. package/lib/providers/index.js +5 -5
  77. package/lib/providers/index.js.map +1 -1
  78. package/lib/providers/static.d.ts +3 -3
  79. package/lib/providers/static.d.ts.map +1 -1
  80. package/lib/providers/static.js +14 -12
  81. package/lib/providers/static.js.map +1 -1
  82. package/lib/proxy.d.ts +10 -10
  83. package/lib/proxy.d.ts.map +1 -1
  84. package/lib/proxy.js +18 -16
  85. package/lib/proxy.js.map +1 -1
  86. package/lib/public-repo.d.ts +10 -10
  87. package/lib/public-repo.d.ts.map +1 -1
  88. package/lib/public-repo.js +17 -14
  89. package/lib/public-repo.js.map +1 -1
  90. package/lib/reload.d.ts +4 -4
  91. package/lib/reload.d.ts.map +1 -1
  92. package/lib/reload.js +24 -14
  93. package/lib/reload.js.map +1 -1
  94. package/package.json +10 -10
@@ -2,10 +2,11 @@
2
2
  * `host` — the host broker for typed person actions: create, review, reply, edit, run catalogued
3
3
  * actions, and fork with the gh account each request names.
4
4
  *
5
- * This runs on the host, beside the checkout the container also sees, and it does exactly one thing:
6
- * it turns a request file into a pull request, review, reply, pull request edit, catalogued action
7
- * or fork, using the gh account the request names. That is the whole point — content authored by a person is what automated reviewers will
8
- * actually review, and the app cannot be that person.
5
+ * This runs on the host, beside the checkout the container also sees, and it does exactly one
6
+ * thing: it turns a request file into a pull request, review, reply, pull request edit, catalogued
7
+ * action or fork, using the gh account the request names. That is the whole point — content
8
+ * authored by a person is what automated reviewers will actually review, and the app cannot be that
9
+ * person.
9
10
  *
10
11
  * It has no other power than the catalogued actions: it cannot push or merge, and it never runs a
11
12
  * shell; the program it calls is configuration, and it is called with an argument list built here.
@@ -13,13 +14,12 @@
13
14
  import { execFile, spawn } from 'node:child_process';
14
15
  import { existsSync, mkdtempSync, rmSync, statSync, writeFileSync } from 'node:fs';
15
16
  import { homedir, tmpdir } from 'node:os';
16
- import { StringDecoder } from 'node:string_decoder';
17
17
  import { join, resolve } from 'node:path';
18
+ import { StringDecoder } from 'node:string_decoder';
18
19
  import { promisify } from 'node:util';
19
- import { channelDir, clearHostBroker, discoverCheckouts, findGitDir, listRequests, reclaimAbandoned, readHostBroker, claimRequest, removeRequest, requestProblems, writeHostBroker, writeJsonAtomic, readJson, writeResult, PACKAGE_VERSION, canonicalPath, apiBodyJson, HOST, REPOSITORY, ANSWER_TIMEOUT_MS, buildGhArgv, getGhAction, } from 'gcb-core';
20
- import { fail, listFlag, parseArgs, say, validateArgs, warn } from 'gcb-core';
21
- import { applyProxyChoice, fetcherFor, parseProxy } from '../http.js';
20
+ import { channelDir, clearHostBroker, discoverCheckouts, findGitDir, listRequests, reclaimAbandoned, readHostBroker, claimRequest, removeRequest, requestProblems, writeHostBroker, writeJsonAtomic, readJson, writeResult, PACKAGE_VERSION, canonicalPath, apiBodyJson, HOST, REPOSITORY, ANSWER_TIMEOUT_MS, buildGhArgv, getGhAction, fail, listFlag, parseArgs, say, validateArgs, warn, } from 'gcb-core';
22
21
  import { loadConfig, resolveConfigPath } from '../config.js';
22
+ import { applyProxyChoice, fetcherFor, parseProxy } from '../http.js';
23
23
  import { hostConfig, parseAllowEntry, repoAllowed } from '../policy.js';
24
24
  import { isForkOfRepository, isPublicRepository } from '../public-repo.js';
25
25
  const run = promisify(execFile);
@@ -29,17 +29,20 @@ const CREATE_TIMEOUT_MS = 120_000;
29
29
  export function hostStatusPath() {
30
30
  return join(homedir(), '.gcb-host', 'status.json');
31
31
  }
32
- /** Per-stream byte budget for a host action, small enough that JSON escaping stays under readResult's cap. */
32
+ /**
33
+ * Per-stream byte budget for a host action, small enough that JSON escaping stays under
34
+ * readResult's cap.
35
+ */
33
36
  const HOST_ACTION_OUTPUT_BYTES = 48_000;
34
37
  /**
35
38
  * Run an action with streaming output caps.
36
39
  *
37
- * execFile's maxBuffer kills the child once a fixed buffer fills; a large diff or log would then be
40
+ * ExecFile's maxBuffer kills the child once a fixed buffer fills; a large diff or log would then be
38
41
  * reported as a gh failure even though only the excess output is unusable. Read both streams to the
39
42
  * end, keep the first bytes of each, and tell the caller where the cut is.
40
43
  */
41
44
  export function runBounded(command, args, options) {
42
- return new Promise((resolve) => {
45
+ return new Promise(resolve => {
43
46
  let child;
44
47
  try {
45
48
  child = spawn(command, [...args], {
@@ -74,10 +77,12 @@ export function runBounded(command, args, options) {
74
77
  const limit = stream === 'out' ? outKeep : errKeep;
75
78
  const used = stream === 'out' ? outBytes : errBytes;
76
79
  if (used >= limit) {
77
- if (stream === 'out')
80
+ if (stream === 'out') {
78
81
  outCut = true;
79
- else
82
+ }
83
+ else {
80
84
  errCut = true;
85
+ }
81
86
  return;
82
87
  }
83
88
  const piece = chunk.subarray(0, limit - used);
@@ -85,19 +90,21 @@ export function runBounded(command, args, options) {
85
90
  if (stream === 'out') {
86
91
  out += text;
87
92
  outBytes += piece.length;
88
- if (piece.length < chunk.length)
93
+ if (piece.length < chunk.length) {
89
94
  outCut = true;
95
+ }
90
96
  }
91
97
  else {
92
98
  err += text;
93
99
  errBytes += piece.length;
94
- if (piece.length < chunk.length)
100
+ if (piece.length < chunk.length) {
95
101
  errCut = true;
102
+ }
96
103
  }
97
104
  };
98
105
  child.stdout?.on('data', (chunk) => append(stdout, chunk, 'out'));
99
106
  child.stderr?.on('data', (chunk) => append(stderr, chunk, 'err'));
100
- child.on('error', (error) => finish({ stdout: out, stderr: err || error.message, code: 1 }));
107
+ child.on('error', error => finish({ stdout: out, stderr: err || error.message, code: 1 }));
101
108
  const timer = setTimeout(() => {
102
109
  timedOut = true;
103
110
  if (child.pid !== undefined) {
@@ -110,21 +117,26 @@ export function runBounded(command, args, options) {
110
117
  }
111
118
  }, options.timeoutMs);
112
119
  function finish(result) {
113
- if (settled)
120
+ if (settled) {
114
121
  return;
122
+ }
115
123
  settled = true;
116
124
  clearTimeout(timer);
117
125
  out += stdout.end();
118
126
  err += stderr.end();
119
- if (outCut)
127
+ if (outCut) {
120
128
  out += suffix;
121
- if (errCut)
129
+ }
130
+ if (errCut) {
122
131
  err += suffix;
123
- if (err === '' && result.stderr !== '')
132
+ }
133
+ if (err === '' && result.stderr !== '') {
124
134
  err = result.stderr;
135
+ }
125
136
  if (timedOut) {
126
- if (err !== '' && !err.endsWith('\n'))
137
+ if (err !== '' && !err.endsWith('\n')) {
127
138
  err += '\n';
139
+ }
128
140
  err += timeoutNote;
129
141
  }
130
142
  resolve({ ...result, stdout: out, stderr: err });
@@ -163,24 +175,25 @@ directory, and only the roots this process scans are host-side paths.
163
175
  /**
164
176
  * Read a pull request's number and URL out of a program's output.
165
177
  *
166
- * `gh pr create` prints the URL, and so does anything that behaves like it. Nothing else is trusted:
167
- * without a URL in the output the host broker reports a failure rather than guessing.
178
+ * `gh pr create` prints the URL, and so does anything that behaves like it. Nothing else is
179
+ * trusted: without a URL in the output the host broker reports a failure rather than guessing.
168
180
  *
169
- * @param output - the program's standard output.
170
- * @returns the number and URL, or `undefined`.
181
+ * @param output - The program's standard output.
182
+ * @returns The number and URL, or `undefined`.
171
183
  */
172
184
  export function pullRequestUrl(output) {
173
185
  const match = /https:\/\/[^\s]+\/pull\/(\d+)/.exec(output);
174
186
  const digits = match?.[1];
175
- if (!match || digits === undefined)
187
+ if (!match || digits === undefined) {
176
188
  return undefined;
189
+ }
177
190
  return { number: Number(digits), url: match[0] };
178
191
  }
179
192
  /**
180
193
  * Run the `host` command.
181
194
  *
182
- * @param argv - arguments after the command name.
183
- * @returns the process exit code.
195
+ * @param argv - Arguments after the command name.
196
+ * @returns The process exit code.
184
197
  */
185
198
  export async function runHostBroker(argv) {
186
199
  const statusMode = argv[0] === 'status';
@@ -193,14 +206,16 @@ export async function runHostBroker(argv) {
193
206
  const explicit = args.value('repo');
194
207
  const single = explicit === undefined ? undefined : resolve(explicit);
195
208
  const roots = listFlag(args, 'root');
196
- const scanned = roots.length > 0 ? roots.map((root) => resolve(root)) : [homedir()];
209
+ const scanned = roots.length > 0 ? roots.map(root => resolve(root)) : [homedir()];
197
210
  const depth = Number(args.value('depth') ?? '4');
198
211
  const command = args.value('command') ?? process.env['GCB_HOST_COMMAND'] ?? 'gh';
199
212
  const configPath = args.value('config');
200
213
  const resolvedConfig = resolveConfigPath(configPath);
201
214
  const statusFile = hostStatusPath();
202
215
  const startedAt = new Date().toISOString();
203
- let loadedConfig = null;
216
+ // The selected path is what a fork or explicit repository would load; it may not exist yet.
217
+ const configSelected = Boolean(configPath) || Boolean(process.env['GIT_BROKER_CONFIG']) || existsSync(resolvedConfig);
218
+ let loadedConfig = configSelected ? resolvedConfig : null;
204
219
  /** Remove the status file only when this process owns it. */
205
220
  function removeOwnStatus() {
206
221
  try {
@@ -218,8 +233,9 @@ export async function runHostBroker(argv) {
218
233
  }
219
234
  /** Rewrite the host-private status file; the channel marker is not trusted for this. */
220
235
  function writeStatus() {
221
- if (once)
236
+ if (once) {
222
237
  return;
238
+ }
223
239
  let collision;
224
240
  try {
225
241
  collision = canonicalPath(resolvedConfig) === canonicalPath(statusFile);
@@ -228,8 +244,9 @@ export async function runHostBroker(argv) {
228
244
  warn(`could not compare the config and status paths: ${error.message}`);
229
245
  return;
230
246
  }
231
- if (collision)
247
+ if (collision) {
232
248
  fail('the config cannot be the host status file');
249
+ }
233
250
  try {
234
251
  writeJsonAtomic(statusFile, {
235
252
  pid: process.pid,
@@ -248,17 +265,20 @@ export async function runHostBroker(argv) {
248
265
  const blockFor = (host) => {
249
266
  if (config === undefined) {
250
267
  try {
251
- config = loadConfig(resolveConfigPath(configPath));
268
+ config = loadConfig(resolvedConfig);
252
269
  loadedConfig = resolvedConfig;
253
270
  writeStatus();
254
271
  }
255
272
  catch (error) {
256
- throw new Error(`a fork, reply or explicit repository needs the broker config allowlist: ${error.message}; pass --config or set GIT_BROKER_CONFIG`);
273
+ throw new Error(`a fork, reply or explicit repository needs the broker config allowlist: ${error.message}; pass --config or set GIT_BROKER_CONFIG`, { cause: error });
257
274
  }
258
275
  }
259
276
  return hostConfig(config, host);
260
277
  };
261
- /** Whether the broker config allowlist permits this host/repository for a fork, reply or explicit repository. */
278
+ /**
279
+ * Whether the broker config allowlist permits this host/repository for a fork, reply or explicit
280
+ * repository.
281
+ */
262
282
  const allowed = (host, repo) => {
263
283
  const block = blockFor(host);
264
284
  return block !== null && repoAllowed(block.allow, repo);
@@ -273,17 +293,20 @@ export async function runHostBroker(argv) {
273
293
  }
274
294
  /** The checkouts to serve: one named checkout, or everything under the roots. */
275
295
  const targets = () => {
276
- if (single === undefined)
277
- return scanned.flatMap((root) => discoverCheckouts(root, depth));
296
+ if (single === undefined) {
297
+ return scanned.flatMap(root => discoverCheckouts(root, depth));
298
+ }
278
299
  const gitDir = findGitDir(single);
279
- if (gitDir === undefined)
300
+ if (gitDir === undefined) {
280
301
  fail(`no git repository at or above ${single}`);
302
+ }
281
303
  return [{ repo: single, dir: channelDir(gitDir) }];
282
304
  };
283
305
  if (statusMode) {
284
306
  validateArgs(args, argv.slice(1), new Set(), 'status');
285
- if (args.positionals.length > 0)
307
+ if (args.positionals.length > 0) {
286
308
  fail('status takes no arguments');
309
+ }
287
310
  const parsed = readJson(statusFile);
288
311
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
289
312
  fail('no host broker is running');
@@ -298,8 +321,9 @@ export async function runHostBroker(argv) {
298
321
  process.kill(pid, 0);
299
322
  }
300
323
  catch (error) {
301
- if (error.code === 'ESRCH')
324
+ if (error.code === 'ESRCH') {
302
325
  fail('no host broker is running');
326
+ }
303
327
  }
304
328
  const output = {
305
329
  pid,
@@ -332,8 +356,9 @@ export async function runHostBroker(argv) {
332
356
  removeOwnStatus();
333
357
  for (const dir of advertised) {
334
358
  try {
335
- if (readHostBroker(dir)?.pid === process.pid)
359
+ if (readHostBroker(dir)?.pid === process.pid) {
336
360
  clearHostBroker(dir);
361
+ }
337
362
  }
338
363
  catch {
339
364
  // Nothing useful is left to do while exiting.
@@ -343,7 +368,9 @@ export async function runHostBroker(argv) {
343
368
  const stop = () => process.exit(0);
344
369
  process.on('SIGINT', stop);
345
370
  process.on('SIGTERM', stop);
346
- say(single === undefined ? `serving the checkouts under ${scanned.join(', ')}` : `serving ${single}`);
371
+ say(single === undefined
372
+ ? `serving the checkouts under ${scanned.join(', ')}`
373
+ : `serving ${single}`);
347
374
  say(`creating pull requests, issues, reviews, replies and forks with ${command}${extra.length > 0 ? ` ${extra.join(' ')}` : ''}, as the request's account or this user`);
348
375
  for (;;) {
349
376
  for (const { repo, dir } of targets()) {
@@ -357,19 +384,20 @@ export async function runHostBroker(argv) {
357
384
  warn(`could not serve ${repo}: ${error.message}`);
358
385
  }
359
386
  }
360
- if (once)
387
+ if (once) {
361
388
  return 0;
362
- await new Promise((done) => setTimeout(done, Math.max(interval, 0) * 1000));
389
+ }
390
+ await new Promise(done => setTimeout(done, Math.max(interval, 0) * 1000));
363
391
  }
364
392
  }
365
393
  /**
366
394
  * Answer everything waiting in the channel.
367
395
  *
368
- * @param dir - the channel directory.
369
- * @param repo - the checkout to create pull requests or issues in.
370
- * @param command - the program to call.
371
- * @param extra - arguments to pass before the subcommand.
372
- * @returns how many requests were answered.
396
+ * @param dir - The channel directory.
397
+ * @param repo - The checkout to create pull requests or issues in.
398
+ * @param command - The program to call.
399
+ * @param extra - Arguments to pass before the subcommand.
400
+ * @returns How many requests were answered.
373
401
  */
374
402
  async function sweep(dir, repo, command, extra, allowed, blockFor) {
375
403
  let answered = 0;
@@ -422,20 +450,7 @@ async function sweep(dir, repo, command, extra, allowed, blockFor) {
422
450
  const result = await openOne(repo, command, extra, { ...request, proxy }, allowed, blockFor);
423
451
  writeResult(dir, id, result);
424
452
  removeRequest(dir, id);
425
- if (result.error !== undefined) {
426
- warn(`failed to open ${request.kind === 'fork'
427
- ? 'fork'
428
- : request.kind === 'reply'
429
- ? `reply on #${request.number}`
430
- : request.kind === 'review'
431
- ? `review on #${request.number}`
432
- : request.kind === 'edit'
433
- ? `edit on #${request.number}`
434
- : request.kind === 'action'
435
- ? request.action
436
- : request.head}: ${result.error}`);
437
- }
438
- else {
453
+ if (result.error === undefined) {
439
454
  say(request.kind === 'fork'
440
455
  ? `forked ${request.repo} as ${request.account ?? 'this user'}: ${result.url}`
441
456
  : request.kind === 'reply'
@@ -448,6 +463,19 @@ async function sweep(dir, repo, command, extra, allowed, blockFor) {
448
463
  ? `ran ${request.action} as ${request.account ?? 'this user'}`
449
464
  : `opened #${result.number} as ${request.account ?? 'this user'}: ${result.url}`);
450
465
  }
466
+ else {
467
+ warn(`failed to open ${request.kind === 'fork'
468
+ ? 'fork'
469
+ : request.kind === 'reply'
470
+ ? `reply on #${request.number}`
471
+ : request.kind === 'review'
472
+ ? `review on #${request.number}`
473
+ : request.kind === 'edit'
474
+ ? `edit on #${request.number}`
475
+ : request.kind === 'action'
476
+ ? request.action
477
+ : request.head}: ${result.error}`);
478
+ }
451
479
  answered += 1;
452
480
  }
453
481
  return answered;
@@ -480,32 +508,37 @@ function hostEnv(proxy, account) {
480
508
  }
481
509
  /** Unauthenticated public-repository check for the host route. */
482
510
  async function repoIsPublic(host, repository, proxy, apiBaseUrl, apiVersion = '2022-11-28') {
483
- const base = apiBaseUrl ?? (host.toLowerCase() === 'github.com' ? 'https://api.github.com' : `https://${host}/api/v3`);
511
+ const base = apiBaseUrl ??
512
+ (host.toLowerCase() === 'github.com' ? 'https://api.github.com' : `https://${host}/api/v3`);
484
513
  return isPublicRepository(fetcherFor(proxy), base, repository, apiVersion);
485
514
  }
486
515
  /**
487
516
  * Whether an allowlisted fork's parent is the requested repository.
488
517
  *
489
- * @param host - the forge host.
490
- * @param allow - the configured allowlist entries.
491
- * @param repository - the requested parent repository.
492
- * @param proxy - the proxy choice for the check.
493
- * @param apiBaseUrl - an optional API base.
494
- * @param apiVersion - the API version header.
495
- * @returns true when one allowlisted fork names the repository as its parent.
518
+ * @param host - The forge host.
519
+ * @param allow - The configured allowlist entries.
520
+ * @param repository - The requested parent repository.
521
+ * @param proxy - The proxy choice for the check.
522
+ * @param apiBaseUrl - An optional API base.
523
+ * @param apiVersion - The API version header.
524
+ * @returns True when one allowlisted fork names the repository as its parent.
496
525
  */
497
526
  async function forkAllows(host, allow, repository, proxy, apiBaseUrl, apiVersion = '2022-11-28') {
498
- const base = apiBaseUrl ?? (host.toLowerCase() === 'github.com' ? 'https://api.github.com' : `https://${host}/api/v3`);
527
+ const base = apiBaseUrl ??
528
+ (host.toLowerCase() === 'github.com' ? 'https://api.github.com' : `https://${host}/api/v3`);
499
529
  for (const raw of allow) {
500
530
  const entry = parseAllowEntry(raw);
501
- if (!entry || entry.owner === '*' || entry.repo === '*')
531
+ if (!entry || entry.owner === '*' || entry.repo === '*') {
502
532
  continue;
533
+ }
503
534
  const fork = `${entry.owner}/${entry.repo}`;
504
- if (fork === repository.toLowerCase())
535
+ if (fork === repository.toLowerCase()) {
505
536
  continue;
537
+ }
506
538
  try {
507
- if (await isForkOfRepository(fetcherFor(proxy), base, fork, repository, apiVersion))
539
+ if (await isForkOfRepository(fetcherFor(proxy), base, fork, repository, apiVersion)) {
508
540
  return true;
541
+ }
509
542
  }
510
543
  catch {
511
544
  // A failed check is not evidence of a relationship; keep looking, then fail closed.
@@ -514,14 +547,16 @@ async function forkAllows(host, allow, repository, proxy, apiBaseUrl, apiVersion
514
547
  return false;
515
548
  }
516
549
  /**
517
- * Run one person operation: check the allowlist, then call the program and turn its failure into a result.
550
+ * Run one person operation: check the allowlist, then call the program and turn its failure into a
551
+ * result.
518
552
  *
519
553
  * The typed routes differ only in the argv they build, so the check and the catch live here once
520
554
  * instead of being copied per route.
521
555
  */
522
556
  async function personRun(host, repository, allowed, work) {
523
- if (!HOST.test(host))
557
+ if (!HOST.test(host)) {
524
558
  return { error: `not a host: ${JSON.stringify(host)}` };
559
+ }
525
560
  try {
526
561
  if (!allowed(host, repository)) {
527
562
  return { error: `repository ${repository} is not in the allowlist for ${host}` };
@@ -554,11 +589,11 @@ async function withBodyFile(body, work, name = 'body.md') {
554
589
  /**
555
590
  * Create one pull request, issue, review, reply, edit or comment.
556
591
  *
557
- * @param repo - the checkout to run it in.
558
- * @param command - the program to call.
559
- * @param extra - arguments to pass before the subcommand.
560
- * @param request - what to do.
561
- * @returns the number and URL, or the reason it did not work.
592
+ * @param repo - The checkout to run it in.
593
+ * @param command - The program to call.
594
+ * @param extra - Arguments to pass before the subcommand.
595
+ * @param request - What to do.
596
+ * @returns The number and URL, or the reason it did not work.
562
597
  */
563
598
  async function openOne(repo, command, extra, request, allowed, blockFor) {
564
599
  if (request.kind === 'fork') {
@@ -582,15 +617,18 @@ async function openOne(repo, command, extra, request, allowed, blockFor) {
582
617
  env: forkEnv(request.proxy, request.account, host),
583
618
  });
584
619
  const match = /https:\/\/[^\s]+\/[^\s/]+\/[^\s/]+/.exec(stdout);
585
- if (match?.[0] !== undefined)
620
+ if (match?.[0] !== undefined) {
586
621
  return { url: match[0] };
622
+ }
587
623
  // With no TTY, gh prints the URL only for a fresh fork; an existing fork older than a minute
588
624
  // is `owner/name already exists` on stderr with empty stdout and exit 0.
589
625
  const existing = /^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+) already exists$/m.exec(stderr);
590
626
  if (existing?.[1] !== undefined) {
591
627
  return { url: `https://${host}/${existing[1]}` };
592
628
  }
593
- return { error: `no fork URL in the output: ${(stdout.trim() || stderr.trim()).slice(0, 300)}` };
629
+ return {
630
+ error: `no fork URL in the output: ${(stdout.trim() || stderr.trim()).slice(0, 300)}`,
631
+ };
594
632
  }
595
633
  catch (error) {
596
634
  const failure = error;
@@ -626,7 +664,13 @@ async function openOne(repo, command, extra, request, allowed, blockFor) {
626
664
  if (!Number.isSafeInteger(number) || number <= 0) {
627
665
  return { error: `${String(number)} is not an issue number` };
628
666
  }
629
- const probe = await runBounded(command, [...extra, 'api', `repos/${request.repo}/issues/${String(number)}`, '--jq', '.pull_request != null'], {
667
+ const probe = await runBounded(command, [
668
+ ...extra,
669
+ 'api',
670
+ `repos/${request.repo}/issues/${String(number)}`,
671
+ '--jq',
672
+ '.pull_request != null',
673
+ ], {
630
674
  cwd: repo,
631
675
  env,
632
676
  timeoutMs: Math.max(1, deadline - Date.now()),
@@ -645,12 +689,13 @@ async function openOne(repo, command, extra, request, allowed, blockFor) {
645
689
  cwd: repo,
646
690
  env,
647
691
  timeoutMs: Math.max(1, deadline - Date.now()),
648
- writeCaveat: Object.values(action.permissions).some((permission) => permission === 'write'),
692
+ writeCaveat: Object.values(action.permissions).some(permission => permission === 'write'),
649
693
  });
650
694
  return result;
651
695
  };
652
- if (body === undefined)
696
+ if (body === undefined) {
653
697
  return invoke();
698
+ }
654
699
  if (action.api?.jsonBody === true) {
655
700
  return withBodyFile(apiBodyJson(body), invoke, 'body.json');
656
701
  }
@@ -722,7 +767,9 @@ async function openOne(repo, command, extra, request, allowed, blockFor) {
722
767
  }
723
768
  const reply = (answer ?? {});
724
769
  if (typeof reply.html_url !== 'string' || reply.html_url === '') {
725
- return { error: `no reply URL in the output: ${(stdout.trim() || stderr.trim()).slice(0, 300)}` };
770
+ return {
771
+ error: `no reply URL in the output: ${(stdout.trim() || stderr.trim()).slice(0, 300)}`,
772
+ };
726
773
  }
727
774
  return { number: request.number, url: reply.html_url };
728
775
  });
@@ -773,7 +820,9 @@ async function openOne(repo, command, extra, request, allowed, blockFor) {
773
820
  env: forkEnv(request.proxy, request.account, host),
774
821
  }).then(({ stdout }) => {
775
822
  const match = /https:\/\/[^\s]+\/pull\/\d+(?:#pullrequestreview-\d+)?/.exec(stdout);
776
- return match?.[0] === undefined ? { number: request.number } : { number: request.number, url: match[0] };
823
+ return match?.[0] === undefined
824
+ ? { number: request.number }
825
+ : { number: request.number, url: match[0] };
777
826
  });
778
827
  return request.body.trim() === '' ? invoke() : withBodyFile(request.body, invoke);
779
828
  });
@@ -782,8 +831,9 @@ async function openOne(repo, command, extra, request, allowed, blockFor) {
782
831
  // repository this machine has no checkout of. Without one, gh still resolves the checkout.
783
832
  const target = request.repo;
784
833
  if (target !== undefined) {
785
- if (!REPOSITORY.test(target))
834
+ if (!REPOSITORY.test(target)) {
786
835
  return { error: `not a repository: ${JSON.stringify(target)}` };
836
+ }
787
837
  try {
788
838
  if (!allowed('github.com', target)) {
789
839
  return { error: `repository ${target} is not in the allowlist for github.com` };
@@ -804,7 +854,10 @@ async function openOne(repo, command, extra, request, allowed, blockFor) {
804
854
  // above is the boundary, and no checkout of the target is required.
805
855
  ...(request.draft === true ? ['--draft'] : []),
806
856
  ...(target === undefined ? [] : ['--repo', target]),
807
- '--base', request.base, '--head', request.head,
857
+ '--base',
858
+ request.base,
859
+ '--head',
860
+ request.head,
808
861
  `--title=${request.title}`,
809
862
  '--body-file',
810
863
  bodyFile,
@@ -818,7 +871,9 @@ async function openOne(repo, command, extra, request, allowed, blockFor) {
818
871
  });
819
872
  const found = pullRequestUrl(stdout);
820
873
  if (!found) {
821
- return { error: `no pull request URL in the output: ${(stdout.trim() || stderr.trim()).slice(0, 300)}` };
874
+ return {
875
+ error: `no pull request URL in the output: ${(stdout.trim() || stderr.trim()).slice(0, 300)}`,
876
+ };
822
877
  }
823
878
  return found;
824
879
  });