@osdlc/typed-gh 0.6.0 → 0.9.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 +56 -1
- package/CONTRIBUTING.md +4 -3
- package/README.md +117 -36
- package/dist/async.d.ts +37 -23
- package/dist/async.js +52 -38
- 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 +8 -3
- package/dist/index.js +8 -2
- package/dist/parse.d.ts +0 -1
- package/dist/repo.d.ts +0 -1
- package/dist/run.d.ts +37 -0
- package/dist/run.js +49 -0
- package/dist/sync.d.ts +53 -29
- package/dist/sync.js +64 -42
- package/dist/types.d.ts +0 -1
- package/dist/verbs.d.ts +25 -8
- package/dist/verbs.js +34 -8
- package/package.json +13 -17
- package/async.ts +0 -379
- 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 -120
- package/parse.ts +0 -302
- package/repo.ts +0 -133
- package/sync.ts +0 -438
- package/types.ts +0 -226
- package/verbs.ts +0 -1324
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
|
-
}
|