@remogram/provider-github-api 0.1.0-beta.2 → 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 +162 -20
- 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,19 @@ 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,
|
|
15
26
|
apiProviderCommands,
|
|
16
27
|
} from '@remogram/core';
|
|
17
28
|
|
|
@@ -40,6 +51,8 @@ query RemogramPrView($owner: String!, $repo: String!, $number: Int!) {
|
|
|
40
51
|
const AUTH_CAPABILITIES = [
|
|
41
52
|
'repo_status',
|
|
42
53
|
'ref_compare',
|
|
54
|
+
'ref_inventory',
|
|
55
|
+
'cr_inventory',
|
|
43
56
|
'pr_status',
|
|
44
57
|
'pr_checks',
|
|
45
58
|
'merge_plan',
|
|
@@ -153,22 +166,77 @@ export async function githubFetch(config, parsed, path, options = {}) {
|
|
|
153
166
|
});
|
|
154
167
|
}
|
|
155
168
|
|
|
156
|
-
const MAX_CHECK_PAGES =
|
|
169
|
+
const MAX_CHECK_PAGES = MAX_CHECK_STATUS_PAGES;
|
|
157
170
|
|
|
158
|
-
export
|
|
159
|
-
const
|
|
160
|
-
|
|
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
|
+
}) {
|
|
161
192
|
const all = [];
|
|
162
|
-
let
|
|
193
|
+
let truncated = false;
|
|
194
|
+
let url = startUrl;
|
|
195
|
+
let activeLimit = initialLimit;
|
|
163
196
|
for (let page = 0; page < MAX_CHECK_PAGES && url; page += 1) {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
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));
|
|
168
212
|
const linkHeader = headers?.get?.('link') ?? headers?.get?.('Link') ?? null;
|
|
169
|
-
|
|
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;
|
|
170
224
|
}
|
|
171
|
-
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
|
+
});
|
|
172
240
|
}
|
|
173
241
|
|
|
174
242
|
export function graphqlEndpoint(config, parsed = {}) {
|
|
@@ -264,15 +332,21 @@ export async function repoStatus(ctx) {
|
|
|
264
332
|
}
|
|
265
333
|
|
|
266
334
|
export function providerCapabilities() {
|
|
335
|
+
const check_sources = ['commit_statuses', 'check_runs'];
|
|
267
336
|
return {
|
|
268
337
|
commands: STRUCTURED_COMMANDS,
|
|
269
338
|
auth_envs: ['GITHUB_TOKEN', 'GH_TOKEN'],
|
|
270
|
-
check_sources
|
|
339
|
+
check_sources,
|
|
271
340
|
mergeability_confidence: 'derived',
|
|
272
341
|
host_binding: 'verified_remote_host',
|
|
273
342
|
pagination: 'supported',
|
|
274
343
|
write_support: false,
|
|
275
344
|
...forgeIngestCapabilityFacts(),
|
|
345
|
+
...checkPaginationCapabilityFacts({
|
|
346
|
+
strategy: 'link_header',
|
|
347
|
+
pageSizeParam: 'per_page',
|
|
348
|
+
sourceCount: check_sources.length,
|
|
349
|
+
}),
|
|
276
350
|
};
|
|
277
351
|
}
|
|
278
352
|
|
|
@@ -383,12 +457,14 @@ export async function prChecks(ctx, opts) {
|
|
|
383
457
|
|
|
384
458
|
const statusPath = repoApiPath(ctx.config, 'commits', sha, 'statuses');
|
|
385
459
|
const checkRunsPath = repoApiPath(ctx.config, 'commits', sha, 'check-runs');
|
|
386
|
-
const [
|
|
460
|
+
const [statusResult, checkRunResult] = await Promise.all([
|
|
387
461
|
githubFetchPaginated(ctx.config, ctx.parsed, statusPath, (body) =>
|
|
388
462
|
Array.isArray(body) ? body : [],
|
|
389
463
|
),
|
|
390
464
|
githubFetchPaginated(ctx.config, ctx.parsed, checkRunsPath, (body) => body?.check_runs ?? []),
|
|
391
465
|
]);
|
|
466
|
+
const statusRecords = statusResult.items;
|
|
467
|
+
const checkRunRecords = checkRunResult.items;
|
|
392
468
|
const mappedStatuses = statusRecords.map((s) => ({
|
|
393
469
|
context: sanitizeField(s.context),
|
|
394
470
|
state: normalizeCommitStatusState(s.state),
|
|
@@ -400,18 +476,19 @@ export async function prChecks(ctx, opts) {
|
|
|
400
476
|
description: sanitizeField(checkRunDescription(run)),
|
|
401
477
|
}));
|
|
402
478
|
const mapped = [...mappedStatuses, ...mappedCheckRuns];
|
|
403
|
-
|
|
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
|
+
};
|
|
404
486
|
}
|
|
405
487
|
|
|
406
488
|
export async function mergePlan(ctx, opts) {
|
|
407
489
|
const view = await prView(ctx, opts);
|
|
408
490
|
const checks = await prChecks(ctx, { number: view.pr_number });
|
|
409
|
-
const blockers =
|
|
410
|
-
if (view.mergeability === 'conflicted') blockers.push('merge_conflict');
|
|
411
|
-
if (view.state !== 'open') blockers.push('pr_not_open');
|
|
412
|
-
if (checks.check_conclusion === 'failure') blockers.push('checks_failed');
|
|
413
|
-
if (checks.check_conclusion === 'missing') blockers.push('checks_missing');
|
|
414
|
-
if (checks.check_conclusion === 'pending') blockers.push('checks_pending');
|
|
491
|
+
const blockers = mergeBlockersFromFacts(view, checks);
|
|
415
492
|
return {
|
|
416
493
|
pr_number: view.pr_number,
|
|
417
494
|
mergeability: view.mergeability,
|
|
@@ -420,6 +497,68 @@ export async function mergePlan(ctx, opts) {
|
|
|
420
497
|
};
|
|
421
498
|
}
|
|
422
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
|
+
|
|
423
562
|
export async function syncPlan(ctx, remoteName = 'origin') {
|
|
424
563
|
assertGitRemote(remoteName, 'remote');
|
|
425
564
|
apiBase(ctx.config, ctx.parsed);
|
|
@@ -453,6 +592,9 @@ export const provider = {
|
|
|
453
592
|
providerCapabilities,
|
|
454
593
|
repoStatus,
|
|
455
594
|
refsCompare,
|
|
595
|
+
refsInventory,
|
|
596
|
+
listOpenPulls,
|
|
597
|
+
crInventory: crInventorySlice,
|
|
456
598
|
prView,
|
|
457
599
|
prChecks,
|
|
458
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
|
}
|