@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.
Files changed (2) hide show
  1. package/index.js +162 -20
  2. 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 = 50;
169
+ const MAX_CHECK_PAGES = MAX_CHECK_STATUS_PAGES;
157
170
 
158
- export async function githubFetchPaginated(config, parsed, path, slice) {
159
- const base = apiBase(config, parsed);
160
- const { token } = requireToken();
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 url = `${base}${path}`;
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 { body, headers } = await fetchJsonWithMeta(url, {
165
- headers: authHeaders(token),
166
- });
167
- all.push(...slice(body));
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
- url = parseLinkHeader(linkHeader).next ?? null;
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: ['commit_statuses', 'check_runs'],
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 [statusRecords, checkRunRecords] = await Promise.all([
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
- return { head_sha: sha, check_conclusion: summarizeChecks(mapped), statuses: mapped };
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.2",
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.2"
25
+ "@remogram/core": "0.1.0-beta.3"
26
26
  }
27
27
  }