@remogram/provider-github-api 0.1.0-beta.6 → 0.1.0-beta.9

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.
@@ -0,0 +1,13 @@
1
+ /** @type {((ctx: object, opts: { branchRef: string }) => Promise<object>) | null} */
2
+ let branchProtectionImpl = null;
3
+
4
+ export function setBranchProtectionImpl(fn) {
5
+ branchProtectionImpl = fn;
6
+ }
7
+
8
+ export async function resolveBranchProtection(ctx, opts) {
9
+ if (typeof branchProtectionImpl !== 'function') {
10
+ throw new Error('branch protection impl not registered');
11
+ }
12
+ return branchProtectionImpl(ctx, opts);
13
+ }
package/index.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  buildMergePlanFromProviderFacts,
16
16
  ERROR_CODES,
17
17
  forgeError,
18
+ LIVE_REACHABILITY_TIMEOUT_MS,
18
19
  forgeIngestCapabilityFacts,
19
20
  checkPaginationCapabilityFacts,
20
21
  openPullListCapabilityFacts,
@@ -39,6 +40,7 @@ import {
39
40
  githubOpenPullSortQuery,
40
41
  buildProviderIdentityFromGitHubUser,
41
42
  buildBranchProtectionFromGitHubProtection,
43
+ buildPrChecksBody,
42
44
  buildCrFilesBody,
43
45
  buildCrFilesFromGiteaFiles,
44
46
  buildCrCommentsBody,
@@ -48,12 +50,17 @@ import {
48
50
  buildChecksConclusionObservedEvent,
49
51
  appendForgeChangeEvents,
50
52
  buildCommitStatusSetBody,
53
+ idempotencyPacketFields,
51
54
  parseStatusSetArgs,
52
55
  normalizeStatusSetState,
53
56
  statusSetIdempotencyScanCapabilityFacts,
54
57
  MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
55
58
  assertWriteCommandConfigured,
56
59
  } from '@remogram/core';
60
+ import {
61
+ resolveBranchProtection,
62
+ setBranchProtectionImpl,
63
+ } from './branch-protection-internal.js';
57
64
 
58
65
  const PUBLIC_GITHUB_HOST = 'github.com';
59
66
  const PUBLIC_GITHUB_API = 'https://api.github.com';
@@ -207,6 +214,25 @@ export async function githubFetch(config, parsed, path, options = {}) {
207
214
  });
208
215
  }
209
216
 
217
+ export async function apiReachability(ctx) {
218
+ if (!githubToken()) {
219
+ throw Object.assign(new Error('GitHub token not set'), {
220
+ forgeError: forgeError(
221
+ ERROR_CODES.UNAUTHENTICATED_PROVIDER,
222
+ 'GITHUB_TOKEN or GH_TOKEN not set',
223
+ ),
224
+ });
225
+ }
226
+ const { token } = requireToken();
227
+ const url = `${apiBase(ctx.config, ctx.parsed)}${repoApiPath(ctx.config)}`;
228
+ await fetchJson(
229
+ url,
230
+ { headers: authHeaders(token) },
231
+ LIVE_REACHABILITY_TIMEOUT_MS,
232
+ );
233
+ return { repo_accessible: true };
234
+ }
235
+
210
236
  const MAX_CHECK_PAGES = MAX_CHECK_STATUS_PAGES;
211
237
 
212
238
  export function resolveGitHubLinkNextPage({ trustedOrigin, currentUrl, linkHeader, pageIndex, maxPages }) {
@@ -435,10 +461,10 @@ export async function refsCompare(ctx, baseRef, headRef) {
435
461
  }
436
462
  const counts = gitAheadBehind(ctx.cwd, baseSha, headSha);
437
463
  return {
438
- base_ref: sanitizeField(baseRef),
439
- base_sha: baseSha,
440
- head_ref: sanitizeField(headRef),
441
- head_sha: headSha,
464
+ compare_base_ref: sanitizeField(baseRef),
465
+ compare_base_sha: baseSha,
466
+ compare_head_ref: sanitizeField(headRef),
467
+ compare_head_sha: headSha,
442
468
  ...counts,
443
469
  };
444
470
  }
@@ -460,10 +486,10 @@ export async function prView(ctx, opts) {
460
486
  url: sanitizeUrl(pr.html_url ?? pr.url),
461
487
  title: sanitizeField(pr.title),
462
488
  state: sanitizeField(pr.state),
463
- base_ref: sanitizeField(pr.base?.ref),
464
- base_sha: sanitizeField(pr.base?.sha),
465
- head_ref: sanitizeField(pr.head?.ref),
466
- head_sha: sanitizeField(pr.head?.sha),
489
+ forge_target_branch_ref: sanitizeField(pr.base?.ref),
490
+ forge_target_sha: sanitizeField(pr.base?.sha),
491
+ forge_source_branch_ref: sanitizeField(pr.head?.ref),
492
+ forge_source_sha: sanitizeField(pr.head?.sha),
467
493
  mergeability: mergeability(pr),
468
494
  };
469
495
  }
@@ -509,6 +535,7 @@ export async function prChecks(ctx, opts) {
509
535
  apiBase(ctx.config, ctx.parsed);
510
536
  requireToken();
511
537
  let sha;
538
+ let requiredContexts = [];
512
539
  if (opts.ref) {
513
540
  assertGitRef(opts.ref, 'ref');
514
541
  sha = gitRevParse(ctx.cwd, opts.ref);
@@ -520,6 +547,11 @@ export async function prChecks(ctx, opts) {
520
547
  } else {
521
548
  const pr = await fetchPullGraphql(ctx.config, ctx.parsed, opts.number);
522
549
  sha = pr.head?.sha;
550
+ const targetBranch = pr.base?.ref;
551
+ if (targetBranch) {
552
+ const protection = await resolveBranchProtection(ctx, { branchRef: targetBranch });
553
+ requiredContexts = protection.required_status_contexts ?? [];
554
+ }
523
555
  }
524
556
  if (!sha) {
525
557
  throw Object.assign(new Error('No SHA'), {
@@ -541,20 +573,27 @@ export async function prChecks(ctx, opts) {
541
573
  context: sanitizeField(s.context),
542
574
  state: normalizeCommitStatusState(s.state),
543
575
  description: sanitizeField(s.description),
576
+ ...(s.target_url ? { target_url: sanitizeField(s.target_url) } : {}),
577
+ sha,
578
+ source: 'commit_status',
544
579
  }));
545
580
  const mappedCheckRuns = checkRunRecords.map((run) => ({
546
581
  context: sanitizeField(run.name),
547
582
  state: normalizeCheckRunState(run),
548
583
  description: sanitizeField(checkRunDescription(run)),
584
+ ...(run.details_url ? { target_url: sanitizeField(run.details_url) } : {}),
585
+ sha: sanitizeField(run.head_sha) || sha,
586
+ source: 'check_run',
549
587
  }));
550
588
  const mapped = [...mappedStatuses, ...mappedCheckRuns];
551
589
  const checks_truncated = statusResult.truncated || checkRunResult.truncated;
552
- return {
553
- head_sha: sha,
590
+ return buildPrChecksBody({
591
+ forge_source_sha: sha,
554
592
  check_conclusion: summarizeChecks(mapped),
555
593
  checks_truncated,
556
594
  statuses: mapped,
557
- };
595
+ required_contexts: requiredContexts,
596
+ });
558
597
  }
559
598
 
560
599
  export async function mergePlan(ctx, opts) {
@@ -743,28 +782,54 @@ async function paginateGitHubOpenPullList(ctx, opts, sliceSort, paginationOpts =
743
782
  : null;
744
783
 
745
784
  if (listLimit == null && numberSortFullCollect) {
746
- const {
747
- items: offsetItems,
748
- list_truncated: offsetTruncated,
749
- walked_count: offsetWalkedCount,
750
- } = await paginateOffsetListPages({
751
- pageSize: GITHUB_PAGE_SIZE,
752
- retainMax: null,
753
- trustedEntryCount: trustedTotalCount,
754
- seededFirstPage: seededFirstPage
755
- ? { items: seededFirstPage.items, usedLimit: seededFirstPage.usedLimit }
756
- : null,
757
- fetchPage: async ({ page, limit }) => {
758
- const pageUrl = `${base}${listPath}&page=${page}`;
759
- const { body } = await fetchJsonWithMeta(withPerPageParam(pageUrl, limit), {
760
- headers: authHeaders(token),
761
- });
762
- return Array.isArray(body) ? body : [];
763
- },
764
- });
765
- all.push(...offsetItems);
766
- listTruncated = offsetTruncated;
767
- walkedCount = offsetWalkedCount;
785
+ if (seededFirstPage?.linkHeader) {
786
+ const trustedOrigin = new URL(base).origin;
787
+ const startUrl = seededFirstPage.listUrl ?? `${base}${listPath}`;
788
+ const linkSeed = {
789
+ items: seededFirstPage.items,
790
+ linkHeader: seededFirstPage.linkHeader,
791
+ currentUrl: startUrl,
792
+ usedLimit: seededFirstPage.usedLimit,
793
+ };
794
+ const {
795
+ items: linkItems,
796
+ truncated: linkTruncated,
797
+ walked_count: linkWalkedCount,
798
+ } = await paginateGitHubLinkPages({
799
+ trustedOrigin,
800
+ startUrl,
801
+ token,
802
+ initialLimit: seededFirstPage.usedLimit ?? DEFAULT_CHECK_STATUS_PAGE_SIZE,
803
+ mapPageItems: (body) => (Array.isArray(body) ? body : []),
804
+ seededFirstPage: linkSeed,
805
+ });
806
+ all.push(...linkItems);
807
+ listTruncated = linkTruncated;
808
+ walkedCount = linkWalkedCount;
809
+ } else {
810
+ const {
811
+ items: offsetItems,
812
+ list_truncated: offsetTruncated,
813
+ walked_count: offsetWalkedCount,
814
+ } = await paginateOffsetListPages({
815
+ pageSize: GITHUB_PAGE_SIZE,
816
+ retainMax: null,
817
+ trustedEntryCount: trustedTotalCount,
818
+ seededFirstPage: seededFirstPage
819
+ ? { items: seededFirstPage.items, usedLimit: seededFirstPage.usedLimit }
820
+ : null,
821
+ fetchPage: async ({ page, limit }) => {
822
+ const pageUrl = `${base}${listPath}&page=${page}`;
823
+ const { body } = await fetchJsonWithMeta(withPerPageParam(pageUrl, limit), {
824
+ headers: authHeaders(token),
825
+ });
826
+ return Array.isArray(body) ? body : [];
827
+ },
828
+ });
829
+ all.push(...offsetItems);
830
+ listTruncated = offsetTruncated;
831
+ walkedCount = offsetWalkedCount;
832
+ }
768
833
  } else if (listLimit == null) {
769
834
  const trustedOrigin = new URL(base).origin;
770
835
  const startUrl = `${base}${listPath}`;
@@ -1000,12 +1065,18 @@ export async function findCommitStatusByContext(ctx, sha, context) {
1000
1065
 
1001
1066
  export async function statusSet(ctx, args) {
1002
1067
  assertWriteCommandConfigured(ctx.config, 'status_set');
1003
- const parsed = parseStatusSetArgs(args);
1068
+ const { idempotencyFingerprint = null, ...rest } = args;
1069
+ const parsed = parseStatusSetArgs(rest);
1004
1070
  const existing = await findCommitStatusByContext(ctx, parsed.sha, parsed.context);
1005
1071
  if (existing) {
1006
1072
  const existingState = normalizeStatusSetState(existing.state ?? existing.status);
1007
1073
  if (existingState === parsed.state) {
1008
- return buildCommitStatusSetBody(existing, parsed, { reusedExisting: true });
1074
+ return buildCommitStatusSetBody(existing, parsed, {
1075
+ reusedExisting: true,
1076
+ idempotencyFields: idempotencyFingerprint
1077
+ ? idempotencyPacketFields(idempotencyFingerprint, { reusedExisting: true })
1078
+ : null,
1079
+ });
1009
1080
  }
1010
1081
  }
1011
1082
  const payload = {
@@ -1019,7 +1090,11 @@ export async function statusSet(ctx, args) {
1019
1090
  headers: { 'Content-Type': 'application/json' },
1020
1091
  body: JSON.stringify(payload),
1021
1092
  });
1022
- return buildCommitStatusSetBody(response, parsed);
1093
+ return buildCommitStatusSetBody(response, parsed, {
1094
+ idempotencyFields: idempotencyFingerprint
1095
+ ? idempotencyPacketFields(idempotencyFingerprint, { reusedExisting: false })
1096
+ : null,
1097
+ });
1023
1098
  }
1024
1099
 
1025
1100
  export async function branchProtection(ctx, { branchRef }) {
@@ -1043,6 +1118,7 @@ export async function branchProtection(ctx, { branchRef }) {
1043
1118
  export const provider = {
1044
1119
  id: 'github-api',
1045
1120
  providerCapabilities,
1121
+ apiReachability,
1046
1122
  repoStatus,
1047
1123
  refsCompare,
1048
1124
  refsInventory,
@@ -1059,3 +1135,5 @@ export const provider = {
1059
1135
  forgeChanges,
1060
1136
  statusSet,
1061
1137
  };
1138
+
1139
+ setBranchProtectionImpl(branchProtection);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remogram/provider-github-api",
3
- "version": "0.1.0-beta.6",
3
+ "version": "0.1.0-beta.9",
4
4
  "description": "GitHub API provider for remogram",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -16,12 +16,13 @@
16
16
  "*.js"
17
17
  ],
18
18
  "exports": {
19
- ".": "./index.js"
19
+ ".": "./index.js",
20
+ "./branch-protection-internal.js": "./branch-protection-internal.js"
20
21
  },
21
22
  "engines": {
22
23
  "node": ">=20"
23
24
  },
24
25
  "dependencies": {
25
- "@remogram/core": "0.1.0-beta.6"
26
+ "@remogram/core": "0.1.0-beta.9"
26
27
  }
27
28
  }