@osdlc/typed-gh 0.7.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +116 -92
- package/SECURITY.md +4 -4
- package/dist/async.d.ts +28 -27
- package/dist/async.js +37 -43
- package/dist/doors.d.ts +25 -23
- package/dist/doors.js +29 -27
- package/dist/failure.d.ts +8 -8
- package/dist/failure.js +6 -6
- package/dist/index.d.ts +14 -14
- package/dist/index.js +15 -15
- package/dist/parse.d.ts +21 -21
- package/dist/parse.js +34 -36
- package/dist/repo.d.ts +13 -13
- package/dist/repo.js +13 -13
- package/dist/run.d.ts +36 -0
- package/dist/run.js +48 -0
- package/dist/sync.d.ts +66 -64
- package/dist/sync.js +71 -78
- package/dist/types.d.ts +31 -32
- package/dist/types.js +8 -8
- package/dist/verbs.d.ts +122 -122
- package/dist/verbs.js +128 -112
- package/package.json +5 -5
package/dist/verbs.js
CHANGED
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The verb table
|
|
2
|
+
* The verb table: every `gh` call this library knows, as data.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the other's parsing.
|
|
4
|
+
* It has the same shape as `@osdlc/typed-git`'s. A {@link GhVerb} pairs the
|
|
5
|
+
* argv to run with the function that reads the result, and nothing here
|
|
6
|
+
* starts a process. So the sync client, the async client and callers with
|
|
7
|
+
* their own runner all use the same definitions and the same parsing.
|
|
9
8
|
*
|
|
10
9
|
* `checked` marks a verb the runner throws for on a non-zero exit, with
|
|
11
|
-
* stderr
|
|
12
|
-
* unchecked and `parse`
|
|
13
|
-
*
|
|
14
|
-
* `doors.ts`) instead
|
|
15
|
-
* `absent` or is a `probe`.
|
|
10
|
+
* stderr. That is every mutation, and `prDiff`, which is a read. Other
|
|
11
|
+
* queries run unchecked, and `parse` turns a failure into `null`, which is
|
|
12
|
+
* what `soft` returns. The clients' methods that throw read them through
|
|
13
|
+
* `settle` (see `doors.ts`) instead. There a failure throws, except where the
|
|
14
|
+
* verb declares `absent` or is a `probe`.
|
|
16
15
|
*/
|
|
17
16
|
import { stderrOrError } from '@osdlc/command-seam';
|
|
18
17
|
import { parseLabels, parsePrSearchHit, parsePullRequest, parseRepository } from "./parse.js";
|
|
19
18
|
import { isAbsent } from "./failure.js";
|
|
20
19
|
import { isRepoSlug, parsePrUrl } from "./repo.js";
|
|
21
|
-
/** Whether a failed run says GitHub has no such `thing
|
|
20
|
+
/** Whether a failed run says GitHub has no such `thing`. Reads the same text the classifier reads. */
|
|
22
21
|
function noSuch(thing) {
|
|
23
22
|
return (r) => isAbsent(thing, `${r.stderr}\n${r.stdout}`);
|
|
24
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* A bare host, with an optional port: letters, digits, dots and hyphens, that
|
|
26
|
+
* starts and ends with a letter or digit, then optionally `:` and digits
|
|
27
|
+
* (`github.com`, `ghe.acme.com`, `localhost:8080`). No scheme, path, spaces or
|
|
28
|
+
* leading `-`, so the value cannot be read by gh as a flag or a URL. It does
|
|
29
|
+
* not check that the host exists or that its labels are well formed, and it
|
|
30
|
+
* does not accept IPv6 literals.
|
|
31
|
+
*/
|
|
25
32
|
const HOSTNAME = /^[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::\d+)?$/;
|
|
33
|
+
/** The target's host, or `undefined` when it has none. `''` is "not set". */
|
|
34
|
+
function hostOf(target) {
|
|
35
|
+
return target?.hostname === '' ? undefined : target?.hostname;
|
|
36
|
+
}
|
|
26
37
|
/**
|
|
27
38
|
* Throw `RangeError` unless `target.repo` is `owner/name` and
|
|
28
39
|
* `target.hostname` is a bare host. Run by every verb that uses a target, so
|
|
@@ -32,11 +43,12 @@ export function checkTarget(target) {
|
|
|
32
43
|
if (target?.repo !== undefined && !isRepoSlug(target.repo)) {
|
|
33
44
|
throw new RangeError(`repo must be owner/name, got ${JSON.stringify(target.repo)}`);
|
|
34
45
|
}
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
const hostname = hostOf(target);
|
|
47
|
+
if (hostname !== undefined && !HOSTNAME.test(hostname)) {
|
|
48
|
+
throw new RangeError(`hostname must be a bare host, got ${JSON.stringify(hostname)}`);
|
|
37
49
|
}
|
|
38
50
|
}
|
|
39
|
-
/** `--repo [host/]owner/name` for `pr`, `issue` and `label` verbs
|
|
51
|
+
/** `--repo [host/]owner/name` for `pr`, `issue` and `label` verbs. `repo` overrides the target's. */
|
|
40
52
|
function repoArgs(target, repo) {
|
|
41
53
|
checkTarget(target);
|
|
42
54
|
const slug = repo ?? target?.repo;
|
|
@@ -44,17 +56,20 @@ function repoArgs(target, repo) {
|
|
|
44
56
|
return [];
|
|
45
57
|
if (repo !== undefined)
|
|
46
58
|
checkTarget({ repo });
|
|
47
|
-
|
|
59
|
+
const hostname = hostOf(target);
|
|
60
|
+
return ['--repo', hostname === undefined ? slug : `${hostname}/${slug}`];
|
|
48
61
|
}
|
|
49
62
|
/** `--hostname <host>` for `api` and `auth` verbs. */
|
|
50
63
|
function hostArgs(target) {
|
|
51
64
|
checkTarget(target);
|
|
52
|
-
|
|
65
|
+
const hostname = hostOf(target);
|
|
66
|
+
return hostname === undefined ? [] : ['--hostname', hostname];
|
|
53
67
|
}
|
|
54
68
|
/** `GH_HOST` for the verbs gh gives no host flag. */
|
|
55
69
|
function hostEnv(target) {
|
|
56
70
|
checkTarget(target);
|
|
57
|
-
|
|
71
|
+
const hostname = hostOf(target);
|
|
72
|
+
return hostname === undefined ? undefined : { GH_HOST: hostname };
|
|
58
73
|
}
|
|
59
74
|
/**
|
|
60
75
|
* `endpoint` with `{owner}` and `{repo}` filled from the target. With no
|
|
@@ -68,21 +83,21 @@ function fillRepo(endpoint, target) {
|
|
|
68
83
|
return endpoint.replaceAll('{owner}', owner).replaceAll('{repo}', name);
|
|
69
84
|
}
|
|
70
85
|
/**
|
|
71
|
-
* The
|
|
86
|
+
* The smallest useful set: enough to identify a PR and nothing more.
|
|
72
87
|
*
|
|
73
|
-
*
|
|
74
|
-
* `
|
|
75
|
-
*
|
|
76
|
-
*
|
|
88
|
+
* Use it when the question is "which PR is this branch?". `mergeable` and
|
|
89
|
+
* `mergeStateStatus` are left out on purpose. GitHub computes them per PR
|
|
90
|
+
* when asked, so asking for them across many branches can hit the rate
|
|
91
|
+
* limit.
|
|
77
92
|
*/
|
|
78
93
|
export const PR_REF_FIELDS = ['number', 'state', 'headRefName', 'url'];
|
|
79
94
|
/**
|
|
80
|
-
* The
|
|
81
|
-
*
|
|
95
|
+
* The smaller field set for triage: enough to decide what to do with a PR,
|
|
96
|
+
* without its body, comments, reviews, or check rollup.
|
|
82
97
|
*
|
|
83
|
-
* `mergeable` and `mergeStateStatus` are the
|
|
84
|
-
* them
|
|
85
|
-
* PRs
|
|
98
|
+
* `mergeable` and `mergeStateStatus` are the costly pair. GitHub computes
|
|
99
|
+
* them only when asked and answers `UNKNOWN` until it has. So a first query
|
|
100
|
+
* over many PRs often needs a retry. `prViewSettled` on the clients does it.
|
|
86
101
|
*/
|
|
87
102
|
export const PR_LIST_FIELDS = [
|
|
88
103
|
'number',
|
|
@@ -103,7 +118,7 @@ export const PR_LIST_FIELDS = [
|
|
|
103
118
|
'createdAt',
|
|
104
119
|
'updatedAt',
|
|
105
120
|
];
|
|
106
|
-
/** {@link PR_LIST_FIELDS} plus the
|
|
121
|
+
/** {@link PR_LIST_FIELDS} plus the costly fields, for a single PR. */
|
|
107
122
|
export const PR_VIEW_FIELDS = [
|
|
108
123
|
...PR_LIST_FIELDS,
|
|
109
124
|
'body',
|
|
@@ -125,8 +140,8 @@ function oneOrNull(r) {
|
|
|
125
140
|
return parsePullRequest(data);
|
|
126
141
|
}
|
|
127
142
|
/**
|
|
128
|
-
* Parse a `--json` payload that is an array. Null
|
|
129
|
-
* failed
|
|
143
|
+
* Parse a `--json` payload that is an array. Null, not `[]`, when the call
|
|
144
|
+
* failed. A caller must be able to tell "no PRs" from "couldn't ask".
|
|
130
145
|
*/
|
|
131
146
|
function manyOrNull(r) {
|
|
132
147
|
if (r.exitCode !== 0)
|
|
@@ -163,10 +178,9 @@ function limited(rows, limit) {
|
|
|
163
178
|
/** A mutation's result carries no value. */
|
|
164
179
|
function nothing() { }
|
|
165
180
|
/**
|
|
166
|
-
* Parse a `gh label list --json name,color,description` payload. Null
|
|
167
|
-
* `[]
|
|
168
|
-
*
|
|
169
|
-
* never ran must not collapse onto the same value.
|
|
181
|
+
* Parse a `gh label list --json name,color,description` payload. Null, not
|
|
182
|
+
* `[]`, when the call failed, as in {@link manyOrNull}. A repo with no labels
|
|
183
|
+
* and a `gh` call that never ran must not give the same value.
|
|
170
184
|
*/
|
|
171
185
|
function manyLabelsOrNull(r) {
|
|
172
186
|
if (r.exitCode !== 0)
|
|
@@ -182,7 +196,7 @@ function manyLabelsOrNull(r) {
|
|
|
182
196
|
return null;
|
|
183
197
|
return parseLabels(data);
|
|
184
198
|
}
|
|
185
|
-
/** The `gh issue list`/`view --json` field set
|
|
199
|
+
/** The `gh issue list`/`view --json` field set an {@link IssueRow} reads. */
|
|
186
200
|
const ISSUE_FIELDS = 'number,title,body,labels,state';
|
|
187
201
|
/** Parse one `gh issue list`/`view --json` row into an {@link IssueRow}. */
|
|
188
202
|
function parseIssueRow(data) {
|
|
@@ -214,9 +228,8 @@ function oneIssueOrNull(r) {
|
|
|
214
228
|
return parseIssueRow(data);
|
|
215
229
|
}
|
|
216
230
|
/**
|
|
217
|
-
* Parse a `gh issue list --json` payload that is an array. Null
|
|
218
|
-
* on failure,
|
|
219
|
-
* makes for PRs.
|
|
231
|
+
* Parse a `gh issue list --json` payload that is an array. Null, not `[]`,
|
|
232
|
+
* on failure, as {@link manyOrNull} does for PRs.
|
|
220
233
|
*/
|
|
221
234
|
function manyIssuesOrNull(r) {
|
|
222
235
|
if (r.exitCode !== 0)
|
|
@@ -238,7 +251,7 @@ function manyIssuesOrNull(r) {
|
|
|
238
251
|
}
|
|
239
252
|
return rows;
|
|
240
253
|
}
|
|
241
|
-
/** `gh pr list
|
|
254
|
+
/** `gh pr list …`: open PRs by default. `opts.repo` overrides the target's repo. */
|
|
242
255
|
export function prList(opts = {}, target) {
|
|
243
256
|
const limit = opts.limit ?? 100;
|
|
244
257
|
checkLimit(limit);
|
|
@@ -251,9 +264,12 @@ export function prList(opts = {}, target) {
|
|
|
251
264
|
argv.push('--search', opts.search);
|
|
252
265
|
if (opts.author !== undefined)
|
|
253
266
|
argv.push('--author', opts.author);
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
const asked = opts.
|
|
267
|
+
// A row without `number` does not parse and is dropped, so always ask for it.
|
|
268
|
+
// The exact-head filter below also reads `headRefName`.
|
|
269
|
+
const asked = withFields(opts.fields ?? PR_LIST_FIELDS, [
|
|
270
|
+
'number',
|
|
271
|
+
...(opts.head === undefined ? [] : ['headRefName']),
|
|
272
|
+
]);
|
|
257
273
|
argv.push('--json', asked.join(','));
|
|
258
274
|
argv.push('--limit', String(limit + 1));
|
|
259
275
|
const head = opts.head;
|
|
@@ -283,7 +299,7 @@ function notAFlag(what, value) {
|
|
|
283
299
|
}
|
|
284
300
|
}
|
|
285
301
|
/**
|
|
286
|
-
* `gh pr view <number|branch|url> --json
|
|
302
|
+
* `gh pr view <number|branch|url> --json …`: one PR, with the full field set
|
|
287
303
|
* by default.
|
|
288
304
|
*
|
|
289
305
|
* A branch is the PR whose head is that branch, as gh picks it. A URL names
|
|
@@ -315,15 +331,15 @@ export function prView(ref, fields = PR_VIEW_FIELDS, target) {
|
|
|
315
331
|
/**
|
|
316
332
|
* Open PRs whose head is exactly `branch`.
|
|
317
333
|
*
|
|
318
|
-
* The
|
|
319
|
-
*
|
|
334
|
+
* The usual way to ask "which PR is this branch?". It uses the smallest field
|
|
335
|
+
* set and a small limit. More than one row comes back only when forks
|
|
320
336
|
* share the branch name.
|
|
321
337
|
*/
|
|
322
338
|
export function openPrsForBranch(branch, limit = 5, target) {
|
|
323
339
|
return prList({ head: branch, limit, fields: PR_REF_FIELDS }, target);
|
|
324
340
|
}
|
|
325
341
|
/**
|
|
326
|
-
* `gh pr view <n> --json body -q .body
|
|
342
|
+
* `gh pr view <n> --json body -q .body`: just the PR body. Null when gh
|
|
327
343
|
* failed. gh's `-q` output ends with a newline that is not part of the body,
|
|
328
344
|
* so every trailing `\r` and `\n` is trimmed (including any that were part of
|
|
329
345
|
* the body, such as the `\r\n` of a body saved with CRLF line endings).
|
|
@@ -337,10 +353,10 @@ export function prBody(number, target) {
|
|
|
337
353
|
};
|
|
338
354
|
}
|
|
339
355
|
/**
|
|
340
|
-
* `gh api [-i] [-H If-None-Match: …] <endpoint
|
|
356
|
+
* `gh api [-i] [-H If-None-Match: …] <endpoint>`: a GET against the REST API. `{owner}` and
|
|
341
357
|
* `{repo}` placeholders in the endpoint are filled from the target's repo, or by gh from the
|
|
342
358
|
* cwd's repo when the target names none.
|
|
343
|
-
* Unchecked
|
|
359
|
+
* Unchecked. Failure is a value (`ok: false`), so a caller can tell a 304
|
|
344
360
|
* from an outage.
|
|
345
361
|
*/
|
|
346
362
|
export function api(endpoint, opts = {}, target) {
|
|
@@ -377,16 +393,16 @@ function flattenPages(body) {
|
|
|
377
393
|
return body.flat();
|
|
378
394
|
}
|
|
379
395
|
/**
|
|
380
|
-
* `gh api graphql -f query=… [--paginate --slurp]
|
|
396
|
+
* `gh api graphql -f query=… [--paginate --slurp]`: a GraphQL query.
|
|
381
397
|
*
|
|
382
|
-
* Returns each page's `data`, one entry per page (one without `paginate`)
|
|
383
|
-
*
|
|
384
|
-
* carries `errors` or lacks `data
|
|
398
|
+
* Returns each page's `data`, one entry per page (one without `paginate`).
|
|
399
|
+
* Returns null when the call failed, the output did not parse, or any page
|
|
400
|
+
* carries `errors` or lacks `data`. A partial answer counts as a failure. Read one
|
|
385
401
|
* connection across the pages with {@link nodesAt}.
|
|
386
402
|
*
|
|
387
403
|
* A string variable of exactly `{owner}` or `{repo}` is filled from the
|
|
388
404
|
* target's repo, or by gh from the clone. A mutation document throws
|
|
389
|
-
* `RangeError
|
|
405
|
+
* `RangeError`. This is a read, and changes go through `raw`.
|
|
390
406
|
*
|
|
391
407
|
* @experimental
|
|
392
408
|
*/
|
|
@@ -455,8 +471,8 @@ export function graphql(query, variables = {}, opts = {}, target) {
|
|
|
455
471
|
};
|
|
456
472
|
}
|
|
457
473
|
/**
|
|
458
|
-
* The `nodes` of one connection, joined across {@link graphql} pages
|
|
459
|
-
* `path` into each page and
|
|
474
|
+
* The `nodes` of one connection, joined across {@link graphql} pages. It
|
|
475
|
+
* follows `path` into each page and takes its `nodes` array. Null when `pages` is
|
|
460
476
|
* null, or any page lacks the path or a `nodes` array, so a page that came
|
|
461
477
|
* back short is never read as fewer rows.
|
|
462
478
|
*
|
|
@@ -482,7 +498,7 @@ export function nodesAt(pages, path) {
|
|
|
482
498
|
}
|
|
483
499
|
return out;
|
|
484
500
|
}
|
|
485
|
-
/**
|
|
501
|
+
/** Split one header block off the front of `text`: the block, and what follows its blank line. */
|
|
486
502
|
function takeHeaderBlock(text) {
|
|
487
503
|
const crlf = text.indexOf('\r\n\r\n');
|
|
488
504
|
const lf = text.indexOf('\n\n');
|
|
@@ -495,7 +511,7 @@ function takeHeaderBlock(text) {
|
|
|
495
511
|
const STATUS_LINE = /^HTTP\/[\d.]+\s+\d{3}/i;
|
|
496
512
|
/**
|
|
497
513
|
* Split a `gh api -i` payload (status line + headers, blank line, body) and decode it.
|
|
498
|
-
* A real `gh` prints one block.
|
|
514
|
+
* A real `gh` prints one block. If there are several, the last one is read:
|
|
499
515
|
* its status and ETag, and the body that follows it.
|
|
500
516
|
*/
|
|
501
517
|
function parseApi(r, withHeaders) {
|
|
@@ -516,7 +532,7 @@ function parseApi(r, withHeaders) {
|
|
|
516
532
|
}
|
|
517
533
|
let body = null;
|
|
518
534
|
let malformed = false;
|
|
519
|
-
if (
|
|
535
|
+
if (bodyText.trim() !== '') {
|
|
520
536
|
try {
|
|
521
537
|
body = JSON.parse(bodyText);
|
|
522
538
|
}
|
|
@@ -525,17 +541,21 @@ function parseApi(r, withHeaders) {
|
|
|
525
541
|
}
|
|
526
542
|
}
|
|
527
543
|
const ok = r.exitCode === 0 && status >= 200 && status < 400;
|
|
544
|
+
const rawMessage = body !== null && typeof body === 'object' && !Array.isArray(body)
|
|
545
|
+
? body['message']
|
|
546
|
+
: undefined;
|
|
528
547
|
return {
|
|
529
548
|
ok,
|
|
530
549
|
status,
|
|
531
550
|
etag,
|
|
532
551
|
body,
|
|
552
|
+
message: typeof rawMessage === 'string' ? rawMessage : null,
|
|
533
553
|
malformed,
|
|
534
554
|
stderr: r.stderr.trim(),
|
|
535
555
|
...(r.error === undefined ? {} : { error: r.error }),
|
|
536
556
|
};
|
|
537
557
|
}
|
|
538
|
-
/** `gh --version
|
|
558
|
+
/** `gh --version`: checks that `gh` is installed. True when `gh` runs. */
|
|
539
559
|
export function installed() {
|
|
540
560
|
return {
|
|
541
561
|
argv: ['--version'],
|
|
@@ -545,7 +565,7 @@ export function installed() {
|
|
|
545
565
|
parse: (r) => r.exitCode === 0,
|
|
546
566
|
};
|
|
547
567
|
}
|
|
548
|
-
/** `gh auth status [--hostname <host>]
|
|
568
|
+
/** `gh auth status [--hostname <host>]`: checks credentials, for one host when the target names it. */
|
|
549
569
|
export function authStatus(target) {
|
|
550
570
|
return {
|
|
551
571
|
argv: ['auth', 'status', ...hostArgs(target)],
|
|
@@ -556,8 +576,8 @@ export function authStatus(target) {
|
|
|
556
576
|
};
|
|
557
577
|
}
|
|
558
578
|
/**
|
|
559
|
-
* `gh label list --json name,color,description
|
|
560
|
-
* up to `limit` (default 1000
|
|
579
|
+
* `gh label list --json name,color,description`: every label on the repo,
|
|
580
|
+
* up to `limit` (default 1000. `gh`'s own default is 30).
|
|
561
581
|
*/
|
|
562
582
|
export function labelList(opts = {}, target) {
|
|
563
583
|
const limit = opts.limit ?? 1000;
|
|
@@ -577,7 +597,7 @@ export function labelList(opts = {}, target) {
|
|
|
577
597
|
parse: (r) => limited(manyLabelsOrNull(r), limit),
|
|
578
598
|
};
|
|
579
599
|
}
|
|
580
|
-
/** `gh issue list --json
|
|
600
|
+
/** `gh issue list --json …`: open issues, optionally by label. */
|
|
581
601
|
export function issueList(opts = {}, target) {
|
|
582
602
|
const limit = opts.limit ?? 200;
|
|
583
603
|
checkLimit(limit);
|
|
@@ -587,7 +607,7 @@ export function issueList(opts = {}, target) {
|
|
|
587
607
|
argv.push('--json', ISSUE_FIELDS, '--limit', String(limit + 1));
|
|
588
608
|
return { argv, checked: false, readOnly: true, parse: (r) => limited(manyIssuesOrNull(r), limit) };
|
|
589
609
|
}
|
|
590
|
-
/** `gh issue view <n> --json
|
|
610
|
+
/** `gh issue view <n> --json …`: one issue, same field set as {@link issueList}. */
|
|
591
611
|
export function issueView(number, target) {
|
|
592
612
|
return {
|
|
593
613
|
argv: ['issue', 'view', String(number), ...repoArgs(target), '--json', ISSUE_FIELDS],
|
|
@@ -636,8 +656,8 @@ function rowsOrNull(r, parse) {
|
|
|
636
656
|
return data.map(parse).filter((row) => row !== null);
|
|
637
657
|
}
|
|
638
658
|
/**
|
|
639
|
-
* `gh repo list <owner> --json
|
|
640
|
-
* from the target (`GH_HOST`)
|
|
659
|
+
* `gh repo list <owner> --json …`: an owner's repositories. The host comes
|
|
660
|
+
* from the target (`GH_HOST`). The target's repo is not used.
|
|
641
661
|
*/
|
|
642
662
|
export function repoList(owner, opts = {}, target) {
|
|
643
663
|
notAFlag('repoList owner', owner);
|
|
@@ -661,7 +681,7 @@ export function repoList(owner, opts = {}, target) {
|
|
|
661
681
|
};
|
|
662
682
|
}
|
|
663
683
|
/**
|
|
664
|
-
* `gh repo view [<[host/]owner/name>] --json
|
|
684
|
+
* `gh repo view [<[host/]owner/name>] --json …`: one repository. That is `repo`,
|
|
665
685
|
* else the target's repo, else the clone at the client's cwd.
|
|
666
686
|
*/
|
|
667
687
|
export function repoView(repo, fields = REPO_FIELDS, target) {
|
|
@@ -692,9 +712,9 @@ export function repoView(repo, fields = REPO_FIELDS, target) {
|
|
|
692
712
|
};
|
|
693
713
|
}
|
|
694
714
|
/**
|
|
695
|
-
* `gh search prs [flags] -- <query
|
|
715
|
+
* `gh search prs [flags] -- <query>`: pull requests across repositories,
|
|
696
716
|
* with GitHub's search syntax in `query` (it may start with `-`, as in
|
|
697
|
-
* `-label:bug`). The host comes from the target (`GH_HOST`)
|
|
717
|
+
* `-label:bug`). The host comes from the target (`GH_HOST`). The target's
|
|
698
718
|
* repo is not used, so pass `opts.repo` or `opts.owner` to narrow.
|
|
699
719
|
*/
|
|
700
720
|
export function searchPrs(query, opts = {}, target) {
|
|
@@ -748,7 +768,7 @@ export function createPr(opts, target) {
|
|
|
748
768
|
},
|
|
749
769
|
};
|
|
750
770
|
}
|
|
751
|
-
/** `gh pr edit <n
|
|
771
|
+
/** `gh pr edit <n>`: change the title or the body. */
|
|
752
772
|
export function editPr(number, opts, target) {
|
|
753
773
|
const argv = ['pr', 'edit', String(number), ...repoArgs(target)];
|
|
754
774
|
if (opts.title !== undefined)
|
|
@@ -757,18 +777,18 @@ export function editPr(number, opts, target) {
|
|
|
757
777
|
argv.push('--body', opts.body);
|
|
758
778
|
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
759
779
|
}
|
|
760
|
-
/** `gh pr edit <n> --body
|
|
780
|
+
/** `gh pr edit <n> --body`: rewrite just the body. */
|
|
761
781
|
export function editPrBody(number, body, target) {
|
|
762
782
|
return editPr(number, { body }, target);
|
|
763
783
|
}
|
|
764
|
-
/** `gh pr update-branch <n> [--rebase]
|
|
784
|
+
/** `gh pr update-branch <n> [--rebase]`: bring the head up to date with its base. */
|
|
765
785
|
export function prUpdateBranch(number, style = 'merge', target) {
|
|
766
786
|
const argv = ['pr', 'update-branch', String(number), ...repoArgs(target)];
|
|
767
787
|
if (style === 'rebase')
|
|
768
788
|
argv.push('--rebase');
|
|
769
789
|
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
770
790
|
}
|
|
771
|
-
/** `gh pr ready <n> [--undo]
|
|
791
|
+
/** `gh pr ready <n> [--undo]`: mark a draft PR ready for review, or (with `draft`) convert it back to a draft. */
|
|
772
792
|
export function prSetDraft(number, draft, target) {
|
|
773
793
|
const argv = ['pr', 'ready', String(number), ...repoArgs(target)];
|
|
774
794
|
if (draft)
|
|
@@ -776,13 +796,13 @@ export function prSetDraft(number, draft, target) {
|
|
|
776
796
|
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
777
797
|
}
|
|
778
798
|
/**
|
|
779
|
-
* `gh api -X PATCH repos/{owner}/{repo}/issues/comments/<id> -f body
|
|
799
|
+
* `gh api -X PATCH repos/{owner}/{repo}/issues/comments/<id> -f body=…`: rewrite one
|
|
780
800
|
* conversation comment.
|
|
781
801
|
*
|
|
782
|
-
* `commentId` is the comment's numeric REST id, as text
|
|
802
|
+
* `commentId` is the comment's numeric REST id, as text. That is the `id` that
|
|
783
803
|
* `gh api repos/{owner}/{repo}/issues/<n>/comments` returns. It is not the
|
|
784
804
|
* GraphQL node id (such as `IC_kw…`) that `gh pr view --json comments` gives
|
|
785
|
-
* in `Comment.id
|
|
805
|
+
* in `Comment.id`. The REST path answers 404 for a node id.
|
|
786
806
|
*
|
|
787
807
|
* Throws `RangeError` when `commentId` is not all digits, so a node id fails
|
|
788
808
|
* at once and no text can change which endpoint the PATCH reaches.
|
|
@@ -810,10 +830,10 @@ export function issueCommentEdit(commentId, body, target) {
|
|
|
810
830
|
}
|
|
811
831
|
/**
|
|
812
832
|
* `gh pr merge <n> --<method> --match-head-commit <sha>`. The head sha is
|
|
813
|
-
* required
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
*
|
|
833
|
+
* required. GitHub refuses the merge when the PR head moved since the caller
|
|
834
|
+
* checked it. So a push that lands between the check and the merge cannot be
|
|
835
|
+
* merged without review. The branch is left alone (no `--delete-branch`).
|
|
836
|
+
* Deleting it is the caller's job.
|
|
817
837
|
*/
|
|
818
838
|
export function prMerge(number, opts, target) {
|
|
819
839
|
const argv = [
|
|
@@ -830,14 +850,13 @@ export function prMerge(number, opts, target) {
|
|
|
830
850
|
const REVIEW_THREADS_QUERY = 'query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){reviewThreads(first:100){pageInfo{hasNextPage} nodes{isResolved isOutdated comments(last:1){nodes{author{login}}}}}}}}';
|
|
831
851
|
/**
|
|
832
852
|
* A PR's review threads over GraphQL (`gh pr view --json` has no thread
|
|
833
|
-
* field). Null
|
|
834
|
-
* expected shape, or more than 100 threads exist
|
|
835
|
-
* PR is clear to merge must
|
|
836
|
-
* threads".
|
|
853
|
+
* field). Null, never `[]`, when the call failed, the payload was not the
|
|
854
|
+
* expected shape, or more than 100 threads exist. A caller deciding whether a
|
|
855
|
+
* PR is clear to merge must tell "could not tell" from "no threads".
|
|
837
856
|
*/
|
|
838
857
|
export function prReviewThreads(number, target) {
|
|
839
858
|
checkTarget(target);
|
|
840
|
-
// `-F` lets gh fill the placeholders from the clone
|
|
859
|
+
// `-F` lets gh fill the placeholders from the clone. Real values go as plain strings.
|
|
841
860
|
const [owner, name] = target?.repo?.split('/') ?? [];
|
|
842
861
|
const repo = owner === undefined || name === undefined
|
|
843
862
|
? ['-F', 'owner={owner}', '-F', 'name={repo}']
|
|
@@ -883,7 +902,7 @@ export function prReviewThreads(number, target) {
|
|
|
883
902
|
}
|
|
884
903
|
/**
|
|
885
904
|
* `gh label create <name> --color <hex> --description <text> --force`.
|
|
886
|
-
* `--force` makes it safe to repeat
|
|
905
|
+
* `--force` makes it safe to repeat. When the label already exists, its color
|
|
887
906
|
* and description are set to these instead of failing.
|
|
888
907
|
*/
|
|
889
908
|
export function createLabel(opts, target) {
|
|
@@ -900,7 +919,7 @@ export function createLabel(opts, target) {
|
|
|
900
919
|
];
|
|
901
920
|
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
902
921
|
}
|
|
903
|
-
/** `gh label edit <name
|
|
922
|
+
/** `gh label edit <name>`: change the color or description. A flag not given is left out. */
|
|
904
923
|
export function editLabel(name, opts, target) {
|
|
905
924
|
const argv = ['label', 'edit', name, ...repoArgs(target)];
|
|
906
925
|
if (opts.color !== undefined)
|
|
@@ -910,21 +929,18 @@ export function editLabel(name, opts, target) {
|
|
|
910
929
|
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
911
930
|
}
|
|
912
931
|
/**
|
|
913
|
-
* `gh issue edit <n> [--remove-label <current>] --add-label <new
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
* label immediately before calling this. `currentLabel` is `null` when the
|
|
926
|
-
* issue carries no prior label to remove — the `--remove-label` flag is then
|
|
927
|
-
* omitted rather than passed empty.
|
|
932
|
+
* `gh issue edit <n> [--remove-label <current>] --add-label <new>`: swap one
|
|
933
|
+
* label for another on an issue in one call. The issue is never left with
|
|
934
|
+
* neither label or both labels between the remove and the add.
|
|
935
|
+
*
|
|
936
|
+
* That covers only the call itself. `currentLabel` is whatever the caller
|
|
937
|
+
* read earlier, perhaps from a `gh issue list` minutes before. It is not read
|
|
938
|
+
* again here. If someone relabeled the issue since then, `--remove-label
|
|
939
|
+
* <currentLabel>` names a label the issue no longer has. `gh` skips that
|
|
940
|
+
* removal with no error and still adds `newLabel`. The issue then has both
|
|
941
|
+
* its current label and `newLabel`. A caller that cares reads the label again
|
|
942
|
+
* just before calling this. Pass `null` for `currentLabel` when there is no
|
|
943
|
+
* label to remove. The `--remove-label` flag is then left out.
|
|
928
944
|
*/
|
|
929
945
|
export function setIssueLabel(number, currentLabel, newLabel, target) {
|
|
930
946
|
const argv = ['issue', 'edit', String(number), ...repoArgs(target)];
|
|
@@ -933,7 +949,7 @@ export function setIssueLabel(number, currentLabel, newLabel, target) {
|
|
|
933
949
|
argv.push('--add-label', newLabel);
|
|
934
950
|
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
935
951
|
}
|
|
936
|
-
/** `gh pr diff <n
|
|
952
|
+
/** `gh pr diff <n>`: the PR's unified diff, as text. Checked, so an empty diff is a value and a failed call throws. */
|
|
937
953
|
export function prDiff(number, target) {
|
|
938
954
|
return {
|
|
939
955
|
argv: ['pr', 'diff', String(number), ...repoArgs(target)],
|
|
@@ -942,7 +958,7 @@ export function prDiff(number, target) {
|
|
|
942
958
|
parse: (r) => r.stdout,
|
|
943
959
|
};
|
|
944
960
|
}
|
|
945
|
-
/** `gh pr comment <n> --body <body
|
|
961
|
+
/** `gh pr comment <n> --body <body>`: post a conversation comment, returning the URL gh prints ('' if none). */
|
|
946
962
|
export function prComment(number, body, target) {
|
|
947
963
|
return {
|
|
948
964
|
argv: ['pr', 'comment', String(number), ...repoArgs(target), '--body', body],
|
|
@@ -960,10 +976,10 @@ export function prComment(number, body, target) {
|
|
|
960
976
|
}
|
|
961
977
|
/**
|
|
962
978
|
* Create a PR review with inline comments, returning the review's URL (empty
|
|
963
|
-
* when GitHub's reply carried none). The event is always `COMMENT
|
|
964
|
-
* library does not offer a way to approve or request changes.
|
|
979
|
+
* when GitHub's reply carried none). The event is always `COMMENT`. This
|
|
980
|
+
* library does not offer a way to approve or request changes. It goes through
|
|
965
981
|
* `gh api` with the payload on stdin, because `gh pr review` cannot attach
|
|
966
|
-
*
|
|
982
|
+
* comments to lines. `{owner}`/`{repo}` come from the target's repo, or
|
|
967
983
|
* are filled in by `gh` from the client's working directory.
|
|
968
984
|
*
|
|
969
985
|
* @experimental
|
|
@@ -1034,10 +1050,10 @@ export function branchPrFields(fields) {
|
|
|
1034
1050
|
/**
|
|
1035
1051
|
* True when GitHub has not finished computing this PR's mergeability.
|
|
1036
1052
|
*
|
|
1037
|
-
* The answer
|
|
1038
|
-
*
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1053
|
+
* The first answer may not be final. GitHub starts the merge computation when
|
|
1054
|
+
* asked and answers `UNKNOWN` until it finishes. A caller that treats the
|
|
1055
|
+
* first `UNKNOWN` as final misjudges a PR whose computation is slow. So ask a
|
|
1056
|
+
* few times before trusting it.
|
|
1041
1057
|
*/
|
|
1042
1058
|
export function needsMergeStateRetry(pr) {
|
|
1043
1059
|
return pr.mergeable === 'UNKNOWN' || pr.mergeStateStatus === 'UNKNOWN';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@osdlc/typed-gh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "A typed GitHub client for TypeScript over the gh CLI. A total PullRequest/Review/CheckRun model, pure argv builders and parsers, and a tri-modal client — queries return parsed values, mutations throw, raw() is the escape hatch. Uses gh's own credentials; reads no token.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"author": "sksizer",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"LICENSE-APACHE"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@osdlc/command-seam": "^0.9.
|
|
26
|
-
"@osdlc/typed-git": "^0.
|
|
25
|
+
"@osdlc/command-seam": "^0.9.1",
|
|
26
|
+
"@osdlc/typed-git": "^0.7.0",
|
|
27
27
|
"@osdlc/result": "^0.1.0"
|
|
28
28
|
},
|
|
29
29
|
"engines": {
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"prettier": "3.9.6",
|
|
55
55
|
"typescript": "~6.0.3",
|
|
56
56
|
"vitest": "^5.0.5",
|
|
57
|
-
"@osdlc/
|
|
58
|
-
"@osdlc/
|
|
57
|
+
"@osdlc/tsconfig": "^0.4.1",
|
|
58
|
+
"@osdlc/prettier-config": "^0.3.1"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "tsc -p tsconfig.build.json",
|