@osdlc/typed-gh 0.5.0 → 0.7.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 +61 -1
- package/CONTRIBUTING.md +4 -3
- package/README.md +247 -56
- package/dist/async.d.ts +70 -28
- package/dist/async.js +127 -44
- package/dist/doors.d.ts +82 -0
- package/dist/doors.js +183 -0
- package/dist/failure.d.ts +59 -0
- package/dist/failure.js +93 -0
- package/dist/index.d.ts +12 -8
- package/dist/index.js +9 -4
- package/dist/parse.d.ts +5 -2
- package/dist/parse.js +48 -0
- package/dist/repo.d.ts +42 -11
- package/dist/repo.js +76 -12
- package/dist/sync.d.ts +104 -28
- package/dist/sync.js +155 -46
- package/dist/types.d.ts +37 -1
- package/dist/verbs.d.ts +213 -45
- package/dist/verbs.js +504 -86
- package/package.json +13 -17
- package/async.ts +0 -285
- package/dist/async.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/parse.d.ts.map +0 -1
- package/dist/repo.d.ts.map +0 -1
- package/dist/sync.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/verbs.d.ts.map +0 -1
- package/index.ts +0 -97
- package/parse.ts +0 -252
- package/repo.ts +0 -59
- package/sync.ts +0 -326
- package/types.ts +0 -186
- package/verbs.ts +0 -799
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,64 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @osdlc/typed-gh
|
|
2
|
+
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d9b0f77: **Breaking:** the plain door throws the classified failure. A failed `gh` (a mutation, `prDiff`, `rawChecked`, a query) now throws a `FailureError` whose `failure` is the `GhFailure` and whose `failure.cause` is the `CommandFailed`, where it threw the `CommandFailed` itself. The bare `CommandFailed` only had command-seam's generic `exit` kind, so a signed-out `gh` during a write exited 1 from `runMain` instead of 23. Read `error.failure.argv` and `error.failure.cause.result` where you read `error.argv` and `error.result`.
|
|
8
|
+
|
|
9
|
+
**Breaking:** the single-thing queries no longer treat every `Could not resolve to a …` as absence. `prView` answers `null` only for a missing pull request, `issueView` only for a missing issue and `repoView` only for a missing repository; a mistyped `--repo` is now a `not-found` failure, and a bare `HTTP 404` is a failure. The absence test reads stderr and stdout, as the classifier does.
|
|
10
|
+
|
|
11
|
+
An `HTTP 403` is now `not-authenticated` (SAML, a missing scope), the same as Forgejo's 401 and 403, unless the text says it is a rate limit, which stays `rate-limited`. The `try` and `soft` doors recognise a thrown failure by shape, so two installed copies of `@osdlc/command-seam` or `@osdlc/result` agree.
|
|
12
|
+
- d9b0f77: `Gh` and `AsyncGh` get `typed-git`'s three doors. **Breaking:** a query no longer answers `null` for a failure. The plain method returns its value or throws `CommandFailed` (a `FailureError` carrying a `GhFailure` of kind `unreadable` when `gh` exited 0 but its output could not be read). A query for one thing (`prView`, `issueView`, `repoView`, and `prFind`, `openPrFor`, `prViewSettled` over them) still answers `null` when GitHub says there is no such thing. `prList`, `prBody`, `graphql`, `labelList`, `issueList`, `repoList`, `searchPrs` and `prReviewThreads` now return a non-null value and throw. `installed`, `authenticated`, `authStatus`, `ready` and `api` are unchanged: their failure is part of their value.
|
|
13
|
+
|
|
14
|
+
New: `gh.try.*` returns `Result<T, GhFailure>` from `@osdlc/result`; `gh.soft.*` returns `null` for a failed query, which is the old query behaviour, now opt-in (`TriedSync`, `TriedAsync`, `Soft`). `GhFailure` carries a `kind`, a one-line `message`, a `hint` for a person, `detail`, `argv`, `exitCode` and the `CommandFailed` as `cause`. `GhFailureKind` is every `@osdlc/command-seam` kind plus `not-authenticated`, `network`, `not-found`, `rate-limited` and `unreadable`, read from `gh`'s stderr. `classifyGhFailure(commandFailed)` is exported. `@osdlc/result` is now a dependency.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- fb712e0: Ship the compiled `dist/` only. The `bun` export condition and the TypeScript source are no longer in the package, so Bun, Node and every bundler load the same compiled ESM and `.d.ts` files. Bun still works through the compiled `dist/`.
|
|
19
|
+
- fb712e0: Make every package follow the same conventions. The published manifests and READMEs change; the runtime code and the `dist/` files do not.
|
|
20
|
+
|
|
21
|
+
- `author` is `sksizer` in every manifest (`@osdlc/typed-gh` had none).
|
|
22
|
+
- `engines.node` is no wider than any dependency's. `@osdlc/typed-gh` and `@osdlc/code-host` now declare `^22.22.2 || ^24.15.0 || >=26.0.0`, the range of `@osdlc/typed-git` and its dependency `hosted-git-info`. `@osdlc/cli-tool` and `@osdlc/trust-repo` declare `>=22`, not `>=22.18`: a `dist/` install needs no TypeScript stripping.
|
|
23
|
+
- `@osdlc/tsconfig` and `@osdlc/prettier-config` are ESM with `sideEffects: false`, and ship SECURITY.md and CONTRIBUTING.md. `@osdlc/prettier-config` has an `exports` map (`.` is `index.json`) in place of `main`.
|
|
24
|
+
- Every README has `## Install` and `## Licence` sections, and every CHANGELOG starts `# @osdlc/<name>`.
|
|
25
|
+
- `@osdlc/color` and `@osdlc/markdown-contract` no longer set a coverage threshold.
|
|
26
|
+
- `@osdlc/yaml-splice` reaches its WebAssembly glue through a `#wasm` entry in `imports`. The shipped `dist/` files are the same.
|
|
27
|
+
- Updated dependencies [d9b0f77]
|
|
28
|
+
- Updated dependencies [d9b0f77]
|
|
29
|
+
- Updated dependencies [fb712e0]
|
|
30
|
+
- Updated dependencies [fb712e0]
|
|
31
|
+
- Updated dependencies [d9b0f77]
|
|
32
|
+
- Updated dependencies [d9b0f77]
|
|
33
|
+
- Updated dependencies [d9b0f77]
|
|
34
|
+
- Updated dependencies [d9b0f77]
|
|
35
|
+
- @osdlc/command-seam@0.9.0
|
|
36
|
+
- @osdlc/typed-git@0.6.0
|
|
37
|
+
- @osdlc/result@0.1.0
|
|
38
|
+
|
|
39
|
+
## 0.6.0
|
|
40
|
+
|
|
41
|
+
### Minor Changes
|
|
42
|
+
|
|
43
|
+
- 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:`.
|
|
44
|
+
|
|
45
|
+
- `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`.
|
|
46
|
+
- `hostname` is any GitHub host: `github.com`, GitHub Enterprise Server, or a `<tenant>.ghe.com` host.
|
|
47
|
+
- Breaking: `parsePrUrl` accepts any GitHub host and returns `hostname` in its `PullRequestRef`. Before, it returned null for a host other than `github.com`.
|
|
48
|
+
- `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`.
|
|
49
|
+
- `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.
|
|
50
|
+
- Breaking: `prList({ head })` matches the head branch exactly, through `gh pr list --head`. Before, it passed `--search head:<branch>`, which matched by prefix.
|
|
51
|
+
- 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.
|
|
52
|
+
- `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`.
|
|
53
|
+
- `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`.
|
|
54
|
+
- Every `GhVerb` carries `readOnly`: true for every query and `prDiff`, false for every mutation. `GhVerb` also has an optional `env`.
|
|
55
|
+
- `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`.
|
|
56
|
+
- New types: `GhTarget`, `GhOptions`, `AsyncGhOptions`, `ListResult`, `PrRef`, `Repository`, `RepositoryVisibility`, `PrSearchHit`, `RepoListOptions` and `SearchPrsOptions`.
|
|
57
|
+
|
|
58
|
+
### Patch Changes
|
|
59
|
+
|
|
60
|
+
- Updated dependencies [4d13bd9]
|
|
61
|
+
- @osdlc/typed-git@0.5.3
|
|
2
62
|
|
|
3
63
|
## 0.5.0
|
|
4
64
|
|
package/CONTRIBUTING.md
CHANGED
|
@@ -39,8 +39,9 @@ pnpm --filter @osdlc/typed-gh smoke
|
|
|
39
39
|
|
|
40
40
|
## Running the tests
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
pnpm installs and runs the scripts, and the `test` script runs Vitest under
|
|
43
|
+
Node. Build first, since the workspace packages resolve each other through
|
|
44
|
+
`dist/`. The tests run offline: a fake
|
|
44
45
|
runner stands in for `gh`, so they need no network, no login and no `gh`
|
|
45
46
|
binary.
|
|
46
47
|
|
|
@@ -71,4 +72,4 @@ version and writes the CHANGELOG from the pending changesets. See
|
|
|
71
72
|
call.
|
|
72
73
|
- Add a changeset for a change users can see. See above.
|
|
73
74
|
- Record a design choice in
|
|
74
|
-
[`docs/decisions/`](https://github.com/osdlc/osdlc/tree/main/packages/ts/typed-gh/docs/decisions).
|
|
75
|
+
[`docs/planning/decisions/`](https://github.com/osdlc/osdlc/tree/main/packages/ts/typed-gh/docs/planning/decisions).
|
package/README.md
CHANGED
|
@@ -15,7 +15,9 @@ scripts, release bots and coding agents. It is the GitHub-side companion of
|
|
|
15
15
|
npm install @osdlc/typed-gh
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
`pnpm add @osdlc/typed-gh` and `yarn add @osdlc/typed-gh` work too.
|
|
19
|
+
|
|
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/`), and the `gh` CLI on `PATH`, signed in with `gh auth login`.
|
|
19
21
|
It is supported on macOS and Linux. Windows is not supported yet; it will be once a Windows CI job
|
|
20
22
|
runs the tests.
|
|
21
23
|
|
|
@@ -25,7 +27,7 @@ import { Gh, type PrListOptions, type PullRequest } from '@osdlc/typed-gh'
|
|
|
25
27
|
const gh = new Gh(process.cwd())
|
|
26
28
|
|
|
27
29
|
const filter: PrListOptions = { state: 'open', base: 'main' }
|
|
28
|
-
const prs: PullRequest[] = gh.prList(filter) ?? []
|
|
30
|
+
const prs: PullRequest[] = gh.prList(filter)?.rows ?? []
|
|
29
31
|
```
|
|
30
32
|
|
|
31
33
|
The `examples` folder in the repository runs the same calls as tests.
|
|
@@ -36,8 +38,8 @@ The `examples` folder in the repository runs the same calls as tests.
|
|
|
36
38
|
Octokit.
|
|
37
39
|
- **You sign in with a token, a GitHub App or OAuth.** This package reads no token.
|
|
38
40
|
- **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`.
|
|
41
|
+
- **You need the whole API typed.** This package covers the calls listed below. `api()` is a GET
|
|
42
|
+
and `graphql()` is a query. Anything else goes through `raw`.
|
|
41
43
|
|
|
42
44
|
## How it compares
|
|
43
45
|
|
|
@@ -88,17 +90,138 @@ and `ReviewDecision` (`APPROVED`, `CHANGES_REQUESTED`, `REVIEW_REQUIRED`,
|
|
|
88
90
|
`null`). A value GitHub adds to either reads as `UNKNOWN` or `null`. A check
|
|
89
91
|
that decides whether to merge can then match every member.
|
|
90
92
|
|
|
91
|
-
## Three
|
|
93
|
+
## Three doors, as in `typed-git`, so failure is never ambiguous
|
|
94
|
+
|
|
95
|
+
| | a value | on failure |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| **`gh.query()`** (the plain method) | the value | throws a `FailureError` carrying the classified `GhFailure` (kind `unreadable` when `gh` succeeded but its output cannot be read) |
|
|
98
|
+
| **`gh.try.query()`** | `{ ok: true, value }` | `{ ok: false, error: GhFailure }` |
|
|
99
|
+
| **`gh.soft.query()`** | the value | `null` (queries only; a mutation has no soft form) |
|
|
100
|
+
| **`raw(argv)`** | the raw `CommandResult`; you decide | the same |
|
|
101
|
+
|
|
102
|
+
A query returns a list or a value, never an empty list on failure.
|
|
103
|
+
`prList()` returning `{ rows: [], truncated: false }` means *this repository
|
|
104
|
+
has no open PRs*. A failed call throws, or is `{ ok: false }` through `try`, or
|
|
105
|
+
`null` through `soft`: *we could not ask*. If the two were the same, an
|
|
106
|
+
expired credential would read as "nothing to do".
|
|
107
|
+
|
|
108
|
+
**Absence is an answer.** A query for one thing (`prView`, `issueView`,
|
|
109
|
+
`repoView`, and `prFind` and `openPrFor` over them) returns `null` on every
|
|
110
|
+
door when GitHub says there is no such thing: "Could not resolve to a
|
|
111
|
+
PullRequest" or "no pull requests found for branch" for `prView`, "Could not
|
|
112
|
+
resolve to an Issue" for `issueView`, "Could not resolve to a Repository" for
|
|
113
|
+
`repoView`. `gh.try.prView(99)` is `{ ok: true, value: null }`. Every other
|
|
114
|
+
failure is a failure, including a wrong repository: `prView(7)` against a
|
|
115
|
+
mistyped `--repo` is a `not-found` failure, not an absent PR. The check reads
|
|
116
|
+
stderr and stdout, as the classifier does.
|
|
117
|
+
|
|
118
|
+
**Probes answer.** `installed()`, `authenticated()`, `authStatus()`, `ready()`
|
|
119
|
+
and `api()` return their failure as part of their value, in every door, because
|
|
120
|
+
"gh is not installed" is the answer to the question they ask.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { describe } from '@osdlc/result'
|
|
124
|
+
|
|
125
|
+
const result = gh.try.prList({ limit: 50 })
|
|
126
|
+
if (!result.ok) {
|
|
127
|
+
console.error(describe(result.error)) // e.g. "gh is not signed in to GitHub. Run `gh auth login`."
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `GhFailure`
|
|
132
|
+
|
|
133
|
+
The error of `try` is a `GhFailure`, a `Failure` from `@osdlc/result`: a `kind`
|
|
134
|
+
to branch on, a one-line `message` and a `hint` for a person (`describe(error)`
|
|
135
|
+
joins them), `gh`'s own output in `detail`, the `argv` and `exitCode`, and the
|
|
136
|
+
`CommandFailed` it was read from in `cause`. The plain door throws the same
|
|
137
|
+
failure, wrapped in a `FailureError` (`error.failure`), so a CLI that reports a
|
|
138
|
+
thrown failure, such as `runMain` from `@osdlc/cli-tool`, shows "gh is not
|
|
139
|
+
signed in" for a write and exits with the same code as for a read.
|
|
140
|
+
|
|
141
|
+
| `kind` | When (gh's stderr) | `message` / `hint` |
|
|
142
|
+
| --- | --- | --- |
|
|
143
|
+
| `not-installed` | `gh` is not on `PATH` | `` `gh` is not installed or not on PATH. `` / ``Install it from https://cli.github.com, then run `gh auth login`.`` |
|
|
144
|
+
| `not-authenticated` | `gh auth login`, `HTTP 401`, `HTTP 403` (unless a rate limit), `Bad credentials`, `not logged in` | `gh is not signed in to GitHub.` / ``Run `gh auth login`.`` |
|
|
145
|
+
| `network` | `error connecting to`, `Could not resolve host`, `no such host`, `dial tcp`, `i/o timeout` | `GitHub could not be reached.` / `Check your network connection.` |
|
|
146
|
+
| `not-found` | `Could not resolve to a …`, `HTTP 404`, `no pull requests found for branch` | `GitHub has no such pull request, issue or repository, or you cannot see it.` / `Check the number or name, and that your account can see the repository.` |
|
|
147
|
+
| `rate-limited` | `API rate limit exceeded`, `secondary rate limit`, `HTTP 429` | `GitHub's API rate limit was reached.` / `Wait a few minutes and try again.` |
|
|
148
|
+
| `unreadable` | `gh` exited 0 but its output could not be read completely | `gh answered, but its output could not be read completely.` (no hint, no `cause`) |
|
|
149
|
+
| `not-executable`, `bad-cwd`, `timeout`, `output-cap`, `signal`, `aborted`, `spawn` | as in `@osdlc/command-seam` | command-seam's message, no hint |
|
|
150
|
+
| `exit` | any other non-zero exit | `gh`'s first stderr line |
|
|
151
|
+
|
|
152
|
+
The patterns are `gh`'s English messages; anything else is a plain `exit`,
|
|
153
|
+
never a wrong kind. Rate limiting is checked before authentication, then
|
|
154
|
+
network, then not-found. `classifyGhFailure(commandFailed)` makes the same
|
|
155
|
+
classification for a `CommandFailed` you caught yourself.
|
|
156
|
+
|
|
157
|
+
## A list says when its limit cut it short
|
|
158
|
+
|
|
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 }`: at most `limit`
|
|
161
|
+
rows, and `truncated: true` when there were more.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const page: ListResult<PullRequest> = gh.prList({ limit: 50 })
|
|
165
|
+
|
|
166
|
+
if (page.truncated) console.warn('more than 50 open PRs; raise the limit')
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`prList`, `issueList`, `labelList`, `repoList` and `searchPrs` all work this
|
|
170
|
+
way. A `limit` that is not a positive integer throws `RangeError`.
|
|
171
|
+
|
|
172
|
+
## A repository without a clone
|
|
92
173
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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, so
|
|
176
|
+
no clone is needed:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const acme = new Gh(process.cwd(), { repo: 'acme/widgets', hostname: 'github.acme.com' })
|
|
180
|
+
|
|
181
|
+
acme.prView(42)
|
|
182
|
+
acme.withRepo('acme/gadgets').prList()
|
|
183
|
+
acme.api('repos/{owner}/{repo}/branches')
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The target goes into the argv, so a failure's `argv` shows which repository a
|
|
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}`, and `authStatus`
|
|
189
|
+
asks about that host. `repoList` and `searchPrs` have no host flag in gh, so
|
|
190
|
+
they get `GH_HOST` in the environment. `withRepo` and `at` keep the runner and
|
|
191
|
+
the rest of the target.
|
|
192
|
+
|
|
193
|
+
`hostname` is any GitHub host: `github.com`, a GitHub Enterprise Server host,
|
|
194
|
+
or a `<tenant>.ghe.com` host. gh speaks only GitHub's API, so Forgejo, Gitea
|
|
195
|
+
and GitLab are out of reach. A `repo` that is not `owner/name`, or a
|
|
196
|
+
`hostname` that is not a bare host, throws `RangeError`.
|
|
197
|
+
|
|
198
|
+
## Finding a pull request
|
|
199
|
+
|
|
200
|
+
`prView` takes a number, a branch or a URL. A URL names its own repository
|
|
201
|
+
and host, so the client's target is not added to it.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
gh.prView(42)
|
|
205
|
+
gh.prView('feature/login')
|
|
206
|
+
gh.prView('https://github.acme.com/acme/widgets/pull/42')
|
|
207
|
+
```
|
|
98
208
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
209
|
+
`prFind` takes what a person typed and runs the lookup it needs:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const pr: PullRequest | null = gh.prFind(process.argv[2] ?? '')
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
It reads the text with `parsePrRef`, which starts no process: `42` and `#42`
|
|
216
|
+
are numbers, `owner/name#42` is a number in that repository, a URL is a URL,
|
|
217
|
+
and any other valid branch name is a branch. Bare digits are tried as a PR
|
|
218
|
+
number first and then as a branch, since a branch can be named `42`. A branch
|
|
219
|
+
gives its open PR, or else its newest closed or merged one, so a tool run
|
|
220
|
+
after the merge still finds it.
|
|
221
|
+
|
|
222
|
+
`prList({ head })` matches the head branch exactly: `task/foo` never returns
|
|
223
|
+
`task/foo-2`. Two forks can share a branch name; ask for
|
|
224
|
+
`headRepositoryOwner` to tell them apart.
|
|
102
225
|
|
|
103
226
|
## Two shape hazards it absorbs
|
|
104
227
|
|
|
@@ -143,7 +266,7 @@ about staleness, never the measure of it. Compute that from the commit graph
|
|
|
143
266
|
|
|
144
267
|
## Changing pull requests
|
|
145
268
|
|
|
146
|
-
Mutations throw `
|
|
269
|
+
Mutations throw the classified failure (a `FailureError`) when `gh` fails. `gh.try.editPr(…)` returns the failure as a `Result` instead; a mutation has no `soft` form.
|
|
147
270
|
|
|
148
271
|
```ts
|
|
149
272
|
const url = gh.createPr(options)
|
|
@@ -171,23 +294,23 @@ gh.prMerge(9, { method, headSha: 'abc1234' })
|
|
|
171
294
|
```
|
|
172
295
|
|
|
173
296
|
`prReviewThreads(n)` reads the review threads, so you can look for open ones before you merge. It
|
|
174
|
-
|
|
175
|
-
than 100 threads). That is not the same as "no threads".
|
|
297
|
+
throws when it could not read all of them (a failed call, an unexpected payload, or more
|
|
298
|
+
than 100 threads; `soft.prReviewThreads` is `null`). That is not the same as "no threads".
|
|
176
299
|
|
|
177
300
|
```ts
|
|
178
|
-
const threads: ReviewThread[]
|
|
301
|
+
const threads: ReviewThread[] = gh.prReviewThreads(9)
|
|
179
302
|
|
|
180
|
-
threads
|
|
303
|
+
threads.filter((t) => !t.isResolved)
|
|
181
304
|
```
|
|
182
305
|
|
|
183
306
|
## Issues and labels
|
|
184
307
|
|
|
185
308
|
```ts
|
|
186
|
-
const issue: IssueRow | null = gh.issueView(7)
|
|
187
|
-
const triage: IssueRow
|
|
309
|
+
const issue: IssueRow | null = gh.issueView(7) // null: there is no issue 7
|
|
310
|
+
const triage: ListResult<IssueRow> = gh.issueList({ label: 'status:triage', limit: 50 })
|
|
188
311
|
gh.setIssueLabel(7, 'status:triage', 'status:define')
|
|
189
312
|
|
|
190
|
-
const labels: Label
|
|
313
|
+
const labels: ListResult<Label> = gh.labelList({ limit: 100 })
|
|
191
314
|
gh.createLabel({ name: 'status:triage', color: '1f6feb', description: 'Triage stage' })
|
|
192
315
|
gh.editLabel('status:triage', { description: 'Waiting for triage' })
|
|
193
316
|
```
|
|
@@ -209,7 +332,7 @@ const review = gh.createReview(9, {
|
|
|
209
332
|
gh.issueCommentEdit('4204154439', 'Updated: CI is green.')
|
|
210
333
|
```
|
|
211
334
|
|
|
212
|
-
`prDiff` returns the unified diff and throws
|
|
335
|
+
`prDiff` returns the unified diff and throws the classified failure when `gh` cannot produce it.
|
|
213
336
|
|
|
214
337
|
`createReview` is experimental. It posts a review of kind `COMMENT`: it cannot approve or request
|
|
215
338
|
changes. Each inline comment sits on one line of the new side of the diff. It returns the review's
|
|
@@ -237,9 +360,9 @@ const prs = verb.parse(result)
|
|
|
237
360
|
## `AsyncGh`
|
|
238
361
|
|
|
239
362
|
The awaitable form of `Gh`, with the same methods, for a fan-out that must not block the event loop.
|
|
240
|
-
Same
|
|
241
|
-
|
|
242
|
-
hatches), driven by the same verb table, so the two cannot drift. It takes an
|
|
363
|
+
Same three doors, promised (the plain method rejects with the classified failure,
|
|
364
|
+
`try` resolves to a `Result<T, GhFailure>`, `soft` resolves `null` for a failed
|
|
365
|
+
query, `raw`/`rawChecked` are the hatches), driven by the same verb table, so the two cannot drift. It takes an
|
|
243
366
|
`AsyncCommandRunner` and defaults to `spawnRunnerAsync("gh")`.
|
|
244
367
|
|
|
245
368
|
```ts
|
|
@@ -247,14 +370,15 @@ import { AsyncGh } from '@osdlc/typed-gh'
|
|
|
247
370
|
|
|
248
371
|
const gh = new AsyncGh('/repo')
|
|
249
372
|
const prs = await Promise.all(branches.map((b) => gh.openPrFor(b)))
|
|
250
|
-
const body = await gh.prBody(42) // string
|
|
373
|
+
const body = await gh.prBody(42) // string; rejects when gh fails
|
|
251
374
|
await gh.editPrBody(42, `${body}\n\nfooter`)
|
|
252
375
|
await gh.prComment(42, 'rebased') // the comment's URL, or ''
|
|
253
376
|
await gh.prUpdateBranch(42, 'rebase')
|
|
254
377
|
```
|
|
255
378
|
|
|
256
|
-
These methods take `AsyncCallOptions`: `raw`, `rawChecked`, `prList`, `prView`, `
|
|
257
|
-
`
|
|
379
|
+
These methods take `AsyncCallOptions`: `raw`, `rawChecked`, `prList`, `prView`, `prFind`,
|
|
380
|
+
`openPrFor`, `prViewSettled`, `prBody`, `api`, `graphql`, `labelList`, `issueList`, `issueView`,
|
|
381
|
+
`repoList`, `repoView` and `searchPrs`. The options are `timeoutMs` and an `AbortSignal` in
|
|
258
382
|
`signal`.
|
|
259
383
|
|
|
260
384
|
`api(endpoint, opts)` (on `Gh` and `AsyncGh`) is a REST GET through `gh api`.
|
|
@@ -263,7 +387,50 @@ It never throws; the result is `{ ok, status, etag, body, malformed, stderr, err
|
|
|
263
387
|
was stopped (a missing binary, a timeout).
|
|
264
388
|
Pass `etag` for a conditional request (`If-None-Match`; a 304 comes back as
|
|
265
389
|
`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
|
|
390
|
+
without one. If `gh` prints more than one header block, the last one is read. `{owner}` and
|
|
391
|
+
`{repo}` in the endpoint come from the client's repo, or are filled in by gh from the clone.
|
|
392
|
+
|
|
393
|
+
## Reading every page
|
|
394
|
+
|
|
395
|
+
`api` with `paginate: true` fetches every page and, when each page is an array, joins them into one
|
|
396
|
+
`body`:
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
const branches = gh.api('repos/{owner}/{repo}/branches', { paginate: true }).body
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
`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`): a partial answer is no answer. `nodesAt` joins
|
|
404
|
+
one connection's `nodes` across the pages, and is `null` when any page lacks them.
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
const query = `query($owner: String!, $name: String!, $endCursor: String) {
|
|
408
|
+
repository(owner: $owner, name: $name) {
|
|
409
|
+
pullRequests(first: 100, after: $endCursor) {
|
|
410
|
+
pageInfo { hasNextPage endCursor }
|
|
411
|
+
nodes { number title }
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}`
|
|
415
|
+
const pages = gh.graphql(query, { owner: '{owner}', name: '{repo}' }, { paginate: true })
|
|
416
|
+
const prs = nodesAt(pages, ['repository', 'pullRequests'])
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
With `paginate`, the query must take `$endCursor` and page one connection; gh follows only that
|
|
420
|
+
one. `graphql` refuses a mutation with `RangeError`; send one through `raw`.
|
|
421
|
+
|
|
422
|
+
## Repositories and search
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
const repos: ListResult<Repository> = gh.repoList('acme', { archived: false, limit: 200 })
|
|
426
|
+
const repo: Repository | null = gh.repoView('acme/widgets') // null: no such repository
|
|
427
|
+
const hits: ListResult<PrSearchHit> = gh.searchPrs('TICKET-123', { owner: 'acme' })
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
A `Repository` carries `nameWithOwner`, `owner`, `name`, `description`, `defaultBranch`,
|
|
431
|
+
`visibility`, `isArchived` and `url`. `gh search prs` has its own field set: a `PrSearchHit` names
|
|
432
|
+
its `repository` but has no head or base branch, so it is not a `PullRequest`. Its `state` is
|
|
433
|
+
upper-cased to match. The query takes GitHub's search syntax and may start with `-`.
|
|
267
434
|
|
|
268
435
|
## Injecting the subprocess seam
|
|
269
436
|
|
|
@@ -274,7 +441,7 @@ suite runs offline:
|
|
|
274
441
|
```ts
|
|
275
442
|
const empty: CommandRunner = { run: () => ({ exitCode: 0, stdout: '[]', stderr: '' }) }
|
|
276
443
|
|
|
277
|
-
new Gh('/repo', { runner: empty }).prList() // []
|
|
444
|
+
new Gh('/repo', { runner: empty }).prList() // { rows: [], truncated: false }
|
|
278
445
|
```
|
|
279
446
|
|
|
280
447
|
This package re-exports `CommandFailed` and the runner and result types (`CommandRunner`,
|
|
@@ -289,42 +456,58 @@ experimental.
|
|
|
289
456
|
|
|
290
457
|
### Clients
|
|
291
458
|
|
|
292
|
-
- `Gh`: the synchronous client, bound to one working directory
|
|
459
|
+
- `Gh`: the synchronous client, bound to one working directory, and optionally a repository and
|
|
460
|
+
host.
|
|
293
461
|
- `AsyncGh`: the awaitable client, with the same methods.
|
|
294
|
-
- `
|
|
462
|
+
- `GhOptions` and `AsyncGhOptions`: the runner, `repo` and `hostname` a client is built with.
|
|
463
|
+
- `AsyncCallOptions`: `timeoutMs` and `signal`, accepted by sixteen methods of `AsyncGh`.
|
|
295
464
|
- `defaultRunnerFactory` and `RunnerFactory`: how a client makes its default runner for `gh`.
|
|
296
465
|
|
|
297
466
|
Methods on both clients:
|
|
298
467
|
|
|
299
468
|
- 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
|
-
|
|
469
|
+
exit. `at` returns the same client rooted at another directory, and `withRepo` the same client
|
|
470
|
+
aimed at another repository.
|
|
471
|
+
- Pull request queries: `prList`, `prView`, `prFind`, `openPrFor`, `prViewSettled`, `prBody`,
|
|
472
|
+
`prReviewThreads` and `searchPrs`.
|
|
303
473
|
- Pull request changes: `createPr`, `editPr`, `editPrBody`, `prComment`, `prUpdateBranch`,
|
|
304
474
|
`prSetDraft`, `prMerge`, `prDiff` and `createReview`.
|
|
305
475
|
- Issue and label queries: `issueList`, `issueView` and `labelList`.
|
|
476
|
+
- Repository queries: `repoList` and `repoView`.
|
|
306
477
|
- Issue and label changes: `setIssueLabel`, `createLabel`, `editLabel` and `issueCommentEdit`.
|
|
307
|
-
- REST: `api` is a GET through `gh api`
|
|
478
|
+
- REST and GraphQL: `api` is a GET through `gh api` and never throws (failure is `ok: false`); `graphql` is a query and throws on a failed or partial answer.
|
|
308
479
|
- Setup checks: `installed`, `authenticated`, `authStatus` and `ready`.
|
|
309
480
|
- `ownerRepo` reads the owner and repository from a remote URL. It starts no process.
|
|
481
|
+
- `try` and `soft`: the whole surface again as a `Result<T, GhFailure>`, and the queries again with `null` for a failure. `TriedSync`, `TriedAsync` and `Soft` are their types.
|
|
482
|
+
|
|
483
|
+
### Failures
|
|
484
|
+
|
|
485
|
+
- `GhFailure` and `GhFailureKind`: the error of `try`, and its kinds (see "`GhFailure`" above).
|
|
486
|
+
- `classifyGhFailure(commandFailed)`: the `GhFailure` for a `CommandFailed`.
|
|
310
487
|
|
|
311
488
|
### Verbs
|
|
312
489
|
|
|
313
|
-
A verb is plain data: argv, a `checked` flag,
|
|
314
|
-
|
|
315
|
-
|
|
490
|
+
A verb is plain data: argv, a `checked` flag, a `readOnly` flag, optional stdin and environment,
|
|
491
|
+
and a `parse` function. `readOnly` is true for every query and `prDiff`, and false for every
|
|
492
|
+
mutation, so a caller that keeps an allowlist of safe calls can check a verb before running it.
|
|
493
|
+
Each verb takes an optional `GhTarget` last. Each client method has a verb of the same name, except
|
|
494
|
+
the escape hatches, `at`, `withRepo`, `prViewSettled`, `openPrFor`, `prFind`, `authenticated`,
|
|
495
|
+
`ready` and `ownerRepo`. `openPrsForBranch` is a verb with no method.
|
|
316
496
|
|
|
317
|
-
- `GhVerb`: the shape of a verb.
|
|
497
|
+
- `GhVerb`: the shape of a verb. `GhTarget`: the `repo` and `hostname` a verb talks to.
|
|
318
498
|
- Verb builders: `prList`, `prView`, `prBody`, `openPrsForBranch`, `prReviewThreads`, `prDiff`,
|
|
319
499
|
`createPr`, `editPr`, `editPrBody`, `prComment`, `prUpdateBranch`, `prSetDraft`, `prMerge`,
|
|
320
500
|
`createReview`, `issueList`, `issueView`, `labelList`, `setIssueLabel`, `createLabel`,
|
|
321
|
-
`editLabel`, `issueCommentEdit`, `
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
-
|
|
327
|
-
`
|
|
501
|
+
`editLabel`, `issueCommentEdit`, `repoList`, `repoView`, `searchPrs`, `api`, `graphql`,
|
|
502
|
+
`installed` and `authStatus`.
|
|
503
|
+
- `pickPrForBranch`: the first row whose head branch is exactly the one you name, for rows that
|
|
504
|
+
came from somewhere other than `prList({ head })`, such as a `head:` search.
|
|
505
|
+
- `nodesAt`: one connection's nodes, joined across `graphql` pages.
|
|
506
|
+
- Field lists: `PR_REF_FIELDS` (enough to identify a PR), `PR_LIST_FIELDS` (for triage),
|
|
507
|
+
`PR_VIEW_FIELDS` (the full set), `REPO_FIELDS` and `SEARCH_PR_FIELDS`.
|
|
508
|
+
- Option and result types: `ListResult`, `PrListOptions`, `IssueListOptions`, `RepoListOptions`,
|
|
509
|
+
`SearchPrsOptions`, `CreatePrOptions`, `CreateReviewOptions`, `ReviewComment`,
|
|
510
|
+
`CreateLabelOptions`, `ApiOptions`, `ApiResponse`, `GraphqlOptions`, `GraphqlVariable`,
|
|
328
511
|
`AuthStatus`, `PrMergeMethod` and `ReviewThread`.
|
|
329
512
|
|
|
330
513
|
### Parsers
|
|
@@ -334,18 +517,22 @@ method has a verb of the same name, except the escape hatches, `at`, `prViewSett
|
|
|
334
517
|
- `normalizeAuthorLogin`: drops a `[bot]` suffix from a login.
|
|
335
518
|
- `parseOwnerRepo`: reads a remote URL into `{ owner, repo }`, or `null`.
|
|
336
519
|
- `ownerRepoSlug`: writes `{ owner, repo }` as `owner/repo`.
|
|
337
|
-
- `parsePrUrl`: reads a pull request URL into its owner, repository and
|
|
520
|
+
- `parsePrUrl`: reads a pull request URL on any GitHub host into its host, owner, repository and
|
|
521
|
+
number, or `null`.
|
|
522
|
+
- `parsePrRef`: reads what a person typed to name a pull request into a `PrRef`, or `null`.
|
|
338
523
|
|
|
339
524
|
### Types
|
|
340
525
|
|
|
341
|
-
- Model: `PullRequest`, `Review`, `Comment`, `CheckRun`, `Actor`, `Label
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
526
|
+
- Model: `PullRequest`, `Review`, `Comment`, `CheckRun`, `Actor`, `Label`, `IssueRow`,
|
|
527
|
+
`Repository` and `PrSearchHit`.
|
|
528
|
+
- Enums: `PullRequestState`, `MergeableState`, `MergeStateStatus`, `ReviewDecision`, `ReviewState`,
|
|
529
|
+
`CheckConclusion` and `RepositoryVisibility`.
|
|
530
|
+
- `OwnerRepo`, `PullRequestRef` and `PrRef`: a repository, a pull request in one, and what a person
|
|
531
|
+
typed to name a pull request.
|
|
345
532
|
|
|
346
533
|
### From the command seam
|
|
347
534
|
|
|
348
|
-
- `CommandFailed`:
|
|
535
|
+
- `CommandFailed`: re-exported from `@osdlc/command-seam`. A failed call throws a `FailureError` whose `failure.cause` is the `CommandFailed`, which carries the argv and the result.
|
|
349
536
|
- `CommandRunner`, `AsyncCommandRunner`, `CommandResult` and `CommandRunOptions`: the runner types
|
|
350
537
|
that appear in signatures.
|
|
351
538
|
|
|
@@ -364,6 +551,10 @@ The experimental items:
|
|
|
364
551
|
- `createReview`, the verb and the method on both clients
|
|
365
552
|
- `CreateReviewOptions`
|
|
366
553
|
- `ReviewComment`
|
|
554
|
+
- `graphql`, the verb and the method on both clients
|
|
555
|
+
- `nodesAt`
|
|
556
|
+
- `GraphqlOptions`
|
|
557
|
+
- `GraphqlVariable`
|
|
367
558
|
|
|
368
559
|
Every other export and method is stable, `prDiff` included.
|
|
369
560
|
|
|
@@ -376,10 +567,10 @@ shell, so nothing in it is specific to one shell.
|
|
|
376
567
|
|
|
377
568
|
- **Closing a PR and deleting a branch.** `prMerge` merges and leaves the branch alone. Closing and
|
|
378
569
|
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", …])`.
|
|
570
|
+
- **A full REST or GraphQL client.** `api()` is a GET and `graphql()` a query; anything else goes
|
|
571
|
+
through `raw(["api", …])`.
|
|
381
572
|
- **Anything that reads a token.**
|
|
382
573
|
|
|
383
574
|
## Licence
|
|
384
575
|
|
|
385
|
-
`MIT OR Apache-2.0`, at your option. See
|
|
576
|
+
`MIT OR Apache-2.0`, at your option. See [LICENSE-MIT](./LICENSE-MIT) and [LICENSE-APACHE](./LICENSE-APACHE).
|