@osdlc/typed-gh 0.6.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/sync.ts DELETED
@@ -1,438 +0,0 @@
1
- /**
2
- * `Gh` — the synchronous GitHub client, the companion of `@osdlc/typed-git`'s
3
- * `Git`, bound to one working directory and one {@link CommandRunner}.
4
- *
5
- * Authentication is `gh`'s, not ours: no token is read, stored, or passed.
6
- * That is the whole reason this shells out to the CLI instead of speaking
7
- * REST — the credential already exists in the user's keychain, and a library
8
- * that reaches for `GITHUB_TOKEN` is a library that can leak one.
9
- *
10
- * Tri-modal, same as the git client: queries return parsed values and never
11
- * throw, mutations throw `CommandFailed`, and `raw`/`rawChecked` are the
12
- * escape hatches.
13
- */
14
-
15
- import {
16
- runChecked,
17
- resolveRunner,
18
- spawnRunner,
19
- type CommandResult,
20
- type CommandRunner,
21
- type CommandRunOptions,
22
- } from '@osdlc/command-seam'
23
-
24
- import * as v from './verbs.ts'
25
- import type {
26
- ApiOptions,
27
- ApiResponse,
28
- AuthStatus,
29
- GhTarget,
30
- GhVerb,
31
- IssueListOptions,
32
- ListResult,
33
- PrListOptions,
34
- } from './verbs.ts'
35
- import { ownerRepoSlug, parseOwnerRepo, parsePrRef } from './repo.ts'
36
- import type { IssueRow, Label, OwnerRepo, PrSearchHit, PullRequest, Repository } from './types.ts'
37
-
38
- /**
39
- * Factory of per-binary runners — the injectable subprocess seam a CLI tail
40
- * threads through its call graph. Production uses `spawnRunner`; tests supply
41
- * a recording factory so a whole tail runs offline.
42
- */
43
- export type RunnerFactory = (binary: string) => CommandRunner
44
-
45
- /** Default factory: real, spawnSync-backed runners. */
46
- export const defaultRunnerFactory: RunnerFactory = spawnRunner
47
-
48
- /** How long to wait between merge-state retries, in milliseconds. */
49
- const MERGE_STATE_RETRY_DELAY_MS = 1_500
50
-
51
- /** What a client is built with: the runner, and the repository and host it talks to. */
52
- export interface GhOptions extends GhTarget {
53
- runner?: CommandRunner | undefined
54
- }
55
-
56
- /**
57
- * A typed gh client bound to one working directory, and optionally to one
58
- * repository and host.
59
- *
60
- * With `repo`, every call names that repository (`--repo`, or the
61
- * `{owner}/{repo}` of an `api` endpoint), so no clone is needed. Without it,
62
- * gh uses the repository of the clone at `cwd`. With `hostname`, every call
63
- * goes to that GitHub host. An invalid `repo` or `hostname` throws
64
- * `RangeError`.
65
- *
66
- * Every `timeoutMs` option is a hard cap on the call. An invalid value
67
- * throws, as in command-seam: `RangeError` for 0, a negative value or `NaN`;
68
- * `TypeError` for a non-number.
69
- */
70
- export class Gh {
71
- readonly cwd: string
72
- readonly repo: string | undefined
73
- readonly hostname: string | undefined
74
- private readonly target: GhTarget
75
- private readonly runner: CommandRunner
76
-
77
- constructor(cwd: string, opts: GhOptions = {}) {
78
- v.checkTarget(opts)
79
- this.cwd = cwd
80
- this.repo = opts.repo
81
- this.hostname = opts.hostname
82
- this.target = { repo: opts.repo, hostname: opts.hostname }
83
- this.runner = resolveRunner('gh', opts.runner)
84
- }
85
-
86
- /** The same client rooted at a different directory (same runner, repo and host). */
87
- at(cwd: string): Gh {
88
- return new Gh(cwd, { runner: this.runner, repo: this.repo, hostname: this.hostname })
89
- }
90
-
91
- /** The same client aimed at another repository (same runner, directory and host). */
92
- withRepo(repo: string): Gh {
93
- return new Gh(this.cwd, { runner: this.runner, repo, hostname: this.hostname })
94
- }
95
-
96
- // ── escape hatches ────────────────────────────────────────────────────────
97
-
98
- /** Unchecked passthrough for verbs not yet lifted into the API. */
99
- raw(argv: string[], opts: { timeoutMs?: number | undefined } = {}): CommandResult {
100
- return this.runner.run(argv, { cwd: this.cwd, ...opts })
101
- }
102
-
103
- /** Checked passthrough — throws `CommandFailed` on a non-zero exit. */
104
- rawChecked(argv: string[], opts: { timeoutMs?: number | undefined } = {}): CommandResult {
105
- return runChecked(this.runner, argv, { cwd: this.cwd, ...opts })
106
- }
107
-
108
- /** Run one verb from the table, honouring its checked/unchecked contract. */
109
- private exec<T>(verb: GhVerb<T>, timeoutMs?: number | undefined): T {
110
- const opts: CommandRunOptions = { cwd: this.cwd }
111
- if (verb.input !== undefined) opts.input = verb.input
112
- if (verb.env !== undefined) opts.env = verb.env
113
- if (timeoutMs !== undefined) opts.timeoutMs = timeoutMs
114
- const result = verb.checked
115
- ? runChecked(this.runner, verb.argv, opts)
116
- : this.runner.run(verb.argv, opts)
117
- return verb.parse(result)
118
- }
119
-
120
- // ── queries ───────────────────────────────────────────────────────────────
121
-
122
- /** Pull requests matching `opts`, or null when gh failed. */
123
- prList(
124
- opts: PrListOptions & { timeoutMs?: number | undefined } = {},
125
- ): ListResult<PullRequest> | null {
126
- return this.exec(v.prList(opts, this.target), opts.timeoutMs)
127
- }
128
-
129
- /**
130
- * One PR by number, branch or URL, with the heavy fields; null when gh
131
- * failed or there is no such PR.
132
- */
133
- prView(
134
- ref: number | string,
135
- opts: { fields?: readonly string[] | undefined; timeoutMs?: number | undefined } = {},
136
- ): PullRequest | null {
137
- return this.exec(v.prView(ref, opts.fields ?? v.PR_VIEW_FIELDS, this.target), opts.timeoutMs)
138
- }
139
-
140
- /**
141
- * The one PR that `input` names, or null: what a person typed, such as `42`,
142
- * `#42`, `owner/name#42`, a URL or a branch (read by `parsePrRef`).
143
- *
144
- * Digits are a PR number first; when no such PR comes back, they are tried
145
- * as a branch. A branch gives its open PR, or else its newest closed or
146
- * merged one, so a tool run after the merge still finds it. Fields default
147
- * to {@link PR_LIST_FIELDS}.
148
- */
149
- prFind(
150
- input: string,
151
- opts: { fields?: readonly string[] | undefined; timeoutMs?: number | undefined } = {},
152
- ): PullRequest | null {
153
- const ref = parsePrRef(input)
154
- if (ref === null) return null
155
- const fields = opts.fields ?? v.PR_LIST_FIELDS
156
- const call = { fields, timeoutMs: opts.timeoutMs }
157
- if (ref.kind === 'url') return this.prView(ref.url, call)
158
- if (ref.kind === 'branch') return prForBranchOn(this, ref.name, call)
159
- const client = ref.repo === null ? this : this.withRepo(ownerRepoSlug(ref.repo))
160
- const pr = client.prView(ref.number, call)
161
- return pr !== null || ref.orBranch === null ? pr : prForBranchOn(this, ref.orBranch, call)
162
- }
163
-
164
- /**
165
- * The open PR whose head exactly equals `branch`, or null.
166
- *
167
- * Defaults to {@link PR_REF_FIELDS}: identifying a PR must not cost a
168
- * merge-state computation. Pass `opts.fields` to ask for more (e.g.
169
- * `isDraft`) in the SAME call, when a caller needs a field beyond identity
170
- * before deciding whether to act on the PR at all — asking `prList`/`prView`
171
- * separately would cost a second round trip for the common case where the
172
- * caller already knows it wants that field up front.
173
- */
174
- openPrFor(
175
- branch: string,
176
- opts: { limit?: number | undefined; fields?: readonly string[] | undefined } = {},
177
- ): PullRequest | null {
178
- const rows = this.prList({
179
- head: branch,
180
- limit: opts.limit ?? 5,
181
- fields: opts.fields ?? v.PR_REF_FIELDS,
182
- })
183
- return v.pickPrForBranch(rows?.rows ?? null, branch)
184
- }
185
-
186
- /**
187
- * {@link prView}, re-asked until GitHub settles the merge state.
188
- *
189
- * GitHub computes mergeability lazily: the first query kicks it off and
190
- * answers `UNKNOWN` while it runs. A caller that believes that first answer
191
- * mis-triages every PR that is merely slow to compute, so this re-asks
192
- * `attempts` times before returning what it has. The last answer is
193
- * returned whether or not it settled — `UNKNOWN` is a legitimate final
194
- * verdict and callers must still handle it.
195
- *
196
- * `sleep` is injected so a test does not spend real seconds waiting.
197
- */
198
- prViewSettled(
199
- number: number,
200
- opts: {
201
- attempts?: number | undefined
202
- fields?: readonly string[] | undefined
203
- sleep?: ((ms: number) => void) | undefined
204
- timeoutMs?: number | undefined
205
- } = {},
206
- ): PullRequest | null {
207
- const attempts = Math.max(1, opts.attempts ?? 3)
208
- const sleep = opts.sleep ?? sleepSync
209
- let last: PullRequest | null = null
210
- for (let i = 0; i < attempts; i += 1) {
211
- last = this.prView(number, opts)
212
- if (last === null || !v.needsMergeStateRetry(last)) return last
213
- if (i < attempts - 1) sleep(MERGE_STATE_RETRY_DELAY_MS)
214
- }
215
- return last
216
- }
217
-
218
- /** A PR's body text, or null when gh failed. */
219
- prBody(number: number, opts: { timeoutMs?: number | undefined } = {}): string | null {
220
- return this.exec(v.prBody(number, this.target), opts.timeoutMs)
221
- }
222
-
223
- /**
224
- * A REST GET through `gh api`. Never throws: failure is `ok: false` on the
225
- * returned {@link ApiResponse}.
226
- */
227
- api(endpoint: string, opts: ApiOptions & { timeoutMs?: number | undefined } = {}): ApiResponse {
228
- return this.exec(v.api(endpoint, opts, this.target), opts.timeoutMs)
229
- }
230
-
231
- /**
232
- * A GraphQL query; each page's `data`, or null on a failed or partial
233
- * result. See the `graphql` verb.
234
- *
235
- * @experimental
236
- */
237
- graphql(
238
- query: string,
239
- variables: Record<string, v.GraphqlVariable> = {},
240
- opts: v.GraphqlOptions & { timeoutMs?: number | undefined } = {},
241
- ): unknown[] | null {
242
- return this.exec(v.graphql(query, variables, opts, this.target), opts.timeoutMs)
243
- }
244
-
245
- /** Every label on the repository (up to `opts.limit`, default 1000), or null when gh failed. */
246
- labelList(
247
- opts: { limit?: number | undefined; timeoutMs?: number | undefined } = {},
248
- ): ListResult<Label> | null {
249
- return this.exec(v.labelList(opts, this.target), opts.timeoutMs)
250
- }
251
-
252
- /** Issues matching `opts` (open by default, up to 200), or null when gh failed. */
253
- issueList(
254
- opts: IssueListOptions & { timeoutMs?: number | undefined } = {},
255
- ): ListResult<IssueRow> | null {
256
- return this.exec(v.issueList(opts, this.target), opts.timeoutMs)
257
- }
258
-
259
- /** One issue with its labels, or null when gh failed. */
260
- issueView(number: number, opts: { timeoutMs?: number | undefined } = {}): IssueRow | null {
261
- return this.exec(v.issueView(number, this.target), opts.timeoutMs)
262
- }
263
-
264
- /** An owner's repositories (up to `opts.limit`, default 100), or null when gh failed. */
265
- repoList(
266
- owner: string,
267
- opts: v.RepoListOptions & { timeoutMs?: number | undefined } = {},
268
- ): ListResult<Repository> | null {
269
- return this.exec(v.repoList(owner, opts, this.target), opts.timeoutMs)
270
- }
271
-
272
- /** One repository: `repo`, else the client's repo, else the clone at `cwd`. Null when gh failed. */
273
- repoView(
274
- repo?: string | undefined,
275
- opts: { fields?: readonly string[] | undefined; timeoutMs?: number | undefined } = {},
276
- ): Repository | null {
277
- return this.exec(v.repoView(repo, opts.fields ?? v.REPO_FIELDS, this.target), opts.timeoutMs)
278
- }
279
-
280
- /** Pull requests across repositories matching a search query, or null when gh failed. */
281
- searchPrs(
282
- query: string,
283
- opts: v.SearchPrsOptions & { timeoutMs?: number | undefined } = {},
284
- ): ListResult<PrSearchHit> | null {
285
- return this.exec(v.searchPrs(query, opts, this.target), opts.timeoutMs)
286
- }
287
-
288
- /** True iff the `gh` binary is on PATH and runs. */
289
- installed(): boolean {
290
- return this.exec(v.installed())
291
- }
292
-
293
- /**
294
- * True iff `gh` holds credentials. Asked on its own only by a caller that
295
- * reports the two halves separately — "gh is missing" and "gh is not logged
296
- * in" are different things to tell a person, and a report that collapses
297
- * them sends someone to install software they already have.
298
- */
299
- authenticated(): boolean {
300
- return this.authStatus().ok
301
- }
302
-
303
- /** Login state and what `gh auth status` printed; `authenticated()` is its `ok`. */
304
- authStatus(): AuthStatus {
305
- return this.exec(v.authStatus(this.target))
306
- }
307
-
308
- /**
309
- * True iff `gh` is installed AND authenticated — the two-step probe.
310
- *
311
- * Short-circuits: an absent binary cannot be asked about credentials.
312
- */
313
- ready(): boolean {
314
- return this.installed() && this.authenticated()
315
- }
316
-
317
- /**
318
- * `{owner, repo}` parsed from the remote URL you pass, or null when it is
319
- * null or names no project.
320
- *
321
- * Pure: it reads no git config, starts no process and does not ask GitHub.
322
- */
323
- ownerRepo(remoteUrl: string | null): OwnerRepo | null {
324
- return remoteUrl === null ? null : parseOwnerRepo(remoteUrl)
325
- }
326
-
327
- // ── mutations (checked) ───────────────────────────────────────────────────
328
-
329
- /** Open a PR and return its URL ('' if gh printed none). */
330
- createPr(opts: v.CreatePrOptions): string {
331
- return this.exec(v.createPr(opts, this.target))
332
- }
333
-
334
- /** Retitle or rewrite a PR body. */
335
- editPr(number: number, opts: { title?: string | undefined; body?: string | undefined }): void {
336
- this.exec(v.editPr(number, opts, this.target))
337
- }
338
-
339
- /** The PR's unified diff; throws `CommandFailed` when gh cannot produce it. */
340
- prDiff(number: number): string {
341
- return this.exec(v.prDiff(number, this.target))
342
- }
343
-
344
- /** Post one conversation comment on a PR; returns its URL ('' if gh printed none). */
345
- prComment(number: number, body: string): string {
346
- return this.exec(v.prComment(number, body, this.target))
347
- }
348
-
349
- /**
350
- * Create a `COMMENT` review with inline comments; returns the review's URL ('' if none came back).
351
- *
352
- * @experimental
353
- */
354
- createReview(number: number, opts: v.CreateReviewOptions): string {
355
- return this.exec(v.createReview(number, opts, this.target))
356
- }
357
-
358
- /** Rewrite just a PR's body. */
359
- editPrBody(number: number, body: string): void {
360
- this.exec(v.editPrBody(number, body, this.target))
361
- }
362
-
363
- /** Bring a PR's head branch up to date with its base. */
364
- prUpdateBranch(number: number, style: 'merge' | 'rebase' = 'merge'): void {
365
- this.exec(v.prUpdateBranch(number, style, this.target))
366
- }
367
-
368
- /** Mark a PR ready for review (`draft: false`) or convert it to a draft (`draft: true`). */
369
- prSetDraft(number: number, draft: boolean): void {
370
- this.exec(v.prSetDraft(number, draft, this.target))
371
- }
372
-
373
- /**
374
- * Rewrite one PR conversation comment's body. `commentId` is the numeric REST id; anything else throws `RangeError`.
375
- *
376
- * @experimental
377
- */
378
- issueCommentEdit(commentId: string, body: string): void {
379
- this.exec(v.issueCommentEdit(commentId, body, this.target))
380
- }
381
-
382
- /** Merge a PR; GitHub refuses when its head is no longer `opts.headSha`. */
383
- prMerge(number: number, opts: { method: v.PrMergeMethod; headSha: string }): void {
384
- this.exec(v.prMerge(number, opts, this.target))
385
- }
386
-
387
- /** A PR's review threads, or null when they could not be read completely. */
388
- prReviewThreads(number: number): v.ReviewThread[] | null {
389
- return this.exec(v.prReviewThreads(number, this.target))
390
- }
391
-
392
- /** Create a label (or force it to `opts`'s color/description if it already exists). */
393
- createLabel(opts: v.CreateLabelOptions): void {
394
- this.exec(v.createLabel(opts, this.target))
395
- }
396
-
397
- /** Recolor and/or redescribe an existing label. */
398
- editLabel(
399
- name: string,
400
- opts: { color?: string | undefined; description?: string | undefined },
401
- ): void {
402
- this.exec(v.editLabel(name, opts, this.target))
403
- }
404
-
405
- /**
406
- * Swap an issue's label: remove `currentLabel` (when given) and add
407
- * `newLabel`, in a single `gh issue edit` call — never a briefly-unlabeled
408
- * or briefly-double-labeled intermediate state.
409
- */
410
- setIssueLabel(number: number, currentLabel: string | null, newLabel: string): void {
411
- this.exec(v.setIssueLabel(number, currentLabel, newLabel, this.target))
412
- }
413
- }
414
-
415
- /** The open PR for `branch`, else its newest closed or merged one. */
416
- function prForBranchOn(
417
- gh: Gh,
418
- branch: string,
419
- call: { fields: readonly string[]; timeoutMs?: number | undefined },
420
- ): PullRequest | null {
421
- const rows = gh.prList({
422
- head: branch,
423
- state: 'all',
424
- limit: 20,
425
- fields: v.branchPrFields(call.fields),
426
- timeoutMs: call.timeoutMs,
427
- })
428
- return v.branchPr(rows)
429
- }
430
-
431
- /**
432
- * Block the calling thread. Crude on purpose: the synchronous client has no
433
- * event loop to await on, and the alternative — making every caller async to
434
- * accommodate one retry delay — colors the whole call graph.
435
- */
436
- function sleepSync(ms: number): void {
437
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
438
- }
package/types.ts DELETED
@@ -1,226 +0,0 @@
1
- /**
2
- * The GitHub model, typed.
3
- *
4
- * `gh` returns GraphQL-shaped JSON whose fields depend on what you asked for,
5
- * so every field here is total: a value, or the enum member that means "not
6
- * known". Absent and unknowable are the same answer to a caller, and giving
7
- * them one representation is what keeps a consumer from reading a field it
8
- * never requested as a fact.
9
- *
10
- * Most enums are open on purpose. GitHub adds members, and a client that
11
- * crashes on an unrecognized one is worse than a client that carries it
12
- * through: unions end in `(string & {})` so a new value still types as the
13
- * enum while the known members keep their autocomplete.
14
- *
15
- * Two are closed: {@link MergeableState} and {@link ReviewDecision}. Callers
16
- * compare them member by member to decide whether to merge, so the parser
17
- * maps a value it does not know to `UNKNOWN` or `null` instead of passing it
18
- * on.
19
- */
20
-
21
- /**
22
- * Can this PR merge? `UNKNOWN` also covers "GitHub has not computed it yet".
23
- * Closed: a value the parser does not know reads as `UNKNOWN`.
24
- */
25
- export type MergeableState = 'MERGEABLE' | 'CONFLICTING' | 'UNKNOWN'
26
-
27
- /**
28
- * The merge-button state.
29
- *
30
- * `BEHIND` only ever appears when branch protection *requires* an up-to-date
31
- * branch. Without that rule a branch a hundred commits behind still reports
32
- * `CLEAN`, so this is a corroborating hint about staleness and never the
33
- * measure of it — compute that from the commit graph.
34
- */
35
- export type MergeStateStatus =
36
- | 'BEHIND'
37
- | 'BLOCKED'
38
- | 'CLEAN'
39
- | 'DIRTY'
40
- | 'DRAFT'
41
- | 'HAS_HOOKS'
42
- | 'UNKNOWN'
43
- | 'UNSTABLE'
44
- | (string & {})
45
-
46
- /** Where a pull request is in its life: open, closed without a merge, or merged. */
47
- export type PullRequestState = 'OPEN' | 'CLOSED' | 'MERGED' | (string & {})
48
-
49
- /**
50
- * The review state branch protection reads: approved, changes requested, or a
51
- * review still required. `null` when there is no decision, or the repository
52
- * requires no review. Closed: a value the parser does not know reads as `null`.
53
- */
54
- export type ReviewDecision = 'APPROVED' | 'CHANGES_REQUESTED' | 'REVIEW_REQUIRED' | null
55
-
56
- /** The state of one review. `PENDING` is a review its author has not submitted yet. */
57
- export type ReviewState =
58
- 'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED' | 'DISMISSED' | 'PENDING' | (string & {})
59
-
60
- /** Who wrote something. `login` is normalized — a `[bot]` suffix is stripped. */
61
- export interface Actor {
62
- login: string
63
- /** True when GitHub marks the account as a bot, or the login carried `[bot]`. */
64
- isBot: boolean
65
- }
66
-
67
- /** A repository label. `color` is hex digits such as `d73a4a`; a field the payload lacks is `''`. */
68
- export interface Label {
69
- name: string
70
- color: string
71
- description: string
72
- }
73
-
74
- /**
75
- * A GitHub issue, off `gh issue list`/`gh issue view --json
76
- * number,title,body,labels,state`. Deliberately a small field set — not the
77
- * full PR-sized model, since nothing here reads reviews, checks or comments
78
- * off an issue.
79
- */
80
- export interface IssueRow {
81
- number: number
82
- title: string
83
- body: string
84
- labels: Label[]
85
- state: string
86
- }
87
-
88
- /**
89
- * How a finished check ended, as GitHub reports it. Null while the check is
90
- * still running, and always null for a legacy status context. Read
91
- * {@link CheckRun.verdict} for the answer folded across both node types.
92
- */
93
- export type CheckConclusion =
94
- | 'SUCCESS'
95
- | 'FAILURE'
96
- | 'NEUTRAL'
97
- | 'CANCELLED'
98
- | 'TIMED_OUT'
99
- | 'ACTION_REQUIRED'
100
- | 'SKIPPED'
101
- | 'STALE'
102
- | 'STARTUP_FAILURE'
103
- | (string & {})
104
- | null
105
-
106
- /** One rolled-up check, with the two GitHub shapes folded into one. */
107
- export interface CheckRun {
108
- /** A CheckRun's `name`, or a legacy StatusContext's `context`. */
109
- name: string
110
- /** `QUEUED` / `IN_PROGRESS` / `COMPLETED`; null for a status context. */
111
- status: string | null
112
- conclusion: CheckConclusion
113
- /**
114
- * The normalized verdict. A check still running is `pending`, not a pass —
115
- * treating "no failure yet" as green is how a red build merges.
116
- */
117
- verdict: 'pending' | 'success' | 'failure' | 'neutral'
118
- /** A CheckRun's `detailsUrl`, or a legacy StatusContext's `targetUrl` — where to read the log. Null when the code host gives none. */
119
- url: string | null
120
- }
121
-
122
- /** One conversation comment on a pull request. */
123
- export interface Comment {
124
- /**
125
- * GitHub's id for this comment, as a string: a GraphQL node id, or a REST id
126
- * turned into text. Null only when the payload omits it (`gh` always sends
127
- * one). A REST id of 2^53 or more loses precision when `gh` output is
128
- * parsed. Current ids are far below that.
129
- */
130
- id: string | null
131
- author: Actor | null
132
- body: string
133
- createdAt: string | null
134
- }
135
-
136
- /** One review on a pull request. */
137
- export interface Review {
138
- /** Same as {@link Comment.id}. A REST id of 2^53 or more loses precision when `gh` output is parsed. */
139
- id: string | null
140
- author: Actor | null
141
- body: string
142
- state: ReviewState
143
- submittedAt: string | null
144
- }
145
-
146
- /**
147
- * A pull request.
148
- *
149
- * Fields `gh` was not asked for come back at their "not known" value, so this
150
- * type is safe to build from any field subset. `number` is the only field
151
- * without one — a PR with no number is not a PR, and parsing returns null.
152
- */
153
- export interface PullRequest {
154
- number: number
155
- state: PullRequestState
156
- title: string
157
- body: string
158
- url: string
159
- isDraft: boolean
160
- baseRefName: string
161
- headRefName: string
162
- /** Head commit sha, when requested — the thing to pin a force-push lease to. */
163
- headRefOid: string | null
164
- /**
165
- * True when the head branch lives in a fork. Its ref is then not on the
166
- * base repository's `origin`; fetch `refs/pull/<n>/head` instead.
167
- */
168
- isCrossRepository: boolean
169
- mergeable: MergeableState
170
- mergeStateStatus: MergeStateStatus
171
- reviewDecision: ReviewDecision
172
- author: Actor | null
173
- labels: Label[]
174
- checks: CheckRun[]
175
- comments: Comment[]
176
- reviews: Review[]
177
- mergedAt: string | null
178
- createdAt: string | null
179
- updatedAt: string | null
180
- }
181
-
182
- /** A repository coordinate. */
183
- export interface OwnerRepo {
184
- owner: string
185
- repo: string
186
- }
187
-
188
- /** Who can see a repository. */
189
- export type RepositoryVisibility = 'PUBLIC' | 'PRIVATE' | 'INTERNAL' | (string & {})
190
-
191
- /**
192
- * A repository, off `gh repo list`/`gh repo view --json`. A field not asked
193
- * for comes back at its "not known" value. `nameWithOwner` is the only field
194
- * without one: a row without it is dropped.
195
- */
196
- export interface Repository {
197
- /** `owner/name`. */
198
- nameWithOwner: string
199
- owner: string
200
- name: string
201
- description: string
202
- /** The default branch; null when not asked for, or the repository is empty. */
203
- defaultBranch: string | null
204
- visibility: RepositoryVisibility | null
205
- isArchived: boolean
206
- url: string
207
- }
208
-
209
- /**
210
- * One pull request found by `gh search prs`. Search has its own field set:
211
- * it names the repository but carries no head or base branch, so this is not
212
- * a {@link PullRequest}. `state` is upper-cased to match `PullRequest`.
213
- */
214
- export interface PrSearchHit {
215
- number: number
216
- title: string
217
- state: PullRequestState
218
- url: string
219
- isDraft: boolean
220
- repository: OwnerRepo | null
221
- author: Actor | null
222
- labels: Label[]
223
- createdAt: string | null
224
- updatedAt: string | null
225
- closedAt: string | null
226
- }