@osdlc/typed-gh 0.5.0 → 0.6.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.
- package/CHANGELOG.md +24 -0
- package/README.md +168 -36
- package/async.ts +131 -37
- package/dist/async.d.ts +43 -15
- package/dist/async.d.ts.map +1 -1
- package/dist/async.js +92 -30
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/parse.d.ts +5 -1
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +48 -0
- package/dist/repo.d.ts +42 -10
- package/dist/repo.d.ts.map +1 -1
- package/dist/repo.js +76 -12
- package/dist/sync.d.ts +64 -12
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +107 -29
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/verbs.d.ts +194 -41
- package/dist/verbs.d.ts.map +1 -1
- package/dist/verbs.js +482 -83
- package/index.ts +26 -3
- package/package.json +3 -3
- package/parse.ts +50 -0
- package/repo.ts +87 -13
- package/sync.ts +146 -34
- package/types.ts +40 -0
- package/verbs.ts +606 -81
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d558059: Reach a repository without a clone, find a pull request from what a person typed, read every page of a list, and list repositories and search pull requests. Some changes break callers of 0.5. They are marked `Breaking:`.
|
|
8
|
+
|
|
9
|
+
- `Gh` and `AsyncGh` take optional `repo` (`owner/name`) and `hostname` options, and `withRepo(repo)` returns the same client aimed at another repository. The target goes into each call's argv: `--repo host/owner/name` on `pr`, `issue` and `label` calls, `--hostname` and a filled `{owner}/{repo}` on `api` calls, and `--hostname` on `authStatus`. Every verb builder takes an optional `GhTarget` as its last argument. An invalid `repo` or `hostname` throws `RangeError`.
|
|
10
|
+
- `hostname` is any GitHub host: `github.com`, GitHub Enterprise Server, or a `<tenant>.ghe.com` host.
|
|
11
|
+
- Breaking: `parsePrUrl` accepts any GitHub host and returns `hostname` in its `PullRequestRef`. Before, it returned null for a host other than `github.com`.
|
|
12
|
+
- `prView` takes a number, a branch or a URL. A URL is passed without the client's target. A string that starts with `-` throws `RangeError`.
|
|
13
|
+
- `prFind(input)` returns the one PR that `input` names, and `parsePrRef(input)` reads it without running `gh`: `42`, `#42`, `owner/name#42`, a URL, or a branch name. Bare digits are a number first and then a branch. A branch gives its open PR, else its newest closed or merged one.
|
|
14
|
+
- Breaking: `prList({ head })` matches the head branch exactly, through `gh pr list --head`. Before, it passed `--search head:<branch>`, which matched by prefix.
|
|
15
|
+
- Breaking: `prList`, `issueList` and `labelList` return `{ rows, truncated } | null` instead of an array or null. Each asks `gh` for one row more than `limit`, and `truncated` is true when it came back. A `limit` that is not a positive integer throws `RangeError`. `openPrsForBranch` returns the same shape. The new `ListResult` type names it.
|
|
16
|
+
- `api` takes `paginate: true` to fetch every page; pages of arrays are joined into one `body`. With `etag` or `includeHeaders` it throws `RangeError`. An endpoint that starts with `-` throws `RangeError`.
|
|
17
|
+
- `graphql(query, variables, { paginate })` runs a GraphQL query and returns each page's `data`, or null when the call failed or any page has `errors` or no `data`. It refuses a mutation. `nodesAt(pages, path)` joins one connection's nodes across pages. Both are experimental, with `GraphqlOptions` and `GraphqlVariable`.
|
|
18
|
+
- Every `GhVerb` carries `readOnly`: true for every query and `prDiff`, false for every mutation. `GhVerb` also has an optional `env`.
|
|
19
|
+
- `repoList(owner)` and `repoView(repo?)` return the new `Repository` type. `searchPrs(query)` returns `PrSearchHit` rows, since `gh search prs` has its own field set. `REPO_FIELDS` and `SEARCH_PR_FIELDS` name their fields. `gh repo list` and `gh search` take no host flag, so the host reaches them as `GH_HOST`.
|
|
20
|
+
- New types: `GhTarget`, `GhOptions`, `AsyncGhOptions`, `ListResult`, `PrRef`, `Repository`, `RepositoryVisibility`, `PrSearchHit`, `RepoListOptions` and `SearchPrsOptions`.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [4d13bd9]
|
|
25
|
+
- @osdlc/typed-git@0.5.3
|
|
26
|
+
|
|
3
27
|
## 0.5.0
|
|
4
28
|
|
|
5
29
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ import { Gh, type PrListOptions, type PullRequest } from '@osdlc/typed-gh'
|
|
|
25
25
|
const gh = new Gh(process.cwd())
|
|
26
26
|
|
|
27
27
|
const filter: PrListOptions = { state: 'open', base: 'main' }
|
|
28
|
-
const prs: PullRequest[] = gh.prList(filter) ?? []
|
|
28
|
+
const prs: PullRequest[] = gh.prList(filter)?.rows ?? []
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
The `examples` folder in the repository runs the same calls as tests.
|
|
@@ -36,8 +36,8 @@ The `examples` folder in the repository runs the same calls as tests.
|
|
|
36
36
|
Octokit.
|
|
37
37
|
- **You sign in with a token, a GitHub App or OAuth.** This package reads no token.
|
|
38
38
|
- **You make many calls or poll in a loop.** Each call starts a `gh` process.
|
|
39
|
-
- **You need the whole API typed.** This package covers the calls listed below. `api()` is a GET
|
|
40
|
-
Anything else goes through `raw`.
|
|
39
|
+
- **You need the whole API typed.** This package covers the calls listed below. `api()` is a GET
|
|
40
|
+
and `graphql()` is a query. Anything else goes through `raw`.
|
|
41
41
|
|
|
42
42
|
## How it compares
|
|
43
43
|
|
|
@@ -96,9 +96,79 @@ that decides whether to merge can then match every member.
|
|
|
96
96
|
| **mutations** | throw `CommandFailed`, carrying argv and the full result. |
|
|
97
97
|
| **`raw(argv)`** | hand back the raw `CommandResult`; you decide. |
|
|
98
98
|
|
|
99
|
-
A query returns `null`, never
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
A query returns `null`, never an empty list on failure. `prList()` returning
|
|
100
|
+
`{ rows: [], truncated: false }` means *this repository has no open PRs*.
|
|
101
|
+
`null` means *we could not ask*. If the two were the same, an expired
|
|
102
|
+
credential would read as "nothing to do".
|
|
103
|
+
|
|
104
|
+
## A list says when its limit cut it short
|
|
105
|
+
|
|
106
|
+
`gh --limit` cuts a list without saying so. Every list query here asks for
|
|
107
|
+
one row more than `limit` and returns `{ rows, truncated }`: at most `limit`
|
|
108
|
+
rows, and `truncated: true` when there were more.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const page: ListResult<PullRequest> | null = gh.prList({ limit: 50 })
|
|
112
|
+
|
|
113
|
+
if (page?.truncated) console.warn('more than 50 open PRs; raise the limit')
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`prList`, `issueList`, `labelList`, `repoList` and `searchPrs` all work this
|
|
117
|
+
way. A `limit` that is not a positive integer throws `RangeError`.
|
|
118
|
+
|
|
119
|
+
## A repository without a clone
|
|
120
|
+
|
|
121
|
+
By default a client talks to the repository of the clone at its directory.
|
|
122
|
+
Name a repository, a host, or both, and every call targets them instead, so
|
|
123
|
+
no clone is needed:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const acme = new Gh(process.cwd(), { repo: 'acme/widgets', hostname: 'github.acme.com' })
|
|
127
|
+
|
|
128
|
+
acme.prView(42)
|
|
129
|
+
acme.withRepo('acme/gadgets').prList()
|
|
130
|
+
acme.api('repos/{owner}/{repo}/branches')
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The target goes into the argv, so a `CommandFailed` shows which repository a
|
|
134
|
+
call hit: `pr`, `issue` and `label` calls get `--repo github.acme.com/acme/widgets`,
|
|
135
|
+
`api` calls get `--hostname` and a filled `{owner}/{repo}`, and `authStatus`
|
|
136
|
+
asks about that host. `repoList` and `searchPrs` have no host flag in gh, so
|
|
137
|
+
they get `GH_HOST` in the environment. `withRepo` and `at` keep the runner and
|
|
138
|
+
the rest of the target.
|
|
139
|
+
|
|
140
|
+
`hostname` is any GitHub host: `github.com`, a GitHub Enterprise Server host,
|
|
141
|
+
or a `<tenant>.ghe.com` host. gh speaks only GitHub's API, so Forgejo, Gitea
|
|
142
|
+
and GitLab are out of reach. A `repo` that is not `owner/name`, or a
|
|
143
|
+
`hostname` that is not a bare host, throws `RangeError`.
|
|
144
|
+
|
|
145
|
+
## Finding a pull request
|
|
146
|
+
|
|
147
|
+
`prView` takes a number, a branch or a URL. A URL names its own repository
|
|
148
|
+
and host, so the client's target is not added to it.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
gh.prView(42)
|
|
152
|
+
gh.prView('feature/login')
|
|
153
|
+
gh.prView('https://github.acme.com/acme/widgets/pull/42')
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`prFind` takes what a person typed and runs the lookup it needs:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const pr: PullRequest | null = gh.prFind(process.argv[2] ?? '')
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
It reads the text with `parsePrRef`, which starts no process: `42` and `#42`
|
|
163
|
+
are numbers, `owner/name#42` is a number in that repository, a URL is a URL,
|
|
164
|
+
and any other valid branch name is a branch. Bare digits are tried as a PR
|
|
165
|
+
number first and then as a branch, since a branch can be named `42`. A branch
|
|
166
|
+
gives its open PR, or else its newest closed or merged one, so a tool run
|
|
167
|
+
after the merge still finds it.
|
|
168
|
+
|
|
169
|
+
`prList({ head })` matches the head branch exactly: `task/foo` never returns
|
|
170
|
+
`task/foo-2`. Two forks can share a branch name; ask for
|
|
171
|
+
`headRepositoryOwner` to tell them apart.
|
|
102
172
|
|
|
103
173
|
## Two shape hazards it absorbs
|
|
104
174
|
|
|
@@ -184,10 +254,10 @@ threads?.filter((t) => !t.isResolved)
|
|
|
184
254
|
|
|
185
255
|
```ts
|
|
186
256
|
const issue: IssueRow | null = gh.issueView(7)
|
|
187
|
-
const triage: IssueRow
|
|
257
|
+
const triage: ListResult<IssueRow> | null = gh.issueList({ label: 'status:triage', limit: 50 })
|
|
188
258
|
gh.setIssueLabel(7, 'status:triage', 'status:define')
|
|
189
259
|
|
|
190
|
-
const labels: Label
|
|
260
|
+
const labels: ListResult<Label> | null = gh.labelList({ limit: 100 })
|
|
191
261
|
gh.createLabel({ name: 'status:triage', color: '1f6feb', description: 'Triage stage' })
|
|
192
262
|
gh.editLabel('status:triage', { description: 'Waiting for triage' })
|
|
193
263
|
```
|
|
@@ -253,8 +323,9 @@ await gh.prComment(42, 'rebased') // the comment's URL, or ''
|
|
|
253
323
|
await gh.prUpdateBranch(42, 'rebase')
|
|
254
324
|
```
|
|
255
325
|
|
|
256
|
-
These methods take `AsyncCallOptions`: `raw`, `rawChecked`, `prList`, `prView`, `
|
|
257
|
-
`
|
|
326
|
+
These methods take `AsyncCallOptions`: `raw`, `rawChecked`, `prList`, `prView`, `prFind`,
|
|
327
|
+
`openPrFor`, `prViewSettled`, `prBody`, `api`, `graphql`, `labelList`, `issueList`, `issueView`,
|
|
328
|
+
`repoList`, `repoView` and `searchPrs`. The options are `timeoutMs` and an `AbortSignal` in
|
|
258
329
|
`signal`.
|
|
259
330
|
|
|
260
331
|
`api(endpoint, opts)` (on `Gh` and `AsyncGh`) is a REST GET through `gh api`.
|
|
@@ -263,7 +334,50 @@ It never throws; the result is `{ ok, status, etag, body, malformed, stderr, err
|
|
|
263
334
|
was stopped (a missing binary, a timeout).
|
|
264
335
|
Pass `etag` for a conditional request (`If-None-Match`; a 304 comes back as
|
|
265
336
|
`status: 304, body: null`), or `includeHeaders: true` to read status and ETag
|
|
266
|
-
without one. If `gh` prints more than one header block, the last one is read. `{owner}` and
|
|
337
|
+
without one. If `gh` prints more than one header block, the last one is read. `{owner}` and
|
|
338
|
+
`{repo}` in the endpoint come from the client's repo, or are filled in by gh from the clone.
|
|
339
|
+
|
|
340
|
+
## Reading every page
|
|
341
|
+
|
|
342
|
+
`api` with `paginate: true` fetches every page and, when each page is an array, joins them into one
|
|
343
|
+
`body`:
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
const branches = gh.api('repos/{owner}/{repo}/branches', { paginate: true }).body
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
`graphql` (experimental) runs a query and returns each page's `data`. It returns `null` when the
|
|
350
|
+
call failed or any page has `errors` or no `data`: a partial answer is no answer. `nodesAt` joins
|
|
351
|
+
one connection's `nodes` across the pages, and is `null` when any page lacks them.
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
const query = `query($owner: String!, $name: String!, $endCursor: String) {
|
|
355
|
+
repository(owner: $owner, name: $name) {
|
|
356
|
+
pullRequests(first: 100, after: $endCursor) {
|
|
357
|
+
pageInfo { hasNextPage endCursor }
|
|
358
|
+
nodes { number title }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}`
|
|
362
|
+
const pages = gh.graphql(query, { owner: '{owner}', name: '{repo}' }, { paginate: true })
|
|
363
|
+
const prs = nodesAt(pages, ['repository', 'pullRequests'])
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
With `paginate`, the query must take `$endCursor` and page one connection; gh follows only that
|
|
367
|
+
one. `graphql` refuses a mutation with `RangeError`; send one through `raw`.
|
|
368
|
+
|
|
369
|
+
## Repositories and search
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
const repos: ListResult<Repository> | null = gh.repoList('acme', { archived: false, limit: 200 })
|
|
373
|
+
const repo: Repository | null = gh.repoView('acme/widgets')
|
|
374
|
+
const hits: ListResult<PrSearchHit> | null = gh.searchPrs('TICKET-123', { owner: 'acme' })
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
A `Repository` carries `nameWithOwner`, `owner`, `name`, `description`, `defaultBranch`,
|
|
378
|
+
`visibility`, `isArchived` and `url`. `gh search prs` has its own field set: a `PrSearchHit` names
|
|
379
|
+
its `repository` but has no head or base branch, so it is not a `PullRequest`. Its `state` is
|
|
380
|
+
upper-cased to match. The query takes GitHub's search syntax and may start with `-`.
|
|
267
381
|
|
|
268
382
|
## Injecting the subprocess seam
|
|
269
383
|
|
|
@@ -274,7 +388,7 @@ suite runs offline:
|
|
|
274
388
|
```ts
|
|
275
389
|
const empty: CommandRunner = { run: () => ({ exitCode: 0, stdout: '[]', stderr: '' }) }
|
|
276
390
|
|
|
277
|
-
new Gh('/repo', { runner: empty }).prList() // []
|
|
391
|
+
new Gh('/repo', { runner: empty }).prList() // { rows: [], truncated: false }
|
|
278
392
|
```
|
|
279
393
|
|
|
280
394
|
This package re-exports `CommandFailed` and the runner and result types (`CommandRunner`,
|
|
@@ -289,42 +403,52 @@ experimental.
|
|
|
289
403
|
|
|
290
404
|
### Clients
|
|
291
405
|
|
|
292
|
-
- `Gh`: the synchronous client, bound to one working directory
|
|
406
|
+
- `Gh`: the synchronous client, bound to one working directory, and optionally a repository and
|
|
407
|
+
host.
|
|
293
408
|
- `AsyncGh`: the awaitable client, with the same methods.
|
|
294
|
-
- `
|
|
409
|
+
- `GhOptions` and `AsyncGhOptions`: the runner, `repo` and `hostname` a client is built with.
|
|
410
|
+
- `AsyncCallOptions`: `timeoutMs` and `signal`, accepted by sixteen methods of `AsyncGh`.
|
|
295
411
|
- `defaultRunnerFactory` and `RunnerFactory`: how a client makes its default runner for `gh`.
|
|
296
412
|
|
|
297
413
|
Methods on both clients:
|
|
298
414
|
|
|
299
415
|
- Escape hatches: `raw` runs any `gh` call and returns the result. `rawChecked` throws on a failed
|
|
300
|
-
exit. `at` returns the same client rooted at another directory
|
|
301
|
-
|
|
302
|
-
|
|
416
|
+
exit. `at` returns the same client rooted at another directory, and `withRepo` the same client
|
|
417
|
+
aimed at another repository.
|
|
418
|
+
- Pull request queries: `prList`, `prView`, `prFind`, `openPrFor`, `prViewSettled`, `prBody`,
|
|
419
|
+
`prReviewThreads` and `searchPrs`.
|
|
303
420
|
- Pull request changes: `createPr`, `editPr`, `editPrBody`, `prComment`, `prUpdateBranch`,
|
|
304
421
|
`prSetDraft`, `prMerge`, `prDiff` and `createReview`.
|
|
305
422
|
- Issue and label queries: `issueList`, `issueView` and `labelList`.
|
|
423
|
+
- Repository queries: `repoList` and `repoView`.
|
|
306
424
|
- Issue and label changes: `setIssueLabel`, `createLabel`, `editLabel` and `issueCommentEdit`.
|
|
307
|
-
- REST: `api` is a GET through `gh api`
|
|
425
|
+
- REST and GraphQL: `api` is a GET through `gh api` and `graphql` a query; neither throws.
|
|
308
426
|
- Setup checks: `installed`, `authenticated`, `authStatus` and `ready`.
|
|
309
427
|
- `ownerRepo` reads the owner and repository from a remote URL. It starts no process.
|
|
310
428
|
|
|
311
429
|
### Verbs
|
|
312
430
|
|
|
313
|
-
A verb is plain data: argv, a `checked` flag,
|
|
314
|
-
|
|
315
|
-
|
|
431
|
+
A verb is plain data: argv, a `checked` flag, a `readOnly` flag, optional stdin and environment,
|
|
432
|
+
and a `parse` function. `readOnly` is true for every query and `prDiff`, and false for every
|
|
433
|
+
mutation, so a caller that keeps an allowlist of safe calls can check a verb before running it.
|
|
434
|
+
Each verb takes an optional `GhTarget` last. Each client method has a verb of the same name, except
|
|
435
|
+
the escape hatches, `at`, `withRepo`, `prViewSettled`, `openPrFor`, `prFind`, `authenticated`,
|
|
436
|
+
`ready` and `ownerRepo`. `openPrsForBranch` is a verb with no method.
|
|
316
437
|
|
|
317
|
-
- `GhVerb`: the shape of a verb.
|
|
438
|
+
- `GhVerb`: the shape of a verb. `GhTarget`: the `repo` and `hostname` a verb talks to.
|
|
318
439
|
- Verb builders: `prList`, `prView`, `prBody`, `openPrsForBranch`, `prReviewThreads`, `prDiff`,
|
|
319
440
|
`createPr`, `editPr`, `editPrBody`, `prComment`, `prUpdateBranch`, `prSetDraft`, `prMerge`,
|
|
320
441
|
`createReview`, `issueList`, `issueView`, `labelList`, `setIssueLabel`, `createLabel`,
|
|
321
|
-
`editLabel`, `issueCommentEdit`, `
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
-
|
|
327
|
-
`
|
|
442
|
+
`editLabel`, `issueCommentEdit`, `repoList`, `repoView`, `searchPrs`, `api`, `graphql`,
|
|
443
|
+
`installed` and `authStatus`.
|
|
444
|
+
- `pickPrForBranch`: the first row whose head branch is exactly the one you name, for rows that
|
|
445
|
+
came from somewhere other than `prList({ head })`, such as a `head:` search.
|
|
446
|
+
- `nodesAt`: one connection's nodes, joined across `graphql` pages.
|
|
447
|
+
- Field lists: `PR_REF_FIELDS` (enough to identify a PR), `PR_LIST_FIELDS` (for triage),
|
|
448
|
+
`PR_VIEW_FIELDS` (the full set), `REPO_FIELDS` and `SEARCH_PR_FIELDS`.
|
|
449
|
+
- Option and result types: `ListResult`, `PrListOptions`, `IssueListOptions`, `RepoListOptions`,
|
|
450
|
+
`SearchPrsOptions`, `CreatePrOptions`, `CreateReviewOptions`, `ReviewComment`,
|
|
451
|
+
`CreateLabelOptions`, `ApiOptions`, `ApiResponse`, `GraphqlOptions`, `GraphqlVariable`,
|
|
328
452
|
`AuthStatus`, `PrMergeMethod` and `ReviewThread`.
|
|
329
453
|
|
|
330
454
|
### Parsers
|
|
@@ -334,14 +458,18 @@ method has a verb of the same name, except the escape hatches, `at`, `prViewSett
|
|
|
334
458
|
- `normalizeAuthorLogin`: drops a `[bot]` suffix from a login.
|
|
335
459
|
- `parseOwnerRepo`: reads a remote URL into `{ owner, repo }`, or `null`.
|
|
336
460
|
- `ownerRepoSlug`: writes `{ owner, repo }` as `owner/repo`.
|
|
337
|
-
- `parsePrUrl`: reads a pull request URL into its owner, repository and
|
|
461
|
+
- `parsePrUrl`: reads a pull request URL on any GitHub host into its host, owner, repository and
|
|
462
|
+
number, or `null`.
|
|
463
|
+
- `parsePrRef`: reads what a person typed to name a pull request into a `PrRef`, or `null`.
|
|
338
464
|
|
|
339
465
|
### Types
|
|
340
466
|
|
|
341
|
-
- Model: `PullRequest`, `Review`, `Comment`, `CheckRun`, `Actor`, `Label
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
467
|
+
- Model: `PullRequest`, `Review`, `Comment`, `CheckRun`, `Actor`, `Label`, `IssueRow`,
|
|
468
|
+
`Repository` and `PrSearchHit`.
|
|
469
|
+
- Enums: `PullRequestState`, `MergeableState`, `MergeStateStatus`, `ReviewDecision`, `ReviewState`,
|
|
470
|
+
`CheckConclusion` and `RepositoryVisibility`.
|
|
471
|
+
- `OwnerRepo`, `PullRequestRef` and `PrRef`: a repository, a pull request in one, and what a person
|
|
472
|
+
typed to name a pull request.
|
|
345
473
|
|
|
346
474
|
### From the command seam
|
|
347
475
|
|
|
@@ -364,6 +492,10 @@ The experimental items:
|
|
|
364
492
|
- `createReview`, the verb and the method on both clients
|
|
365
493
|
- `CreateReviewOptions`
|
|
366
494
|
- `ReviewComment`
|
|
495
|
+
- `graphql`, the verb and the method on both clients
|
|
496
|
+
- `nodesAt`
|
|
497
|
+
- `GraphqlOptions`
|
|
498
|
+
- `GraphqlVariable`
|
|
367
499
|
|
|
368
500
|
Every other export and method is stable, `prDiff` included.
|
|
369
501
|
|
|
@@ -376,8 +508,8 @@ shell, so nothing in it is specific to one shell.
|
|
|
376
508
|
|
|
377
509
|
- **Closing a PR and deleting a branch.** `prMerge` merges and leaves the branch alone. Closing and
|
|
378
510
|
deleting belong to a caller's own policy layer, where the audit trail lives.
|
|
379
|
-
- **A full REST client.** `api()` is a GET; anything else goes
|
|
380
|
-
`raw(["api", …])`.
|
|
511
|
+
- **A full REST or GraphQL client.** `api()` is a GET and `graphql()` a query; anything else goes
|
|
512
|
+
through `raw(["api", …])`.
|
|
381
513
|
- **Anything that reads a token.**
|
|
382
514
|
|
|
383
515
|
## Licence
|
package/async.ts
CHANGED
|
@@ -16,22 +16,24 @@ import {
|
|
|
16
16
|
type CommandResult,
|
|
17
17
|
} from '@osdlc/command-seam'
|
|
18
18
|
|
|
19
|
-
import { parseOwnerRepo } from './repo.ts'
|
|
20
|
-
import type { IssueRow, Label, OwnerRepo, PullRequest } from './types.ts'
|
|
19
|
+
import { ownerRepoSlug, parseOwnerRepo, parsePrRef } from './repo.ts'
|
|
20
|
+
import type { IssueRow, Label, OwnerRepo, PrSearchHit, PullRequest, Repository } from './types.ts'
|
|
21
21
|
import * as v from './verbs.ts'
|
|
22
22
|
import type {
|
|
23
23
|
ApiOptions,
|
|
24
24
|
ApiResponse,
|
|
25
25
|
AuthStatus,
|
|
26
|
+
GhTarget,
|
|
26
27
|
GhVerb,
|
|
27
28
|
IssueListOptions,
|
|
29
|
+
ListResult,
|
|
28
30
|
PrListOptions,
|
|
29
31
|
} from './verbs.ts'
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
|
-
* Per-call options for {@link AsyncGh}.
|
|
33
|
-
* `prList`, `prView`, `openPrFor`, `prViewSettled`, `prBody`, `api`, `
|
|
34
|
-
* `issueList` and `
|
|
34
|
+
* Per-call options for {@link AsyncGh}. Sixteen methods accept them: `raw`, `rawChecked`,
|
|
35
|
+
* `prList`, `prView`, `prFind`, `openPrFor`, `prViewSettled`, `prBody`, `api`, `graphql`,
|
|
36
|
+
* `labelList`, `issueList`, `issueView`, `repoList`, `repoView` and `searchPrs`.
|
|
35
37
|
*/
|
|
36
38
|
export interface AsyncCallOptions {
|
|
37
39
|
/**
|
|
@@ -47,19 +49,39 @@ export interface AsyncCallOptions {
|
|
|
47
49
|
/** How long to wait between merge-state retries, in milliseconds. */
|
|
48
50
|
const MERGE_STATE_RETRY_DELAY_MS = 1_500
|
|
49
51
|
|
|
50
|
-
/**
|
|
52
|
+
/** What an {@link AsyncGh} is built with: the runner, and the repository and host it talks to. */
|
|
53
|
+
export interface AsyncGhOptions extends GhTarget {
|
|
54
|
+
runner?: AsyncCommandRunner | undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A typed, awaitable gh client bound to one working directory, and optionally
|
|
59
|
+
* to one repository and host. `repo` and `hostname` work as on `Gh`.
|
|
60
|
+
*/
|
|
51
61
|
export class AsyncGh {
|
|
52
62
|
readonly cwd: string
|
|
63
|
+
readonly repo: string | undefined
|
|
64
|
+
readonly hostname: string | undefined
|
|
65
|
+
private readonly target: GhTarget
|
|
53
66
|
private readonly runner: AsyncCommandRunner
|
|
54
67
|
|
|
55
|
-
constructor(cwd: string, opts:
|
|
68
|
+
constructor(cwd: string, opts: AsyncGhOptions = {}) {
|
|
69
|
+
v.checkTarget(opts)
|
|
56
70
|
this.cwd = cwd
|
|
71
|
+
this.repo = opts.repo
|
|
72
|
+
this.hostname = opts.hostname
|
|
73
|
+
this.target = { repo: opts.repo, hostname: opts.hostname }
|
|
57
74
|
this.runner = resolveRunnerAsync('gh', opts.runner)
|
|
58
75
|
}
|
|
59
76
|
|
|
60
|
-
/** The same client rooted at a different directory (same runner). */
|
|
77
|
+
/** The same client rooted at a different directory (same runner, repo and host). */
|
|
61
78
|
at(cwd: string): AsyncGh {
|
|
62
|
-
return new AsyncGh(cwd, { runner: this.runner })
|
|
79
|
+
return new AsyncGh(cwd, { runner: this.runner, repo: this.repo, hostname: this.hostname })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The same client aimed at another repository (same runner, directory and host). */
|
|
83
|
+
withRepo(repo: string): AsyncGh {
|
|
84
|
+
return new AsyncGh(this.cwd, { runner: this.runner, repo, hostname: this.hostname })
|
|
63
85
|
}
|
|
64
86
|
|
|
65
87
|
// ── escape hatches ────────────────────────────────────────────────────────
|
|
@@ -78,6 +100,7 @@ export class AsyncGh {
|
|
|
78
100
|
private async exec<T>(verb: GhVerb<T>, call: AsyncCallOptions = {}): Promise<T> {
|
|
79
101
|
const opts: AsyncCommandRunOptions = { cwd: this.cwd }
|
|
80
102
|
if (verb.input !== undefined) opts.input = verb.input
|
|
103
|
+
if (verb.env !== undefined) opts.env = verb.env
|
|
81
104
|
if (call.timeoutMs !== undefined) opts.timeoutMs = call.timeoutMs
|
|
82
105
|
if (call.signal !== undefined) opts.signal = call.signal
|
|
83
106
|
const result = verb.checked
|
|
@@ -89,16 +112,31 @@ export class AsyncGh {
|
|
|
89
112
|
// ── queries ───────────────────────────────────────────────────────────────
|
|
90
113
|
|
|
91
114
|
/** Pull requests matching `opts`, or null when gh failed. */
|
|
92
|
-
prList(opts: PrListOptions & AsyncCallOptions = {}): Promise<PullRequest
|
|
93
|
-
return this.exec(v.prList(opts), opts)
|
|
115
|
+
prList(opts: PrListOptions & AsyncCallOptions = {}): Promise<ListResult<PullRequest> | null> {
|
|
116
|
+
return this.exec(v.prList(opts, this.target), opts)
|
|
94
117
|
}
|
|
95
118
|
|
|
96
|
-
/** One PR with the heavy fields, or null
|
|
119
|
+
/** One PR by number, branch or URL, with the heavy fields, or null. See `Gh.prView`. */
|
|
97
120
|
prView(
|
|
98
|
-
|
|
121
|
+
ref: number | string,
|
|
99
122
|
opts: { fields?: readonly string[] | undefined } & AsyncCallOptions = {},
|
|
100
123
|
): Promise<PullRequest | null> {
|
|
101
|
-
return this.exec(v.prView(
|
|
124
|
+
return this.exec(v.prView(ref, opts.fields ?? v.PR_VIEW_FIELDS, this.target), opts)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The one PR that `input` names, or null. See `Gh.prFind`. */
|
|
128
|
+
async prFind(
|
|
129
|
+
input: string,
|
|
130
|
+
opts: { fields?: readonly string[] | undefined } & AsyncCallOptions = {},
|
|
131
|
+
): Promise<PullRequest | null> {
|
|
132
|
+
const ref = parsePrRef(input)
|
|
133
|
+
if (ref === null) return null
|
|
134
|
+
const call = { ...opts, fields: opts.fields ?? v.PR_LIST_FIELDS }
|
|
135
|
+
if (ref.kind === 'url') return this.prView(ref.url, call)
|
|
136
|
+
if (ref.kind === 'branch') return prForBranchOn(this, ref.name, call)
|
|
137
|
+
const client = ref.repo === null ? this : this.withRepo(ownerRepoSlug(ref.repo))
|
|
138
|
+
const pr = await client.prView(ref.number, call)
|
|
139
|
+
return pr !== null || ref.orBranch === null ? pr : prForBranchOn(this, ref.orBranch, call)
|
|
102
140
|
}
|
|
103
141
|
|
|
104
142
|
/** The open PR whose head exactly equals `branch`, or null. See `Gh.openPrFor`. */
|
|
@@ -116,7 +154,7 @@ export class AsyncGh {
|
|
|
116
154
|
timeoutMs: opts.timeoutMs,
|
|
117
155
|
signal: opts.signal,
|
|
118
156
|
})
|
|
119
|
-
return v.pickPrForBranch(rows, branch)
|
|
157
|
+
return v.pickPrForBranch(rows?.rows ?? null, branch)
|
|
120
158
|
}
|
|
121
159
|
|
|
122
160
|
/** {@link prView}, re-asked until GitHub settles the merge state. See `Gh.prViewSettled`. */
|
|
@@ -141,27 +179,66 @@ export class AsyncGh {
|
|
|
141
179
|
|
|
142
180
|
/** A PR's body text, or null when gh failed. */
|
|
143
181
|
prBody(number: number, opts: AsyncCallOptions = {}): Promise<string | null> {
|
|
144
|
-
return this.exec(v.prBody(number), opts)
|
|
182
|
+
return this.exec(v.prBody(number, this.target), opts)
|
|
145
183
|
}
|
|
146
184
|
|
|
147
185
|
/** A REST GET through `gh api`; failure is `ok: false`, never a rejection. */
|
|
148
186
|
api(endpoint: string, opts: ApiOptions & AsyncCallOptions = {}): Promise<ApiResponse> {
|
|
149
|
-
return this.exec(v.api(endpoint, opts), opts)
|
|
187
|
+
return this.exec(v.api(endpoint, opts, this.target), opts)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* A GraphQL query; each page's `data`, or null. See `Gh.graphql`.
|
|
192
|
+
*
|
|
193
|
+
* @experimental
|
|
194
|
+
*/
|
|
195
|
+
graphql(
|
|
196
|
+
query: string,
|
|
197
|
+
variables: Record<string, v.GraphqlVariable> = {},
|
|
198
|
+
opts: v.GraphqlOptions & AsyncCallOptions = {},
|
|
199
|
+
): Promise<unknown[] | null> {
|
|
200
|
+
return this.exec(v.graphql(query, variables, opts, this.target), opts)
|
|
150
201
|
}
|
|
151
202
|
|
|
152
203
|
/** Every label on the repository (up to `opts.limit`, default 1000), or null when gh failed. */
|
|
153
|
-
labelList(
|
|
154
|
-
|
|
204
|
+
labelList(
|
|
205
|
+
opts: { limit?: number | undefined } & AsyncCallOptions = {},
|
|
206
|
+
): Promise<ListResult<Label> | null> {
|
|
207
|
+
return this.exec(v.labelList(opts, this.target), opts)
|
|
155
208
|
}
|
|
156
209
|
|
|
157
210
|
/** Issues matching `opts`, or null when gh failed. */
|
|
158
|
-
issueList(opts: IssueListOptions & AsyncCallOptions = {}): Promise<IssueRow
|
|
159
|
-
return this.exec(v.issueList(opts), opts)
|
|
211
|
+
issueList(opts: IssueListOptions & AsyncCallOptions = {}): Promise<ListResult<IssueRow> | null> {
|
|
212
|
+
return this.exec(v.issueList(opts, this.target), opts)
|
|
160
213
|
}
|
|
161
214
|
|
|
162
215
|
/** One issue with its labels, or null when gh failed. */
|
|
163
216
|
issueView(number: number, opts: AsyncCallOptions = {}): Promise<IssueRow | null> {
|
|
164
|
-
return this.exec(v.issueView(number), opts)
|
|
217
|
+
return this.exec(v.issueView(number, this.target), opts)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** An owner's repositories, or null when gh failed. See `Gh.repoList`. */
|
|
221
|
+
repoList(
|
|
222
|
+
owner: string,
|
|
223
|
+
opts: v.RepoListOptions & AsyncCallOptions = {},
|
|
224
|
+
): Promise<ListResult<Repository> | null> {
|
|
225
|
+
return this.exec(v.repoList(owner, opts, this.target), opts)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** One repository, or null when gh failed. See `Gh.repoView`. */
|
|
229
|
+
repoView(
|
|
230
|
+
repo?: string | undefined,
|
|
231
|
+
opts: { fields?: readonly string[] | undefined } & AsyncCallOptions = {},
|
|
232
|
+
): Promise<Repository | null> {
|
|
233
|
+
return this.exec(v.repoView(repo, opts.fields ?? v.REPO_FIELDS, this.target), opts)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Pull requests across repositories matching a search query, or null. See `Gh.searchPrs`. */
|
|
237
|
+
searchPrs(
|
|
238
|
+
query: string,
|
|
239
|
+
opts: v.SearchPrsOptions & AsyncCallOptions = {},
|
|
240
|
+
): Promise<ListResult<PrSearchHit> | null> {
|
|
241
|
+
return this.exec(v.searchPrs(query, opts, this.target), opts)
|
|
165
242
|
}
|
|
166
243
|
|
|
167
244
|
/** True iff the `gh` binary is on PATH and runs. */
|
|
@@ -176,7 +253,7 @@ export class AsyncGh {
|
|
|
176
253
|
|
|
177
254
|
/** Login state and what `gh auth status` printed. */
|
|
178
255
|
authStatus(): Promise<AuthStatus> {
|
|
179
|
-
return this.exec(v.authStatus())
|
|
256
|
+
return this.exec(v.authStatus(this.target))
|
|
180
257
|
}
|
|
181
258
|
|
|
182
259
|
/** True iff `gh` is installed AND authenticated; short-circuits. */
|
|
@@ -196,7 +273,7 @@ export class AsyncGh {
|
|
|
196
273
|
|
|
197
274
|
/** Open a PR and resolve to its URL ('' if gh printed none). */
|
|
198
275
|
createPr(opts: v.CreatePrOptions): Promise<string> {
|
|
199
|
-
return this.exec(v.createPr(opts))
|
|
276
|
+
return this.exec(v.createPr(opts, this.target))
|
|
200
277
|
}
|
|
201
278
|
|
|
202
279
|
/** Retitle or rewrite a PR body. */
|
|
@@ -204,22 +281,22 @@ export class AsyncGh {
|
|
|
204
281
|
number: number,
|
|
205
282
|
opts: { title?: string | undefined; body?: string | undefined },
|
|
206
283
|
): Promise<void> {
|
|
207
|
-
return this.exec(v.editPr(number, opts))
|
|
284
|
+
return this.exec(v.editPr(number, opts, this.target))
|
|
208
285
|
}
|
|
209
286
|
|
|
210
287
|
/** Rewrite just a PR's body. */
|
|
211
288
|
editPrBody(number: number, body: string): Promise<void> {
|
|
212
|
-
return this.exec(v.editPrBody(number, body))
|
|
289
|
+
return this.exec(v.editPrBody(number, body, this.target))
|
|
213
290
|
}
|
|
214
291
|
|
|
215
292
|
/** The PR's unified diff; rejects with `CommandFailed` when gh cannot produce it. */
|
|
216
293
|
prDiff(number: number): Promise<string> {
|
|
217
|
-
return this.exec(v.prDiff(number))
|
|
294
|
+
return this.exec(v.prDiff(number, this.target))
|
|
218
295
|
}
|
|
219
296
|
|
|
220
297
|
/** Post one conversation comment on a PR; resolves to its URL ('' if gh printed none). */
|
|
221
298
|
prComment(number: number, body: string): Promise<string> {
|
|
222
|
-
return this.exec(v.prComment(number, body))
|
|
299
|
+
return this.exec(v.prComment(number, body, this.target))
|
|
223
300
|
}
|
|
224
301
|
|
|
225
302
|
/**
|
|
@@ -228,17 +305,17 @@ export class AsyncGh {
|
|
|
228
305
|
* @experimental
|
|
229
306
|
*/
|
|
230
307
|
createReview(number: number, opts: v.CreateReviewOptions): Promise<string> {
|
|
231
|
-
return this.exec(v.createReview(number, opts))
|
|
308
|
+
return this.exec(v.createReview(number, opts, this.target))
|
|
232
309
|
}
|
|
233
310
|
|
|
234
311
|
/** Bring a PR's head branch up to date with its base. */
|
|
235
312
|
prUpdateBranch(number: number, style: 'merge' | 'rebase' = 'merge'): Promise<void> {
|
|
236
|
-
return this.exec(v.prUpdateBranch(number, style))
|
|
313
|
+
return this.exec(v.prUpdateBranch(number, style, this.target))
|
|
237
314
|
}
|
|
238
315
|
|
|
239
316
|
/** Mark a PR ready for review (`draft: false`) or convert it to a draft (`draft: true`). */
|
|
240
317
|
prSetDraft(number: number, draft: boolean): Promise<void> {
|
|
241
|
-
return this.exec(v.prSetDraft(number, draft))
|
|
318
|
+
return this.exec(v.prSetDraft(number, draft, this.target))
|
|
242
319
|
}
|
|
243
320
|
|
|
244
321
|
/**
|
|
@@ -248,22 +325,22 @@ export class AsyncGh {
|
|
|
248
325
|
*/
|
|
249
326
|
async issueCommentEdit(commentId: string, body: string): Promise<void> {
|
|
250
327
|
// async, so a bad id rejects the Promise instead of throwing at the call.
|
|
251
|
-
return this.exec(v.issueCommentEdit(commentId, body))
|
|
328
|
+
return this.exec(v.issueCommentEdit(commentId, body, this.target))
|
|
252
329
|
}
|
|
253
330
|
|
|
254
331
|
/** Merge a PR; GitHub refuses when its head is no longer `opts.headSha`. */
|
|
255
332
|
prMerge(number: number, opts: { method: v.PrMergeMethod; headSha: string }): Promise<void> {
|
|
256
|
-
return this.exec(v.prMerge(number, opts))
|
|
333
|
+
return this.exec(v.prMerge(number, opts, this.target))
|
|
257
334
|
}
|
|
258
335
|
|
|
259
336
|
/** A PR's review threads, or null when they could not be read completely. */
|
|
260
337
|
prReviewThreads(number: number): Promise<v.ReviewThread[] | null> {
|
|
261
|
-
return this.exec(v.prReviewThreads(number))
|
|
338
|
+
return this.exec(v.prReviewThreads(number, this.target))
|
|
262
339
|
}
|
|
263
340
|
|
|
264
341
|
/** Create a label (or force it to `opts`'s color/description if it exists). */
|
|
265
342
|
createLabel(opts: v.CreateLabelOptions): Promise<void> {
|
|
266
|
-
return this.exec(v.createLabel(opts))
|
|
343
|
+
return this.exec(v.createLabel(opts, this.target))
|
|
267
344
|
}
|
|
268
345
|
|
|
269
346
|
/** Recolor and/or redescribe an existing label. */
|
|
@@ -271,15 +348,32 @@ export class AsyncGh {
|
|
|
271
348
|
name: string,
|
|
272
349
|
opts: { color?: string | undefined; description?: string | undefined },
|
|
273
350
|
): Promise<void> {
|
|
274
|
-
return this.exec(v.editLabel(name, opts))
|
|
351
|
+
return this.exec(v.editLabel(name, opts, this.target))
|
|
275
352
|
}
|
|
276
353
|
|
|
277
354
|
/** Swap an issue's label in a single `gh issue edit` call. */
|
|
278
355
|
setIssueLabel(number: number, currentLabel: string | null, newLabel: string): Promise<void> {
|
|
279
|
-
return this.exec(v.setIssueLabel(number, currentLabel, newLabel))
|
|
356
|
+
return this.exec(v.setIssueLabel(number, currentLabel, newLabel, this.target))
|
|
280
357
|
}
|
|
281
358
|
}
|
|
282
359
|
|
|
360
|
+
/** The open PR for `branch`, else its newest closed or merged one. */
|
|
361
|
+
async function prForBranchOn(
|
|
362
|
+
gh: AsyncGh,
|
|
363
|
+
branch: string,
|
|
364
|
+
call: { fields: readonly string[] } & AsyncCallOptions,
|
|
365
|
+
): Promise<PullRequest | null> {
|
|
366
|
+
const rows = await gh.prList({
|
|
367
|
+
head: branch,
|
|
368
|
+
state: 'all',
|
|
369
|
+
limit: 20,
|
|
370
|
+
fields: v.branchPrFields(call.fields),
|
|
371
|
+
timeoutMs: call.timeoutMs,
|
|
372
|
+
signal: call.signal,
|
|
373
|
+
})
|
|
374
|
+
return v.branchPr(rows)
|
|
375
|
+
}
|
|
376
|
+
|
|
283
377
|
function sleepAsync(ms: number): Promise<void> {
|
|
284
378
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
285
379
|
}
|