@osdlc/typed-gh 0.9.6 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/sync.d.ts CHANGED
@@ -130,6 +130,7 @@ export declare class Gh {
130
130
  openPrFor(branch: string, opts?: {
131
131
  limit?: number | undefined;
132
132
  fields?: readonly string[] | undefined;
133
+ timeoutMs?: number | undefined;
133
134
  }): PullRequest | null;
134
135
  /**
135
136
  * {@link prView}, asked again until GitHub has computed the merge state.
package/dist/sync.js CHANGED
@@ -154,6 +154,7 @@ export class Gh {
154
154
  head: branch,
155
155
  limit: opts.limit ?? 5,
156
156
  fields: opts.fields ?? v.PR_REF_FIELDS,
157
+ timeoutMs: opts.timeoutMs,
157
158
  });
158
159
  return v.pickPrForBranch(rows.rows, branch);
159
160
  }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * `@osdlc/typed-gh/testing`: rows of `gh --json` output, for faking `gh` in
3
+ * tests.
4
+ *
5
+ * A separate subpath, not part of the package entry, so production code does
6
+ * not carry fixture data. Nothing in `src/` but this file knows about it.
7
+ *
8
+ * Each builder returns gh's raw JSON shape, what `gh … --json` prints
9
+ * (`nameWithOwner`, `headRefName`, `defaultBranchRef: { name }`), not the
10
+ * parsed model. Feed the rows to a fake runner through {@link ghResult} and
11
+ * the real parsers read them, so a test exercises the same parsing a real
12
+ * `gh` call does.
13
+ *
14
+ * The defaults fill every field the parsers require, which is where hand-made
15
+ * rows go wrong: a row the parser rejects is dropped without an error.
16
+ *
17
+ * - A pull request needs an integer `number` (`parsePullRequest`), and
18
+ * `prList({ head })` also keeps only rows whose `headRefName` equals
19
+ * `head`.
20
+ * - An issue and a search hit need a numeric `number`.
21
+ * - A repository needs `nameWithOwner` as `owner/name`, both parts
22
+ * non-empty (`parseRepository`).
23
+ * - A label needs a non-empty `name` (`parseLabels`).
24
+ * - A check needs a `name`, or a `context` for a legacy status context
25
+ * (`parseCheckRun`).
26
+ *
27
+ * Overrides merge shallowly: a field you pass replaces the default whole, and
28
+ * a field you pass as `undefined` is left out of the row, to test a payload
29
+ * that lacks it. A default that names another field (a pull request's `url`
30
+ * names its `number`) follows your override of that field. The default
31
+ * values may change in a minor release, as long as every parser still
32
+ * accepts them, so assert on what you set, not on a default.
33
+ */
34
+ import type { CommandResult } from '@osdlc/command-seam';
35
+ import type { CheckConclusion, MergeableState, MergeStateStatus, PullRequestState, RepositoryVisibility, ReviewState } from './types.ts';
36
+ /**
37
+ * What a builder takes: any field of the row, or `undefined` to leave that
38
+ * field out.
39
+ */
40
+ export type RowOverrides<T> = {
41
+ [K in keyof T]?: T[K] | undefined;
42
+ };
43
+ /**
44
+ * A successful `gh` call that printed `payload` as JSON: exit code 0, the
45
+ * payload on stdout with gh's trailing newline, and nothing on stderr. Pass an
46
+ * array of rows for a list verb and one row for a view verb.
47
+ */
48
+ export declare function ghResult(payload: unknown): CommandResult;
49
+ /**
50
+ * A pull request's or search hit's `author` as `gh --json` prints it. A bot
51
+ * is `{ is_bot: true, login: 'app/<name>' }`.
52
+ */
53
+ export interface GhActorJson {
54
+ login: string;
55
+ is_bot?: boolean;
56
+ name?: string;
57
+ id?: string;
58
+ }
59
+ /**
60
+ * A comment's or review's `author` as `gh --json` prints it: only `login`.
61
+ * gh leaves out `id`, `name` and `is_bot` there, so a bot comment does not
62
+ * read as a bot unless its login ends in `[bot]`.
63
+ */
64
+ export interface GhCommentAuthorJson {
65
+ login: string;
66
+ }
67
+ /** One entry of a `labels` field, or one row of `gh label list --json name,color,description`. */
68
+ export interface GhLabelJson {
69
+ name: string;
70
+ /** Six hex digits, no `#`. */
71
+ color: string;
72
+ description: string;
73
+ }
74
+ /** A label row. The default is GitHub's own `bug` label. */
75
+ export declare function labelRow(overrides?: RowOverrides<GhLabelJson>): GhLabelJson;
76
+ /** A modern `CheckRun` node of `statusCheckRollup`. */
77
+ export interface GhCheckRunJson {
78
+ __typename: 'CheckRun';
79
+ name: string;
80
+ workflowName: string;
81
+ /** `QUEUED`, `IN_PROGRESS` or `COMPLETED`. */
82
+ status: string;
83
+ /** gh prints `""` while the check runs. */
84
+ conclusion: Exclude<CheckConclusion, null> | '';
85
+ detailsUrl: string;
86
+ startedAt: string;
87
+ completedAt: string;
88
+ }
89
+ /** A check run. The default completed with `SUCCESS`, so it reads as a green check. */
90
+ export declare function checkRunRow(overrides?: RowOverrides<GhCheckRunJson>): GhCheckRunJson;
91
+ /** A legacy `StatusContext` node of `statusCheckRollup`. */
92
+ export interface GhStatusContextJson {
93
+ __typename: 'StatusContext';
94
+ context: string;
95
+ /** `SUCCESS`, `PENDING`, `FAILURE` or `ERROR`. */
96
+ state: string;
97
+ targetUrl: string;
98
+ startedAt: string;
99
+ }
100
+ /** A legacy status context. The default is a `SUCCESS`. */
101
+ export declare function statusContextRow(overrides?: RowOverrides<GhStatusContextJson>): GhStatusContextJson;
102
+ /** One entry of a pull request's `comments`. */
103
+ export interface GhCommentJson {
104
+ /** The GraphQL node id. */
105
+ id: string;
106
+ author: GhCommentAuthorJson | null;
107
+ authorAssociation: string;
108
+ body: string;
109
+ createdAt: string;
110
+ url: string;
111
+ }
112
+ /** A comment by `octocat`. */
113
+ export declare function commentRow(overrides?: RowOverrides<GhCommentJson>): GhCommentJson;
114
+ /** One entry of a pull request's `reviews`. */
115
+ export interface GhReviewJson {
116
+ /** The GraphQL node id. */
117
+ id: string;
118
+ author: GhCommentAuthorJson | null;
119
+ authorAssociation: string;
120
+ body: string;
121
+ state: ReviewState;
122
+ submittedAt: string;
123
+ commit: {
124
+ oid: string;
125
+ };
126
+ }
127
+ /** An approving review by `octocat`. */
128
+ export declare function reviewRow(overrides?: RowOverrides<GhReviewJson>): GhReviewJson;
129
+ /** A pull request as `gh pr list`/`view --json` prints it, with every field the parser reads. */
130
+ export interface GhPrJson {
131
+ /** Required: a row without an integer `number` is dropped. */
132
+ number: number;
133
+ state: PullRequestState;
134
+ title: string;
135
+ body: string;
136
+ url: string;
137
+ isDraft: boolean;
138
+ baseRefName: string;
139
+ /** Required by `prList({ head })`, which keeps only rows whose head is exactly `head`. */
140
+ headRefName: string;
141
+ headRefOid: string;
142
+ isCrossRepository: boolean;
143
+ mergeable: MergeableState;
144
+ mergeStateStatus: MergeStateStatus;
145
+ /** gh prints `""` when there is no decision. */
146
+ reviewDecision: 'APPROVED' | 'CHANGES_REQUESTED' | 'REVIEW_REQUIRED' | '';
147
+ author: GhActorJson | null;
148
+ labels: GhLabelJson[];
149
+ comments: GhCommentJson[];
150
+ reviews: GhReviewJson[];
151
+ statusCheckRollup: (GhCheckRunJson | GhStatusContextJson)[];
152
+ mergedAt: string | null;
153
+ createdAt: string;
154
+ updatedAt: string;
155
+ }
156
+ /**
157
+ * An open, mergeable pull request #1 from `feature/add-retries` into `main`
158
+ * of `octo-org/hello-world`, by `octocat`, with no labels, comments, reviews
159
+ * or checks. Its `url` follows `number`.
160
+ */
161
+ export declare function prRow(overrides?: RowOverrides<GhPrJson>): GhPrJson;
162
+ /** An issue as `gh issue list`/`view --json number,title,body,labels,state` prints it. */
163
+ export interface GhIssueJson {
164
+ /** Required: a row without a numeric `number` is dropped. */
165
+ number: number;
166
+ title: string;
167
+ body: string;
168
+ labels: GhLabelJson[];
169
+ /** `OPEN` or `CLOSED`. */
170
+ state: string;
171
+ }
172
+ /** An open issue #1 with no labels. */
173
+ export declare function issueRow(overrides?: RowOverrides<GhIssueJson>): GhIssueJson;
174
+ /** A repository as `gh repo list`/`view --json` prints it, with every field the parser reads. */
175
+ export interface GhRepoJson {
176
+ /** Required: `owner/name`, both parts non-empty, or the row is dropped. */
177
+ nameWithOwner: string;
178
+ description: string;
179
+ /** `{ name: '' }` for an empty repository, as gh prints it. `null` parses the same. */
180
+ defaultBranchRef: {
181
+ name: string;
182
+ } | null;
183
+ visibility: RepositoryVisibility;
184
+ isArchived: boolean;
185
+ url: string;
186
+ }
187
+ /** The public, unarchived `octo-org/hello-world` on `main`. Its `url` follows `nameWithOwner`. */
188
+ export declare function repoRow(overrides?: RowOverrides<GhRepoJson>): GhRepoJson;
189
+ /** A row of `gh search prs --json`, with every field the parser reads. */
190
+ export interface GhSearchPrJson {
191
+ /** Required: a row without a numeric `number` is dropped. */
192
+ number: number;
193
+ title: string;
194
+ /** Search answers in lower case: `open`, `closed` or `merged`. */
195
+ state: string;
196
+ url: string;
197
+ isDraft: boolean;
198
+ repository: {
199
+ name: string;
200
+ nameWithOwner: string;
201
+ };
202
+ author: GhActorJson | null;
203
+ labels: GhLabelJson[];
204
+ createdAt: string;
205
+ updatedAt: string;
206
+ /**
207
+ * gh prints `0001-01-01T00:00:00Z`, Go's zero time, for a hit that is not
208
+ * closed. The parser reads that as `null`.
209
+ */
210
+ closedAt: string | null;
211
+ }
212
+ /**
213
+ * An open pull request #1 in `octo-org/hello-world`, as search returns it. Its
214
+ * `url` follows `number` and `repository`.
215
+ */
216
+ export declare function searchPrRow(overrides?: RowOverrides<GhSearchPrJson>): GhSearchPrJson;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * `@osdlc/typed-gh/testing`: rows of `gh --json` output, for faking `gh` in
3
+ * tests.
4
+ *
5
+ * A separate subpath, not part of the package entry, so production code does
6
+ * not carry fixture data. Nothing in `src/` but this file knows about it.
7
+ *
8
+ * Each builder returns gh's raw JSON shape, what `gh … --json` prints
9
+ * (`nameWithOwner`, `headRefName`, `defaultBranchRef: { name }`), not the
10
+ * parsed model. Feed the rows to a fake runner through {@link ghResult} and
11
+ * the real parsers read them, so a test exercises the same parsing a real
12
+ * `gh` call does.
13
+ *
14
+ * The defaults fill every field the parsers require, which is where hand-made
15
+ * rows go wrong: a row the parser rejects is dropped without an error.
16
+ *
17
+ * - A pull request needs an integer `number` (`parsePullRequest`), and
18
+ * `prList({ head })` also keeps only rows whose `headRefName` equals
19
+ * `head`.
20
+ * - An issue and a search hit need a numeric `number`.
21
+ * - A repository needs `nameWithOwner` as `owner/name`, both parts
22
+ * non-empty (`parseRepository`).
23
+ * - A label needs a non-empty `name` (`parseLabels`).
24
+ * - A check needs a `name`, or a `context` for a legacy status context
25
+ * (`parseCheckRun`).
26
+ *
27
+ * Overrides merge shallowly: a field you pass replaces the default whole, and
28
+ * a field you pass as `undefined` is left out of the row, to test a payload
29
+ * that lacks it. A default that names another field (a pull request's `url`
30
+ * names its `number`) follows your override of that field. The default
31
+ * values may change in a minor release, as long as every parser still
32
+ * accepts them, so assert on what you set, not on a default.
33
+ */
34
+ /** `defaults` with `overrides` merged over them. An `undefined` override drops the field. */
35
+ function build(defaults, overrides) {
36
+ const row = { ...defaults };
37
+ for (const [key, value] of Object.entries(overrides)) {
38
+ if (value === undefined)
39
+ delete row[key];
40
+ else
41
+ row[key] = value;
42
+ }
43
+ return row;
44
+ }
45
+ const HOST = 'https://github.com';
46
+ const REPO = 'octo-org/hello-world';
47
+ const CREATED_AT = '2024-01-15T09:30:00Z';
48
+ const UPDATED_AT = '2024-01-16T14:05:00Z';
49
+ const HEAD_OID = '6dcb09b5b57875f334f61aebed695e2e4193db5e';
50
+ // ── the result ──────────────────────────────────────────────────────────────
51
+ /**
52
+ * A successful `gh` call that printed `payload` as JSON: exit code 0, the
53
+ * payload on stdout with gh's trailing newline, and nothing on stderr. Pass an
54
+ * array of rows for a list verb and one row for a view verb.
55
+ */
56
+ export function ghResult(payload) {
57
+ return { exitCode: 0, stdout: `${JSON.stringify(payload)}\n`, stderr: '' };
58
+ }
59
+ /** A new object each call, so a test that edits one row's author edits only that row. */
60
+ function octocat() {
61
+ return { id: 'MDQ6VXNlcjU4MzIzMQ==', is_bot: false, login: 'octocat', name: 'The Octocat' };
62
+ }
63
+ /** A label row. The default is GitHub's own `bug` label. */
64
+ export function labelRow(overrides = {}) {
65
+ return build({ name: 'bug', color: 'd73a4a', description: "Something isn't working" }, overrides);
66
+ }
67
+ /** A check run. The default completed with `SUCCESS`, so it reads as a green check. */
68
+ export function checkRunRow(overrides = {}) {
69
+ return build({
70
+ __typename: 'CheckRun',
71
+ name: 'test',
72
+ workflowName: 'CI',
73
+ status: 'COMPLETED',
74
+ conclusion: 'SUCCESS',
75
+ detailsUrl: `${HOST}/${REPO}/actions/runs/1/job/1`,
76
+ startedAt: CREATED_AT,
77
+ completedAt: UPDATED_AT,
78
+ }, overrides);
79
+ }
80
+ /** A legacy status context. The default is a `SUCCESS`. */
81
+ export function statusContextRow(overrides = {}) {
82
+ return build({
83
+ __typename: 'StatusContext',
84
+ context: 'ci/build',
85
+ state: 'SUCCESS',
86
+ targetUrl: 'https://ci.example.com/builds/1',
87
+ startedAt: CREATED_AT,
88
+ }, overrides);
89
+ }
90
+ /** A comment by `octocat`. */
91
+ export function commentRow(overrides = {}) {
92
+ return build({
93
+ id: 'IC_kwDOAbCdEf5xYz12',
94
+ author: { login: 'octocat' },
95
+ authorAssociation: 'MEMBER',
96
+ body: 'Looks good to me.',
97
+ createdAt: UPDATED_AT,
98
+ url: `${HOST}/${REPO}/pull/1#issuecomment-1`,
99
+ }, overrides);
100
+ }
101
+ /** An approving review by `octocat`. */
102
+ export function reviewRow(overrides = {}) {
103
+ return build({
104
+ id: 'PRR_kwDOAbCdEf5xYz34',
105
+ author: { login: 'octocat' },
106
+ authorAssociation: 'MEMBER',
107
+ body: '',
108
+ state: 'APPROVED',
109
+ submittedAt: UPDATED_AT,
110
+ commit: { oid: HEAD_OID },
111
+ }, overrides);
112
+ }
113
+ /**
114
+ * An open, mergeable pull request #1 from `feature/add-retries` into `main`
115
+ * of `octo-org/hello-world`, by `octocat`, with no labels, comments, reviews
116
+ * or checks. Its `url` follows `number`.
117
+ */
118
+ export function prRow(overrides = {}) {
119
+ const number = overrides.number ?? 1;
120
+ return build({
121
+ number,
122
+ state: 'OPEN',
123
+ title: 'Add retries to the fetch loop',
124
+ body: 'Retries a failed fetch three times before giving up.',
125
+ url: `${HOST}/${REPO}/pull/${number}`,
126
+ isDraft: false,
127
+ baseRefName: 'main',
128
+ headRefName: 'feature/add-retries',
129
+ headRefOid: HEAD_OID,
130
+ isCrossRepository: false,
131
+ mergeable: 'MERGEABLE',
132
+ mergeStateStatus: 'CLEAN',
133
+ reviewDecision: '',
134
+ author: octocat(),
135
+ labels: [],
136
+ comments: [],
137
+ reviews: [],
138
+ statusCheckRollup: [],
139
+ mergedAt: null,
140
+ createdAt: CREATED_AT,
141
+ updatedAt: UPDATED_AT,
142
+ }, overrides);
143
+ }
144
+ /** An open issue #1 with no labels. */
145
+ export function issueRow(overrides = {}) {
146
+ return build({
147
+ number: 1,
148
+ title: 'Fetch gives up after one failure',
149
+ body: 'A single network error ends the whole sync.',
150
+ labels: [],
151
+ state: 'OPEN',
152
+ }, overrides);
153
+ }
154
+ /** The public, unarchived `octo-org/hello-world` on `main`. Its `url` follows `nameWithOwner`. */
155
+ export function repoRow(overrides = {}) {
156
+ const nameWithOwner = overrides.nameWithOwner ?? REPO;
157
+ return build({
158
+ nameWithOwner,
159
+ description: 'My first repository on GitHub.',
160
+ defaultBranchRef: { name: 'main' },
161
+ visibility: 'PUBLIC',
162
+ isArchived: false,
163
+ url: `${HOST}/${nameWithOwner}`,
164
+ }, overrides);
165
+ }
166
+ /**
167
+ * An open pull request #1 in `octo-org/hello-world`, as search returns it. Its
168
+ * `url` follows `number` and `repository`.
169
+ */
170
+ export function searchPrRow(overrides = {}) {
171
+ const number = overrides.number ?? 1;
172
+ const repository = overrides.repository ?? { name: 'hello-world', nameWithOwner: REPO };
173
+ return build({
174
+ number,
175
+ title: 'Add retries to the fetch loop',
176
+ state: 'open',
177
+ url: `${HOST}/${repository.nameWithOwner}/pull/${number}`,
178
+ isDraft: false,
179
+ repository,
180
+ author: { id: 'MDQ6VXNlcjU4MzIzMQ==', is_bot: false, login: 'octocat' },
181
+ labels: [],
182
+ createdAt: CREATED_AT,
183
+ updatedAt: UPDATED_AT,
184
+ closedAt: '0001-01-01T00:00:00Z',
185
+ }, overrides);
186
+ }
package/dist/verbs.d.ts CHANGED
@@ -35,9 +35,9 @@ export interface GhVerb<T> {
35
35
  env?: Record<string, string> | undefined;
36
36
  /**
37
37
  * True for a read whose failure is part of its answer. `installed`,
38
- * `authStatus` and `api` return what `parse` makes of any result, in all
39
- * three call styles. Other reads throw from the plain method when gh exits
40
- * non-zero.
38
+ * `authStatus` and `api` return what `parse` makes of any result, from the
39
+ * plain method and from `try`; they have no soft form. Other reads throw
40
+ * from the plain method when gh exits non-zero.
41
41
  */
42
42
  probe?: boolean | undefined;
43
43
  /**
@@ -94,7 +94,10 @@ export declare const PR_VIEW_FIELDS: readonly ["number", "state", "title", "url"
94
94
  /**
95
95
  * A list query's answer: the rows, and whether `limit` cut the list short.
96
96
  * The verb asks gh for one row more than `limit`. `truncated` is true when
97
- * that row came back, and `rows` holds at most `limit`.
97
+ * that row came back, and `rows` holds at most `limit`. A query that goes
98
+ * through GitHub search, which returns at most 1000 results, is also
99
+ * `truncated` when 1000 rows came back: `prList` with a non-empty `search` or
100
+ * an `author`, `issueList` with a `label`, and `searchPrs`.
98
101
  */
99
102
  export interface ListResult<T> {
100
103
  rows: T[];
@@ -223,8 +226,10 @@ export interface GraphqlOptions {
223
226
  * connection across the pages with {@link nodesAt}.
224
227
  *
225
228
  * A string variable of exactly `{owner}` or `{repo}` is filled from the
226
- * target's repo, or by gh from the clone. A mutation document throws
227
- * `RangeError`. This is a read, and changes go through `raw`.
229
+ * target's repo, or by gh from the clone. A document holding any mutation or
230
+ * subscription throws `RangeError`, as does a variable named `query` or
231
+ * `operationName`, which gh would send as the request's own fields. This is a
232
+ * read, and changes go through `raw`.
228
233
  *
229
234
  * @experimental
230
235
  */