@remogram/provider-github-api 0.1.0-beta.1 → 0.1.0-beta.3
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/index.js +164 -21
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
fetchJson,
|
|
3
3
|
fetchJsonWithMeta,
|
|
4
4
|
parseLinkHeader,
|
|
5
|
+
isTrustedPaginationUrl,
|
|
5
6
|
sanitizeField,
|
|
6
7
|
sanitizeUrl,
|
|
7
8
|
assertGitRef,
|
|
@@ -9,9 +10,20 @@ import {
|
|
|
9
10
|
gitRevParse,
|
|
10
11
|
gitCurrentBranch,
|
|
11
12
|
gitAheadBehind,
|
|
13
|
+
refsInventory,
|
|
14
|
+
crInventory,
|
|
15
|
+
mergeBlockersFromFacts,
|
|
12
16
|
ERROR_CODES,
|
|
13
17
|
forgeError,
|
|
14
18
|
forgeIngestCapabilityFacts,
|
|
19
|
+
checkPaginationCapabilityFacts,
|
|
20
|
+
DEFAULT_CHECK_STATUS_PAGE_SIZE,
|
|
21
|
+
MAX_CHECK_STATUS_PAGES,
|
|
22
|
+
fetchWithIngestPageBackoff,
|
|
23
|
+
paginateOffsetListPages,
|
|
24
|
+
fetchPageWithIngestBackoff,
|
|
25
|
+
withPerPageParam,
|
|
26
|
+
apiProviderCommands,
|
|
15
27
|
} from '@remogram/core';
|
|
16
28
|
|
|
17
29
|
const PUBLIC_GITHUB_HOST = 'github.com';
|
|
@@ -39,13 +51,15 @@ query RemogramPrView($owner: String!, $repo: String!, $number: Int!) {
|
|
|
39
51
|
const AUTH_CAPABILITIES = [
|
|
40
52
|
'repo_status',
|
|
41
53
|
'ref_compare',
|
|
54
|
+
'ref_inventory',
|
|
55
|
+
'cr_inventory',
|
|
42
56
|
'pr_status',
|
|
43
57
|
'pr_checks',
|
|
44
58
|
'merge_plan',
|
|
45
59
|
'sync_plan',
|
|
46
60
|
];
|
|
47
61
|
|
|
48
|
-
const STRUCTURED_COMMANDS =
|
|
62
|
+
const STRUCTURED_COMMANDS = apiProviderCommands();
|
|
49
63
|
|
|
50
64
|
export function githubToken() {
|
|
51
65
|
if (process.env.GITHUB_TOKEN) return { token: process.env.GITHUB_TOKEN, env: 'GITHUB_TOKEN' };
|
|
@@ -152,22 +166,77 @@ export async function githubFetch(config, parsed, path, options = {}) {
|
|
|
152
166
|
});
|
|
153
167
|
}
|
|
154
168
|
|
|
155
|
-
const MAX_CHECK_PAGES =
|
|
169
|
+
const MAX_CHECK_PAGES = MAX_CHECK_STATUS_PAGES;
|
|
156
170
|
|
|
157
|
-
export
|
|
158
|
-
const
|
|
159
|
-
|
|
171
|
+
export function resolveGitHubLinkNextPage({ trustedOrigin, currentUrl, linkHeader, pageIndex, maxPages }) {
|
|
172
|
+
const nextRaw = parseLinkHeader(linkHeader).next ?? null;
|
|
173
|
+
if (!nextRaw) {
|
|
174
|
+
return { nextUrl: null, truncated: false };
|
|
175
|
+
}
|
|
176
|
+
if (!isTrustedPaginationUrl(trustedOrigin, nextRaw, currentUrl)) {
|
|
177
|
+
return { nextUrl: null, truncated: true };
|
|
178
|
+
}
|
|
179
|
+
if (pageIndex === maxPages - 1) {
|
|
180
|
+
return { nextUrl: null, truncated: true };
|
|
181
|
+
}
|
|
182
|
+
return { nextUrl: new URL(nextRaw, currentUrl).href, truncated: false };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function paginateGitHubLinkPages({
|
|
186
|
+
trustedOrigin,
|
|
187
|
+
startUrl,
|
|
188
|
+
token,
|
|
189
|
+
initialLimit = DEFAULT_CHECK_STATUS_PAGE_SIZE,
|
|
190
|
+
mapPageItems,
|
|
191
|
+
}) {
|
|
160
192
|
const all = [];
|
|
161
|
-
let
|
|
193
|
+
let truncated = false;
|
|
194
|
+
let url = startUrl;
|
|
195
|
+
let activeLimit = initialLimit;
|
|
162
196
|
for (let page = 0; page < MAX_CHECK_PAGES && url; page += 1) {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
197
|
+
const currentUrl = url;
|
|
198
|
+
let usedLimit = activeLimit;
|
|
199
|
+
const { body, headers } = await fetchWithIngestPageBackoff(
|
|
200
|
+
(attemptUrl) =>
|
|
201
|
+
fetchJsonWithMeta(attemptUrl, {
|
|
202
|
+
headers: authHeaders(token),
|
|
203
|
+
}),
|
|
204
|
+
(limit) => {
|
|
205
|
+
usedLimit = limit;
|
|
206
|
+
return withPerPageParam(currentUrl, limit);
|
|
207
|
+
},
|
|
208
|
+
activeLimit,
|
|
209
|
+
);
|
|
210
|
+
activeLimit = usedLimit;
|
|
211
|
+
all.push(...mapPageItems(body));
|
|
167
212
|
const linkHeader = headers?.get?.('link') ?? headers?.get?.('Link') ?? null;
|
|
168
|
-
|
|
213
|
+
const linkPage = resolveGitHubLinkNextPage({
|
|
214
|
+
trustedOrigin,
|
|
215
|
+
currentUrl,
|
|
216
|
+
linkHeader,
|
|
217
|
+
pageIndex: page,
|
|
218
|
+
maxPages: MAX_CHECK_PAGES,
|
|
219
|
+
});
|
|
220
|
+
if (linkPage.truncated) {
|
|
221
|
+
truncated = true;
|
|
222
|
+
}
|
|
223
|
+
url = linkPage.nextUrl ? withPerPageParam(linkPage.nextUrl, activeLimit) : null;
|
|
169
224
|
}
|
|
170
|
-
return all;
|
|
225
|
+
return { items: all, truncated };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function githubFetchPaginated(config, parsed, path, slice) {
|
|
229
|
+
const base = apiBase(config, parsed);
|
|
230
|
+
const trustedOrigin = new URL(base).origin;
|
|
231
|
+
const { token } = requireToken();
|
|
232
|
+
const pageQuery = `${path.includes('?') ? '&' : '?'}per_page=${DEFAULT_CHECK_STATUS_PAGE_SIZE}`;
|
|
233
|
+
const startUrl = `${base}${path}${pageQuery}`;
|
|
234
|
+
return paginateGitHubLinkPages({
|
|
235
|
+
trustedOrigin,
|
|
236
|
+
startUrl,
|
|
237
|
+
token,
|
|
238
|
+
mapPageItems: (body) => slice(body),
|
|
239
|
+
});
|
|
171
240
|
}
|
|
172
241
|
|
|
173
242
|
export function graphqlEndpoint(config, parsed = {}) {
|
|
@@ -263,15 +332,21 @@ export async function repoStatus(ctx) {
|
|
|
263
332
|
}
|
|
264
333
|
|
|
265
334
|
export function providerCapabilities() {
|
|
335
|
+
const check_sources = ['commit_statuses', 'check_runs'];
|
|
266
336
|
return {
|
|
267
337
|
commands: STRUCTURED_COMMANDS,
|
|
268
338
|
auth_envs: ['GITHUB_TOKEN', 'GH_TOKEN'],
|
|
269
|
-
check_sources
|
|
339
|
+
check_sources,
|
|
270
340
|
mergeability_confidence: 'derived',
|
|
271
341
|
host_binding: 'verified_remote_host',
|
|
272
342
|
pagination: 'supported',
|
|
273
343
|
write_support: false,
|
|
274
344
|
...forgeIngestCapabilityFacts(),
|
|
345
|
+
...checkPaginationCapabilityFacts({
|
|
346
|
+
strategy: 'link_header',
|
|
347
|
+
pageSizeParam: 'per_page',
|
|
348
|
+
sourceCount: check_sources.length,
|
|
349
|
+
}),
|
|
275
350
|
};
|
|
276
351
|
}
|
|
277
352
|
|
|
@@ -382,12 +457,14 @@ export async function prChecks(ctx, opts) {
|
|
|
382
457
|
|
|
383
458
|
const statusPath = repoApiPath(ctx.config, 'commits', sha, 'statuses');
|
|
384
459
|
const checkRunsPath = repoApiPath(ctx.config, 'commits', sha, 'check-runs');
|
|
385
|
-
const [
|
|
460
|
+
const [statusResult, checkRunResult] = await Promise.all([
|
|
386
461
|
githubFetchPaginated(ctx.config, ctx.parsed, statusPath, (body) =>
|
|
387
462
|
Array.isArray(body) ? body : [],
|
|
388
463
|
),
|
|
389
464
|
githubFetchPaginated(ctx.config, ctx.parsed, checkRunsPath, (body) => body?.check_runs ?? []),
|
|
390
465
|
]);
|
|
466
|
+
const statusRecords = statusResult.items;
|
|
467
|
+
const checkRunRecords = checkRunResult.items;
|
|
391
468
|
const mappedStatuses = statusRecords.map((s) => ({
|
|
392
469
|
context: sanitizeField(s.context),
|
|
393
470
|
state: normalizeCommitStatusState(s.state),
|
|
@@ -399,18 +476,19 @@ export async function prChecks(ctx, opts) {
|
|
|
399
476
|
description: sanitizeField(checkRunDescription(run)),
|
|
400
477
|
}));
|
|
401
478
|
const mapped = [...mappedStatuses, ...mappedCheckRuns];
|
|
402
|
-
|
|
479
|
+
const checks_truncated = statusResult.truncated || checkRunResult.truncated;
|
|
480
|
+
return {
|
|
481
|
+
head_sha: sha,
|
|
482
|
+
check_conclusion: summarizeChecks(mapped),
|
|
483
|
+
checks_truncated,
|
|
484
|
+
statuses: mapped,
|
|
485
|
+
};
|
|
403
486
|
}
|
|
404
487
|
|
|
405
488
|
export async function mergePlan(ctx, opts) {
|
|
406
489
|
const view = await prView(ctx, opts);
|
|
407
490
|
const checks = await prChecks(ctx, { number: view.pr_number });
|
|
408
|
-
const blockers =
|
|
409
|
-
if (view.mergeability === 'conflicted') blockers.push('merge_conflict');
|
|
410
|
-
if (view.state !== 'open') blockers.push('pr_not_open');
|
|
411
|
-
if (checks.check_conclusion === 'failure') blockers.push('checks_failed');
|
|
412
|
-
if (checks.check_conclusion === 'missing') blockers.push('checks_missing');
|
|
413
|
-
if (checks.check_conclusion === 'pending') blockers.push('checks_pending');
|
|
491
|
+
const blockers = mergeBlockersFromFacts(view, checks);
|
|
414
492
|
return {
|
|
415
493
|
pr_number: view.pr_number,
|
|
416
494
|
mergeability: view.mergeability,
|
|
@@ -419,6 +497,68 @@ export async function mergePlan(ctx, opts) {
|
|
|
419
497
|
};
|
|
420
498
|
}
|
|
421
499
|
|
|
500
|
+
export async function listOpenPullsWithMeta(ctx, opts = {}) {
|
|
501
|
+
apiBase(ctx.config, ctx.parsed);
|
|
502
|
+
requireToken();
|
|
503
|
+
const base = apiBase(ctx.config, ctx.parsed);
|
|
504
|
+
const { token } = requireToken();
|
|
505
|
+
const listLimit =
|
|
506
|
+
opts.limit != null && Number.isInteger(Number(opts.limit)) && Number(opts.limit) > 0
|
|
507
|
+
? Number(opts.limit)
|
|
508
|
+
: null;
|
|
509
|
+
const GITHUB_PAGE_SIZE = 100;
|
|
510
|
+
const all = [];
|
|
511
|
+
let listTruncated = false;
|
|
512
|
+
|
|
513
|
+
if (listLimit == null) {
|
|
514
|
+
const trustedOrigin = new URL(base).origin;
|
|
515
|
+
const startUrl = `${base}${repoApiPath(ctx.config, 'pulls')}?state=open`;
|
|
516
|
+
const { items: linkItems, truncated: linkTruncated } = await paginateGitHubLinkPages({
|
|
517
|
+
trustedOrigin,
|
|
518
|
+
startUrl,
|
|
519
|
+
token,
|
|
520
|
+
mapPageItems: (body) => (Array.isArray(body) ? body : []),
|
|
521
|
+
});
|
|
522
|
+
all.push(...linkItems);
|
|
523
|
+
listTruncated = linkTruncated;
|
|
524
|
+
} else {
|
|
525
|
+
const { items: limitItems, list_truncated: limitTruncated } = await paginateOffsetListPages({
|
|
526
|
+
pageSize: GITHUB_PAGE_SIZE,
|
|
527
|
+
listLimit,
|
|
528
|
+
// GitHub/GitLab use fixed pageSize with optional listLimit; mark truncated at maxPages.
|
|
529
|
+
// Gitea passes pageSize=min(listLimit, cap) so the limit branch often exits in one page.
|
|
530
|
+
maxPagesTruncatesWithLimit: true,
|
|
531
|
+
fetchPage: async ({ page, limit }) => {
|
|
532
|
+
const pageUrl = `${base}${repoApiPath(ctx.config, 'pulls')}?state=open&page=${page}`;
|
|
533
|
+
const { body } = await fetchJsonWithMeta(withPerPageParam(pageUrl, limit), {
|
|
534
|
+
headers: authHeaders(token),
|
|
535
|
+
});
|
|
536
|
+
return Array.isArray(body) ? body : [];
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
all.push(...limitItems);
|
|
540
|
+
listTruncated = limitTruncated;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
let numbers = all
|
|
544
|
+
.map((pr) => pr.number)
|
|
545
|
+
.filter((number) => Number.isInteger(number))
|
|
546
|
+
.sort((a, b) => a - b);
|
|
547
|
+
if (listLimit != null && numbers.length > listLimit) {
|
|
548
|
+
numbers = numbers.slice(0, listLimit);
|
|
549
|
+
}
|
|
550
|
+
return { numbers, list_truncated: listTruncated };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export async function listOpenPulls(ctx, opts = {}) {
|
|
554
|
+
const meta = await listOpenPullsWithMeta(ctx, opts);
|
|
555
|
+
return meta.numbers;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export async function crInventorySlice(ctx, opts = {}) {
|
|
559
|
+
return crInventory(ctx, { listOpenPulls, listOpenPullsWithMeta, prView, prChecks }, opts);
|
|
560
|
+
}
|
|
561
|
+
|
|
422
562
|
export async function syncPlan(ctx, remoteName = 'origin') {
|
|
423
563
|
assertGitRemote(remoteName, 'remote');
|
|
424
564
|
apiBase(ctx.config, ctx.parsed);
|
|
@@ -452,6 +592,9 @@ export const provider = {
|
|
|
452
592
|
providerCapabilities,
|
|
453
593
|
repoStatus,
|
|
454
594
|
refsCompare,
|
|
595
|
+
refsInventory,
|
|
596
|
+
listOpenPulls,
|
|
597
|
+
crInventory: crInventorySlice,
|
|
455
598
|
prView,
|
|
456
599
|
prChecks,
|
|
457
600
|
mergePlan,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remogram/provider-github-api",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.3",
|
|
4
4
|
"description": "GitHub API provider for remogram",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,6 +22,6 @@
|
|
|
22
22
|
"node": ">=20"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@remogram/core": "0.1.0-beta.
|
|
25
|
+
"@remogram/core": "0.1.0-beta.3"
|
|
26
26
|
}
|
|
27
27
|
}
|