@osdlc/typed-gh 0.9.6 → 0.12.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 +43 -0
- package/README.md +48 -6
- package/SECURITY.md +4 -3
- package/dist/async.js +29 -27
- package/dist/doors.d.ts +17 -4
- package/dist/doors.js +43 -48
- package/dist/failure.js +3 -8
- package/dist/parse.js +9 -1
- package/dist/repo.d.ts +9 -4
- package/dist/repo.js +12 -12
- package/dist/sync.d.ts +1 -0
- package/dist/sync.js +1 -0
- package/dist/testing.d.ts +216 -0
- package/dist/testing.js +186 -0
- package/dist/verbs.d.ts +11 -6
- package/dist/verbs.js +93 -73
- package/package.json +8 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# @osdlc/typed-gh
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a4ca07d: New `@osdlc/typed-gh/testing` entry point with builders for faking `gh` in tests. `prRow`, `issueRow`, `repoRow`, `searchPrRow`, `labelRow`, `checkRunRow`, `statusContextRow`, `commentRow` and `reviewRow` return gh's raw `--json` rows, with defaults that fill every field the parsers require (a pull request's `number`, a repository's `nameWithOwner`, the `headRefName` that `prList({ head })` filters on), and take typed overrides merged shallowly. `ghResult(payload)` wraps rows in a successful `CommandResult` for a fake runner. The main entry point does not import it.
|
|
8
|
+
|
|
9
|
+
Fixed: `searchPrs` reads gh's zero time (`0001-01-01T00:00:00Z`), which gh prints for a hit that isn't closed, as `closedAt: null`. Before, an open hit had a year-0001 close date.
|
|
10
|
+
|
|
11
|
+
## 0.11.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 272da7a: A list that goes through GitHub search reports `truncated: true` when 1000 rows come back. Search returns at most 1000 results whatever the `limit`, so before, a `limit` of 1000 or more read as complete (`truncated: false`) while more matched. gh uses search for `searchPrs`, for `prList` with a non-empty `search` or an `author`, and for `issueList` with a `label`. A list that does not search is unchanged. `searchPrs` now asks gh for at most 1000 rows: a `limit` of 1000 or more used to fail, because gh refuses a larger search limit.
|
|
16
|
+
|
|
17
|
+
## 0.11.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- da5e6a3: `parseOwnerRepo` is now `@osdlc/typed-git`'s `parseRemoteRepo` without the host. Behaviour change: a remote that `parseGitUrl` recognises but names no owner and repository for is read host-agnostically (`host/owner/repo`) instead of answering null; every other remote reads as before.
|
|
22
|
+
`settle`, `runCheckedGh`, `runCheckedGhAsync`, `gh.try`, `gh.soft`, `classifyGhFailure` and the leading-dash guard now use `@osdlc/command-seam`'s `runFailed`, `tryMethods`, `methodFacade`, `classifyFailed` and `notAnOption`, with no change a caller sees: the same failures, and the same `RangeError` and message for an argument that starts with `-`. `repoView` and every `--repo` verb build `[host/]owner/name` through one helper.
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [da5e6a3]
|
|
27
|
+
- Updated dependencies [da5e6a3]
|
|
28
|
+
- @osdlc/command-seam@0.14.0
|
|
29
|
+
- @osdlc/typed-git@0.10.0
|
|
30
|
+
|
|
31
|
+
## 0.10.0
|
|
32
|
+
|
|
33
|
+
### Minor Changes
|
|
34
|
+
|
|
35
|
+
- b835ae4: Behaviour change: output that the runner cut short (`outputIncomplete`, at the output cap or a timeout) now fails as `CommandFailed` for every verb but a probe, and in `runCheckedGh`/`runCheckedGhAsync`, instead of being parsed as whole. Behaviour change: every `AsyncGh` method that builds a verb now rejects its Promise on a bad argument (a verb's `RangeError`) instead of throwing at the call site.
|
|
36
|
+
`parseOwnerRepo` now reads a GitHub Enterprise remote (a host `parseGitUrl` does not know) through `parseRemoteUrl` instead of answering null. `graphql` refuses a document holding a mutation or subscription anywhere (after a fragment, a leading `,`, or a second operation), and a variable named `query` or `operationName`. `prView` passes a scheme-less PR URL (`github.com/o/r/pull/7`) to gh with `https://`, so gh no longer takes it for a branch name. `repoView` treats a hostname of `''` as not set. `createLabel` and `editLabel` refuse a name that starts with `-`. `openPrFor` takes `timeoutMs`.
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- Updated dependencies [b835ae4]
|
|
41
|
+
- Updated dependencies [b835ae4]
|
|
42
|
+
- Updated dependencies [fdef74f]
|
|
43
|
+
- @osdlc/command-seam@0.13.0
|
|
44
|
+
- @osdlc/typed-git@0.9.0
|
|
45
|
+
|
|
3
46
|
## 0.9.6
|
|
4
47
|
|
|
5
48
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -116,8 +116,9 @@ stderr and stdout, as the classifier does.
|
|
|
116
116
|
|
|
117
117
|
Setup checks return their failure as a value. `installed()`, `authenticated()`,
|
|
118
118
|
`authStatus()`, `ready()` and `api()` return their failure as part of their
|
|
119
|
-
value,
|
|
120
|
-
|
|
119
|
+
value, from the plain method and from `try`. They have no soft form:
|
|
120
|
+
`gh.soft.api(...)` throws a `TypeError`. For them, "gh is not installed" is the
|
|
121
|
+
answer to the question they ask.
|
|
121
122
|
|
|
122
123
|
```ts
|
|
123
124
|
import { describe } from '@osdlc/result'
|
|
@@ -169,6 +170,13 @@ if (page.truncated) console.warn('more than 50 open PRs; raise the limit')
|
|
|
169
170
|
`prList`, `issueList`, `labelList`, `repoList` and `searchPrs` all work this
|
|
170
171
|
way. A `limit` that is not a positive integer throws `RangeError`.
|
|
171
172
|
|
|
173
|
+
GitHub search returns at most 1000 results, whatever the `limit`. gh uses
|
|
174
|
+
search for `searchPrs`, for `prList` with a non-empty `search` or an `author`,
|
|
175
|
+
and for `issueList` with a `label`. Those report `truncated: true` when 1000
|
|
176
|
+
rows come back: there may be more that search would not return. Narrow the
|
|
177
|
+
query to see them. `searchPrs` asks gh for at most 1000 rows, since gh refuses
|
|
178
|
+
a larger search limit.
|
|
179
|
+
|
|
172
180
|
## A repository without a clone
|
|
173
181
|
|
|
174
182
|
By default a client talks to the repository of the clone at its directory.
|
|
@@ -375,7 +383,8 @@ const repos = await runVerbAsync(repoList('acme', {}, { hostname: 'ghe.example.c
|
|
|
375
383
|
})
|
|
376
384
|
```
|
|
377
385
|
|
|
378
|
-
|
|
386
|
+
`runVerb` takes a `CommandRunner` and `runVerbAsync` an `AsyncCommandRunner`. A plain
|
|
387
|
+
`(binary, argv, opts) => result` function is not a runner; wrap it yourself as
|
|
379
388
|
`{ run: (argv, opts) => fn('gh', argv, opts) }`.
|
|
380
389
|
|
|
381
390
|
## `AsyncGh`
|
|
@@ -440,7 +449,7 @@ const prs = nodesAt(pages, ['repository', 'pullRequests'])
|
|
|
440
449
|
```
|
|
441
450
|
|
|
442
451
|
With `paginate`, the query must take `$endCursor` and page one connection. gh follows only that
|
|
443
|
-
one. `graphql` refuses a mutation with `RangeError`. Send one through `raw`.
|
|
452
|
+
one. `graphql` refuses a document holding a mutation or subscription with `RangeError`. Send one through `raw`.
|
|
444
453
|
|
|
445
454
|
## Repositories and search
|
|
446
455
|
|
|
@@ -479,6 +488,36 @@ Import the runners themselves, such as `spawnRunner` and `spawnRunnerAsync`, fro
|
|
|
479
488
|
named `gh` in the working directory cannot run in its place. Pass `{ absolutePathOnly: true }` when
|
|
480
489
|
you build a real runner yourself.
|
|
481
490
|
|
|
491
|
+
## Testing
|
|
492
|
+
|
|
493
|
+
`@osdlc/typed-gh/testing` builds the rows a fake runner answers with. Each builder returns gh's raw
|
|
494
|
+
`--json` shape, so the real parsers read it. Its defaults fill every field a parser requires, such
|
|
495
|
+
as a pull request's `number`, a repository's `nameWithOwner` and the `headRefName` that
|
|
496
|
+
`prList({ head })` filters on. Without them a row is dropped with no error.
|
|
497
|
+
|
|
498
|
+
- `prRow`, `issueRow`, `repoRow`, `searchPrRow` and `labelRow`: one row of `gh pr`, `gh issue`,
|
|
499
|
+
`gh repo`, `gh search prs` and `gh label` output.
|
|
500
|
+
- `checkRunRow`, `statusContextRow`, `commentRow` and `reviewRow`: entries of a pull request's
|
|
501
|
+
`statusCheckRollup`, `comments` and `reviews`.
|
|
502
|
+
- `ghResult(payload)`: a `CommandResult` with exit code 0 and the payload as JSON on stdout.
|
|
503
|
+
- Row types: `GhPrJson`, `GhIssueJson`, `GhRepoJson`, `GhSearchPrJson`, `GhLabelJson`,
|
|
504
|
+
`GhCheckRunJson`, `GhStatusContextJson`, `GhCommentJson`, `GhReviewJson` and `GhActorJson`, and
|
|
505
|
+
`RowOverrides<T>`, what a builder takes.
|
|
506
|
+
|
|
507
|
+
Each builder takes typed overrides, merged shallowly. Pass a field as `undefined` to leave it out.
|
|
508
|
+
A pull request's `url` follows its `number`, and a repository's its `nameWithOwner`. Assert on what
|
|
509
|
+
you set: a default value may change in a minor release.
|
|
510
|
+
|
|
511
|
+
```ts
|
|
512
|
+
import { ghResult, prRow } from '@osdlc/typed-gh/testing'
|
|
513
|
+
|
|
514
|
+
const runner: CommandRunner = {
|
|
515
|
+
run: () => ghResult([prRow({ number: 7, headRefName: 'task/foo' }), prRow({ number: 8 })]),
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
new Gh('/repo', { runner }).openPrFor('task/foo')?.number // 7
|
|
519
|
+
```
|
|
520
|
+
|
|
482
521
|
## API
|
|
483
522
|
|
|
484
523
|
Everything below is exported from `@osdlc/typed-gh`. The Stability section says which items are
|
|
@@ -547,7 +586,9 @@ Each verb takes an optional `GhTarget` last. Each client method has a verb of th
|
|
|
547
586
|
- `parsePullRequest`: reads a `gh --json` payload into a `PullRequest`, or `null`.
|
|
548
587
|
- `anyCheckFailed`: true if any check in a list failed.
|
|
549
588
|
- `normalizeAuthorLogin`: drops a `[bot]` suffix from a login.
|
|
550
|
-
- `parseOwnerRepo`: reads a remote URL into `{ owner, repo }`, or `null`.
|
|
589
|
+
- `parseOwnerRepo`: reads a remote URL into `{ owner, repo }`, or `null`. It is
|
|
590
|
+
`@osdlc/typed-git`'s `parseRemoteRepo` without the host, so any host's remote reads, not only
|
|
591
|
+
GitHub's. Use `parseRemoteRepo` when you need the host too.
|
|
551
592
|
- `ownerRepoSlug`: writes `{ owner, repo }` as `owner/repo`.
|
|
552
593
|
- `parsePrUrl`: reads a pull request URL on any GitHub host into its host, owner, repository and
|
|
553
594
|
number, or `null`.
|
|
@@ -588,7 +629,8 @@ The experimental items:
|
|
|
588
629
|
- `GraphqlOptions`
|
|
589
630
|
- `GraphqlVariable`
|
|
590
631
|
|
|
591
|
-
Every other export and method is stable, `prDiff` included.
|
|
632
|
+
Every other export and method is stable, `prDiff` included. So is `@osdlc/typed-gh/testing`: its
|
|
633
|
+
builders and their row types. Their default values may change in any minor release.
|
|
592
634
|
|
|
593
635
|
## Platforms
|
|
594
636
|
|
package/SECURITY.md
CHANGED
|
@@ -42,9 +42,10 @@ not intend are in scope.
|
|
|
42
42
|
value into a REST path. Pass endpoints you built, not text from a user.
|
|
43
43
|
`issueCommentEdit(commentId)` accepts only digits and throws `RangeError`
|
|
44
44
|
for anything else, so its id cannot change the path.
|
|
45
|
-
-
|
|
46
|
-
remotes, or `GH_REPO`. A checkout whose
|
|
47
|
-
another repository sends mutations there.
|
|
45
|
+
- **Without a `repo` option, `gh` chooses the repository.** It reads the
|
|
46
|
+
working directory's git remotes, or `GH_REPO`. A checkout whose
|
|
47
|
+
`.git/config` points `origin` at another repository sends mutations there.
|
|
48
|
+
Pass `repo` (and `hostname`) to name the target in argv. `ownerRepo` only parses a URL you
|
|
48
49
|
give it. It does not check where `gh` will act.
|
|
49
50
|
- **`raw` and `rawChecked` run any `gh` command.** They have no allow list.
|
|
50
51
|
- **`CommandFailed` carries the full argv.** Its message quotes the arguments,
|
package/dist/async.js
CHANGED
|
@@ -79,12 +79,15 @@ export class AsyncGh {
|
|
|
79
79
|
return (await this.exec(verb, call));
|
|
80
80
|
}
|
|
81
81
|
// ── queries ───────────────────────────────────────────────────────────────
|
|
82
|
+
//
|
|
83
|
+
// Every method that builds a verb is `async`, so a bad argument (a verb's
|
|
84
|
+
// RangeError) rejects the Promise instead of throwing at the call site.
|
|
82
85
|
/** Pull requests matching `opts`. Rejects when gh fails. `soft.prList` resolves null instead. */
|
|
83
|
-
prList(opts = {}) {
|
|
86
|
+
async prList(opts = {}) {
|
|
84
87
|
return this.read(v.prList(opts, this.target), opts);
|
|
85
88
|
}
|
|
86
89
|
/** One PR by number, branch or URL, with the heavy fields, or null. See `Gh.prView`. */
|
|
87
|
-
prView(ref, opts = {}) {
|
|
90
|
+
async prView(ref, opts = {}) {
|
|
88
91
|
return this.exec(v.prView(ref, opts.fields ?? v.PR_VIEW_FIELDS, this.target), opts);
|
|
89
92
|
}
|
|
90
93
|
/** The one PR that `input` names, or null. See `Gh.prFind`. */
|
|
@@ -127,11 +130,11 @@ export class AsyncGh {
|
|
|
127
130
|
return last;
|
|
128
131
|
}
|
|
129
132
|
/** A PR's body text. Rejects when gh fails. */
|
|
130
|
-
prBody(number, opts = {}) {
|
|
133
|
+
async prBody(number, opts = {}) {
|
|
131
134
|
return this.read(v.prBody(number, this.target), opts);
|
|
132
135
|
}
|
|
133
136
|
/** A REST GET through `gh api`. Failure is `ok: false`, never a rejection. */
|
|
134
|
-
api(endpoint, opts = {}) {
|
|
137
|
+
async api(endpoint, opts = {}) {
|
|
135
138
|
return this.exec(v.api(endpoint, opts, this.target), opts);
|
|
136
139
|
}
|
|
137
140
|
/**
|
|
@@ -139,41 +142,41 @@ export class AsyncGh {
|
|
|
139
142
|
*
|
|
140
143
|
* @experimental
|
|
141
144
|
*/
|
|
142
|
-
graphql(query, variables = {}, opts = {}) {
|
|
145
|
+
async graphql(query, variables = {}, opts = {}) {
|
|
143
146
|
return this.read(v.graphql(query, variables, opts, this.target), opts);
|
|
144
147
|
}
|
|
145
148
|
/** Every label on the repository (up to `opts.limit`, default 1000). Rejects when gh fails. */
|
|
146
|
-
labelList(opts = {}) {
|
|
149
|
+
async labelList(opts = {}) {
|
|
147
150
|
return this.read(v.labelList(opts, this.target), opts);
|
|
148
151
|
}
|
|
149
152
|
/** Issues matching `opts`. Rejects when gh fails. */
|
|
150
|
-
issueList(opts = {}) {
|
|
153
|
+
async issueList(opts = {}) {
|
|
151
154
|
return this.read(v.issueList(opts, this.target), opts);
|
|
152
155
|
}
|
|
153
156
|
/** One issue with its labels, or null when there is no such issue. */
|
|
154
|
-
issueView(number, opts = {}) {
|
|
157
|
+
async issueView(number, opts = {}) {
|
|
155
158
|
return this.exec(v.issueView(number, this.target), opts);
|
|
156
159
|
}
|
|
157
160
|
/**
|
|
158
161
|
* An owner's repositories. Rejects when gh fails. Each row's `owner` is in
|
|
159
162
|
* GitHub's casing, not as passed. See `Gh.repoList`.
|
|
160
163
|
*/
|
|
161
|
-
repoList(owner, opts = {}) {
|
|
164
|
+
async repoList(owner, opts = {}) {
|
|
162
165
|
return this.read(v.repoList(owner, opts, this.target), opts);
|
|
163
166
|
}
|
|
164
167
|
/**
|
|
165
168
|
* One repository, or null when there is no such repository. `owner` and
|
|
166
169
|
* `name` are in GitHub's casing, not as passed. See `Gh.repoView`.
|
|
167
170
|
*/
|
|
168
|
-
repoView(repo, opts = {}) {
|
|
171
|
+
async repoView(repo, opts = {}) {
|
|
169
172
|
return this.exec(v.repoView(repo, opts.fields ?? v.REPO_FIELDS, this.target), opts);
|
|
170
173
|
}
|
|
171
174
|
/** Pull requests across repositories matching a search query. Rejects when gh fails. See `Gh.searchPrs`. */
|
|
172
|
-
searchPrs(query, opts = {}) {
|
|
175
|
+
async searchPrs(query, opts = {}) {
|
|
173
176
|
return this.read(v.searchPrs(query, opts, this.target), opts);
|
|
174
177
|
}
|
|
175
178
|
/** True iff the `gh` binary is on PATH and runs. */
|
|
176
|
-
installed() {
|
|
179
|
+
async installed() {
|
|
177
180
|
return this.exec(v.installed());
|
|
178
181
|
}
|
|
179
182
|
/** True iff `gh` holds credentials. */
|
|
@@ -181,7 +184,7 @@ export class AsyncGh {
|
|
|
181
184
|
return (await this.authStatus()).ok;
|
|
182
185
|
}
|
|
183
186
|
/** Login state and what `gh auth status` printed. */
|
|
184
|
-
authStatus() {
|
|
187
|
+
async authStatus() {
|
|
185
188
|
return this.exec(v.authStatus(this.target));
|
|
186
189
|
}
|
|
187
190
|
/** True iff `gh` is installed and authenticated. Skips the auth check when `gh` is not installed. */
|
|
@@ -197,23 +200,23 @@ export class AsyncGh {
|
|
|
197
200
|
}
|
|
198
201
|
// ── mutations (checked) ───────────────────────────────────────────────────
|
|
199
202
|
/** Open a PR and resolve to its URL ('' if gh printed none). */
|
|
200
|
-
createPr(opts) {
|
|
203
|
+
async createPr(opts) {
|
|
201
204
|
return this.exec(v.createPr(opts, this.target));
|
|
202
205
|
}
|
|
203
206
|
/** Retitle or rewrite a PR body. */
|
|
204
|
-
editPr(number, opts) {
|
|
207
|
+
async editPr(number, opts) {
|
|
205
208
|
return this.exec(v.editPr(number, opts, this.target));
|
|
206
209
|
}
|
|
207
210
|
/** Rewrite just a PR's body. */
|
|
208
|
-
editPrBody(number, body) {
|
|
211
|
+
async editPrBody(number, body) {
|
|
209
212
|
return this.exec(v.editPrBody(number, body, this.target));
|
|
210
213
|
}
|
|
211
214
|
/** The PR's unified diff. Rejects with the classified failure when gh cannot produce it. */
|
|
212
|
-
prDiff(number) {
|
|
215
|
+
async prDiff(number) {
|
|
213
216
|
return this.exec(v.prDiff(number, this.target));
|
|
214
217
|
}
|
|
215
218
|
/** Post one conversation comment on a PR. Resolves to its URL ('' if gh printed none). */
|
|
216
|
-
prComment(number, body) {
|
|
219
|
+
async prComment(number, body) {
|
|
217
220
|
return this.exec(v.prComment(number, body, this.target));
|
|
218
221
|
}
|
|
219
222
|
/**
|
|
@@ -221,15 +224,15 @@ export class AsyncGh {
|
|
|
221
224
|
*
|
|
222
225
|
* @experimental
|
|
223
226
|
*/
|
|
224
|
-
createReview(number, opts) {
|
|
227
|
+
async createReview(number, opts) {
|
|
225
228
|
return this.exec(v.createReview(number, opts, this.target));
|
|
226
229
|
}
|
|
227
230
|
/** Bring a PR's head branch up to date with its base. */
|
|
228
|
-
prUpdateBranch(number, style = 'merge') {
|
|
231
|
+
async prUpdateBranch(number, style = 'merge') {
|
|
229
232
|
return this.exec(v.prUpdateBranch(number, style, this.target));
|
|
230
233
|
}
|
|
231
234
|
/** Mark a PR ready for review (`draft: false`) or convert it to a draft (`draft: true`). */
|
|
232
|
-
prSetDraft(number, draft) {
|
|
235
|
+
async prSetDraft(number, draft) {
|
|
233
236
|
return this.exec(v.prSetDraft(number, draft, this.target));
|
|
234
237
|
}
|
|
235
238
|
/**
|
|
@@ -238,27 +241,26 @@ export class AsyncGh {
|
|
|
238
241
|
* @experimental
|
|
239
242
|
*/
|
|
240
243
|
async issueCommentEdit(commentId, body) {
|
|
241
|
-
// async, so a bad id rejects the Promise instead of throwing at the call site.
|
|
242
244
|
return this.exec(v.issueCommentEdit(commentId, body, this.target));
|
|
243
245
|
}
|
|
244
246
|
/** Merge a PR. GitHub refuses when its head is no longer `opts.headSha`. */
|
|
245
|
-
prMerge(number, opts) {
|
|
247
|
+
async prMerge(number, opts) {
|
|
246
248
|
return this.exec(v.prMerge(number, opts, this.target));
|
|
247
249
|
}
|
|
248
250
|
/** A PR's review threads. Rejects when they could not be read completely. `soft.prReviewThreads` resolves null instead. */
|
|
249
|
-
prReviewThreads(number) {
|
|
251
|
+
async prReviewThreads(number) {
|
|
250
252
|
return this.read(v.prReviewThreads(number, this.target));
|
|
251
253
|
}
|
|
252
254
|
/** Create a label (or force it to `opts`'s color/description if it exists). */
|
|
253
|
-
createLabel(opts) {
|
|
255
|
+
async createLabel(opts) {
|
|
254
256
|
return this.exec(v.createLabel(opts, this.target));
|
|
255
257
|
}
|
|
256
258
|
/** Recolor and/or redescribe an existing label. */
|
|
257
|
-
editLabel(name, opts) {
|
|
259
|
+
async editLabel(name, opts) {
|
|
258
260
|
return this.exec(v.editLabel(name, opts, this.target));
|
|
259
261
|
}
|
|
260
262
|
/** Swap an issue's label in a single `gh issue edit` call. */
|
|
261
|
-
setIssueLabel(number, currentLabel, newLabel) {
|
|
263
|
+
async setIssueLabel(number, currentLabel, newLabel) {
|
|
262
264
|
return this.exec(v.setIssueLabel(number, currentLabel, newLabel, this.target));
|
|
263
265
|
}
|
|
264
266
|
}
|
package/dist/doors.d.ts
CHANGED
|
@@ -27,12 +27,17 @@ import type { GhVerb } from './verbs.ts';
|
|
|
27
27
|
* Read the result of running `verb` as the methods that throw do.
|
|
28
28
|
*
|
|
29
29
|
* A checked verb has already thrown on a non-zero exit (the runner is
|
|
30
|
-
*
|
|
30
|
+
* {@link runCheckedGh}). A probe (`installed`, `authStatus`, `api`) carries its
|
|
31
31
|
* failure in its value. A read that exited non-zero throws the classified
|
|
32
32
|
* failure, unless the verb's `absent` says gh's answer was "there is no such
|
|
33
33
|
* thing", which `parse` turns into `null`. A read that exited 0 but parsed to `null`
|
|
34
34
|
* could not be read completely, and throws.
|
|
35
35
|
*
|
|
36
|
+
* A result flagged `outputIncomplete` throws for every verb but a probe, as
|
|
37
|
+
* typed-git's `gate` does: gh exited, but the runner stopped reading at the
|
|
38
|
+
* output cap or a timeout, so a parse would take cut-short output as whole.
|
|
39
|
+
* Both read a result through command-seam's `runFailed`.
|
|
40
|
+
*
|
|
36
41
|
* @throws `FailureError` carrying the classified {@link GhFailure} for a failed run.
|
|
37
42
|
* @throws `FailureError` carrying an `unreadable` {@link GhFailure}.
|
|
38
43
|
*/
|
|
@@ -43,9 +48,14 @@ export declare function settle<T>(verb: GhVerb<T>, result: CommandResult, binary
|
|
|
43
48
|
* `CommandFailed` as its `cause`. Anything else is returned as it is.
|
|
44
49
|
*/
|
|
45
50
|
export declare function ghFailureError(error: unknown): unknown;
|
|
46
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Run `argv` and throw {@link ghFailureError}'s classified failure unless gh
|
|
53
|
+
* exited 0 with its whole output. Output cut short (`outputIncomplete`) is a
|
|
54
|
+
* failure too, which `runChecked` alone would not catch: it checks only the
|
|
55
|
+
* exit code.
|
|
56
|
+
*/
|
|
47
57
|
export declare function runCheckedGh(runner: CommandRunner, argv: string[], opts: CommandRunOptions): CommandResult;
|
|
48
|
-
/** {@link
|
|
58
|
+
/** {@link runCheckedGh} for an awaitable runner, rejecting with the classified failure. */
|
|
49
59
|
export declare function runCheckedGhAsync(runner: AsyncCommandRunner, argv: string[], opts: AsyncCommandRunOptions): Promise<CommandResult>;
|
|
50
60
|
/** The members `try` and `soft` leave out: `raw`, `rawChecked`, helpers that run nothing, and properties. */
|
|
51
61
|
type Triable<C> = Omit<C, 'raw' | 'rawChecked' | 'at' | 'withRepo' | 'ownerRepo' | 'try' | 'soft' | 'cwd' | 'repo' | 'hostname'>;
|
|
@@ -73,7 +83,10 @@ export type Soft<C> = {
|
|
|
73
83
|
export declare function failureOf(error: {
|
|
74
84
|
failure: Failure;
|
|
75
85
|
}): GhFailure;
|
|
76
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Wrap a synchronous client so every method returns a `Result<T, GhFailure>`.
|
|
88
|
+
* The proxy is command-seam's `tryMethods`, so a new method needs no entry.
|
|
89
|
+
*/
|
|
77
90
|
export declare function triedSync<C extends object>(client: C): TriedSync<C>;
|
|
78
91
|
/** Wrap an awaitable client so every method promises a `Result<T, GhFailure>`. */
|
|
79
92
|
export declare function triedAsync<C extends object>(client: C): TriedAsync<C>;
|
package/dist/doors.js
CHANGED
|
@@ -19,30 +19,39 @@
|
|
|
19
19
|
* `try` and `soft` are proxies over the whole client, so a new method needs no
|
|
20
20
|
* change here.
|
|
21
21
|
*/
|
|
22
|
-
import { CommandFailed, isCommandFailed,
|
|
23
|
-
import {
|
|
22
|
+
import { CommandFailed, isCommandFailed, methodFacade, runFailed, tryMethods, tryMethodsAsync, } from '@osdlc/command-seam';
|
|
23
|
+
import { FailureError, isFailure } from '@osdlc/result';
|
|
24
24
|
import { classifyGhFailure, unreadableFailure } from "./failure.js";
|
|
25
25
|
/**
|
|
26
26
|
* Read the result of running `verb` as the methods that throw do.
|
|
27
27
|
*
|
|
28
28
|
* A checked verb has already thrown on a non-zero exit (the runner is
|
|
29
|
-
*
|
|
29
|
+
* {@link runCheckedGh}). A probe (`installed`, `authStatus`, `api`) carries its
|
|
30
30
|
* failure in its value. A read that exited non-zero throws the classified
|
|
31
31
|
* failure, unless the verb's `absent` says gh's answer was "there is no such
|
|
32
32
|
* thing", which `parse` turns into `null`. A read that exited 0 but parsed to `null`
|
|
33
33
|
* could not be read completely, and throws.
|
|
34
34
|
*
|
|
35
|
+
* A result flagged `outputIncomplete` throws for every verb but a probe, as
|
|
36
|
+
* typed-git's `gate` does: gh exited, but the runner stopped reading at the
|
|
37
|
+
* output cap or a timeout, so a parse would take cut-short output as whole.
|
|
38
|
+
* Both read a result through command-seam's `runFailed`.
|
|
39
|
+
*
|
|
35
40
|
* @throws `FailureError` carrying the classified {@link GhFailure} for a failed run.
|
|
36
41
|
* @throws `FailureError` carrying an `unreadable` {@link GhFailure}.
|
|
37
42
|
*/
|
|
38
43
|
export function settle(verb, result, binary) {
|
|
39
|
-
if (verb.
|
|
44
|
+
if (verb.probe === true)
|
|
40
45
|
return verb.parse(result);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
// A checked verb's runner has already thrown on a non-zero exit.
|
|
47
|
+
const failed = verb.checked
|
|
48
|
+
? result.outputIncomplete === true
|
|
49
|
+
: runFailed(result, (r) => verb.absent?.(r) === true);
|
|
50
|
+
if (failed)
|
|
44
51
|
throw ghFailureError(new CommandFailed(verb.argv, result, binary));
|
|
45
|
-
|
|
52
|
+
// A non-zero exit that got this far is gh's "there is no such thing".
|
|
53
|
+
if (verb.checked || result.exitCode !== 0)
|
|
54
|
+
return verb.parse(result);
|
|
46
55
|
const value = verb.parse(result);
|
|
47
56
|
if (value === null) {
|
|
48
57
|
throw new FailureError(unreadableFailure(verb.argv, result.stdout, binary));
|
|
@@ -57,24 +66,35 @@ export function settle(verb, result, binary) {
|
|
|
57
66
|
export function ghFailureError(error) {
|
|
58
67
|
return isCommandFailed(error) ? new FailureError(classifyGhFailure(error)) : error;
|
|
59
68
|
}
|
|
60
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Run `argv` and throw {@link ghFailureError}'s classified failure unless gh
|
|
71
|
+
* exited 0 with its whole output. Output cut short (`outputIncomplete`) is a
|
|
72
|
+
* failure too, which `runChecked` alone would not catch: it checks only the
|
|
73
|
+
* exit code.
|
|
74
|
+
*/
|
|
61
75
|
export function runCheckedGh(runner, argv, opts) {
|
|
62
76
|
try {
|
|
63
|
-
return
|
|
77
|
+
return whole(runner.run(argv, opts), argv, runner.binary);
|
|
64
78
|
}
|
|
65
79
|
catch (error) {
|
|
66
80
|
throw ghFailureError(error);
|
|
67
81
|
}
|
|
68
82
|
}
|
|
69
|
-
/** {@link
|
|
83
|
+
/** {@link runCheckedGh} for an awaitable runner, rejecting with the classified failure. */
|
|
70
84
|
export async function runCheckedGhAsync(runner, argv, opts) {
|
|
71
85
|
try {
|
|
72
|
-
return await
|
|
86
|
+
return whole(await runner.run(argv, opts), argv, runner.binary);
|
|
73
87
|
}
|
|
74
88
|
catch (error) {
|
|
75
89
|
throw ghFailureError(error);
|
|
76
90
|
}
|
|
77
91
|
}
|
|
92
|
+
/** `result`, or a `CommandFailed` thrown when it is a failure (command-seam's `runFailed`). */
|
|
93
|
+
function whole(result, argv, binary) {
|
|
94
|
+
if (runFailed(result))
|
|
95
|
+
throw new CommandFailed(argv, result, binary);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
78
98
|
/**
|
|
79
99
|
* The queries that have a soft form. A mutation has none. If a write's failure
|
|
80
100
|
* were dropped, the caller would believe the write worked.
|
|
@@ -110,45 +130,20 @@ export function failureOf(error) {
|
|
|
110
130
|
const thrown = error;
|
|
111
131
|
return isCommandFailed(thrown) ? classifyGhFailure(thrown) : error.failure;
|
|
112
132
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// Read from the target, not the proxy. A getter (`try` itself) must run
|
|
117
|
-
// against the real client, not recurse through the proxy.
|
|
118
|
-
const value = Reflect.get(target, prop);
|
|
119
|
-
if (typeof value !== 'function')
|
|
120
|
-
return value;
|
|
121
|
-
const method = value;
|
|
122
|
-
const name = String(prop);
|
|
123
|
-
return (...args) => wrap(() => method.apply(target, args), name);
|
|
124
|
-
},
|
|
125
|
-
});
|
|
133
|
+
/** The failure a thrown value carries, or `null` for a bug to rethrow. */
|
|
134
|
+
function recognize(error) {
|
|
135
|
+
return isGhFailure(error) ? failureOf(error) : null;
|
|
126
136
|
}
|
|
127
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Wrap a synchronous client so every method returns a `Result<T, GhFailure>`.
|
|
139
|
+
* The proxy is command-seam's `tryMethods`, so a new method needs no entry.
|
|
140
|
+
*/
|
|
128
141
|
export function triedSync(client) {
|
|
129
|
-
return
|
|
130
|
-
try {
|
|
131
|
-
return ok(call());
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
if (isGhFailure(error))
|
|
135
|
-
return err(failureOf(error));
|
|
136
|
-
throw error;
|
|
137
|
-
}
|
|
138
|
-
});
|
|
142
|
+
return tryMethods(client, recognize);
|
|
139
143
|
}
|
|
140
144
|
/** Wrap an awaitable client so every method promises a `Result<T, GhFailure>`. */
|
|
141
145
|
export function triedAsync(client) {
|
|
142
|
-
return
|
|
143
|
-
try {
|
|
144
|
-
return ok(await call());
|
|
145
|
-
}
|
|
146
|
-
catch (error) {
|
|
147
|
-
if (isGhFailure(error))
|
|
148
|
-
return err(failureOf(error));
|
|
149
|
-
throw error;
|
|
150
|
-
}
|
|
151
|
-
});
|
|
146
|
+
return tryMethodsAsync(client, recognize);
|
|
152
147
|
}
|
|
153
148
|
function checkSoft(name) {
|
|
154
149
|
if (!SOFT_METHODS.includes(name)) {
|
|
@@ -157,7 +152,7 @@ function checkSoft(name) {
|
|
|
157
152
|
}
|
|
158
153
|
/** Wrap a synchronous client so a failed query returns `null`. */
|
|
159
154
|
export function softSync(client) {
|
|
160
|
-
return
|
|
155
|
+
return methodFacade(client, (call, name) => {
|
|
161
156
|
checkSoft(name);
|
|
162
157
|
try {
|
|
163
158
|
return call();
|
|
@@ -171,7 +166,7 @@ export function softSync(client) {
|
|
|
171
166
|
}
|
|
172
167
|
/** Wrap an awaitable client so a failed query resolves to `null`. */
|
|
173
168
|
export function softAsync(client) {
|
|
174
|
-
return
|
|
169
|
+
return methodFacade(client, async (call, name) => {
|
|
175
170
|
checkSoft(name);
|
|
176
171
|
try {
|
|
177
172
|
return await call();
|
package/dist/failure.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* in English. A message that matches none stays the subprocess layer's `exit`
|
|
8
8
|
* with `gh`'s first stderr line, never a wrong kind.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { classifyFailed } from '@osdlc/command-seam';
|
|
11
11
|
import {} from '@osdlc/result';
|
|
12
12
|
const INSTALL_HINT = 'Install it from https://cli.github.com, then run `gh auth login`.';
|
|
13
13
|
const RATE_LIMITED = /API rate limit exceeded|secondary rate limit|rate limit exceeded|HTTP 429/i;
|
|
@@ -37,13 +37,8 @@ export function isAbsent(thing, text) {
|
|
|
37
37
|
export function classifyGhFailure(failed) {
|
|
38
38
|
// The client's runner may not name its binary. It is gh either way, so the
|
|
39
39
|
// messages say `gh` rather than "The command".
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
: failed.failure;
|
|
43
|
-
const base = { ...f, cause: failed };
|
|
44
|
-
if (f.kind === 'not-installed')
|
|
45
|
-
return { ...base, hint: INSTALL_HINT };
|
|
46
|
-
if (f.kind !== 'exit')
|
|
40
|
+
const base = classifyFailed(failed, { binary: 'gh', installHint: INSTALL_HINT });
|
|
41
|
+
if (base.kind !== 'exit')
|
|
47
42
|
return base;
|
|
48
43
|
const text = `${failed.result.stderr}\n${failed.result.stdout}`;
|
|
49
44
|
if (RATE_LIMITED.test(text)) {
|
package/dist/parse.js
CHANGED
|
@@ -266,6 +266,14 @@ export function parsePrSearchHit(raw) {
|
|
|
266
266
|
labels: parseLabels(raw['labels']),
|
|
267
267
|
createdAt: strOrNull(raw['createdAt']),
|
|
268
268
|
updatedAt: strOrNull(raw['updatedAt']),
|
|
269
|
-
closedAt:
|
|
269
|
+
closedAt: searchTime(raw['closedAt']),
|
|
270
270
|
};
|
|
271
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* gh search prints Go's zero time (`0001-01-01T00:00:00Z`) for a time that is
|
|
274
|
+
* not set, such as `closedAt` on an open pull request. That is no time.
|
|
275
|
+
*/
|
|
276
|
+
function searchTime(x) {
|
|
277
|
+
const s = strOrNull(x);
|
|
278
|
+
return s === null || s.startsWith('0001-01-01') ? null : s;
|
|
279
|
+
}
|
package/dist/repo.d.ts
CHANGED
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
* Reading the GitHub owner and repository from a git remote, and a pull
|
|
3
3
|
* request from a URL or what a person typed.
|
|
4
4
|
*
|
|
5
|
-
* `parseOwnerRepo`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* `parseOwnerRepo` is `@osdlc/typed-git`'s `parseRemoteRepo` without the
|
|
6
|
+
* host, so it reads every remote form typed-git reads (scp-style, a trailing
|
|
7
|
+
* `.git`, an `ssh://` port, a bare `owner/repo`, an Enterprise host) and no
|
|
8
|
+
* caller writes its own regex over `remote.origin.url`.
|
|
9
9
|
*/
|
|
10
10
|
import type { OwnerRepo } from './types.ts';
|
|
11
11
|
/**
|
|
12
12
|
* `{owner, repo}` from any git remote reference, or null when it names no
|
|
13
13
|
* project. Handles https, ssh, scp-style, and bare `owner/repo`.
|
|
14
|
+
*
|
|
15
|
+
* It is typed-git's `parseRemoteRepo` without the host: a clone URL on a host
|
|
16
|
+
* `parseGitUrl` does not know, such as a GitHub Enterprise host or another
|
|
17
|
+
* forge's `host/owner/repo`, is read host-agnostically. It does not check
|
|
18
|
+
* that the host is GitHub; read `parseRemoteRepo(url).host` for that.
|
|
14
19
|
*/
|
|
15
20
|
export declare function parseOwnerRepo(remoteUrl: string): OwnerRepo | null;
|
|
16
21
|
/** `owner/repo`, the form `gh api` paths and `--repo` both take. */
|
package/dist/repo.js
CHANGED
|
@@ -2,24 +2,24 @@
|
|
|
2
2
|
* Reading the GitHub owner and repository from a git remote, and a pull
|
|
3
3
|
* request from a URL or what a person typed.
|
|
4
4
|
*
|
|
5
|
-
* `parseOwnerRepo`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* `parseOwnerRepo` is `@osdlc/typed-git`'s `parseRemoteRepo` without the
|
|
6
|
+
* host, so it reads every remote form typed-git reads (scp-style, a trailing
|
|
7
|
+
* `.git`, an `ssh://` port, a bare `owner/repo`, an Enterprise host) and no
|
|
8
|
+
* caller writes its own regex over `remote.origin.url`.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { parseRemoteRepo } from '@osdlc/typed-git';
|
|
11
11
|
/**
|
|
12
12
|
* `{owner, repo}` from any git remote reference, or null when it names no
|
|
13
13
|
* project. Handles https, ssh, scp-style, and bare `owner/repo`.
|
|
14
|
+
*
|
|
15
|
+
* It is typed-git's `parseRemoteRepo` without the host: a clone URL on a host
|
|
16
|
+
* `parseGitUrl` does not know, such as a GitHub Enterprise host or another
|
|
17
|
+
* forge's `host/owner/repo`, is read host-agnostically. It does not check
|
|
18
|
+
* that the host is GitHub; read `parseRemoteRepo(url).host` for that.
|
|
14
19
|
*/
|
|
15
20
|
export function parseOwnerRepo(remoteUrl) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
return null;
|
|
19
|
-
const { user, project } = info;
|
|
20
|
-
if (!user || !project)
|
|
21
|
-
return null;
|
|
22
|
-
return { owner: user, repo: project };
|
|
21
|
+
const remote = parseRemoteRepo(remoteUrl);
|
|
22
|
+
return remote === null ? null : { owner: remote.owner, repo: remote.repo };
|
|
23
23
|
}
|
|
24
24
|
/** `owner/repo`, the form `gh api` paths and `--repo` both take. */
|
|
25
25
|
export function ownerRepoSlug(r) {
|