@codetreker/daynight 0.0.0-stage → 0.2.1

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.
Files changed (47) hide show
  1. package/dist/cli.d.ts +17 -0
  2. package/dist/cli.js +164 -0
  3. package/dist/cli.js.map +1 -0
  4. package/dist/config.d.ts +32 -0
  5. package/dist/config.js +263 -0
  6. package/dist/config.js.map +1 -0
  7. package/dist/coordinator.d.ts +39 -0
  8. package/dist/coordinator.js +390 -0
  9. package/dist/coordinator.js.map +1 -0
  10. package/dist/copilot.d.ts +44 -0
  11. package/dist/copilot.js +263 -0
  12. package/dist/copilot.js.map +1 -0
  13. package/dist/diagnostics.d.ts +1 -0
  14. package/dist/diagnostics.js +16 -0
  15. package/dist/diagnostics.js.map +1 -0
  16. package/dist/git.d.ts +41 -0
  17. package/dist/git.js +539 -0
  18. package/dist/git.js.map +1 -0
  19. package/dist/github.d.ts +70 -0
  20. package/dist/github.js +675 -0
  21. package/dist/github.js.map +1 -0
  22. package/dist/instance-lock.d.ts +5 -0
  23. package/dist/instance-lock.js +115 -0
  24. package/dist/instance-lock.js.map +1 -0
  25. package/dist/main.d.ts +2 -0
  26. package/dist/main.js +12 -0
  27. package/dist/main.js.map +1 -0
  28. package/dist/paths.d.ts +33 -0
  29. package/dist/paths.js +155 -0
  30. package/dist/paths.js.map +1 -0
  31. package/dist/processes.d.ts +23 -0
  32. package/dist/processes.js +182 -0
  33. package/dist/processes.js.map +1 -0
  34. package/dist/repositories.d.ts +15 -0
  35. package/dist/repositories.js +159 -0
  36. package/dist/repositories.js.map +1 -0
  37. package/dist/review-list.d.ts +5 -0
  38. package/dist/review-list.js +123 -0
  39. package/dist/review-list.js.map +1 -0
  40. package/dist/runtime.d.ts +24 -0
  41. package/dist/runtime.js +77 -0
  42. package/dist/runtime.js.map +1 -0
  43. package/dist/store.d.ts +136 -0
  44. package/dist/store.js +579 -0
  45. package/dist/store.js.map +1 -0
  46. package/package.json +33 -3
  47. package/README.md +0 -3
package/dist/github.js ADDED
@@ -0,0 +1,675 @@
1
+ import { execFile } from "node:child_process";
2
+ export class GitHubApiError extends Error {
3
+ status;
4
+ retryAt;
5
+ constructor(message, options = {}) {
6
+ super(message, { cause: options.cause });
7
+ this.name = "GitHubApiError";
8
+ this.status = options.status === undefined ? null : options.status;
9
+ this.retryAt = options.retryAt === undefined ? null : options.retryAt;
10
+ }
11
+ }
12
+ export const executeCommand = (file, args, options) => new Promise((resolve) => {
13
+ const env = { ...process.env, GH_PROMPT_DISABLED: "1", GH_FORCE_TTY: "", NO_COLOR: "1", PAGER: "cat", GH_PAGER: "cat", ...options.environment };
14
+ delete env.GH_DEBUG;
15
+ execFile(file, [...args], {
16
+ encoding: "utf8", windowsHide: true, timeout: options.timeoutMs, maxBuffer: options.maxBufferBytes,
17
+ env, ...(options.signal === undefined ? {} : { signal: options.signal }),
18
+ }, (error, stdout, stderr) => resolve({ error, stdout, stderr }));
19
+ });
20
+ const object = (value, subject) => {
21
+ if (typeof value !== "object" || value === null || Array.isArray(value))
22
+ throw new Error(`${subject} must be an object.`);
23
+ return value;
24
+ };
25
+ const positiveInteger = (value, subject) => {
26
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
27
+ throw new Error(`${subject} must be a positive safe integer.`);
28
+ return value;
29
+ };
30
+ const text = (value, subject) => {
31
+ if (typeof value !== "string" || value.trim() === "" || /[\r\n\0]/.test(value))
32
+ throw new Error(`${subject} must be nonempty text without control characters.`);
33
+ return value;
34
+ };
35
+ const pullRequestNumber = (value) => positiveInteger(object(value, "PR").number, "PR number");
36
+ const observation = (value) => {
37
+ const data = object(value, "PR");
38
+ const number = pullRequestNumber(data);
39
+ if (data.state !== "open" && data.state !== "closed")
40
+ throw new Error(`PR ${number} has an invalid state.`);
41
+ if (typeof data.draft !== "boolean")
42
+ throw new Error(`PR ${number} has an invalid Draft flag.`);
43
+ if (data.merged_at !== null && (typeof data.merged_at !== "string" || !Number.isFinite(Date.parse(data.merged_at))))
44
+ throw new Error(`PR ${number} has an invalid merge time.`);
45
+ if (data.merged !== undefined && typeof data.merged !== "boolean")
46
+ throw new Error(`PR ${number} has an invalid merged flag.`);
47
+ const merged = data.merged_at !== null || data.merged === true;
48
+ if (merged && data.state !== "closed")
49
+ throw new Error(`PR ${number} is merged but not closed.`);
50
+ if (data.merged === false && data.merged_at !== null)
51
+ throw new Error(`PR ${number} has inconsistent merge fields.`);
52
+ const head = object(data.head, `PR ${number} head`);
53
+ const base = object(data.base, `PR ${number} base`);
54
+ return {
55
+ number, state: merged ? "merged" : data.state, draft: data.draft,
56
+ headSha: text(head.sha, `PR ${number} head SHA`), baseRef: text(base.ref, `PR ${number} base branch`),
57
+ baseSha: text(base.sha, `PR ${number} base SHA`),
58
+ };
59
+ };
60
+ const pullRequestChecksQuery = `query PullRequestChecks($owner: String!, $name: String!, $number: Int!, $headCursor: String, $mergeCursor: String) {
61
+ repository(owner: $owner, name: $name) {
62
+ pullRequest(number: $number) {
63
+ state isDraft headRefOid baseRefName
64
+ commits(last: 1) { nodes { commit {
65
+ oid statusCheckRollup { state }
66
+ checkSuites(first: 100, after: $headCursor) { ...SuitePage }
67
+ } } }
68
+ potentialMergeCommit {
69
+ oid statusCheckRollup { state }
70
+ checkSuites(first: 100, after: $mergeCursor) { ...SuitePage }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ fragment SuitePage on CheckSuiteConnection {
76
+ totalCount
77
+ nodes {
78
+ id status conclusion
79
+ workflowRun { event runNumber runAttempt workflow { id } }
80
+ }
81
+ pageInfo { hasNextPage endCursor }
82
+ }`;
83
+ const suitePage = (value, subject) => {
84
+ const connection = object(value, `${subject} check suites`);
85
+ const totalCount = connection.totalCount;
86
+ if (typeof totalCount !== "number" || !Number.isSafeInteger(totalCount) || totalCount < 0)
87
+ throw new Error(`${subject} has an invalid check-suite count.`);
88
+ if (!Array.isArray(connection.nodes) || connection.nodes.length > 100)
89
+ throw new Error(`${subject} has an invalid check-suite page.`);
90
+ const suites = connection.nodes.map((value) => {
91
+ const suite = object(value, `${subject} check suite`);
92
+ if (typeof suite.status !== "string" || !["REQUESTED", "QUEUED", "IN_PROGRESS", "COMPLETED", "WAITING", "PENDING"].includes(suite.status))
93
+ throw new Error(`${subject} has an invalid check-suite status.`);
94
+ if (suite.conclusion !== null && (typeof suite.conclusion !== "string" || !["ACTION_REQUIRED", "TIMED_OUT", "CANCELLED", "FAILURE", "SUCCESS", "NEUTRAL", "SKIPPED", "STARTUP_FAILURE", "STALE"].includes(suite.conclusion)))
95
+ throw new Error(`${subject} has an invalid check-suite conclusion.`);
96
+ let workflow = null;
97
+ if (suite.workflowRun !== null) {
98
+ const run = object(suite.workflowRun, `${subject} workflow run`);
99
+ workflow = {
100
+ key: JSON.stringify([text(object(run.workflow, `${subject} workflow`).id, `${subject} workflow ID`), text(run.event, `${subject} workflow event`)]),
101
+ runNumber: positiveInteger(run.runNumber, `${subject} workflow run number`),
102
+ runAttempt: positiveInteger(run.runAttempt, `${subject} workflow run attempt`),
103
+ };
104
+ }
105
+ return { id: text(suite.id, `${subject} check-suite ID`), ready: suite.status === "COMPLETED" && typeof suite.conclusion === "string" && ["SUCCESS", "NEUTRAL", "SKIPPED"].includes(suite.conclusion), workflow };
106
+ });
107
+ const info = object(connection.pageInfo, `${subject} check-suite page info`);
108
+ if (typeof info.hasNextPage !== "boolean" || (info.endCursor !== null && typeof info.endCursor !== "string"))
109
+ throw new Error(`${subject} has invalid check-suite pagination.`);
110
+ const cursor = info.endCursor === null ? null : text(info.endCursor, `${subject} check-suite cursor`);
111
+ if (info.hasNextPage && (cursor === null || suites.length === 0))
112
+ throw new Error(`${subject} has an incomplete check-suite page.`);
113
+ return { totalCount, suites, cursor: info.hasNextPage ? cursor : null };
114
+ };
115
+ const suiteCollection = (subject) => {
116
+ let ready = true;
117
+ let expectedCount = null;
118
+ let complete = false;
119
+ let cursor = null;
120
+ const cursors = new Set();
121
+ const collected = new Set();
122
+ const workflows = new Map();
123
+ return {
124
+ observe: (page) => {
125
+ if (expectedCount === null)
126
+ expectedCount = page.totalCount;
127
+ else if (page.totalCount !== expectedCount)
128
+ throw new Error(`${subject} check-suite count changed during pagination.`);
129
+ for (const suite of page.suites) {
130
+ if (complete && !collected.has(suite.id))
131
+ throw new Error(`${subject} check-suite membership changed after collection.`);
132
+ // Without a workflow identity, no later suite can prove this one was superseded.
133
+ if (suite.workflow === null) {
134
+ ready &&= suite.ready;
135
+ continue;
136
+ }
137
+ const { key, runNumber, runAttempt } = suite.workflow;
138
+ const previous = workflows.get(key);
139
+ // New runs increment runNumber; reruns increment runAttempt while retaining runNumber.
140
+ // https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#github-context
141
+ if (previous === undefined || runNumber > previous.runNumber || (runNumber === previous.runNumber && runAttempt > previous.runAttempt)) {
142
+ workflows.set(key, { runNumber, runAttempt, ready: suite.ready });
143
+ }
144
+ else if (runNumber === previous.runNumber && runAttempt === previous.runAttempt) {
145
+ previous.ready &&= suite.ready;
146
+ }
147
+ }
148
+ if (complete)
149
+ return;
150
+ for (const suite of page.suites) {
151
+ if (collected.has(suite.id))
152
+ throw new Error(`${subject} repeated a check-suite ID during pagination.`);
153
+ collected.add(suite.id);
154
+ }
155
+ if (collected.size > expectedCount)
156
+ throw new Error(`${subject} returned more check suites than its count.`);
157
+ if (page.cursor === null) {
158
+ if (collected.size !== expectedCount)
159
+ throw new Error(`${subject} returned an incomplete check-suite collection.`);
160
+ complete = true;
161
+ }
162
+ else {
163
+ if (collected.size === expectedCount)
164
+ throw new Error(`${subject} has a check-suite cursor after its complete collection.`);
165
+ if (cursors.has(page.cursor))
166
+ throw new Error(`${subject} repeated a check-suite cursor.`);
167
+ cursors.add(page.cursor);
168
+ cursor = page.cursor;
169
+ }
170
+ },
171
+ complete: () => complete,
172
+ cursor: () => cursor,
173
+ ready: () => ready && [...workflows.values()].every((run) => run.ready),
174
+ };
175
+ };
176
+ const checksReady = (value, subject) => {
177
+ if (value === null)
178
+ return true;
179
+ const rollup = object(value, `${subject} status-check rollup`);
180
+ if (typeof rollup.state !== "string" || !["ERROR", "EXPECTED", "FAILURE", "PENDING", "SUCCESS"].includes(rollup.state))
181
+ throw new Error(`${subject} has an invalid status-check state.`);
182
+ return rollup.state === "SUCCESS";
183
+ };
184
+ const pullRequestChecks = (value) => {
185
+ const data = object(object(value, "GraphQL response").data, "GraphQL data");
186
+ const repository = object(data.repository, "GitHub repository");
187
+ const pullRequest = object(repository.pullRequest, "PR");
188
+ if (pullRequest.state !== "OPEN" && pullRequest.state !== "CLOSED" && pullRequest.state !== "MERGED")
189
+ throw new Error("PR has an invalid state.");
190
+ if (typeof pullRequest.isDraft !== "boolean")
191
+ throw new Error("PR has an invalid Draft flag.");
192
+ const headSha = text(pullRequest.headRefOid, "PR head SHA");
193
+ const baseRef = text(pullRequest.baseRefName, "PR base branch");
194
+ const commits = object(pullRequest.commits, "PR commits");
195
+ if (!Array.isArray(commits.nodes) || commits.nodes.length !== 1)
196
+ throw new Error("PR must have exactly one latest commit.");
197
+ const head = object(object(commits.nodes[0], "PR latest commit node").commit, "PR latest commit");
198
+ if (text(head.oid, "PR latest commit SHA") !== headSha)
199
+ throw new Error("PR latest commit does not match its head SHA.");
200
+ const headReady = checksReady(head.statusCheckRollup, "PR head");
201
+ let mergeReady = true;
202
+ let mergeSha = null;
203
+ let mergeSuites = null;
204
+ if (pullRequest.potentialMergeCommit !== null) {
205
+ const merge = object(pullRequest.potentialMergeCommit, "PR potential merge commit");
206
+ mergeSha = text(merge.oid, "PR potential merge commit SHA");
207
+ mergeReady = checksReady(merge.statusCheckRollup, "PR potential merge commit");
208
+ mergeSuites = suitePage(merge.checkSuites, "PR potential merge commit");
209
+ }
210
+ return { headSha, baseRef, mergeSha, headSuites: suitePage(head.checkSuites, "PR head"), mergeSuites,
211
+ ready: pullRequest.state === "OPEN" && !pullRequest.isDraft && headReady && mergeReady };
212
+ };
213
+ const parseHttpFrame = (stdout) => {
214
+ const separator = /\r?\n\r?\n/.exec(stdout);
215
+ if (separator === null)
216
+ throw new Error("GitHub CLI output has no complete HTTP header block.");
217
+ const lines = stdout.slice(0, separator.index).split(/\r?\n/);
218
+ const statusLine = /^HTTP\/\d(?:\.\d)? ([1-5]\d\d)(?: [^\r\n]*)?$/.exec(lines.shift());
219
+ if (statusLine === null)
220
+ throw new Error("GitHub CLI output has an invalid HTTP status line.");
221
+ const headers = new Map();
222
+ for (const line of lines) {
223
+ const match = /^([!#$%&'*+.^_`|~\da-z-]+):[ \t]*(.*)$/i.exec(line);
224
+ if (match === null)
225
+ throw new Error("GitHub CLI output has an invalid HTTP header.");
226
+ const key = match[1].toLowerCase();
227
+ const values = headers.get(key);
228
+ if (values === undefined)
229
+ headers.set(key, [match[2]]);
230
+ else
231
+ values.push(match[2]);
232
+ }
233
+ return { status: Number(statusLine[1]), headers, body: stdout.slice(separator.index + separator[0].length) };
234
+ };
235
+ const apiBase = (host) => {
236
+ const origin = new URL(`https://${host}`);
237
+ if (origin.host === "github.com")
238
+ return new URL("https://api.github.com/");
239
+ if (origin.host.endsWith(".ghe.com"))
240
+ return new URL(`https://api.${origin.host}/`);
241
+ return new URL(`https://${origin.host}/api/v3/`);
242
+ };
243
+ const graphQLApi = (host) => {
244
+ const rest = apiBase(host);
245
+ return new URL(rest.pathname === "/" ? "graphql" : "/api/graphql", rest);
246
+ };
247
+ const secondaryRateLimit = (body) => {
248
+ let data;
249
+ try {
250
+ data = JSON.parse(body);
251
+ }
252
+ catch (error) {
253
+ if (error instanceof SyntaxError)
254
+ return false;
255
+ throw error;
256
+ }
257
+ return typeof data === "object" && data !== null && !Array.isArray(data) &&
258
+ "message" in data && typeof data.message === "string" && /rate limit|abuse detection/i.test(data.message);
259
+ };
260
+ const rateLimitRetryAt = (response, now, graphQLRateLimited = false) => {
261
+ if (response.status !== 403 && response.status !== 429 && !graphQLRateLimited)
262
+ return null;
263
+ const retryAfter = response.headers.get("retry-after") ?? [];
264
+ const remaining = response.headers.get("x-ratelimit-remaining") ?? [];
265
+ const limited = graphQLRateLimited || response.status === 429 || remaining.includes("0") || retryAfter.length !== 0 || secondaryRateLimit(response.body);
266
+ if (!limited)
267
+ return null;
268
+ const deadlines = [now + 60_000];
269
+ for (const value of retryAfter) {
270
+ const deadline = /^\d+$/.test(value) ? now + Number(value) * 1000 : Date.parse(value);
271
+ if (!Number.isSafeInteger(deadline))
272
+ throw new Error("GitHub returned an invalid Retry-After deadline.");
273
+ deadlines.push(deadline);
274
+ }
275
+ if (remaining.includes("0") || graphQLRateLimited) {
276
+ for (const value of response.headers.get("x-ratelimit-reset") ?? []) {
277
+ const deadline = /^\d+$/.test(value) ? Number(value) * 1000 : NaN;
278
+ if (!Number.isSafeInteger(deadline))
279
+ throw new Error("GitHub returned an invalid rate-limit reset deadline.");
280
+ deadlines.push(deadline);
281
+ }
282
+ }
283
+ return Math.max(...deadlines);
284
+ };
285
+ const parseLinks = (header) => {
286
+ const links = [];
287
+ let position = 0;
288
+ const invalid = () => { throw new Error("GitHub returned an invalid pagination Link header."); };
289
+ const whitespace = () => { while (header[position] === " " || header[position] === "\t")
290
+ position++; };
291
+ const token = () => {
292
+ const start = position;
293
+ while (position < header.length && /[!#$%&'*+.^_`|~\da-z-]/i.test(header[position]))
294
+ position++;
295
+ if (position === start)
296
+ invalid();
297
+ return header.slice(start, position);
298
+ };
299
+ const value = () => {
300
+ if (header[position] !== '"')
301
+ return token();
302
+ position++;
303
+ let result = "";
304
+ while (position < header.length) {
305
+ let character = header[position++];
306
+ if (character === '"')
307
+ return result;
308
+ if (character === "\\") {
309
+ if (position === header.length)
310
+ invalid();
311
+ character = header[position++];
312
+ }
313
+ const code = character.charCodeAt(0);
314
+ if ((code < 32 && code !== 9) || code === 127)
315
+ invalid();
316
+ result += character;
317
+ }
318
+ return invalid();
319
+ };
320
+ whitespace();
321
+ while (position < header.length) {
322
+ if (header[position++] !== "<")
323
+ invalid();
324
+ const start = position;
325
+ while (position < header.length && header[position] !== ">") {
326
+ if (/[\x00-\x20\x7f<]/.test(header[position]))
327
+ invalid();
328
+ position++;
329
+ }
330
+ if (position === header.length)
331
+ invalid();
332
+ const target = header.slice(start, position++);
333
+ let relations = [];
334
+ let hasRelation = false;
335
+ whitespace();
336
+ while (position < header.length && header[position] !== ",") {
337
+ if (header[position++] !== ";")
338
+ invalid();
339
+ whitespace();
340
+ const name = token().toLowerCase();
341
+ whitespace();
342
+ let parameter = null;
343
+ if (header[position] === "=") {
344
+ position++;
345
+ whitespace();
346
+ parameter = value();
347
+ whitespace();
348
+ }
349
+ // Later rel parameters are ignored: https://www.rfc-editor.org/rfc/rfc8288.html#section-3.3
350
+ if (name === "rel" && !hasRelation) {
351
+ if (parameter === null || parameter === "" || parameter.trim() !== parameter)
352
+ invalid();
353
+ hasRelation = true;
354
+ relations = parameter.split(/ +/);
355
+ for (const relation of relations) {
356
+ if (/^[a-z][a-z\d.-]*$/i.test(relation))
357
+ continue;
358
+ try {
359
+ new URL(relation);
360
+ }
361
+ catch {
362
+ invalid();
363
+ }
364
+ }
365
+ }
366
+ }
367
+ links.push({ target, relations });
368
+ if (position === header.length)
369
+ break;
370
+ position++;
371
+ whitespace();
372
+ if (position === header.length)
373
+ invalid();
374
+ }
375
+ if (links.length === 0)
376
+ invalid();
377
+ return links;
378
+ };
379
+ const nextPage = (response, current, repository, scan) => {
380
+ let next = null;
381
+ for (const header of response.headers.get("link") ?? []) {
382
+ for (const entry of parseLinks(header)) {
383
+ if (!entry.relations.some((relation) => relation.toLowerCase() === "next"))
384
+ continue;
385
+ if (next !== null)
386
+ throw new Error("GitHub returned more than one next page.");
387
+ const url = new URL(entry.target);
388
+ const base = apiBase(repository.host);
389
+ const prefix = base.pathname;
390
+ const allowed = [`${prefix}repos/${repository.owner}/${repository.name}/pulls`, `${prefix}repositories/${repository.githubId}/pulls`];
391
+ if (url.protocol !== "https:" || url.origin !== base.origin || url.username !== "" || url.password !== "" || url.hash !== "" || !allowed.includes(url.pathname)) {
392
+ throw new Error("GitHub pagination points outside the expected repository API.");
393
+ }
394
+ const keys = new Set([...scan.searchParams.keys(), ...url.searchParams.keys()]);
395
+ for (const key of keys) {
396
+ const values = url.searchParams.getAll(key);
397
+ if (key !== "page" && (values.length !== 1 || values[0] !== scan.searchParams.get(key)))
398
+ throw new Error("GitHub pagination changed scan parameters.");
399
+ }
400
+ const pages = url.searchParams.getAll("page");
401
+ if (pages.length !== 1 || !/^[1-9]\d*$/.test(pages[0]) || !Number.isSafeInteger(Number(pages[0])) || Number(pages[0]) <= Number(current.searchParams.get("page"))) {
402
+ throw new Error("GitHub pagination did not advance to a later page.");
403
+ }
404
+ next = url;
405
+ }
406
+ }
407
+ return next;
408
+ };
409
+ // Credential command errors may contain captured output, so only process metadata is retained.
410
+ export const sanitizedProcessError = (error) => {
411
+ const sanitized = new Error("GitHub CLI did not complete successfully.");
412
+ if (typeof error === "object" && error !== null) {
413
+ const failure = error;
414
+ if (typeof failure.code === "number" || (typeof failure.code === "string" && /^[A-Z][A-Z\d_]*$/.test(failure.code)))
415
+ Object.assign(sanitized, { code: failure.code });
416
+ if (typeof failure.killed === "boolean")
417
+ Object.assign(sanitized, { killed: failure.killed });
418
+ if (failure.signal === null || (typeof failure.signal === "string" && /^SIG[A-Z\d]+$/.test(failure.signal)))
419
+ Object.assign(sanitized, { signal: failure.signal });
420
+ }
421
+ return sanitized;
422
+ };
423
+ export const prepareGitHubAuthentication = async (host, execute, options) => {
424
+ const origin = new URL(`https://${host}`);
425
+ const sensitive = origin.port !== "";
426
+ let environment;
427
+ if (sensitive && !process.env.GH_ENTERPRISE_TOKEN && !process.env.GITHUB_ENTERPRISE_TOKEN) {
428
+ let authentication;
429
+ try {
430
+ authentication = await execute("gh", ["auth", "token", "--hostname", origin.hostname], options);
431
+ }
432
+ catch (cause) {
433
+ throw new GitHubApiError(`Cannot obtain prepared GitHub authentication for ${origin.hostname}.`, { cause: sanitizedProcessError(cause) });
434
+ }
435
+ const token = authentication.stdout.trim();
436
+ if (authentication.error !== null || options.signal?.aborted || !/^[^\s\0]+$/.test(token)) {
437
+ throw new GitHubApiError(`Cannot obtain prepared GitHub authentication for ${origin.hostname}.`, { cause: sanitizedProcessError(authentication.error) });
438
+ }
439
+ environment = { GH_ENTERPRISE_TOKEN: token };
440
+ }
441
+ return { hostname: origin.hostname, environment, sensitive };
442
+ };
443
+ export const createGitHubClient = (options = {}) => {
444
+ const execute = options.execute === undefined ? executeCommand : options.execute;
445
+ const now = options.now === undefined ? Date.now : options.now;
446
+ const timeoutMs = options.timeoutMs === undefined ? 30_000 : options.timeoutMs;
447
+ const maxBufferBytes = options.maxBufferBytes === undefined ? 16 * 1024 * 1024 : options.maxBufferBytes;
448
+ positiveInteger(timeoutMs, "GitHub request timeout");
449
+ positiveInteger(maxBufferBytes, "GitHub response size limit");
450
+ if (timeoutMs > 2_147_483_647)
451
+ throw new Error("GitHub request timeout exceeds the supported timer limit.");
452
+ const cooldowns = new Map();
453
+ const requireAvailableHost = (host, origin) => {
454
+ const cooldown = cooldowns.get(origin);
455
+ if (cooldown !== undefined) {
456
+ if (now() < cooldown.retryAt) {
457
+ throw new GitHubApiError(`GitHub API requests for ${host} are paused until ${cooldown.retryAt}.`, {
458
+ status: cooldown.status, retryAt: cooldown.retryAt, cause: cooldown.error,
459
+ });
460
+ }
461
+ cooldowns.delete(origin);
462
+ }
463
+ };
464
+ const request = async (host, endpoint, signal, query) => {
465
+ const base = apiBase(host);
466
+ requireAvailableHost(host, base.origin);
467
+ const origin = new URL(`https://${host}`);
468
+ const ported = origin.port !== "";
469
+ let target = endpoint;
470
+ if (ported) {
471
+ const url = query === undefined ? new URL(endpoint, base) : graphQLApi(host);
472
+ if (url.origin !== base.origin || url.username !== "" || url.password !== "" || url.hash !== "" || (query === undefined && !url.pathname.startsWith(base.pathname))) {
473
+ throw new GitHubApiError(`Cannot construct a GitHub API URL for ${host}.`);
474
+ }
475
+ target = url.href;
476
+ }
477
+ const authentication = await prepareGitHubAuthentication(host, execute, { timeoutMs, maxBufferBytes, ...(signal === undefined ? {} : { signal }) });
478
+ requireAvailableHost(host, base.origin);
479
+ let result;
480
+ try {
481
+ // gh only groups flat variables for the literal "graphql" endpoint, not absolute URLs.
482
+ // https://github.com/cli/cli/blob/fa92838bef95fe4a29c1587693885d5e9bdb6cf1/pkg/cmd/api/http.go#L16-L55
483
+ const variable = (name) => ported ? `variables[${name}]` : name;
484
+ const fields = query === undefined ? [] : ["--raw-field", `query=${pullRequestChecksQuery}`,
485
+ "--raw-field", `${variable("owner")}=${query.owner}`, "--raw-field", `${variable("name")}=${query.name}`, "--field", `${variable("number")}=${query.number}`,
486
+ ...["headCursor", "mergeCursor"].flatMap((key) => [query[key] === null ? "--field" : "--raw-field", `${variable(key)}=${query[key] === null ? "null" : query[key]}`])];
487
+ result = await execute("gh", ["api", "--include", "--method", query === undefined ? "GET" : "POST", "--hostname", ported ? origin.hostname : host,
488
+ "--header", "Accept: application/vnd.github+json", "--header", "X-GitHub-Api-Version: 2022-11-28", ...fields, target], { timeoutMs, maxBufferBytes, ...(signal === undefined ? {} : { signal }), ...(authentication.environment === undefined ? {} : { environment: authentication.environment }) });
489
+ }
490
+ catch (cause) {
491
+ throw new GitHubApiError(`GitHub request failed for ${host}/${endpoint}.`, { cause: ported ? sanitizedProcessError(cause) : cause });
492
+ }
493
+ const failure = result.error;
494
+ const publicFailure = !ported || failure === null ? failure : sanitizedProcessError(failure);
495
+ if (signal?.aborted || (failure !== null && (!("code" in failure) || typeof failure.code !== "number" || ("killed" in failure && failure.killed === true) || ("signal" in failure && failure.signal !== null && failure.signal !== undefined)))) {
496
+ throw new GitHubApiError(`GitHub request did not produce complete output for ${host}/${endpoint}.`, {
497
+ cause: failure === null ? (ported ? sanitizedProcessError(signal?.reason) : signal?.reason) : publicFailure,
498
+ });
499
+ }
500
+ let response;
501
+ try {
502
+ response = parseHttpFrame(result.stdout);
503
+ }
504
+ catch (cause) {
505
+ const publicCause = ported ? sanitizedProcessError(cause) : cause;
506
+ throw new GitHubApiError(`Cannot parse GitHub response for ${host}/${endpoint}.`, {
507
+ cause: publicFailure === null ? publicCause : new AggregateError([publicFailure, publicCause], "GitHub CLI failed and its response was invalid."),
508
+ });
509
+ }
510
+ // A 429 may carry HTML: https://www.rfc-editor.org/rfc/rfc6585.html#section-4
511
+ if (response.status < 200 || response.status >= 300) {
512
+ let retryAt;
513
+ try {
514
+ retryAt = rateLimitRetryAt(response, now());
515
+ }
516
+ catch (cause) {
517
+ throw new GitHubApiError(`GitHub HTTP ${response.status} returned invalid retry metadata for ${host}/${endpoint}.`, {
518
+ status: response.status, cause: publicFailure === null ? cause : new AggregateError([publicFailure, cause]),
519
+ });
520
+ }
521
+ const error = new GitHubApiError(`GitHub HTTP ${response.status} for ${host}/${endpoint}.`, {
522
+ status: response.status, retryAt, ...(publicFailure === null ? {} : { cause: publicFailure }),
523
+ });
524
+ if (retryAt !== null)
525
+ cooldowns.set(base.origin, { error, status: response.status, retryAt });
526
+ throw error;
527
+ }
528
+ if (publicFailure !== null && query === undefined)
529
+ throw new GitHubApiError(`GitHub CLI exited unsuccessfully after HTTP ${response.status} for ${host}/${endpoint}.`, { cause: publicFailure });
530
+ let body;
531
+ try {
532
+ body = JSON.parse(response.body);
533
+ }
534
+ catch (cause) {
535
+ throw new GitHubApiError(`Cannot parse GitHub response for ${host}/${endpoint}.`, { cause: ported ? sanitizedProcessError(cause) : cause });
536
+ }
537
+ if (query !== undefined) {
538
+ const errors = object(body, "GraphQL response").errors;
539
+ if (errors !== undefined) {
540
+ if (!Array.isArray(errors))
541
+ throw new GitHubApiError(`GitHub GraphQL returned invalid errors for ${host}/${endpoint}.`);
542
+ if (errors.length !== 0) {
543
+ const limited = errors.some((error) => object(error, "GraphQL error").type === "RATE_LIMITED");
544
+ let retryAt;
545
+ try {
546
+ retryAt = rateLimitRetryAt(response, now(), limited);
547
+ }
548
+ catch (cause) {
549
+ throw new GitHubApiError(`GitHub GraphQL returned invalid retry metadata for ${host}/${endpoint}.`, {
550
+ status: response.status, cause: publicFailure === null ? cause : new AggregateError([publicFailure, cause]),
551
+ });
552
+ }
553
+ const error = new GitHubApiError(`GitHub GraphQL returned errors for ${host}/${endpoint}.`, {
554
+ status: response.status, retryAt, ...(publicFailure === null ? {} : { cause: publicFailure }),
555
+ });
556
+ if (retryAt !== null)
557
+ cooldowns.set(base.origin, { error, status: response.status, retryAt });
558
+ throw error;
559
+ }
560
+ }
561
+ }
562
+ if (publicFailure !== null)
563
+ throw new GitHubApiError(`GitHub CLI exited unsuccessfully after HTTP ${response.status} for ${host}/${endpoint}.`, { cause: publicFailure });
564
+ return { ...response, body };
565
+ };
566
+ const scan = async (repository, state, project, signal) => {
567
+ const base = apiBase(repository.host);
568
+ const first = new URL(`repos/${repository.owner}/${repository.name}/pulls`, base);
569
+ first.search = new URLSearchParams({ state, sort: "created", direction: "asc", per_page: "100", page: "1" }).toString();
570
+ let page = first;
571
+ const visited = new Set();
572
+ const results = new Map();
573
+ while (page !== null) {
574
+ if (visited.has(page.href))
575
+ throw new Error("GitHub pagination repeated a page.");
576
+ visited.add(page.href);
577
+ const response = await request(repository.host, page.pathname.slice(base.pathname.length) + page.search, signal);
578
+ if (!Array.isArray(response.body))
579
+ throw new Error("GitHub PR list must be an array.");
580
+ for (const item of response.body)
581
+ results.set(pullRequestNumber(item), project(item));
582
+ page = nextPage(response, page, repository, first);
583
+ }
584
+ return [...results.values()];
585
+ };
586
+ return {
587
+ resolveRepository: async (configuration, signal) => {
588
+ const response = await request(configuration.host, `repos/${configuration.owner}/${configuration.name}`, signal);
589
+ const data = object(response.body, "GitHub repository");
590
+ return { host: configuration.host, githubId: String(positiveInteger(data.id, "GitHub repository ID")), owner: configuration.owner, name: configuration.name };
591
+ },
592
+ listPullRequestNumbers: (repository, signal) => scan(repository, "all", pullRequestNumber, signal),
593
+ listOpenPullRequests: async (repository, cutoff, signal) => {
594
+ const results = (await scan(repository, "open", (item) => item, signal)).filter((item) => pullRequestNumber(item) > cutoff).map(observation);
595
+ if (results.some((item) => item.state !== "open"))
596
+ throw new Error("GitHub open PR list contains a closed PR.");
597
+ return results;
598
+ },
599
+ getPullRequest: async (repository, number, signal) => {
600
+ positiveInteger(number, "PR number");
601
+ const response = await request(repository.host, `repos/${repository.owner}/${repository.name}/pulls/${number}`, signal);
602
+ const result = observation(response.body);
603
+ if (result.number !== number)
604
+ throw new Error("GitHub returned a different PR than requested.");
605
+ return result;
606
+ },
607
+ getPullRequestChecks: async (repository, number, signal) => {
608
+ positiveInteger(number, "PR number");
609
+ const query = { owner: repository.owner, name: repository.name, number, headCursor: null, mergeCursor: null };
610
+ const head = suiteCollection("PR HEAD");
611
+ const merge = suiteCollection("PR merge commit");
612
+ let first = null;
613
+ let ready = true;
614
+ do {
615
+ const response = await request(repository.host, "graphql", signal, query);
616
+ const snapshot = pullRequestChecks(response.body);
617
+ if (first === null)
618
+ first = snapshot;
619
+ else if (snapshot.headSha !== first.headSha || snapshot.baseRef !== first.baseRef || snapshot.mergeSha !== first.mergeSha)
620
+ throw new Error("PR CI target changed during check-suite pagination.");
621
+ ready &&= snapshot.ready;
622
+ head.observe(snapshot.headSuites);
623
+ if (snapshot.mergeSuites !== null)
624
+ merge.observe(snapshot.mergeSuites);
625
+ query.headCursor = head.cursor();
626
+ query.mergeCursor = merge.cursor();
627
+ } while (!head.complete() || (first.mergeSha !== null && !merge.complete()));
628
+ return { headSha: first.headSha, baseRef: first.baseRef, ready: ready && head.ready() && merge.ready() };
629
+ },
630
+ };
631
+ };
632
+ export const pollRepository = async (store, configuration, client, options) => {
633
+ let identity = null;
634
+ try {
635
+ const repository = await client.resolveRepository(configuration, options.signal);
636
+ identity = repository;
637
+ const registered = store.registerRepository({ ...repository, remote: configuration.remote, localPath: configuration.path });
638
+ if (registered.startPrNumber === null) {
639
+ const numbers = await client.listPullRequestNumbers(repository, options.signal);
640
+ const startPrNumber = numbers.reduce((maximum, number) => Math.max(maximum, number), 0);
641
+ return store.applyRepositoryPoll(repository, { kind: "initialize", startPrNumber }, options.now());
642
+ }
643
+ const observations = await client.listOpenPullRequests(repository, registered.startPrNumber, options.signal);
644
+ const observed = new Set(observations.map((item) => item.number));
645
+ for (const previous of store.listPullRequests(repository)) {
646
+ if (previous.state === "open" && !observed.has(previous.number))
647
+ observations.push(await client.getPullRequest(repository, previous.number, options.signal));
648
+ }
649
+ return store.applyRepositoryPoll(repository, { kind: "snapshot", observations }, options.now());
650
+ }
651
+ catch (error) {
652
+ if (identity !== null) {
653
+ try {
654
+ store.recordPollError(identity, error instanceof Error ? error.message : String(error));
655
+ }
656
+ catch (recordError) {
657
+ throw new AggregateError([error, recordError], `GitHub polling and error persistence failed for ${configuration.coordinate}.`);
658
+ }
659
+ }
660
+ throw error;
661
+ }
662
+ };
663
+ export const pollRepositories = async (store, configurations, client, options) => {
664
+ const results = [];
665
+ for (const configuration of configurations) {
666
+ try {
667
+ results.push({ configuration, ok: true, repository: await pollRepository(store, configuration, client, options) });
668
+ }
669
+ catch (error) {
670
+ results.push({ configuration, ok: false, error });
671
+ }
672
+ }
673
+ return results;
674
+ };
675
+ //# sourceMappingURL=github.js.map