@remogram/provider-github-api 0.1.0-beta.4 → 0.1.0-beta.6
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 +250 -6
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
gitAheadBehind,
|
|
13
13
|
refsInventory,
|
|
14
14
|
crInventory,
|
|
15
|
-
|
|
15
|
+
buildMergePlanFromProviderFacts,
|
|
16
16
|
ERROR_CODES,
|
|
17
17
|
forgeError,
|
|
18
18
|
forgeIngestCapabilityFacts,
|
|
@@ -37,6 +37,22 @@ import {
|
|
|
37
37
|
orderOpenPullNumbers,
|
|
38
38
|
buildOpenPullListMeta,
|
|
39
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,
|
|
55
|
+
assertWriteCommandConfigured,
|
|
40
56
|
} from '@remogram/core';
|
|
41
57
|
|
|
42
58
|
const PUBLIC_GITHUB_HOST = 'github.com';
|
|
@@ -70,9 +86,21 @@ const AUTH_CAPABILITIES = [
|
|
|
70
86
|
'pr_checks',
|
|
71
87
|
'merge_plan',
|
|
72
88
|
'sync_plan',
|
|
89
|
+
'status_set',
|
|
90
|
+
'whoami',
|
|
91
|
+
'branch_protection',
|
|
92
|
+
'cr_files',
|
|
93
|
+
'cr_comments',
|
|
94
|
+
'forge_changes',
|
|
73
95
|
];
|
|
74
96
|
|
|
75
|
-
const STRUCTURED_COMMANDS = apiProviderCommands(
|
|
97
|
+
const STRUCTURED_COMMANDS = apiProviderCommands({
|
|
98
|
+
branchProtectionImplemented: true,
|
|
99
|
+
crFilesImplemented: true,
|
|
100
|
+
crCommentsImplemented: true,
|
|
101
|
+
forgeChangesImplemented: true,
|
|
102
|
+
statusSetImplemented: true,
|
|
103
|
+
});
|
|
76
104
|
|
|
77
105
|
export function githubToken() {
|
|
78
106
|
if (process.env.GITHUB_TOKEN) return { token: process.env.GITHUB_TOKEN, env: 'GITHUB_TOKEN' };
|
|
@@ -379,8 +407,10 @@ export function providerCapabilities() {
|
|
|
379
407
|
mergeability_confidence: 'derived',
|
|
380
408
|
host_binding: 'verified_remote_host',
|
|
381
409
|
pagination: 'supported',
|
|
382
|
-
write_support:
|
|
410
|
+
write_support: true,
|
|
411
|
+
write_commands: ['status_set'],
|
|
383
412
|
...forgeIngestCapabilityFacts(),
|
|
413
|
+
...statusSetIdempotencyScanCapabilityFacts(),
|
|
384
414
|
...checkPaginationCapabilityFacts({
|
|
385
415
|
strategy: 'link_header',
|
|
386
416
|
pageSizeParam: 'per_page',
|
|
@@ -528,9 +558,94 @@ export async function prChecks(ctx, opts) {
|
|
|
528
558
|
}
|
|
529
559
|
|
|
530
560
|
export async function mergePlan(ctx, opts) {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
561
|
+
return buildMergePlanFromProviderFacts(ctx, opts, { prView, prChecks, crFiles });
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export async function crFiles(ctx, { number }) {
|
|
565
|
+
if (number == null) {
|
|
566
|
+
throw Object.assign(new Error('--number required'), {
|
|
567
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'Provide --number for PR changed paths'),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
const base = apiBase(ctx.config, ctx.parsed);
|
|
571
|
+
const trustedOrigin = new URL(base).origin;
|
|
572
|
+
const { token } = requireToken();
|
|
573
|
+
const startUrl = `${base}${repoApiPath(ctx.config, 'pulls', number, 'files')}?per_page=${DEFAULT_CHECK_STATUS_PAGE_SIZE}`;
|
|
574
|
+
const { items, truncated, walked_count } = await paginateGitHubLinkPages({
|
|
575
|
+
trustedOrigin,
|
|
576
|
+
startUrl,
|
|
577
|
+
token,
|
|
578
|
+
mapPageItems: (body) => (Array.isArray(body) ? body : []),
|
|
579
|
+
});
|
|
580
|
+
const body = buildCrFilesFromGiteaFiles(number, items);
|
|
581
|
+
if (truncated) {
|
|
582
|
+
return buildCrFilesBody({
|
|
583
|
+
pr_number: body.pr_number,
|
|
584
|
+
changed_paths: body.changed_paths,
|
|
585
|
+
paths_truncated: true,
|
|
586
|
+
path_count: walked_count,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
return body;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export async function crComments(ctx, { number }) {
|
|
593
|
+
if (number == null) {
|
|
594
|
+
throw Object.assign(new Error('--number required'), {
|
|
595
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'Provide --number for PR review comments'),
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
const base = apiBase(ctx.config, ctx.parsed);
|
|
599
|
+
const trustedOrigin = new URL(base).origin;
|
|
600
|
+
const { token } = requireToken();
|
|
601
|
+
const startUrl = `${base}${repoApiPath(ctx.config, 'pulls', number, 'comments')}?per_page=${DEFAULT_CHECK_STATUS_PAGE_SIZE}`;
|
|
602
|
+
const { items, truncated, walked_count } = await paginateGitHubLinkPages({
|
|
603
|
+
trustedOrigin,
|
|
604
|
+
startUrl,
|
|
605
|
+
token,
|
|
606
|
+
mapPageItems: (body) => (Array.isArray(body) ? body : []),
|
|
607
|
+
});
|
|
608
|
+
const body = buildCrCommentsFromGiteaComments(number, items);
|
|
609
|
+
if (truncated) {
|
|
610
|
+
return buildCrCommentsBody({
|
|
611
|
+
pr_number: body.pr_number,
|
|
612
|
+
comments: body.comments,
|
|
613
|
+
comments_truncated: true,
|
|
614
|
+
comment_count: walked_count,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
return body;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export async function forgeChanges(ctx, { since }) {
|
|
621
|
+
requireToken();
|
|
622
|
+
const sinceIso = parseSinceObservedAt(since);
|
|
623
|
+
const base = apiBase(ctx.config, ctx.parsed);
|
|
624
|
+
const trustedOrigin = new URL(base).origin;
|
|
625
|
+
const { token } = requireToken();
|
|
626
|
+
const startUrl = `${base}${repoApiPath(ctx.config, 'pulls')}?state=all&sort=updated&direction=desc&per_page=${DEFAULT_CHECK_STATUS_PAGE_SIZE}`;
|
|
627
|
+
const { items, truncated } = await paginateGitHubLinkPages({
|
|
628
|
+
trustedOrigin,
|
|
629
|
+
startUrl,
|
|
630
|
+
token,
|
|
631
|
+
mapPageItems: (body) => (Array.isArray(body) ? body : []),
|
|
632
|
+
});
|
|
633
|
+
let body = buildForgeChangesFromGiteaPulls(sinceIso, items, { listTruncated: truncated });
|
|
634
|
+
const checkNumbers = new Set();
|
|
635
|
+
for (const event of body.events) {
|
|
636
|
+
if (event.kind === 'pr_opened' || event.kind === 'head_sha_moved') {
|
|
637
|
+
checkNumbers.add(event.pr_number);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const checkEvents = [];
|
|
641
|
+
for (const number of checkNumbers) {
|
|
642
|
+
const checks = await prChecks(ctx, { number });
|
|
643
|
+
checkEvents.push(buildChecksConclusionObservedEvent(number, checks));
|
|
644
|
+
}
|
|
645
|
+
if (checkEvents.length > 0) {
|
|
646
|
+
body = appendForgeChangeEvents(body, checkEvents, { listTruncated: truncated });
|
|
647
|
+
}
|
|
648
|
+
return body;
|
|
534
649
|
}
|
|
535
650
|
|
|
536
651
|
const GITHUB_OPEN_PULL_COMPLIANT_MAX =
|
|
@@ -802,6 +917,129 @@ export async function syncPlan(ctx, remoteName = 'origin') {
|
|
|
802
917
|
};
|
|
803
918
|
}
|
|
804
919
|
|
|
920
|
+
export async function whoami(ctx) {
|
|
921
|
+
const base = apiBase(ctx.config, ctx.parsed);
|
|
922
|
+
const { token } = requireToken();
|
|
923
|
+
const url = `${base}/user`;
|
|
924
|
+
const { body, headers } = await fetchJsonWithMeta(url, {
|
|
925
|
+
headers: authHeaders(token),
|
|
926
|
+
});
|
|
927
|
+
const scopeHeader =
|
|
928
|
+
headers?.get?.('x-oauth-scopes') ?? headers?.get?.('X-OAuth-Scopes') ?? null;
|
|
929
|
+
return buildProviderIdentityFromGitHubUser(body, scopeHeader);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function githubStatusRecordOrder(a, b) {
|
|
933
|
+
const aUpdated = Date.parse(a?.updated_at ?? '') || 0;
|
|
934
|
+
const bUpdated = Date.parse(b?.updated_at ?? '') || 0;
|
|
935
|
+
if (aUpdated !== bUpdated) return aUpdated - bUpdated;
|
|
936
|
+
const aId = Number(a.id) || 0;
|
|
937
|
+
const bId = Number(b.id) || 0;
|
|
938
|
+
return aId - bId;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function statusSetIdempotencyScanIncompleteError(pagesScanned, pageSizeUsed) {
|
|
942
|
+
return forgeError(
|
|
943
|
+
ERROR_CODES.IDEMPOTENCY_SCAN_INCOMPLETE,
|
|
944
|
+
'Cannot prove no commit status exists for sha+context within scan limit; retry or set manually',
|
|
945
|
+
null,
|
|
946
|
+
{
|
|
947
|
+
idempotency_scan: {
|
|
948
|
+
pages: pagesScanned,
|
|
949
|
+
max_pages: MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
|
|
950
|
+
page_size: pageSizeUsed,
|
|
951
|
+
},
|
|
952
|
+
},
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/** Paginated commit-status scan for idempotent status set; fail-closed when scan cap prevents proof of absence. */
|
|
957
|
+
export async function findCommitStatusByContext(ctx, sha, context) {
|
|
958
|
+
const { token } = requireToken();
|
|
959
|
+
const base = apiBase(ctx.config, ctx.parsed);
|
|
960
|
+
const trustedOrigin = new URL(base).origin;
|
|
961
|
+
const path = repoApiPath(ctx.config, 'commits', sha, 'statuses');
|
|
962
|
+
const pageQuery = `${path.includes('?') ? '&' : '?'}per_page=${DEFAULT_CHECK_STATUS_PAGE_SIZE}`;
|
|
963
|
+
let url = `${base}${path}${pageQuery}`;
|
|
964
|
+
let bestMatch = null;
|
|
965
|
+
const activeLimit = DEFAULT_CHECK_STATUS_PAGE_SIZE;
|
|
966
|
+
|
|
967
|
+
for (let page = 1; page <= MAX_OPEN_PULL_IDEMPOTENCY_PAGES; page += 1) {
|
|
968
|
+
const { body, headers } = await fetchJsonWithMeta(url, {
|
|
969
|
+
headers: authHeaders(token),
|
|
970
|
+
});
|
|
971
|
+
const items = Array.isArray(body) ? body : [];
|
|
972
|
+
for (const record of items) {
|
|
973
|
+
if (record?.context !== context) continue;
|
|
974
|
+
if (!bestMatch || githubStatusRecordOrder(record, bestMatch) > 0) {
|
|
975
|
+
bestMatch = record;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
const linkPage = resolveGitHubLinkNextPage({
|
|
979
|
+
trustedOrigin,
|
|
980
|
+
currentUrl: url,
|
|
981
|
+
linkHeader: headers?.get?.('link') ?? headers?.get?.('Link'),
|
|
982
|
+
pageIndex: page - 1,
|
|
983
|
+
maxPages: MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
|
|
984
|
+
});
|
|
985
|
+
if (items.length < activeLimit) {
|
|
986
|
+
return bestMatch;
|
|
987
|
+
}
|
|
988
|
+
if (page === MAX_OPEN_PULL_IDEMPOTENCY_PAGES) {
|
|
989
|
+
throw Object.assign(new Error('Commit status idempotency scan incomplete'), {
|
|
990
|
+
forgeError: statusSetIdempotencyScanIncompleteError(page, activeLimit),
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
if (!linkPage.nextUrl) {
|
|
994
|
+
return bestMatch;
|
|
995
|
+
}
|
|
996
|
+
url = withPerPageParam(linkPage.nextUrl, activeLimit);
|
|
997
|
+
}
|
|
998
|
+
return bestMatch;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
export async function statusSet(ctx, args) {
|
|
1002
|
+
assertWriteCommandConfigured(ctx.config, 'status_set');
|
|
1003
|
+
const parsed = parseStatusSetArgs(args);
|
|
1004
|
+
const existing = await findCommitStatusByContext(ctx, parsed.sha, parsed.context);
|
|
1005
|
+
if (existing) {
|
|
1006
|
+
const existingState = normalizeStatusSetState(existing.state ?? existing.status);
|
|
1007
|
+
if (existingState === parsed.state) {
|
|
1008
|
+
return buildCommitStatusSetBody(existing, parsed, { reusedExisting: true });
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
const payload = {
|
|
1012
|
+
state: parsed.state,
|
|
1013
|
+
context: parsed.context,
|
|
1014
|
+
};
|
|
1015
|
+
if (parsed.description != null) payload.description = parsed.description;
|
|
1016
|
+
if (parsed.target_url != null) payload.target_url = parsed.target_url;
|
|
1017
|
+
const response = await githubFetch(ctx.config, ctx.parsed, repoApiPath(ctx.config, 'statuses', parsed.sha), {
|
|
1018
|
+
method: 'POST',
|
|
1019
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1020
|
+
body: JSON.stringify(payload),
|
|
1021
|
+
});
|
|
1022
|
+
return buildCommitStatusSetBody(response, parsed);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
export async function branchProtection(ctx, { branchRef }) {
|
|
1026
|
+
assertGitRef(branchRef, '--branch-ref');
|
|
1027
|
+
const base = apiBase(ctx.config, ctx.parsed);
|
|
1028
|
+
const { token } = requireToken();
|
|
1029
|
+
const url = `${base}${repoApiPath(ctx.config, 'branches', branchRef, 'protection')}`;
|
|
1030
|
+
try {
|
|
1031
|
+
const { body } = await fetchJsonWithMeta(url, {
|
|
1032
|
+
headers: authHeaders(token),
|
|
1033
|
+
});
|
|
1034
|
+
return buildBranchProtectionFromGitHubProtection(branchRef, body);
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
if (err?.status === 404) {
|
|
1037
|
+
return buildBranchProtectionFromGitHubProtection(branchRef, null);
|
|
1038
|
+
}
|
|
1039
|
+
throw err;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
805
1043
|
export const provider = {
|
|
806
1044
|
id: 'github-api',
|
|
807
1045
|
providerCapabilities,
|
|
@@ -814,4 +1052,10 @@ export const provider = {
|
|
|
814
1052
|
prChecks,
|
|
815
1053
|
mergePlan,
|
|
816
1054
|
syncPlan,
|
|
1055
|
+
whoami,
|
|
1056
|
+
branchProtection,
|
|
1057
|
+
crFiles,
|
|
1058
|
+
crComments,
|
|
1059
|
+
forgeChanges,
|
|
1060
|
+
statusSet,
|
|
817
1061
|
};
|
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.6",
|
|
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.6"
|
|
26
26
|
}
|
|
27
27
|
}
|