@henryqw/pi-pr 3.1.7 → 3.1.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.
- package/README.md +1 -1
- package/extensions/pr-github.ts +40 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ It renames only the workspace. Outside Herdr, it does nothing.
|
|
|
90
90
|
|
|
91
91
|
If Herdr lookup, JSON validation, or rename fails, the PR and normal UI refresh remain available. Each Herdr command has a 10-second timeout. The extension warns with `Herdr workspace rename failed: <error>`.
|
|
92
92
|
|
|
93
|
-
Current-branch discovery
|
|
93
|
+
Current-branch discovery reads pull requests associated with the exact push repository ref. It does not run a global branch search. It finds a fork-head PR whose base is an upstream repository. A unique historical match uses the exact remote push-ref OID, not local HEAD.
|
|
94
94
|
|
|
95
95
|
A no-action state includes a draft, merged or closed pull request, running CI, pending review, or blocked merge policy. It also includes a mutating workflow whose tree is dirty or whose local HEAD differs from the PR head.
|
|
96
96
|
|
package/extensions/pr-github.ts
CHANGED
|
@@ -18,11 +18,11 @@ import type {
|
|
|
18
18
|
} from "./pr-routing.ts";
|
|
19
19
|
|
|
20
20
|
const EXEC_TIMEOUT_MS = 10_000;
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
21
|
+
const PR_DISCOVERY_PAGE_SIZE = 100;
|
|
22
|
+
const PR_DISCOVERY_CAP = 1_000;
|
|
23
|
+
const PR_DISCOVERY_MAX_PAGES = PR_DISCOVERY_CAP / PR_DISCOVERY_PAGE_SIZE;
|
|
24
24
|
const PR_FIELDS = "id,number,url,state,isDraft,baseRefName,baseRefOid,headRefName,headRefOid,headRepository,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup";
|
|
25
|
-
const
|
|
25
|
+
const PR_DISCOVERY_QUERY = "query($owner:String!,$name:String!,$qualifiedName:String!,$endCursor:String){repository(owner:$owner,name:$name){nameWithOwner ref(qualifiedName:$qualifiedName){name associatedPullRequests(first:100,after:$endCursor){totalCount edges{cursor node{__typename number url state baseRepository{nameWithOwner}headRepository{nameWithOwner}headRefName headRefOid}}pageInfo{hasNextPage startCursor endCursor}}}}}";
|
|
26
26
|
const REVIEW_THREADS_QUERY = "query($id:ID!,$endCursor:String){node(id:$id){...on PullRequest{reviewThreads(first:100,after:$endCursor){nodes{isResolved}pageInfo{hasNextPage endCursor}}}}}";
|
|
27
27
|
const BASE_REF_QUERY = "query($owner:String!,$name:String!,$qualifiedName:String!){repository(owner:$owner,name:$name){nameWithOwner ref(qualifiedName:$qualifiedName){name target{oid}}}}";
|
|
28
28
|
const BASE_BRANCH_POLICY_QUERY = "query($owner:String!,$name:String!,$qualifiedName:String!){repository(owner:$owner,name:$name){nameWithOwner ref(qualifiedName:$qualifiedName){name branchProtectionRule{requiresStrictStatusChecks}}}}";
|
|
@@ -167,7 +167,7 @@ type SearchSelection =
|
|
|
167
167
|
| { kind: "target-invalid" };
|
|
168
168
|
|
|
169
169
|
type SearchPage = {
|
|
170
|
-
|
|
170
|
+
totalCount: number;
|
|
171
171
|
candidates: SearchPullRequest[];
|
|
172
172
|
cursors: string[];
|
|
173
173
|
hasNextPage: boolean;
|
|
@@ -634,18 +634,29 @@ function searchPullRequest(value: unknown, host: string): SearchPullRequest {
|
|
|
634
634
|
};
|
|
635
635
|
}
|
|
636
636
|
|
|
637
|
-
function parseSearchPage(output: string,
|
|
637
|
+
function parseSearchPage(output: string, pushTarget: PushTarget): SearchPage | null {
|
|
638
638
|
const page = parseJson(output, "Find pull requests");
|
|
639
639
|
if (!isRecord(page)) fail("Find pull requests", "invalid GitHub CLI output");
|
|
640
640
|
if (page.errors !== undefined) {
|
|
641
641
|
if (!Array.isArray(page.errors)) fail("Find pull requests", "invalid GitHub CLI output");
|
|
642
642
|
if (page.errors.length) fail("Find pull requests", "GitHub GraphQL returned errors");
|
|
643
643
|
}
|
|
644
|
-
const
|
|
644
|
+
const repository = isRecord(page.data) ? page.data.repository : undefined;
|
|
645
|
+
if (!isRecord(repository)) fail("Find pull requests", "invalid GitHub CLI output");
|
|
645
646
|
if (
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
647
|
+
normalizeRepository(repositoryName(repository.nameWithOwner, "Find pull requests", "repository.nameWithOwner")) !==
|
|
648
|
+
pushTarget.repository.normalizedName
|
|
649
|
+
) fail("Find pull requests", "repository does not match push target");
|
|
650
|
+
if (repository.ref === null) return null;
|
|
651
|
+
if (!isRecord(repository.ref)) fail("Find pull requests", "invalid GitHub CLI output");
|
|
652
|
+
if (text(repository.ref.name, "Find pull requests", "ref.name") !== pushTarget.ref) {
|
|
653
|
+
fail("Find pull requests", "ref does not match push target");
|
|
654
|
+
}
|
|
655
|
+
const search = repository.ref.associatedPullRequests;
|
|
656
|
+
if (
|
|
657
|
+
!isRecord(search) || typeof search.totalCount !== "number" ||
|
|
658
|
+
!Number.isSafeInteger(search.totalCount) || search.totalCount < 0 ||
|
|
659
|
+
!Array.isArray(search.edges) || search.edges.length > PR_DISCOVERY_PAGE_SIZE ||
|
|
649
660
|
!isRecord(search.pageInfo)
|
|
650
661
|
) fail("Find pull requests", "invalid GitHub CLI output");
|
|
651
662
|
const candidates: SearchPullRequest[] = [];
|
|
@@ -653,7 +664,7 @@ function parseSearchPage(output: string, host: string): SearchPage {
|
|
|
653
664
|
for (const edge of search.edges) {
|
|
654
665
|
if (!isRecord(edge)) fail("Find pull requests", "invalid GitHub CLI output");
|
|
655
666
|
cursors.push(text(edge.cursor, "Find pull requests", "cursor"));
|
|
656
|
-
candidates.push(searchPullRequest(edge.node, host));
|
|
667
|
+
candidates.push(searchPullRequest(edge.node, pushTarget.repository.host));
|
|
657
668
|
}
|
|
658
669
|
if (new Set(cursors).size !== cursors.length) fail("Find pull requests", "duplicate candidate cursor");
|
|
659
670
|
const { hasNextPage, startCursor, endCursor } = search.pageInfo;
|
|
@@ -664,7 +675,7 @@ function parseSearchPage(output: string, host: string): SearchPage {
|
|
|
664
675
|
fail("Find pull requests", "invalid search pageInfo");
|
|
665
676
|
}
|
|
666
677
|
return {
|
|
667
|
-
|
|
678
|
+
totalCount: search.totalCount,
|
|
668
679
|
candidates,
|
|
669
680
|
cursors,
|
|
670
681
|
hasNextPage,
|
|
@@ -1443,31 +1454,36 @@ async function searchPullRequests(
|
|
|
1443
1454
|
context: PullRequestLoadContext,
|
|
1444
1455
|
pushTarget: PushTarget,
|
|
1445
1456
|
): Promise<SearchSelection> {
|
|
1446
|
-
const
|
|
1457
|
+
const [owner, name] = pushTarget.repository.nameWithOwner.split("/");
|
|
1447
1458
|
const candidates: SearchPullRequest[] = [];
|
|
1448
1459
|
const cursors = new Set<string>();
|
|
1449
|
-
let
|
|
1460
|
+
let totalCount: number | null = null;
|
|
1450
1461
|
let endCursor: string | null = null;
|
|
1451
|
-
for (let pageIndex = 0; pageIndex <
|
|
1462
|
+
for (let pageIndex = 0; pageIndex < PR_DISCOVERY_MAX_PAGES; pageIndex += 1) {
|
|
1452
1463
|
const args = [
|
|
1453
1464
|
"api",
|
|
1454
1465
|
"graphql",
|
|
1455
1466
|
"--hostname",
|
|
1456
1467
|
pushTarget.repository.host,
|
|
1457
1468
|
"-f",
|
|
1458
|
-
`query=${
|
|
1469
|
+
`query=${PR_DISCOVERY_QUERY}`,
|
|
1470
|
+
"-F",
|
|
1471
|
+
`owner=${owner}`,
|
|
1472
|
+
"-F",
|
|
1473
|
+
`name=${name}`,
|
|
1459
1474
|
"-F",
|
|
1460
|
-
`
|
|
1475
|
+
`qualifiedName=refs/heads/${pushTarget.ref}`,
|
|
1461
1476
|
];
|
|
1462
1477
|
if (endCursor !== null) args.push("-F", `endCursor=${endCursor}`);
|
|
1463
1478
|
const result = await execute(pi, context, "Find pull requests", "gh", args);
|
|
1464
|
-
const page = parseSearchPage(result.stdout, pushTarget
|
|
1465
|
-
if (
|
|
1479
|
+
const page = parseSearchPage(result.stdout, pushTarget);
|
|
1480
|
+
if (page === null) return pushTarget.remoteHeadOid === null ? { kind: "none" } : { kind: "target-invalid" };
|
|
1481
|
+
if (totalCount !== null && page.totalCount !== totalCount) {
|
|
1466
1482
|
fail("Find pull requests", "inconsistent search result pages");
|
|
1467
1483
|
}
|
|
1468
|
-
|
|
1469
|
-
if (
|
|
1470
|
-
const expectedPageSize = Math.min(
|
|
1484
|
+
totalCount = page.totalCount;
|
|
1485
|
+
if (totalCount > PR_DISCOVERY_CAP) fail("Find pull requests", "GitHub pull request result cap reached");
|
|
1486
|
+
const expectedPageSize = Math.min(PR_DISCOVERY_PAGE_SIZE, Math.max(0, totalCount - candidates.length));
|
|
1471
1487
|
if (page.candidates.length !== expectedPageSize) fail("Find pull requests", "incomplete search results");
|
|
1472
1488
|
for (const cursor of page.cursors) {
|
|
1473
1489
|
if (cursors.has(cursor)) fail("Find pull requests", "duplicate candidate cursor");
|
|
@@ -1477,7 +1493,7 @@ async function searchPullRequests(
|
|
|
1477
1493
|
if (new Set(candidates.map(({ url }) => url.href.toLowerCase())).size !== candidates.length) {
|
|
1478
1494
|
fail("Find pull requests", "duplicate candidate url");
|
|
1479
1495
|
}
|
|
1480
|
-
const hasMore = candidates.length <
|
|
1496
|
+
const hasMore = candidates.length < totalCount;
|
|
1481
1497
|
if (page.hasNextPage !== hasMore) fail("Find pull requests", "incomplete search results");
|
|
1482
1498
|
if (!hasMore) {
|
|
1483
1499
|
const selected = selectSearchPullRequest(candidates, pushTarget);
|
|
@@ -1497,7 +1513,7 @@ async function searchPullRequests(
|
|
|
1497
1513
|
if (page.endCursor === null) fail("Find pull requests", "invalid search pageInfo");
|
|
1498
1514
|
endCursor = page.endCursor;
|
|
1499
1515
|
}
|
|
1500
|
-
return fail("Find pull requests", "GitHub
|
|
1516
|
+
return fail("Find pull requests", "GitHub pull request result cap reached");
|
|
1501
1517
|
}
|
|
1502
1518
|
|
|
1503
1519
|
export async function loadCurrentPullRequest(
|
package/package.json
CHANGED