gcb-host 0.7.1 → 0.8.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 (94) hide show
  1. package/CHANGELOG.md +22 -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 +8 -10
  16. package/lib/commands/compose.d.ts.map +1 -1
  17. package/lib/commands/compose.js +33 -44
  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 +144 -93
  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 +100 -84
  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,7 +206,7 @@ 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');
@@ -201,9 +214,7 @@ export async function runHostBroker(argv) {
201
214
  const statusFile = hostStatusPath();
202
215
  const startedAt = new Date().toISOString();
203
216
  // The selected path is what a fork or explicit repository would load; it may not exist yet.
204
- const configSelected = Boolean(configPath) ||
205
- Boolean(process.env['GIT_BROKER_CONFIG']) ||
206
- existsSync(resolvedConfig);
217
+ const configSelected = Boolean(configPath) || Boolean(process.env['GIT_BROKER_CONFIG']) || existsSync(resolvedConfig);
207
218
  let loadedConfig = configSelected ? resolvedConfig : null;
208
219
  /** Remove the status file only when this process owns it. */
209
220
  function removeOwnStatus() {
@@ -222,8 +233,9 @@ export async function runHostBroker(argv) {
222
233
  }
223
234
  /** Rewrite the host-private status file; the channel marker is not trusted for this. */
224
235
  function writeStatus() {
225
- if (once)
236
+ if (once) {
226
237
  return;
238
+ }
227
239
  let collision;
228
240
  try {
229
241
  collision = canonicalPath(resolvedConfig) === canonicalPath(statusFile);
@@ -232,8 +244,9 @@ export async function runHostBroker(argv) {
232
244
  warn(`could not compare the config and status paths: ${error.message}`);
233
245
  return;
234
246
  }
235
- if (collision)
247
+ if (collision) {
236
248
  fail('the config cannot be the host status file');
249
+ }
237
250
  try {
238
251
  writeJsonAtomic(statusFile, {
239
252
  pid: process.pid,
@@ -257,12 +270,15 @@ export async function runHostBroker(argv) {
257
270
  writeStatus();
258
271
  }
259
272
  catch (error) {
260
- 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 });
261
274
  }
262
275
  }
263
276
  return hostConfig(config, host);
264
277
  };
265
- /** 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
+ */
266
282
  const allowed = (host, repo) => {
267
283
  const block = blockFor(host);
268
284
  return block !== null && repoAllowed(block.allow, repo);
@@ -277,17 +293,20 @@ export async function runHostBroker(argv) {
277
293
  }
278
294
  /** The checkouts to serve: one named checkout, or everything under the roots. */
279
295
  const targets = () => {
280
- if (single === undefined)
281
- return scanned.flatMap((root) => discoverCheckouts(root, depth));
296
+ if (single === undefined) {
297
+ return scanned.flatMap(root => discoverCheckouts(root, depth));
298
+ }
282
299
  const gitDir = findGitDir(single);
283
- if (gitDir === undefined)
300
+ if (gitDir === undefined) {
284
301
  fail(`no git repository at or above ${single}`);
302
+ }
285
303
  return [{ repo: single, dir: channelDir(gitDir) }];
286
304
  };
287
305
  if (statusMode) {
288
306
  validateArgs(args, argv.slice(1), new Set(), 'status');
289
- if (args.positionals.length > 0)
307
+ if (args.positionals.length > 0) {
290
308
  fail('status takes no arguments');
309
+ }
291
310
  const parsed = readJson(statusFile);
292
311
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
293
312
  fail('no host broker is running');
@@ -302,8 +321,9 @@ export async function runHostBroker(argv) {
302
321
  process.kill(pid, 0);
303
322
  }
304
323
  catch (error) {
305
- if (error.code === 'ESRCH')
324
+ if (error.code === 'ESRCH') {
306
325
  fail('no host broker is running');
326
+ }
307
327
  }
308
328
  const output = {
309
329
  pid,
@@ -336,8 +356,9 @@ export async function runHostBroker(argv) {
336
356
  removeOwnStatus();
337
357
  for (const dir of advertised) {
338
358
  try {
339
- if (readHostBroker(dir)?.pid === process.pid)
359
+ if (readHostBroker(dir)?.pid === process.pid) {
340
360
  clearHostBroker(dir);
361
+ }
341
362
  }
342
363
  catch {
343
364
  // Nothing useful is left to do while exiting.
@@ -347,7 +368,9 @@ export async function runHostBroker(argv) {
347
368
  const stop = () => process.exit(0);
348
369
  process.on('SIGINT', stop);
349
370
  process.on('SIGTERM', stop);
350
- 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}`);
351
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`);
352
375
  for (;;) {
353
376
  for (const { repo, dir } of targets()) {
@@ -361,19 +384,20 @@ export async function runHostBroker(argv) {
361
384
  warn(`could not serve ${repo}: ${error.message}`);
362
385
  }
363
386
  }
364
- if (once)
387
+ if (once) {
365
388
  return 0;
366
- await new Promise((done) => setTimeout(done, Math.max(interval, 0) * 1000));
389
+ }
390
+ await new Promise(done => setTimeout(done, Math.max(interval, 0) * 1000));
367
391
  }
368
392
  }
369
393
  /**
370
394
  * Answer everything waiting in the channel.
371
395
  *
372
- * @param dir - the channel directory.
373
- * @param repo - the checkout to create pull requests or issues in.
374
- * @param command - the program to call.
375
- * @param extra - arguments to pass before the subcommand.
376
- * @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.
377
401
  */
378
402
  async function sweep(dir, repo, command, extra, allowed, blockFor) {
379
403
  let answered = 0;
@@ -426,20 +450,7 @@ async function sweep(dir, repo, command, extra, allowed, blockFor) {
426
450
  const result = await openOne(repo, command, extra, { ...request, proxy }, allowed, blockFor);
427
451
  writeResult(dir, id, result);
428
452
  removeRequest(dir, id);
429
- if (result.error !== undefined) {
430
- warn(`failed to open ${request.kind === 'fork'
431
- ? 'fork'
432
- : request.kind === 'reply'
433
- ? `reply on #${request.number}`
434
- : request.kind === 'review'
435
- ? `review on #${request.number}`
436
- : request.kind === 'edit'
437
- ? `edit on #${request.number}`
438
- : request.kind === 'action'
439
- ? request.action
440
- : request.head}: ${result.error}`);
441
- }
442
- else {
453
+ if (result.error === undefined) {
443
454
  say(request.kind === 'fork'
444
455
  ? `forked ${request.repo} as ${request.account ?? 'this user'}: ${result.url}`
445
456
  : request.kind === 'reply'
@@ -452,6 +463,19 @@ async function sweep(dir, repo, command, extra, allowed, blockFor) {
452
463
  ? `ran ${request.action} as ${request.account ?? 'this user'}`
453
464
  : `opened #${result.number} as ${request.account ?? 'this user'}: ${result.url}`);
454
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
+ }
455
479
  answered += 1;
456
480
  }
457
481
  return answered;
@@ -484,32 +508,37 @@ function hostEnv(proxy, account) {
484
508
  }
485
509
  /** Unauthenticated public-repository check for the host route. */
486
510
  async function repoIsPublic(host, repository, proxy, apiBaseUrl, apiVersion = '2022-11-28') {
487
- 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`);
488
513
  return isPublicRepository(fetcherFor(proxy), base, repository, apiVersion);
489
514
  }
490
515
  /**
491
516
  * Whether an allowlisted fork's parent is the requested repository.
492
517
  *
493
- * @param host - the forge host.
494
- * @param allow - the configured allowlist entries.
495
- * @param repository - the requested parent repository.
496
- * @param proxy - the proxy choice for the check.
497
- * @param apiBaseUrl - an optional API base.
498
- * @param apiVersion - the API version header.
499
- * @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.
500
525
  */
501
526
  async function forkAllows(host, allow, repository, proxy, apiBaseUrl, apiVersion = '2022-11-28') {
502
- 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`);
503
529
  for (const raw of allow) {
504
530
  const entry = parseAllowEntry(raw);
505
- if (!entry || entry.owner === '*' || entry.repo === '*')
531
+ if (!entry || entry.owner === '*' || entry.repo === '*') {
506
532
  continue;
533
+ }
507
534
  const fork = `${entry.owner}/${entry.repo}`;
508
- if (fork === repository.toLowerCase())
535
+ if (fork === repository.toLowerCase()) {
509
536
  continue;
537
+ }
510
538
  try {
511
- if (await isForkOfRepository(fetcherFor(proxy), base, fork, repository, apiVersion))
539
+ if (await isForkOfRepository(fetcherFor(proxy), base, fork, repository, apiVersion)) {
512
540
  return true;
541
+ }
513
542
  }
514
543
  catch {
515
544
  // A failed check is not evidence of a relationship; keep looking, then fail closed.
@@ -518,14 +547,16 @@ async function forkAllows(host, allow, repository, proxy, apiBaseUrl, apiVersion
518
547
  return false;
519
548
  }
520
549
  /**
521
- * 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.
522
552
  *
523
553
  * The typed routes differ only in the argv they build, so the check and the catch live here once
524
554
  * instead of being copied per route.
525
555
  */
526
556
  async function personRun(host, repository, allowed, work) {
527
- if (!HOST.test(host))
557
+ if (!HOST.test(host)) {
528
558
  return { error: `not a host: ${JSON.stringify(host)}` };
559
+ }
529
560
  try {
530
561
  if (!allowed(host, repository)) {
531
562
  return { error: `repository ${repository} is not in the allowlist for ${host}` };
@@ -558,11 +589,11 @@ async function withBodyFile(body, work, name = 'body.md') {
558
589
  /**
559
590
  * Create one pull request, issue, review, reply, edit or comment.
560
591
  *
561
- * @param repo - the checkout to run it in.
562
- * @param command - the program to call.
563
- * @param extra - arguments to pass before the subcommand.
564
- * @param request - what to do.
565
- * @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.
566
597
  */
567
598
  async function openOne(repo, command, extra, request, allowed, blockFor) {
568
599
  if (request.kind === 'fork') {
@@ -586,15 +617,18 @@ async function openOne(repo, command, extra, request, allowed, blockFor) {
586
617
  env: forkEnv(request.proxy, request.account, host),
587
618
  });
588
619
  const match = /https:\/\/[^\s]+\/[^\s/]+\/[^\s/]+/.exec(stdout);
589
- if (match?.[0] !== undefined)
620
+ if (match?.[0] !== undefined) {
590
621
  return { url: match[0] };
622
+ }
591
623
  // With no TTY, gh prints the URL only for a fresh fork; an existing fork older than a minute
592
624
  // is `owner/name already exists` on stderr with empty stdout and exit 0.
593
625
  const existing = /^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+) already exists$/m.exec(stderr);
594
626
  if (existing?.[1] !== undefined) {
595
627
  return { url: `https://${host}/${existing[1]}` };
596
628
  }
597
- 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
+ };
598
632
  }
599
633
  catch (error) {
600
634
  const failure = error;
@@ -630,7 +664,13 @@ async function openOne(repo, command, extra, request, allowed, blockFor) {
630
664
  if (!Number.isSafeInteger(number) || number <= 0) {
631
665
  return { error: `${String(number)} is not an issue number` };
632
666
  }
633
- 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
+ ], {
634
674
  cwd: repo,
635
675
  env,
636
676
  timeoutMs: Math.max(1, deadline - Date.now()),
@@ -649,12 +689,13 @@ async function openOne(repo, command, extra, request, allowed, blockFor) {
649
689
  cwd: repo,
650
690
  env,
651
691
  timeoutMs: Math.max(1, deadline - Date.now()),
652
- writeCaveat: Object.values(action.permissions).some((permission) => permission === 'write'),
692
+ writeCaveat: Object.values(action.permissions).some(permission => permission === 'write'),
653
693
  });
654
694
  return result;
655
695
  };
656
- if (body === undefined)
696
+ if (body === undefined) {
657
697
  return invoke();
698
+ }
658
699
  if (action.api?.jsonBody === true) {
659
700
  return withBodyFile(apiBodyJson(body), invoke, 'body.json');
660
701
  }
@@ -726,7 +767,9 @@ async function openOne(repo, command, extra, request, allowed, blockFor) {
726
767
  }
727
768
  const reply = (answer ?? {});
728
769
  if (typeof reply.html_url !== 'string' || reply.html_url === '') {
729
- 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
+ };
730
773
  }
731
774
  return { number: request.number, url: reply.html_url };
732
775
  });
@@ -777,7 +820,9 @@ async function openOne(repo, command, extra, request, allowed, blockFor) {
777
820
  env: forkEnv(request.proxy, request.account, host),
778
821
  }).then(({ stdout }) => {
779
822
  const match = /https:\/\/[^\s]+\/pull\/\d+(?:#pullrequestreview-\d+)?/.exec(stdout);
780
- 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] };
781
826
  });
782
827
  return request.body.trim() === '' ? invoke() : withBodyFile(request.body, invoke);
783
828
  });
@@ -786,8 +831,9 @@ async function openOne(repo, command, extra, request, allowed, blockFor) {
786
831
  // repository this machine has no checkout of. Without one, gh still resolves the checkout.
787
832
  const target = request.repo;
788
833
  if (target !== undefined) {
789
- if (!REPOSITORY.test(target))
834
+ if (!REPOSITORY.test(target)) {
790
835
  return { error: `not a repository: ${JSON.stringify(target)}` };
836
+ }
791
837
  try {
792
838
  if (!allowed('github.com', target)) {
793
839
  return { error: `repository ${target} is not in the allowlist for github.com` };
@@ -808,7 +854,10 @@ async function openOne(repo, command, extra, request, allowed, blockFor) {
808
854
  // above is the boundary, and no checkout of the target is required.
809
855
  ...(request.draft === true ? ['--draft'] : []),
810
856
  ...(target === undefined ? [] : ['--repo', target]),
811
- '--base', request.base, '--head', request.head,
857
+ '--base',
858
+ request.base,
859
+ '--head',
860
+ request.head,
812
861
  `--title=${request.title}`,
813
862
  '--body-file',
814
863
  bodyFile,
@@ -822,7 +871,9 @@ async function openOne(repo, command, extra, request, allowed, blockFor) {
822
871
  });
823
872
  const found = pullRequestUrl(stdout);
824
873
  if (!found) {
825
- 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
+ };
826
877
  }
827
878
  return found;
828
879
  });