@remogram/provider-github-api 0.1.0-beta.3 → 0.1.0-beta.5
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 +497 -29
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -12,18 +12,46 @@ import {
|
|
|
12
12
|
gitAheadBehind,
|
|
13
13
|
refsInventory,
|
|
14
14
|
crInventory,
|
|
15
|
-
|
|
15
|
+
buildMergePlanBodyFromFacts,
|
|
16
16
|
ERROR_CODES,
|
|
17
17
|
forgeError,
|
|
18
18
|
forgeIngestCapabilityFacts,
|
|
19
19
|
checkPaginationCapabilityFacts,
|
|
20
|
+
openPullListCapabilityFacts,
|
|
20
21
|
DEFAULT_CHECK_STATUS_PAGE_SIZE,
|
|
21
22
|
MAX_CHECK_STATUS_PAGES,
|
|
23
|
+
DEFAULT_OPEN_PULL_LIST_PAGE_SIZE,
|
|
22
24
|
fetchWithIngestPageBackoff,
|
|
23
25
|
paginateOffsetListPages,
|
|
24
26
|
fetchPageWithIngestBackoff,
|
|
25
27
|
withPerPageParam,
|
|
26
28
|
apiProviderCommands,
|
|
29
|
+
normalizeCrInventorySort,
|
|
30
|
+
DEFAULT_CR_INVENTORY_SLICE_SORT,
|
|
31
|
+
isCrInventoryFastPathEligible,
|
|
32
|
+
validateFastPathPageLength,
|
|
33
|
+
isNumberSortFastPathEligible,
|
|
34
|
+
isNumberSortFullCollectRequired,
|
|
35
|
+
resolvePaginatedEntryCount,
|
|
36
|
+
resolveListTruncatedWithTrustedTotal,
|
|
37
|
+
orderOpenPullNumbers,
|
|
38
|
+
buildOpenPullListMeta,
|
|
39
|
+
githubOpenPullSortQuery,
|
|
40
|
+
buildProviderIdentityFromGitHubUser,
|
|
41
|
+
buildBranchProtectionFromGitHubProtection,
|
|
42
|
+
buildCrFilesBody,
|
|
43
|
+
buildCrFilesFromGiteaFiles,
|
|
44
|
+
buildCrCommentsBody,
|
|
45
|
+
buildCrCommentsFromGiteaComments,
|
|
46
|
+
parseSinceObservedAt,
|
|
47
|
+
buildForgeChangesFromGiteaPulls,
|
|
48
|
+
buildChecksConclusionObservedEvent,
|
|
49
|
+
appendForgeChangeEvents,
|
|
50
|
+
buildCommitStatusSetBody,
|
|
51
|
+
parseStatusSetArgs,
|
|
52
|
+
normalizeStatusSetState,
|
|
53
|
+
statusSetIdempotencyScanCapabilityFacts,
|
|
54
|
+
MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
|
|
27
55
|
} from '@remogram/core';
|
|
28
56
|
|
|
29
57
|
const PUBLIC_GITHUB_HOST = 'github.com';
|
|
@@ -57,9 +85,21 @@ const AUTH_CAPABILITIES = [
|
|
|
57
85
|
'pr_checks',
|
|
58
86
|
'merge_plan',
|
|
59
87
|
'sync_plan',
|
|
88
|
+
'status_set',
|
|
89
|
+
'whoami',
|
|
90
|
+
'branch_protection',
|
|
91
|
+
'cr_files',
|
|
92
|
+
'cr_comments',
|
|
93
|
+
'forge_changes',
|
|
60
94
|
];
|
|
61
95
|
|
|
62
|
-
const STRUCTURED_COMMANDS = apiProviderCommands(
|
|
96
|
+
const STRUCTURED_COMMANDS = apiProviderCommands({
|
|
97
|
+
branchProtectionImplemented: true,
|
|
98
|
+
crFilesImplemented: true,
|
|
99
|
+
crCommentsImplemented: true,
|
|
100
|
+
forgeChangesImplemented: true,
|
|
101
|
+
statusSetImplemented: true,
|
|
102
|
+
});
|
|
63
103
|
|
|
64
104
|
export function githubToken() {
|
|
65
105
|
if (process.env.GITHUB_TOKEN) return { token: process.env.GITHUB_TOKEN, env: 'GITHUB_TOKEN' };
|
|
@@ -188,12 +228,36 @@ async function paginateGitHubLinkPages({
|
|
|
188
228
|
token,
|
|
189
229
|
initialLimit = DEFAULT_CHECK_STATUS_PAGE_SIZE,
|
|
190
230
|
mapPageItems,
|
|
231
|
+
seededFirstPage = null,
|
|
191
232
|
}) {
|
|
192
233
|
const all = [];
|
|
193
234
|
let truncated = false;
|
|
235
|
+
let walkedCount = 0;
|
|
194
236
|
let url = startUrl;
|
|
195
237
|
let activeLimit = initialLimit;
|
|
196
|
-
|
|
238
|
+
let pageIndex = 0;
|
|
239
|
+
|
|
240
|
+
if (seededFirstPage) {
|
|
241
|
+
const { items, linkHeader, currentUrl, usedLimit } = seededFirstPage;
|
|
242
|
+
activeLimit = usedLimit;
|
|
243
|
+
const mapped = mapPageItems(items);
|
|
244
|
+
walkedCount += mapped.length;
|
|
245
|
+
all.push(...mapped);
|
|
246
|
+
const linkPage = resolveGitHubLinkNextPage({
|
|
247
|
+
trustedOrigin,
|
|
248
|
+
currentUrl,
|
|
249
|
+
linkHeader,
|
|
250
|
+
pageIndex: 0,
|
|
251
|
+
maxPages: MAX_CHECK_PAGES,
|
|
252
|
+
});
|
|
253
|
+
if (linkPage.truncated) {
|
|
254
|
+
truncated = true;
|
|
255
|
+
}
|
|
256
|
+
url = linkPage.nextUrl ? withPerPageParam(linkPage.nextUrl, activeLimit) : null;
|
|
257
|
+
pageIndex = 1;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (let page = pageIndex; page < MAX_CHECK_PAGES && url; page += 1) {
|
|
197
261
|
const currentUrl = url;
|
|
198
262
|
let usedLimit = activeLimit;
|
|
199
263
|
const { body, headers } = await fetchWithIngestPageBackoff(
|
|
@@ -208,7 +272,9 @@ async function paginateGitHubLinkPages({
|
|
|
208
272
|
activeLimit,
|
|
209
273
|
);
|
|
210
274
|
activeLimit = usedLimit;
|
|
211
|
-
|
|
275
|
+
const mapped = mapPageItems(body);
|
|
276
|
+
walkedCount += mapped.length;
|
|
277
|
+
all.push(...mapped);
|
|
212
278
|
const linkHeader = headers?.get?.('link') ?? headers?.get?.('Link') ?? null;
|
|
213
279
|
const linkPage = resolveGitHubLinkNextPage({
|
|
214
280
|
trustedOrigin,
|
|
@@ -222,7 +288,7 @@ async function paginateGitHubLinkPages({
|
|
|
222
288
|
}
|
|
223
289
|
url = linkPage.nextUrl ? withPerPageParam(linkPage.nextUrl, activeLimit) : null;
|
|
224
290
|
}
|
|
225
|
-
return { items: all, truncated };
|
|
291
|
+
return { items: all, truncated, walked_count: walkedCount };
|
|
226
292
|
}
|
|
227
293
|
|
|
228
294
|
export async function githubFetchPaginated(config, parsed, path, slice) {
|
|
@@ -340,13 +406,18 @@ export function providerCapabilities() {
|
|
|
340
406
|
mergeability_confidence: 'derived',
|
|
341
407
|
host_binding: 'verified_remote_host',
|
|
342
408
|
pagination: 'supported',
|
|
343
|
-
write_support:
|
|
409
|
+
write_support: true,
|
|
410
|
+
write_commands: ['status_set'],
|
|
344
411
|
...forgeIngestCapabilityFacts(),
|
|
412
|
+
...statusSetIdempotencyScanCapabilityFacts(),
|
|
345
413
|
...checkPaginationCapabilityFacts({
|
|
346
414
|
strategy: 'link_header',
|
|
347
415
|
pageSizeParam: 'per_page',
|
|
348
416
|
sourceCount: check_sources.length,
|
|
349
417
|
}),
|
|
418
|
+
...openPullListCapabilityFacts({
|
|
419
|
+
totalCountSource: 'search_api',
|
|
420
|
+
}),
|
|
350
421
|
};
|
|
351
422
|
}
|
|
352
423
|
|
|
@@ -488,48 +559,263 @@ export async function prChecks(ctx, opts) {
|
|
|
488
559
|
export async function mergePlan(ctx, opts) {
|
|
489
560
|
const view = await prView(ctx, opts);
|
|
490
561
|
const checks = await prChecks(ctx, { number: view.pr_number });
|
|
491
|
-
|
|
562
|
+
let mergeOpts = opts;
|
|
563
|
+
if (opts.allowed_paths) {
|
|
564
|
+
try {
|
|
565
|
+
const crFilesBody = await crFiles(ctx, { number: view.pr_number });
|
|
566
|
+
mergeOpts = { ...opts, changed_paths: crFilesBody.changed_paths };
|
|
567
|
+
} catch {
|
|
568
|
+
// Fall back to local git diff in resolveMergePlanPathScope.
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return buildMergePlanBodyFromFacts(ctx, view, checks, mergeOpts);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export async function crFiles(ctx, { number }) {
|
|
575
|
+
if (number == null) {
|
|
576
|
+
throw Object.assign(new Error('--number required'), {
|
|
577
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'Provide --number for PR changed paths'),
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
const base = apiBase(ctx.config, ctx.parsed);
|
|
581
|
+
const trustedOrigin = new URL(base).origin;
|
|
582
|
+
const { token } = requireToken();
|
|
583
|
+
const startUrl = `${base}${repoApiPath(ctx.config, 'pulls', number, 'files')}?per_page=${DEFAULT_CHECK_STATUS_PAGE_SIZE}`;
|
|
584
|
+
const { items, truncated, walked_count } = await paginateGitHubLinkPages({
|
|
585
|
+
trustedOrigin,
|
|
586
|
+
startUrl,
|
|
587
|
+
token,
|
|
588
|
+
mapPageItems: (body) => (Array.isArray(body) ? body : []),
|
|
589
|
+
});
|
|
590
|
+
const body = buildCrFilesFromGiteaFiles(number, items);
|
|
591
|
+
if (truncated) {
|
|
592
|
+
return buildCrFilesBody({
|
|
593
|
+
pr_number: body.pr_number,
|
|
594
|
+
changed_paths: body.changed_paths,
|
|
595
|
+
paths_truncated: true,
|
|
596
|
+
path_count: walked_count,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
return body;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export async function crComments(ctx, { number }) {
|
|
603
|
+
if (number == null) {
|
|
604
|
+
throw Object.assign(new Error('--number required'), {
|
|
605
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'Provide --number for PR review comments'),
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
const base = apiBase(ctx.config, ctx.parsed);
|
|
609
|
+
const trustedOrigin = new URL(base).origin;
|
|
610
|
+
const { token } = requireToken();
|
|
611
|
+
const startUrl = `${base}${repoApiPath(ctx.config, 'pulls', number, 'comments')}?per_page=${DEFAULT_CHECK_STATUS_PAGE_SIZE}`;
|
|
612
|
+
const { items, truncated, walked_count } = await paginateGitHubLinkPages({
|
|
613
|
+
trustedOrigin,
|
|
614
|
+
startUrl,
|
|
615
|
+
token,
|
|
616
|
+
mapPageItems: (body) => (Array.isArray(body) ? body : []),
|
|
617
|
+
});
|
|
618
|
+
const body = buildCrCommentsFromGiteaComments(number, items);
|
|
619
|
+
if (truncated) {
|
|
620
|
+
return buildCrCommentsBody({
|
|
621
|
+
pr_number: body.pr_number,
|
|
622
|
+
comments: body.comments,
|
|
623
|
+
comments_truncated: true,
|
|
624
|
+
comment_count: walked_count,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
return body;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export async function forgeChanges(ctx, { since }) {
|
|
631
|
+
requireToken();
|
|
632
|
+
const sinceIso = parseSinceObservedAt(since);
|
|
633
|
+
const base = apiBase(ctx.config, ctx.parsed);
|
|
634
|
+
const trustedOrigin = new URL(base).origin;
|
|
635
|
+
const { token } = requireToken();
|
|
636
|
+
const startUrl = `${base}${repoApiPath(ctx.config, 'pulls')}?state=all&sort=updated&direction=desc&per_page=${DEFAULT_CHECK_STATUS_PAGE_SIZE}`;
|
|
637
|
+
const { items, truncated } = await paginateGitHubLinkPages({
|
|
638
|
+
trustedOrigin,
|
|
639
|
+
startUrl,
|
|
640
|
+
token,
|
|
641
|
+
mapPageItems: (body) => (Array.isArray(body) ? body : []),
|
|
642
|
+
});
|
|
643
|
+
let body = buildForgeChangesFromGiteaPulls(sinceIso, items, { listTruncated: truncated });
|
|
644
|
+
const checkNumbers = new Set();
|
|
645
|
+
for (const event of body.events) {
|
|
646
|
+
if (event.kind === 'pr_opened' || event.kind === 'head_sha_moved') {
|
|
647
|
+
checkNumbers.add(event.pr_number);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const checkEvents = [];
|
|
651
|
+
for (const number of checkNumbers) {
|
|
652
|
+
const checks = await prChecks(ctx, { number });
|
|
653
|
+
checkEvents.push(buildChecksConclusionObservedEvent(number, checks));
|
|
654
|
+
}
|
|
655
|
+
if (checkEvents.length > 0) {
|
|
656
|
+
body = appendForgeChangeEvents(body, checkEvents, { listTruncated: truncated });
|
|
657
|
+
}
|
|
658
|
+
return body;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const GITHUB_OPEN_PULL_COMPLIANT_MAX =
|
|
662
|
+
DEFAULT_OPEN_PULL_LIST_PAGE_SIZE * MAX_CHECK_STATUS_PAGES;
|
|
663
|
+
const GITHUB_PAGE_SIZE = 100;
|
|
664
|
+
|
|
665
|
+
function githubOpenPullsListPath(config, sliceSort) {
|
|
666
|
+
const params = new URLSearchParams({ state: 'open', ...githubOpenPullSortQuery(sliceSort) });
|
|
667
|
+
return `${repoApiPath(config, 'pulls')}?${params.toString()}`;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function fetchGitHubOpenPullSearchTotal(ctx) {
|
|
671
|
+
const base = apiBase(ctx.config, ctx.parsed);
|
|
672
|
+
const { token } = requireToken();
|
|
673
|
+
const owner = encodeURIComponent(ctx.config.owner);
|
|
674
|
+
const repo = encodeURIComponent(ctx.config.repo);
|
|
675
|
+
const searchPath = `/search/issues?q=repo:${owner}/${repo}+is:pr+state:open&per_page=1`;
|
|
676
|
+
try {
|
|
677
|
+
const { body } = await fetchJsonWithMeta(`${base}${searchPath}`, {
|
|
678
|
+
headers: authHeaders(token),
|
|
679
|
+
});
|
|
680
|
+
if (!body || body.incomplete_results === true) return null;
|
|
681
|
+
const total = Number(body.total_count);
|
|
682
|
+
const maxTrusted = GITHUB_OPEN_PULL_COMPLIANT_MAX * 2;
|
|
683
|
+
if (!Number.isInteger(total) || total <= 0 || total > maxTrusted) return null;
|
|
684
|
+
return total;
|
|
685
|
+
} catch {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function probeGitHubOpenPullPageOne(ctx, retainMax, sliceSort) {
|
|
691
|
+
const totalCount = await fetchGitHubOpenPullSearchTotal(ctx);
|
|
692
|
+
if (totalCount == null) return null;
|
|
693
|
+
|
|
694
|
+
const base = apiBase(ctx.config, ctx.parsed);
|
|
695
|
+
const { token } = requireToken();
|
|
696
|
+
const requestLimit = Math.min(retainMax, GITHUB_PAGE_SIZE);
|
|
697
|
+
const listPath = githubOpenPullsListPath(ctx.config, sliceSort);
|
|
698
|
+
const listUrl = withPerPageParam(`${base}${listPath}`, requestLimit);
|
|
699
|
+
try {
|
|
700
|
+
const { body, headers } = await fetchJsonWithMeta(listUrl, {
|
|
701
|
+
headers: authHeaders(token),
|
|
702
|
+
});
|
|
703
|
+
if (!Array.isArray(body)) return null;
|
|
704
|
+
const listTruncated = totalCount > GITHUB_OPEN_PULL_COMPLIANT_MAX;
|
|
705
|
+
const linkHeader = headers?.get?.('link') ?? headers?.get?.('Link') ?? null;
|
|
706
|
+
return { body, totalCount, listTruncated, requestLimit, listUrl, linkHeader };
|
|
707
|
+
} catch {
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function buildGitHubOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, listTruncated) {
|
|
713
|
+
let numbers = orderOpenPullNumbers(body, (pr) => pr?.number, sliceSort);
|
|
714
|
+
if (numbers.length > retainMax) numbers = numbers.slice(0, retainMax);
|
|
715
|
+
return buildOpenPullListMeta({
|
|
716
|
+
totalCount,
|
|
717
|
+
numbers,
|
|
718
|
+
listTruncated,
|
|
719
|
+
sliceSort,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function githubProbePaginationOpts(probe) {
|
|
724
|
+
const { body, totalCount, requestLimit, listUrl, linkHeader } = probe;
|
|
492
725
|
return {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
checks_conclusion: checks.check_conclusion,
|
|
496
|
-
blockers,
|
|
726
|
+
trustedTotalCount: totalCount,
|
|
727
|
+
seededFirstPage: { items: body, usedLimit: requestLimit, listUrl, linkHeader },
|
|
497
728
|
};
|
|
498
729
|
}
|
|
499
730
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
731
|
+
async function paginateGitHubOpenPullList(ctx, opts, sliceSort, paginationOpts = {}) {
|
|
732
|
+
const {
|
|
733
|
+
trustedTotalCount = null,
|
|
734
|
+
seededFirstPage = null,
|
|
735
|
+
numberSortFullCollect = false,
|
|
736
|
+
} = paginationOpts;
|
|
503
737
|
const base = apiBase(ctx.config, ctx.parsed);
|
|
504
738
|
const { token } = requireToken();
|
|
505
739
|
const listLimit =
|
|
506
740
|
opts.limit != null && Number.isInteger(Number(opts.limit)) && Number(opts.limit) > 0
|
|
507
741
|
? Number(opts.limit)
|
|
508
742
|
: null;
|
|
509
|
-
const GITHUB_PAGE_SIZE = 100;
|
|
510
743
|
const all = [];
|
|
511
744
|
let listTruncated = false;
|
|
745
|
+
let walkedCount = 0;
|
|
746
|
+
const listPath = githubOpenPullsListPath(ctx.config, sliceSort);
|
|
747
|
+
const retainMax =
|
|
748
|
+
listLimit == null &&
|
|
749
|
+
opts.retain_max != null &&
|
|
750
|
+
Number.isInteger(Number(opts.retain_max)) &&
|
|
751
|
+
Number(opts.retain_max) > 0
|
|
752
|
+
? Number(opts.retain_max)
|
|
753
|
+
: null;
|
|
512
754
|
|
|
513
|
-
if (listLimit == null) {
|
|
755
|
+
if (listLimit == null && numberSortFullCollect) {
|
|
756
|
+
const {
|
|
757
|
+
items: offsetItems,
|
|
758
|
+
list_truncated: offsetTruncated,
|
|
759
|
+
walked_count: offsetWalkedCount,
|
|
760
|
+
} = await paginateOffsetListPages({
|
|
761
|
+
pageSize: GITHUB_PAGE_SIZE,
|
|
762
|
+
retainMax: null,
|
|
763
|
+
trustedEntryCount: trustedTotalCount,
|
|
764
|
+
seededFirstPage: seededFirstPage
|
|
765
|
+
? { items: seededFirstPage.items, usedLimit: seededFirstPage.usedLimit }
|
|
766
|
+
: null,
|
|
767
|
+
fetchPage: async ({ page, limit }) => {
|
|
768
|
+
const pageUrl = `${base}${listPath}&page=${page}`;
|
|
769
|
+
const { body } = await fetchJsonWithMeta(withPerPageParam(pageUrl, limit), {
|
|
770
|
+
headers: authHeaders(token),
|
|
771
|
+
});
|
|
772
|
+
return Array.isArray(body) ? body : [];
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
all.push(...offsetItems);
|
|
776
|
+
listTruncated = offsetTruncated;
|
|
777
|
+
walkedCount = offsetWalkedCount;
|
|
778
|
+
} else if (listLimit == null) {
|
|
514
779
|
const trustedOrigin = new URL(base).origin;
|
|
515
|
-
const startUrl = `${base}${
|
|
516
|
-
const
|
|
780
|
+
const startUrl = `${base}${listPath}`;
|
|
781
|
+
const linkSeed = seededFirstPage
|
|
782
|
+
? {
|
|
783
|
+
items: seededFirstPage.items,
|
|
784
|
+
linkHeader: seededFirstPage.linkHeader,
|
|
785
|
+
currentUrl: seededFirstPage.listUrl,
|
|
786
|
+
usedLimit: seededFirstPage.usedLimit,
|
|
787
|
+
}
|
|
788
|
+
: null;
|
|
789
|
+
const {
|
|
790
|
+
items: linkItems,
|
|
791
|
+
truncated: linkTruncated,
|
|
792
|
+
walked_count: linkWalkedCount,
|
|
793
|
+
} = await paginateGitHubLinkPages({
|
|
517
794
|
trustedOrigin,
|
|
518
795
|
startUrl,
|
|
519
796
|
token,
|
|
797
|
+
initialLimit: seededFirstPage?.usedLimit ?? DEFAULT_CHECK_STATUS_PAGE_SIZE,
|
|
520
798
|
mapPageItems: (body) => (Array.isArray(body) ? body : []),
|
|
799
|
+
seededFirstPage: linkSeed,
|
|
521
800
|
});
|
|
522
801
|
all.push(...linkItems);
|
|
523
802
|
listTruncated = linkTruncated;
|
|
803
|
+
walkedCount = linkWalkedCount;
|
|
524
804
|
} else {
|
|
525
|
-
const {
|
|
805
|
+
const {
|
|
806
|
+
items: limitItems,
|
|
807
|
+
list_truncated: limitTruncated,
|
|
808
|
+
walked_count: limitWalkedCount,
|
|
809
|
+
} = await paginateOffsetListPages({
|
|
526
810
|
pageSize: GITHUB_PAGE_SIZE,
|
|
527
811
|
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
812
|
maxPagesTruncatesWithLimit: true,
|
|
813
|
+
trustedEntryCount: trustedTotalCount,
|
|
814
|
+
seededFirstPage: seededFirstPage
|
|
815
|
+
? { items: seededFirstPage.items, usedLimit: seededFirstPage.usedLimit }
|
|
816
|
+
: null,
|
|
531
817
|
fetchPage: async ({ page, limit }) => {
|
|
532
|
-
const pageUrl = `${base}${
|
|
818
|
+
const pageUrl = `${base}${listPath}&page=${page}`;
|
|
533
819
|
const { body } = await fetchJsonWithMeta(withPerPageParam(pageUrl, limit), {
|
|
534
820
|
headers: authHeaders(token),
|
|
535
821
|
});
|
|
@@ -538,16 +824,70 @@ export async function listOpenPullsWithMeta(ctx, opts = {}) {
|
|
|
538
824
|
});
|
|
539
825
|
all.push(...limitItems);
|
|
540
826
|
listTruncated = limitTruncated;
|
|
827
|
+
walkedCount = limitWalkedCount;
|
|
541
828
|
}
|
|
542
829
|
|
|
543
|
-
let numbers = all
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
.
|
|
547
|
-
if (listLimit != null && numbers.length > listLimit) {
|
|
548
|
-
numbers = numbers.slice(0, listLimit);
|
|
830
|
+
let numbers = orderOpenPullNumbers(all, (pr) => pr?.number, sliceSort);
|
|
831
|
+
const outputCap = listLimit ?? retainMax;
|
|
832
|
+
if (outputCap != null && numbers.length > outputCap) {
|
|
833
|
+
numbers = numbers.slice(0, outputCap);
|
|
549
834
|
}
|
|
550
|
-
|
|
835
|
+
const entryCount =
|
|
836
|
+
trustedTotalCount != null
|
|
837
|
+
? resolvePaginatedEntryCount(trustedTotalCount, walkedCount)
|
|
838
|
+
: undefined;
|
|
839
|
+
return {
|
|
840
|
+
numbers,
|
|
841
|
+
list_truncated: resolveListTruncatedWithTrustedTotal({
|
|
842
|
+
listTruncated,
|
|
843
|
+
trustedTotalCount,
|
|
844
|
+
walkedCount,
|
|
845
|
+
fullCollect: numberSortFullCollect,
|
|
846
|
+
}),
|
|
847
|
+
slice_sort: sliceSort,
|
|
848
|
+
...(entryCount != null ? { entry_count: entryCount } : {}),
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export async function listOpenPullsWithMeta(ctx, opts = {}) {
|
|
853
|
+
apiBase(ctx.config, ctx.parsed);
|
|
854
|
+
requireToken();
|
|
855
|
+
const sliceSort = normalizeCrInventorySort(opts.sort ?? DEFAULT_CR_INVENTORY_SLICE_SORT);
|
|
856
|
+
if (!isCrInventoryFastPathEligible(opts)) {
|
|
857
|
+
return paginateGitHubOpenPullList(ctx, opts, sliceSort);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const retainMax = Number(opts.retain_max);
|
|
861
|
+
const probe = await probeGitHubOpenPullPageOne(ctx, retainMax, sliceSort);
|
|
862
|
+
if (!probe) {
|
|
863
|
+
return paginateGitHubOpenPullList(ctx, opts, sliceSort);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const { body, totalCount, listTruncated, requestLimit } = probe;
|
|
867
|
+
|
|
868
|
+
if (listTruncated) {
|
|
869
|
+
if (body.length === 0) {
|
|
870
|
+
return paginateGitHubOpenPullList(ctx, opts, sliceSort, githubProbePaginationOpts(probe));
|
|
871
|
+
}
|
|
872
|
+
return buildGitHubOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, true);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (
|
|
876
|
+
isNumberSortFastPathEligible(totalCount, retainMax, sliceSort) &&
|
|
877
|
+
validateFastPathPageLength(totalCount, requestLimit, body.length)
|
|
878
|
+
) {
|
|
879
|
+
return buildGitHubOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, false);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return paginateGitHubOpenPullList(
|
|
883
|
+
ctx,
|
|
884
|
+
opts,
|
|
885
|
+
sliceSort,
|
|
886
|
+
{
|
|
887
|
+
...githubProbePaginationOpts(probe),
|
|
888
|
+
numberSortFullCollect: isNumberSortFullCollectRequired(totalCount, retainMax, sliceSort),
|
|
889
|
+
},
|
|
890
|
+
);
|
|
551
891
|
}
|
|
552
892
|
|
|
553
893
|
export async function listOpenPulls(ctx, opts = {}) {
|
|
@@ -587,6 +927,128 @@ export async function syncPlan(ctx, remoteName = 'origin') {
|
|
|
587
927
|
};
|
|
588
928
|
}
|
|
589
929
|
|
|
930
|
+
export async function whoami(ctx) {
|
|
931
|
+
const base = apiBase(ctx.config, ctx.parsed);
|
|
932
|
+
const { token } = requireToken();
|
|
933
|
+
const url = `${base}/user`;
|
|
934
|
+
const { body, headers } = await fetchJsonWithMeta(url, {
|
|
935
|
+
headers: authHeaders(token),
|
|
936
|
+
});
|
|
937
|
+
const scopeHeader =
|
|
938
|
+
headers?.get?.('x-oauth-scopes') ?? headers?.get?.('X-OAuth-Scopes') ?? null;
|
|
939
|
+
return buildProviderIdentityFromGitHubUser(body, scopeHeader);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function githubStatusRecordOrder(a, b) {
|
|
943
|
+
const aUpdated = Date.parse(a?.updated_at ?? '') || 0;
|
|
944
|
+
const bUpdated = Date.parse(b?.updated_at ?? '') || 0;
|
|
945
|
+
if (aUpdated !== bUpdated) return aUpdated - bUpdated;
|
|
946
|
+
const aId = Number(a.id) || 0;
|
|
947
|
+
const bId = Number(b.id) || 0;
|
|
948
|
+
return aId - bId;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function statusSetIdempotencyScanIncompleteError(pagesScanned, pageSizeUsed) {
|
|
952
|
+
return forgeError(
|
|
953
|
+
ERROR_CODES.IDEMPOTENCY_SCAN_INCOMPLETE,
|
|
954
|
+
'Cannot prove no commit status exists for sha+context within scan limit; retry or set manually',
|
|
955
|
+
null,
|
|
956
|
+
{
|
|
957
|
+
idempotency_scan: {
|
|
958
|
+
pages: pagesScanned,
|
|
959
|
+
max_pages: MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
|
|
960
|
+
page_size: pageSizeUsed,
|
|
961
|
+
},
|
|
962
|
+
},
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/** Paginated commit-status scan for idempotent status set; fail-closed when scan cap prevents proof of absence. */
|
|
967
|
+
export async function findCommitStatusByContext(ctx, sha, context) {
|
|
968
|
+
const { token } = requireToken();
|
|
969
|
+
const base = apiBase(ctx.config, ctx.parsed);
|
|
970
|
+
const trustedOrigin = new URL(base).origin;
|
|
971
|
+
const path = repoApiPath(ctx.config, 'commits', sha, 'statuses');
|
|
972
|
+
const pageQuery = `${path.includes('?') ? '&' : '?'}per_page=${DEFAULT_CHECK_STATUS_PAGE_SIZE}`;
|
|
973
|
+
let url = `${base}${path}${pageQuery}`;
|
|
974
|
+
let bestMatch = null;
|
|
975
|
+
const activeLimit = DEFAULT_CHECK_STATUS_PAGE_SIZE;
|
|
976
|
+
|
|
977
|
+
for (let page = 1; page <= MAX_OPEN_PULL_IDEMPOTENCY_PAGES; page += 1) {
|
|
978
|
+
const { body, headers } = await fetchJsonWithMeta(url, {
|
|
979
|
+
headers: authHeaders(token),
|
|
980
|
+
});
|
|
981
|
+
const items = Array.isArray(body) ? body : [];
|
|
982
|
+
for (const record of items) {
|
|
983
|
+
if (record?.context !== context) continue;
|
|
984
|
+
if (!bestMatch || githubStatusRecordOrder(record, bestMatch) > 0) {
|
|
985
|
+
bestMatch = record;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
const linkPage = resolveGitHubLinkNextPage({
|
|
989
|
+
trustedOrigin,
|
|
990
|
+
currentUrl: url,
|
|
991
|
+
linkHeader: headers?.get?.('link') ?? headers?.get?.('Link'),
|
|
992
|
+
pageIndex: page - 1,
|
|
993
|
+
maxPages: MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
|
|
994
|
+
});
|
|
995
|
+
if (items.length < activeLimit) {
|
|
996
|
+
return bestMatch;
|
|
997
|
+
}
|
|
998
|
+
if (page === MAX_OPEN_PULL_IDEMPOTENCY_PAGES) {
|
|
999
|
+
throw Object.assign(new Error('Commit status idempotency scan incomplete'), {
|
|
1000
|
+
forgeError: statusSetIdempotencyScanIncompleteError(page, activeLimit),
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
if (!linkPage.nextUrl) {
|
|
1004
|
+
return bestMatch;
|
|
1005
|
+
}
|
|
1006
|
+
url = withPerPageParam(linkPage.nextUrl, activeLimit);
|
|
1007
|
+
}
|
|
1008
|
+
return bestMatch;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
export async function statusSet(ctx, args) {
|
|
1012
|
+
const parsed = parseStatusSetArgs(args);
|
|
1013
|
+
const existing = await findCommitStatusByContext(ctx, parsed.sha, parsed.context);
|
|
1014
|
+
if (existing) {
|
|
1015
|
+
const existingState = normalizeStatusSetState(existing.state ?? existing.status);
|
|
1016
|
+
if (existingState === parsed.state) {
|
|
1017
|
+
return buildCommitStatusSetBody(existing, parsed, { reusedExisting: true });
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
const payload = {
|
|
1021
|
+
state: parsed.state,
|
|
1022
|
+
context: parsed.context,
|
|
1023
|
+
};
|
|
1024
|
+
if (parsed.description != null) payload.description = parsed.description;
|
|
1025
|
+
if (parsed.target_url != null) payload.target_url = parsed.target_url;
|
|
1026
|
+
const response = await githubFetch(ctx.config, ctx.parsed, repoApiPath(ctx.config, 'statuses', parsed.sha), {
|
|
1027
|
+
method: 'POST',
|
|
1028
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1029
|
+
body: JSON.stringify(payload),
|
|
1030
|
+
});
|
|
1031
|
+
return buildCommitStatusSetBody(response, parsed);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
export async function branchProtection(ctx, { branchRef }) {
|
|
1035
|
+
assertGitRef(branchRef, '--branch-ref');
|
|
1036
|
+
const base = apiBase(ctx.config, ctx.parsed);
|
|
1037
|
+
const { token } = requireToken();
|
|
1038
|
+
const url = `${base}${repoApiPath(ctx.config, 'branches', branchRef, 'protection')}`;
|
|
1039
|
+
try {
|
|
1040
|
+
const { body } = await fetchJsonWithMeta(url, {
|
|
1041
|
+
headers: authHeaders(token),
|
|
1042
|
+
});
|
|
1043
|
+
return buildBranchProtectionFromGitHubProtection(branchRef, body);
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
if (err?.status === 404) {
|
|
1046
|
+
return buildBranchProtectionFromGitHubProtection(branchRef, null);
|
|
1047
|
+
}
|
|
1048
|
+
throw err;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
590
1052
|
export const provider = {
|
|
591
1053
|
id: 'github-api',
|
|
592
1054
|
providerCapabilities,
|
|
@@ -599,4 +1061,10 @@ export const provider = {
|
|
|
599
1061
|
prChecks,
|
|
600
1062
|
mergePlan,
|
|
601
1063
|
syncPlan,
|
|
1064
|
+
whoami,
|
|
1065
|
+
branchProtection,
|
|
1066
|
+
crFiles,
|
|
1067
|
+
crComments,
|
|
1068
|
+
forgeChanges,
|
|
1069
|
+
statusSet,
|
|
602
1070
|
};
|
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.5",
|
|
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.5"
|
|
26
26
|
}
|
|
27
27
|
}
|