@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @osdlc/typed-gh
|
|
2
2
|
|
|
3
|
+
## 0.9.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 96385dc: An empty `hostname` now means no host, the same as leaving it out. It used to throw `RangeError`, so a caller that reads the host from its own config had to write `hostname: value || undefined`. Passing `hostname: 'github.com'` is unchanged: it still reaches gh as given, because naming github.com explicitly keeps a call there when `GH_HOST` or gh's default host points at another host. The README now says so.
|
|
8
|
+
|
|
9
|
+
## 0.9.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- cc2abe4: `api` now keeps GitHub's error body when a request fails. `body` is parsed even when gh exits with an error, and a new `message` field holds `body.message`. `ApiResponse` gains this `message` field as required. `malformed` is now also set when a failed call prints something that is not JSON. The docs now say that `status` is only filled in when headers were requested.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 72f5049: `prList` always requests the `number` field. Before, a `fields` list without `number` returned no rows.
|
|
18
|
+
- Updated dependencies [b5aac07]
|
|
19
|
+
- Updated dependencies [41ef520]
|
|
20
|
+
- @osdlc/typed-git@0.7.0
|
|
21
|
+
|
|
22
|
+
## 0.8.0
|
|
23
|
+
|
|
24
|
+
### Minor Changes
|
|
25
|
+
|
|
26
|
+
- ae400e5: Added `runVerb` and `runVerbAsync`, with `RunVerbOptions` and `RunVerbAsyncOptions`. They run one verb through a runner the caller owns and apply what the verb carries: `env` (the target host, as `GH_HOST`, for `repoList` and `searchPrs`), `input` (the payload `createReview` sends on stdin), `timeoutMs` and the checked contract. Running `verb.parse(await run(verb.argv))` yourself dropped `env` and `input` without an error. `Gh` and `AsyncGh` now call them, so there is one copy of that logic.
|
|
27
|
+
|
|
3
28
|
## 0.7.0
|
|
4
29
|
|
|
5
30
|
### Minor Changes
|
package/CONTRIBUTING.md
CHANGED
|
@@ -68,7 +68,7 @@ version and writes the CHANGELOG from the pending changesets. See
|
|
|
68
68
|
the package by name. A test fails if an exported function is not called, or
|
|
69
69
|
an exported type not imported, in `examples/`. `tests/exports.test.ts` lists
|
|
70
70
|
the few exports that have no example yet.
|
|
71
|
-
-
|
|
71
|
+
- Run commands only through `@osdlc/command-seam`, never with a raw `child_process`
|
|
72
72
|
call.
|
|
73
73
|
- Add a changeset for a change users can see. See above.
|
|
74
74
|
- Record a design choice in
|
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@ npm install @osdlc/typed-gh
|
|
|
17
17
|
|
|
18
18
|
`pnpm add @osdlc/typed-gh` and `yarn add @osdlc/typed-gh` work too.
|
|
19
19
|
|
|
20
|
-
It needs Node `^22.22.2 || ^24.15.0 || >=26.0.0`, the range of its dependency `@osdlc/typed-git
|
|
21
|
-
It is supported on macOS and Linux. Windows is not supported yet
|
|
20
|
+
It needs Node `^22.22.2 || ^24.15.0 || >=26.0.0`, the range of its dependency `@osdlc/typed-git`. Bun also works, through the compiled `dist/`. It also needs the `gh` CLI on `PATH`, signed in with `gh auth login`.
|
|
21
|
+
It is supported on macOS and Linux. Windows is not supported yet. It will be once a Windows CI job
|
|
22
22
|
runs the tests.
|
|
23
23
|
|
|
24
24
|
```ts
|
|
@@ -34,11 +34,11 @@ The `examples` folder in the repository runs the same calls as tests.
|
|
|
34
34
|
|
|
35
35
|
## When not to use it
|
|
36
36
|
|
|
37
|
-
-
|
|
37
|
+
- `gh` is not installed. In a browser or a serverless function, use an HTTP client such as
|
|
38
38
|
Octokit.
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
39
|
+
- You sign in with a token, a GitHub App or OAuth. This package reads no token.
|
|
40
|
+
- You make many calls or poll in a loop. Each call starts a `gh` process.
|
|
41
|
+
- You need the whole API typed. This package covers the calls listed below. `api()` is a GET
|
|
42
42
|
and `graphql()` is a query. Anything else goes through `raw`.
|
|
43
43
|
|
|
44
44
|
## How it compares
|
|
@@ -60,15 +60,14 @@ and `@osdlc/typed-git`.
|
|
|
60
60
|
|
|
61
61
|
## Authentication is `gh`'s
|
|
62
62
|
|
|
63
|
-
No token is read, stored, or forwarded. That is
|
|
64
|
-
|
|
65
|
-
keychain
|
|
66
|
-
leak one.
|
|
63
|
+
No token is read, stored, or forwarded. That is why this package runs the CLI
|
|
64
|
+
instead of calling the REST API. The credential is already in the user's
|
|
65
|
+
keychain. A library that reads `GITHUB_TOKEN` can leak it.
|
|
67
66
|
|
|
68
67
|
## A total model
|
|
69
68
|
|
|
70
|
-
`gh --json` returns only the fields you asked for
|
|
71
|
-
`PullRequest` has a "not known" value and none of them is `undefined`:
|
|
69
|
+
`gh --json` returns only the fields you asked for. So every field on
|
|
70
|
+
`PullRequest` has a "not known" value, and none of them is `undefined`:
|
|
72
71
|
|
|
73
72
|
```ts
|
|
74
73
|
const pr = parsePullRequest({ number: 7 })
|
|
@@ -90,34 +89,35 @@ and `ReviewDecision` (`APPROVED`, `CHANGES_REQUESTED`, `REVIEW_REQUIRED`,
|
|
|
90
89
|
`null`). A value GitHub adds to either reads as `UNKNOWN` or `null`. A check
|
|
91
90
|
that decides whether to merge can then match every member.
|
|
92
91
|
|
|
93
|
-
## Three
|
|
92
|
+
## Three ways to call, as in `typed-git`, so a failure is never mistaken for an answer
|
|
94
93
|
|
|
95
94
|
| | a value | on failure |
|
|
96
95
|
| --- | --- | --- |
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
96
|
+
| `gh.query()` (the method that throws) | the value | throws a `FailureError` carrying the classified `GhFailure` (kind `unreadable` when `gh` succeeded but its output cannot be read) |
|
|
97
|
+
| `gh.try.query()` | `{ ok: true, value }` | `{ ok: false, error: GhFailure }` |
|
|
98
|
+
| `gh.soft.query()` | the value | `null` (queries only. A mutation has no soft form.) |
|
|
99
|
+
| `raw(argv)` | the raw `CommandResult`. You decide. | the same |
|
|
101
100
|
|
|
102
101
|
A query returns a list or a value, never an empty list on failure.
|
|
103
|
-
`prList()` returning `{ rows: [], truncated: false }` means
|
|
104
|
-
has no open PRs
|
|
105
|
-
`null` through `soft
|
|
102
|
+
`prList()` returning `{ rows: [], truncated: false }` means this repository
|
|
103
|
+
has no open PRs. A failed call throws, or is `{ ok: false }` through `try`, or
|
|
104
|
+
`null` through `soft`. That means we could not ask. If the two were the same, an
|
|
106
105
|
expired credential would read as "nothing to do".
|
|
107
106
|
|
|
108
|
-
|
|
109
|
-
`repoView`, and `prFind` and `openPrFor
|
|
110
|
-
|
|
107
|
+
A missing item is an answer, not a failure. A query for one thing (`prView`, `issueView`,
|
|
108
|
+
`repoView`, and `prFind` and `openPrFor`, which use them) returns `null` in all
|
|
109
|
+
three call styles when GitHub says there is no such thing: "Could not resolve to a
|
|
111
110
|
PullRequest" or "no pull requests found for branch" for `prView`, "Could not
|
|
112
111
|
resolve to an Issue" for `issueView`, "Could not resolve to a Repository" for
|
|
113
112
|
`repoView`. `gh.try.prView(99)` is `{ ok: true, value: null }`. Every other
|
|
114
|
-
failure is a failure, including a wrong repository
|
|
113
|
+
failure is a failure, including a wrong repository. `prView(7)` against a
|
|
115
114
|
mistyped `--repo` is a `not-found` failure, not an absent PR. The check reads
|
|
116
115
|
stderr and stdout, as the classifier does.
|
|
117
116
|
|
|
118
|
-
|
|
119
|
-
and `api()` return their failure as part of their
|
|
120
|
-
"gh is not installed" is the answer
|
|
117
|
+
Setup checks return their failure as a value. `installed()`, `authenticated()`,
|
|
118
|
+
`authStatus()`, `ready()` and `api()` return their failure as part of their
|
|
119
|
+
value, in all three call styles. For them, "gh is not installed" is the answer
|
|
120
|
+
to the question they ask.
|
|
121
121
|
|
|
122
122
|
```ts
|
|
123
123
|
import { describe } from '@osdlc/result'
|
|
@@ -130,11 +130,11 @@ if (!result.ok) {
|
|
|
130
130
|
|
|
131
131
|
### `GhFailure`
|
|
132
132
|
|
|
133
|
-
The error of `try` is a `GhFailure`, a `Failure` from `@osdlc/result
|
|
133
|
+
The error of `try` is a `GhFailure`, a `Failure` from `@osdlc/result`. It has a `kind`
|
|
134
134
|
to branch on, a one-line `message` and a `hint` for a person (`describe(error)`
|
|
135
135
|
joins them), `gh`'s own output in `detail`, the `argv` and `exitCode`, and the
|
|
136
|
-
`CommandFailed` it was read from in `cause`. The
|
|
137
|
-
failure, wrapped in a `FailureError` (`error.failure`)
|
|
136
|
+
`CommandFailed` it was read from in `cause`. The methods that throw throw the same
|
|
137
|
+
failure, wrapped in a `FailureError` (`error.failure`). So a CLI that reports a
|
|
138
138
|
thrown failure, such as `runMain` from `@osdlc/cli-tool`, shows "gh is not
|
|
139
139
|
signed in" for a write and exits with the same code as for a read.
|
|
140
140
|
|
|
@@ -149,7 +149,7 @@ signed in" for a write and exits with the same code as for a read.
|
|
|
149
149
|
| `not-executable`, `bad-cwd`, `timeout`, `output-cap`, `signal`, `aborted`, `spawn` | as in `@osdlc/command-seam` | command-seam's message, no hint |
|
|
150
150
|
| `exit` | any other non-zero exit | `gh`'s first stderr line |
|
|
151
151
|
|
|
152
|
-
The patterns are `gh`'s English messages
|
|
152
|
+
The patterns are `gh`'s English messages. Anything else is a plain `exit`,
|
|
153
153
|
never a wrong kind. Rate limiting is checked before authentication, then
|
|
154
154
|
network, then not-found. `classifyGhFailure(commandFailed)` makes the same
|
|
155
155
|
classification for a `CommandFailed` you caught yourself.
|
|
@@ -157,7 +157,7 @@ classification for a `CommandFailed` you caught yourself.
|
|
|
157
157
|
## A list says when its limit cut it short
|
|
158
158
|
|
|
159
159
|
`gh --limit` cuts a list without saying so. Every list query here asks for
|
|
160
|
-
one row more than `limit` and returns `{ rows, truncated }
|
|
160
|
+
one row more than `limit` and returns `{ rows, truncated }`. That is at most `limit`
|
|
161
161
|
rows, and `truncated: true` when there were more.
|
|
162
162
|
|
|
163
163
|
```ts
|
|
@@ -172,8 +172,8 @@ way. A `limit` that is not a positive integer throws `RangeError`.
|
|
|
172
172
|
## A repository without a clone
|
|
173
173
|
|
|
174
174
|
By default a client talks to the repository of the clone at its directory.
|
|
175
|
-
Name a repository, a host, or both, and every call targets them instead
|
|
176
|
-
|
|
175
|
+
Name a repository, a host, or both, and every call targets them instead. No
|
|
176
|
+
clone is needed:
|
|
177
177
|
|
|
178
178
|
```ts
|
|
179
179
|
const acme = new Gh(process.cwd(), { repo: 'acme/widgets', hostname: 'github.acme.com' })
|
|
@@ -184,15 +184,18 @@ acme.api('repos/{owner}/{repo}/branches')
|
|
|
184
184
|
```
|
|
185
185
|
|
|
186
186
|
The target goes into the argv, so a failure's `argv` shows which repository a
|
|
187
|
-
call hit
|
|
188
|
-
`api` calls get `--hostname` and a filled `{owner}/{repo}
|
|
187
|
+
call hit. `pr`, `issue` and `label` calls get `--repo github.acme.com/acme/widgets`.
|
|
188
|
+
`api` calls get `--hostname` and a filled `{owner}/{repo}`. `authStatus`
|
|
189
189
|
asks about that host. `repoList` and `searchPrs` have no host flag in gh, so
|
|
190
190
|
they get `GH_HOST` in the environment. `withRepo` and `at` keep the runner and
|
|
191
191
|
the rest of the target.
|
|
192
192
|
|
|
193
193
|
`hostname` is any GitHub host: `github.com`, a GitHub Enterprise Server host,
|
|
194
|
-
or a `<tenant>.ghe.com` host.
|
|
195
|
-
|
|
194
|
+
or a `<tenant>.ghe.com` host. An empty string means no host, so a value read
|
|
195
|
+
from config can be passed as it is. `github.com` is not dropped: it goes to gh
|
|
196
|
+
as `--repo github.com/owner/name`, `--hostname` or `GH_HOST`, which keeps the
|
|
197
|
+
call on github.com even when the environment sets another default host. gh
|
|
198
|
+
supports only GitHub's API, so Forgejo, Gitea and GitLab are not supported. A `repo` that is not `owner/name`, or a
|
|
196
199
|
`hostname` that is not a bare host, throws `RangeError`.
|
|
197
200
|
|
|
198
201
|
## Finding a pull request
|
|
@@ -212,7 +215,7 @@ gh.prView('https://github.acme.com/acme/widgets/pull/42')
|
|
|
212
215
|
const pr: PullRequest | null = gh.prFind(process.argv[2] ?? '')
|
|
213
216
|
```
|
|
214
217
|
|
|
215
|
-
It reads the text with `parsePrRef`, which starts no process
|
|
218
|
+
It reads the text with `parsePrRef`, which starts no process. `42` and `#42`
|
|
216
219
|
are numbers, `owner/name#42` is a number in that repository, a URL is a URL,
|
|
217
220
|
and any other valid branch name is a branch. Bare digits are tried as a PR
|
|
218
221
|
number first and then as a branch, since a branch can be named `42`. A branch
|
|
@@ -220,19 +223,19 @@ gives its open PR, or else its newest closed or merged one, so a tool run
|
|
|
220
223
|
after the merge still finds it.
|
|
221
224
|
|
|
222
225
|
`prList({ head })` matches the head branch exactly: `task/foo` never returns
|
|
223
|
-
`task/foo-2`. Two forks can share a branch name
|
|
226
|
+
`task/foo-2`. Two forks can share a branch name. Ask for
|
|
224
227
|
`headRepositoryOwner` to tell them apart.
|
|
225
228
|
|
|
226
|
-
## Two
|
|
229
|
+
## Two payload differences it handles
|
|
227
230
|
|
|
228
|
-
|
|
229
|
-
login without a suffix (`github-actions`). REST (`gh api`) returns
|
|
230
|
-
one (`github-actions[bot]`). Comparing one with the other never
|
|
231
|
-
no error shows it.
|
|
232
|
-
feedback. Logins are cut to the suffix
|
|
233
|
-
parsed.
|
|
231
|
+
Author logins differ between GraphQL and REST. GraphQL (`gh pr view --json`)
|
|
232
|
+
returns a login without a suffix (`github-actions`). REST (`gh api`) returns
|
|
233
|
+
the suffixed one (`github-actions[bot]`). Comparing one with the other never
|
|
234
|
+
matches, and no error shows it. Then a bot's comments are treated as human
|
|
235
|
+
review feedback. Logins are cut to the form without the suffix once, when a
|
|
236
|
+
payload is parsed.
|
|
234
237
|
|
|
235
|
-
|
|
238
|
+
`statusCheckRollup` holds two node types. A modern `CheckRun` carries
|
|
236
239
|
`name`/`status`/`conclusion`. A legacy `StatusContext` carries
|
|
237
240
|
`context`/`state`. Both map onto one `verdict` axis:
|
|
238
241
|
|
|
@@ -248,21 +251,20 @@ as green, a red build could merge.
|
|
|
248
251
|
|
|
249
252
|
## The merge state is not stable on a cold query
|
|
250
253
|
|
|
251
|
-
GitHub computes mergeability
|
|
252
|
-
`UNKNOWN` until it
|
|
253
|
-
every PR
|
|
254
|
+
GitHub computes mergeability only when asked. The first query starts the
|
|
255
|
+
computation and answers `UNKNOWN` until it finishes. A caller that trusts that
|
|
256
|
+
first answer misjudges every PR whose computation is slow.
|
|
254
257
|
|
|
255
258
|
```ts
|
|
256
259
|
const pr = gh.prViewSettled(7, { attempts: 3 }) // 3 is the default
|
|
257
260
|
```
|
|
258
261
|
|
|
259
|
-
`UNKNOWN`
|
|
262
|
+
`UNKNOWN` can still be the final answer, so callers must handle it.
|
|
260
263
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
(`typed-git`'s `aheadBehind`).
|
|
264
|
+
`mergeStateStatus: "BEHIND"` appears only when branch protection requires an
|
|
265
|
+
up-to-date branch. Without that rule, a branch a hundred commits behind still
|
|
266
|
+
reports `CLEAN`. So `BEHIND` is a hint that a branch is out of date, not a
|
|
267
|
+
measure of it. Compute that from the commit graph (`typed-git`'s `aheadBehind`).
|
|
266
268
|
|
|
267
269
|
## Changing pull requests
|
|
268
270
|
|
|
@@ -283,9 +285,9 @@ a draft.
|
|
|
283
285
|
|
|
284
286
|
## Merging
|
|
285
287
|
|
|
286
|
-
`prMerge` takes the head commit you
|
|
287
|
-
|
|
288
|
-
deleted. That is
|
|
288
|
+
`prMerge` takes the head commit you checked. GitHub refuses the merge if the head has moved since.
|
|
289
|
+
So a push that lands between your check and the merge cannot be merged without review. The branch
|
|
290
|
+
is not deleted. That is up to you.
|
|
289
291
|
|
|
290
292
|
```ts
|
|
291
293
|
const method: PrMergeMethod = 'squash'
|
|
@@ -316,8 +318,8 @@ gh.editLabel('status:triage', { description: 'Waiting for triage' })
|
|
|
316
318
|
```
|
|
317
319
|
|
|
318
320
|
`issueList` returns open issues, up to `limit` (default 200). `labelList` returns up to `limit`
|
|
319
|
-
labels (default 1000
|
|
320
|
-
another in one `gh issue edit` call. `createLabel` passes `--force`, so it is safe to repeat
|
|
321
|
+
labels (default 1000. `gh`'s own default is 30). `setIssueLabel` removes one label and adds
|
|
322
|
+
another in one `gh issue edit` call. `createLabel` passes `--force`, so it is safe to repeat. An
|
|
321
323
|
existing label gets the color and description you pass.
|
|
322
324
|
|
|
323
325
|
## Diffs, reviews and comments
|
|
@@ -334,20 +336,20 @@ gh.issueCommentEdit('4204154439', 'Updated: CI is green.')
|
|
|
334
336
|
|
|
335
337
|
`prDiff` returns the unified diff and throws the classified failure when `gh` cannot produce it.
|
|
336
338
|
|
|
337
|
-
`createReview` is experimental. It posts a review of kind `COMMENT
|
|
339
|
+
`createReview` is experimental. It posts a review of kind `COMMENT`. It cannot approve or request
|
|
338
340
|
changes. Each inline comment sits on one line of the new side of the diff. It returns the review's
|
|
339
341
|
URL.
|
|
340
342
|
|
|
341
|
-
`issueCommentEdit` is experimental. It takes the comment's numeric REST id, as text
|
|
343
|
+
`issueCommentEdit` is experimental. It takes the comment's numeric REST id, as text. That is the `id` from
|
|
342
344
|
`gh api repos/{owner}/{repo}/issues/<n>/comments`. The `Comment.id` that `prView` returns is a
|
|
343
345
|
GraphQL node id, which this REST path does not accept. An id that is not all digits throws
|
|
344
346
|
`RangeError` before `gh` runs (`AsyncGh` returns a rejected Promise).
|
|
345
347
|
|
|
346
|
-
##
|
|
348
|
+
## Verbs and parsers, for running calls yourself
|
|
347
349
|
|
|
348
|
-
The verb table and every parser
|
|
349
|
-
|
|
350
|
-
|
|
350
|
+
The verb table and every parser have no side effects. So code that runs many
|
|
351
|
+
calls at once can run them through its own `AsyncCommandRunner` and reuse the
|
|
352
|
+
parsing:
|
|
351
353
|
|
|
352
354
|
```ts
|
|
353
355
|
import { prList } from '@osdlc/typed-gh'
|
|
@@ -357,13 +359,32 @@ const result = await runner.run(verb.argv, { cwd: '/repo', timeoutMs: 5_000 })
|
|
|
357
359
|
const prs = verb.parse(result)
|
|
358
360
|
```
|
|
359
361
|
|
|
362
|
+
That is only safe for a verb with no `env` or `input`. `repoList` and `searchPrs` send the target
|
|
363
|
+
host as `GH_HOST` (`env`), and `createReview` sends its payload on stdin (`input`). A runner call
|
|
364
|
+
that passes only `verb.argv` drops both, with no error. The query goes to github.com, and the review
|
|
365
|
+
is never sent. `runVerb` (and `runVerbAsync`, for an `AsyncCommandRunner`) run a verb the way `Gh` and
|
|
366
|
+
`AsyncGh` do, with `cwd`, `env`, `input`, `timeoutMs` and the checked contract applied, and return
|
|
367
|
+
the parsed value or throw the classified failure:
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
import { repoList, runVerbAsync } from '@osdlc/typed-gh'
|
|
371
|
+
|
|
372
|
+
const repos = await runVerbAsync(repoList('acme', {}, { hostname: 'ghe.example.com' }), runner, {
|
|
373
|
+
cwd: '/repo',
|
|
374
|
+
timeoutMs: 5_000,
|
|
375
|
+
})
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
They take a `CommandRunner`. A plain `(binary, argv, opts) => result` function is wrapped as
|
|
379
|
+
`{ run: (argv, opts) => fn('gh', argv, opts) }`.
|
|
380
|
+
|
|
360
381
|
## `AsyncGh`
|
|
361
382
|
|
|
362
|
-
The awaitable form of `Gh`, with the same methods, for
|
|
363
|
-
|
|
364
|
-
`try` resolves to a `Result<T, GhFailure
|
|
365
|
-
query
|
|
366
|
-
`AsyncCommandRunner` and defaults to `spawnRunnerAsync("gh")`.
|
|
383
|
+
The awaitable form of `Gh`, with the same methods, for code that runs many calls at once and must
|
|
384
|
+
not block the event loop. It has the same three call styles, as Promises. The plain method rejects
|
|
385
|
+
with the classified failure. `try` resolves to a `Result<T, GhFailure>`. `soft` resolves `null` for
|
|
386
|
+
a failed query. `raw` and `rawChecked` run any `gh` call. Both clients use the same verb table, so
|
|
387
|
+
they cannot drift apart. It takes an `AsyncCommandRunner` and defaults to `spawnRunnerAsync("gh")`.
|
|
367
388
|
|
|
368
389
|
```ts
|
|
369
390
|
import { AsyncGh } from '@osdlc/typed-gh'
|
|
@@ -382,11 +403,12 @@ These methods take `AsyncCallOptions`: `raw`, `rawChecked`, `prList`, `prView`,
|
|
|
382
403
|
`signal`.
|
|
383
404
|
|
|
384
405
|
`api(endpoint, opts)` (on `Gh` and `AsyncGh`) is a REST GET through `gh api`.
|
|
385
|
-
It never throws
|
|
386
|
-
`
|
|
406
|
+
It never throws. The result is `{ ok, status, etag, body, message, malformed, stderr, error? }`.
|
|
407
|
+
On failure `body` still carries GitHub's error object and `message` its text. `status` is read only when headers were requested (`etag` or `includeHeaders`).
|
|
408
|
+
`stderr` is gh's own. `error` holds the runner's message when gh never ran or
|
|
387
409
|
was stopped (a missing binary, a timeout).
|
|
388
|
-
Pass `etag` for a conditional request (`If-None-Match
|
|
389
|
-
`status: 304, body: null`)
|
|
410
|
+
Pass `etag` for a conditional request (`If-None-Match`. A 304 comes back as
|
|
411
|
+
`status: 304, body: null`). Or pass `includeHeaders: true` to read status and ETag
|
|
390
412
|
without one. If `gh` prints more than one header block, the last one is read. `{owner}` and
|
|
391
413
|
`{repo}` in the endpoint come from the client's repo, or are filled in by gh from the clone.
|
|
392
414
|
|
|
@@ -400,7 +422,7 @@ const branches = gh.api('repos/{owner}/{repo}/branches', { paginate: true }).bod
|
|
|
400
422
|
```
|
|
401
423
|
|
|
402
424
|
`graphql` (experimental) runs a query and returns each page's `data`. It throws when the
|
|
403
|
-
call failed or any page has `errors` or no `data` (`soft.graphql` is `null`)
|
|
425
|
+
call failed or any page has `errors` or no `data` (`soft.graphql` is `null`). A partial answer counts as a failure. `nodesAt` joins
|
|
404
426
|
one connection's `nodes` across the pages, and is `null` when any page lacks them.
|
|
405
427
|
|
|
406
428
|
```ts
|
|
@@ -416,8 +438,8 @@ const pages = gh.graphql(query, { owner: '{owner}', name: '{repo}' }, { paginate
|
|
|
416
438
|
const prs = nodesAt(pages, ['repository', 'pullRequests'])
|
|
417
439
|
```
|
|
418
440
|
|
|
419
|
-
With `paginate`, the query must take `$endCursor` and page one connection
|
|
420
|
-
one. `graphql` refuses a mutation with `RangeError
|
|
441
|
+
With `paginate`, the query must take `$endCursor` and page one connection. gh follows only that
|
|
442
|
+
one. `graphql` refuses a mutation with `RangeError`. Send one through `raw`.
|
|
421
443
|
|
|
422
444
|
## Repositories and search
|
|
423
445
|
|
|
@@ -428,15 +450,15 @@ const hits: ListResult<PrSearchHit> = gh.searchPrs('TICKET-123', { owner: 'acme'
|
|
|
428
450
|
```
|
|
429
451
|
|
|
430
452
|
A `Repository` carries `nameWithOwner`, `owner`, `name`, `description`, `defaultBranch`,
|
|
431
|
-
`visibility`, `isArchived` and `url`. `gh search prs` has its own field set
|
|
453
|
+
`visibility`, `isArchived` and `url`. `gh search prs` has its own field set. A `PrSearchHit` names
|
|
432
454
|
its `repository` but has no head or base branch, so it is not a `PullRequest`. Its `state` is
|
|
433
455
|
upper-cased to match. The query takes GitHub's search syntax and may start with `-`.
|
|
434
456
|
|
|
435
|
-
##
|
|
457
|
+
## Passing your own runner
|
|
436
458
|
|
|
437
459
|
`Gh` takes a `CommandRunner` from
|
|
438
|
-
[`@osdlc/command-seam`](https://github.com/osdlc/osdlc/tree/main/packages/ts/command-seam), so
|
|
439
|
-
|
|
460
|
+
[`@osdlc/command-seam`](https://github.com/osdlc/osdlc/tree/main/packages/ts/command-seam), so
|
|
461
|
+
tests can run offline:
|
|
440
462
|
|
|
441
463
|
```ts
|
|
442
464
|
const empty: CommandRunner = { run: () => ({ exitCode: 0, stdout: '[]', stderr: '' }) }
|
|
@@ -462,10 +484,12 @@ experimental.
|
|
|
462
484
|
- `GhOptions` and `AsyncGhOptions`: the runner, `repo` and `hostname` a client is built with.
|
|
463
485
|
- `AsyncCallOptions`: `timeoutMs` and `signal`, accepted by sixteen methods of `AsyncGh`.
|
|
464
486
|
- `defaultRunnerFactory` and `RunnerFactory`: how a client makes its default runner for `gh`.
|
|
487
|
+
- `runVerb` and `runVerbAsync`, with `RunVerbOptions` and `RunVerbAsyncOptions`: run one verb through
|
|
488
|
+
a runner you own, with its `env`, `input`, timeout and checked contract applied.
|
|
465
489
|
|
|
466
490
|
Methods on both clients:
|
|
467
491
|
|
|
468
|
-
-
|
|
492
|
+
- Direct calls: `raw` runs any `gh` call and returns the result. `rawChecked` throws on a failed
|
|
469
493
|
exit. `at` returns the same client rooted at another directory, and `withRepo` the same client
|
|
470
494
|
aimed at another repository.
|
|
471
495
|
- Pull request queries: `prList`, `prView`, `prFind`, `openPrFor`, `prViewSettled`, `prBody`,
|
|
@@ -491,7 +515,7 @@ A verb is plain data: argv, a `checked` flag, a `readOnly` flag, optional stdin
|
|
|
491
515
|
and a `parse` function. `readOnly` is true for every query and `prDiff`, and false for every
|
|
492
516
|
mutation, so a caller that keeps an allowlist of safe calls can check a verb before running it.
|
|
493
517
|
Each verb takes an optional `GhTarget` last. Each client method has a verb of the same name, except
|
|
494
|
-
|
|
518
|
+
`raw`, `rawChecked`, `at`, `withRepo`, `prViewSettled`, `openPrFor`, `prFind`, `authenticated`,
|
|
495
519
|
`ready` and `ownerRepo`. `openPrsForBranch` is a verb with no method.
|
|
496
520
|
|
|
497
521
|
- `GhVerb`: the shape of a verb. `GhTarget`: the `repo` and `hostname` a verb talks to.
|
|
@@ -540,9 +564,9 @@ the escape hatches, `at`, `withRepo`, `prViewSettled`, `openPrFor`, `prFind`, `a
|
|
|
540
564
|
|
|
541
565
|
The package is at 0.x. Each item is stable or experimental.
|
|
542
566
|
|
|
543
|
-
-
|
|
567
|
+
- Stable items change only with a minor release, and a breaking change is marked
|
|
544
568
|
**Breaking** in the CHANGELOG.
|
|
545
|
-
-
|
|
569
|
+
- Experimental items may change or go in any release. Each has an `@experimental` tag in its
|
|
546
570
|
TSDoc.
|
|
547
571
|
|
|
548
572
|
The experimental items:
|
|
@@ -565,11 +589,11 @@ shell, so nothing in it is specific to one shell.
|
|
|
565
589
|
|
|
566
590
|
## What is not here
|
|
567
591
|
|
|
568
|
-
-
|
|
569
|
-
deleting belong
|
|
570
|
-
-
|
|
592
|
+
- Closing a PR and deleting a branch. `prMerge` merges and leaves the branch alone. Closing and
|
|
593
|
+
deleting belong in the caller's own code, which keeps its own record of what it did.
|
|
594
|
+
- A full REST or GraphQL client. `api()` is a GET and `graphql()` a query. Anything else goes
|
|
571
595
|
through `raw(["api", …])`.
|
|
572
|
-
-
|
|
596
|
+
- Anything that reads a token.
|
|
573
597
|
|
|
574
598
|
## Licence
|
|
575
599
|
|
package/SECURITY.md
CHANGED
|
@@ -33,8 +33,8 @@ not intend are in scope.
|
|
|
33
33
|
keychain entry, or `GH_TOKEN` and `GITHUB_TOKEN` in the environment. The
|
|
34
34
|
child process inherits the caller's environment.
|
|
35
35
|
- **No shell is involved.** The package starts `gh` directly and passes each
|
|
36
|
-
value as one argument
|
|
37
|
-
body
|
|
36
|
+
value as one argument. Spaces and shell metacharacters in a title or a
|
|
37
|
+
body have no effect.
|
|
38
38
|
- **`gh` still parses its arguments.** A positional value that starts with `-`,
|
|
39
39
|
such as a label name, can be read as a flag. Check untrusted input before you
|
|
40
40
|
pass it.
|
|
@@ -51,5 +51,5 @@ not intend are in scope.
|
|
|
51
51
|
and PR titles, bodies and comments travel in argv. Do not log the message
|
|
52
52
|
where those are private. `createReview` sends its payload on stdin instead.
|
|
53
53
|
- **`prMerge` needs the head sha.** GitHub refuses the merge when the PR head
|
|
54
|
-
is no longer that sha, so a push made after review
|
|
55
|
-
sha you
|
|
54
|
+
is no longer that sha, so a push made after review is not merged by this
|
|
55
|
+
call. Pass the sha you reviewed, not one read just before the call.
|
package/dist/async.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `AsyncGh
|
|
2
|
+
* `AsyncGh`: the awaitable GitHub client, with the same methods as {@link Gh}.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* a
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* It has the same three call styles, as Promises. The plain method resolves to
|
|
5
|
+
* a value or rejects with a `FailureError` carrying the classified `GhFailure`,
|
|
6
|
+
* for a failed gh or output it cannot read. A query for one thing resolves
|
|
7
|
+
* `null` when GitHub has no such thing. `try` resolves to a
|
|
8
|
+
* `Result<T, GhFailure>`. `soft` resolves `null` for a failure. `raw` and
|
|
9
|
+
* `rawChecked` run any `gh` call. It uses the same verb table as `Gh`, so the
|
|
10
|
+
* two cannot differ on argv or parsing. Use it where the sync client would
|
|
11
|
+
* block the event loop, such as many per-PR queries at once.
|
|
11
12
|
*/
|
|
12
13
|
import { type AsyncCommandRunner, type CommandResult } from '@osdlc/command-seam';
|
|
13
14
|
import { type Soft, type TriedAsync } from './doors.ts';
|
|
@@ -26,7 +27,7 @@ export interface AsyncCallOptions {
|
|
|
26
27
|
* non-number.
|
|
27
28
|
*/
|
|
28
29
|
timeoutMs?: number | undefined;
|
|
29
|
-
/** Aborts the call
|
|
30
|
+
/** Aborts the call. The runner stops the child process. */
|
|
30
31
|
signal?: AbortSignal | undefined;
|
|
31
32
|
}
|
|
32
33
|
/** What an {@link AsyncGh} is built with: the runner, and the repository and host it talks to. */
|
|
@@ -58,15 +59,15 @@ export declare class AsyncGh {
|
|
|
58
59
|
get try(): TriedAsync<AsyncGh>;
|
|
59
60
|
/** Each query again, resolving `null` instead of rejecting when `gh` fails. See `Gh.soft`. */
|
|
60
61
|
get soft(): Soft<AsyncGh>;
|
|
61
|
-
/**
|
|
62
|
+
/** Run any `gh` call and return the result, whatever the exit code. */
|
|
62
63
|
raw(argv: string[], opts?: AsyncCallOptions): Promise<CommandResult>;
|
|
63
|
-
/**
|
|
64
|
+
/** Run any `gh` call. Rejects with the classified failure on a non-zero exit. */
|
|
64
65
|
rawChecked(argv: string[], opts?: AsyncCallOptions): Promise<CommandResult>;
|
|
65
|
-
/** Run one verb from the table,
|
|
66
|
+
/** Run one verb from the table, checked or unchecked as the verb says. */
|
|
66
67
|
private exec;
|
|
67
|
-
/** {@link exec} for a verb that never
|
|
68
|
+
/** {@link exec} for a verb that never returns `null` as a value. A failed or unreadable run rejects. */
|
|
68
69
|
private read;
|
|
69
|
-
/** Pull requests matching `opts`. Rejects when gh fails
|
|
70
|
+
/** Pull requests matching `opts`. Rejects when gh fails. `soft.prList` resolves null instead. */
|
|
70
71
|
prList(opts?: PrListOptions & AsyncCallOptions): Promise<ListResult<PullRequest>>;
|
|
71
72
|
/** One PR by number, branch or URL, with the heavy fields, or null. See `Gh.prView`. */
|
|
72
73
|
prView(ref: number | string, opts?: {
|
|
@@ -81,7 +82,7 @@ export declare class AsyncGh {
|
|
|
81
82
|
limit?: number | undefined;
|
|
82
83
|
fields?: readonly string[] | undefined;
|
|
83
84
|
} & AsyncCallOptions): Promise<PullRequest | null>;
|
|
84
|
-
/** {@link prView},
|
|
85
|
+
/** {@link prView}, asked again until GitHub has computed the merge state. See `Gh.prViewSettled`. */
|
|
85
86
|
prViewSettled(number: number, opts?: {
|
|
86
87
|
attempts?: number | undefined;
|
|
87
88
|
fields?: readonly string[] | undefined;
|
|
@@ -89,10 +90,10 @@ export declare class AsyncGh {
|
|
|
89
90
|
} & AsyncCallOptions): Promise<PullRequest | null>;
|
|
90
91
|
/** A PR's body text. Rejects when gh fails. */
|
|
91
92
|
prBody(number: number, opts?: AsyncCallOptions): Promise<string>;
|
|
92
|
-
/** A REST GET through `gh api
|
|
93
|
+
/** A REST GET through `gh api`. Failure is `ok: false`, never a rejection. */
|
|
93
94
|
api(endpoint: string, opts?: ApiOptions & AsyncCallOptions): Promise<ApiResponse>;
|
|
94
95
|
/**
|
|
95
|
-
* A GraphQL query
|
|
96
|
+
* A GraphQL query. Resolves to each page's `data`. Rejects on a failed or partial result. See `Gh.graphql`.
|
|
96
97
|
*
|
|
97
98
|
* @experimental
|
|
98
99
|
*/
|
|
@@ -103,11 +104,11 @@ export declare class AsyncGh {
|
|
|
103
104
|
} & AsyncCallOptions): Promise<ListResult<Label>>;
|
|
104
105
|
/** Issues matching `opts`. Rejects when gh fails. */
|
|
105
106
|
issueList(opts?: IssueListOptions & AsyncCallOptions): Promise<ListResult<IssueRow>>;
|
|
106
|
-
/** One issue with its labels
|
|
107
|
+
/** One issue with its labels, or null when there is no such issue. */
|
|
107
108
|
issueView(number: number, opts?: AsyncCallOptions): Promise<IssueRow | null>;
|
|
108
109
|
/** An owner's repositories. Rejects when gh fails. See `Gh.repoList`. */
|
|
109
110
|
repoList(owner: string, opts?: v.RepoListOptions & AsyncCallOptions): Promise<ListResult<Repository>>;
|
|
110
|
-
/** One repository
|
|
111
|
+
/** One repository, or null when there is no such repository. See `Gh.repoView`. */
|
|
111
112
|
repoView(repo?: string | undefined, opts?: {
|
|
112
113
|
fields?: readonly string[] | undefined;
|
|
113
114
|
} & AsyncCallOptions): Promise<Repository | null>;
|
|
@@ -119,11 +120,11 @@ export declare class AsyncGh {
|
|
|
119
120
|
authenticated(): Promise<boolean>;
|
|
120
121
|
/** Login state and what `gh auth status` printed. */
|
|
121
122
|
authStatus(): Promise<AuthStatus>;
|
|
122
|
-
/** True iff `gh` is installed
|
|
123
|
+
/** True iff `gh` is installed and authenticated. Skips the auth check when `gh` is not installed. */
|
|
123
124
|
ready(): Promise<boolean>;
|
|
124
125
|
/**
|
|
125
126
|
* `{owner, repo}` parsed from the remote URL you pass, or null when it is
|
|
126
|
-
* null or names no project.
|
|
127
|
+
* null or names no project. It starts no process, so it is sync on purpose.
|
|
127
128
|
*/
|
|
128
129
|
ownerRepo(remoteUrl: string | null): OwnerRepo | null;
|
|
129
130
|
/** Open a PR and resolve to its URL ('' if gh printed none). */
|
|
@@ -135,12 +136,12 @@ export declare class AsyncGh {
|
|
|
135
136
|
}): Promise<void>;
|
|
136
137
|
/** Rewrite just a PR's body. */
|
|
137
138
|
editPrBody(number: number, body: string): Promise<void>;
|
|
138
|
-
/** The PR's unified diff
|
|
139
|
+
/** The PR's unified diff. Rejects with the classified failure when gh cannot produce it. */
|
|
139
140
|
prDiff(number: number): Promise<string>;
|
|
140
|
-
/** Post one conversation comment on a PR
|
|
141
|
+
/** Post one conversation comment on a PR. Resolves to its URL ('' if gh printed none). */
|
|
141
142
|
prComment(number: number, body: string): Promise<string>;
|
|
142
143
|
/**
|
|
143
|
-
* Create a `COMMENT` review with inline comments
|
|
144
|
+
* Create a `COMMENT` review with inline comments. Resolves to the review's URL ('' if none came back).
|
|
144
145
|
*
|
|
145
146
|
* @experimental
|
|
146
147
|
*/
|
|
@@ -150,17 +151,17 @@ export declare class AsyncGh {
|
|
|
150
151
|
/** Mark a PR ready for review (`draft: false`) or convert it to a draft (`draft: true`). */
|
|
151
152
|
prSetDraft(number: number, draft: boolean): Promise<void>;
|
|
152
153
|
/**
|
|
153
|
-
* Rewrite one PR conversation comment's body. `commentId` is the numeric REST id
|
|
154
|
+
* Rewrite one PR conversation comment's body. `commentId` is the numeric REST id. Anything else rejects with `RangeError`.
|
|
154
155
|
*
|
|
155
156
|
* @experimental
|
|
156
157
|
*/
|
|
157
158
|
issueCommentEdit(commentId: string, body: string): Promise<void>;
|
|
158
|
-
/** Merge a PR
|
|
159
|
+
/** Merge a PR. GitHub refuses when its head is no longer `opts.headSha`. */
|
|
159
160
|
prMerge(number: number, opts: {
|
|
160
161
|
method: v.PrMergeMethod;
|
|
161
162
|
headSha: string;
|
|
162
163
|
}): Promise<void>;
|
|
163
|
-
/** A PR's review threads
|
|
164
|
+
/** A PR's review threads. Rejects when they could not be read completely. `soft.prReviewThreads` resolves null instead. */
|
|
164
165
|
prReviewThreads(number: number): Promise<v.ReviewThread[]>;
|
|
165
166
|
/** Create a label (or force it to `opts`'s color/description if it exists). */
|
|
166
167
|
createLabel(opts: v.CreateLabelOptions): Promise<void>;
|