diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts
index 431137c3..128c592b 100644
--- a/packages/server/src/app.ts
+++ b/packages/server/src/app.ts
@@ -594,7 +594,11 @@ export function createApp(deps: AppDeps): express.Express {
/** Who one of the user's own pull requests is waiting on. */
const ownStatus = (request: ReviewRequestRow): string => {
const parts = request.draft ? [draftBadge] : [];
- const reviewers = request.requestedReviewers ?? [];
+ if (!request.requestedReviewers) {
+ parts.push('Status unknown');
+ return parts.join(' ');
+ }
+ const reviewers = request.requestedReviewers;
const openThreads = request.openThreads ?? 0;
if (openThreads === 0 && request.moreThreads) {
parts.push('Open threads unknown ·');
diff --git a/packages/server/src/github.ts b/packages/server/src/github.ts
index 703014c0..1a7d4c73 100644
--- a/packages/server/src/github.ts
+++ b/packages/server/src/github.ts
@@ -113,34 +113,24 @@ const OWN_PULLS_GRAPHQL = `query($q:String!){
search(type:ISSUE,query:$q,first:50){
nodes{
... on PullRequest{
- number
- title
url
- updatedAt
- isDraft
- repository{owner{login} name}
- author{login}
reviewDecision
reviewRequests(first:20){totalCount nodes{requestedReviewer{... on User{login} ... on Bot{login} ... on Team{name}}}}
- reviewThreads(first:${THREADS_PAGE}){totalCount nodes{isResolved isOutdated}}
+ reviewThreads(first:${THREADS_PAGE}){totalCount nodes{isResolved}}
}
}
}
}`;
interface RawOwnPull {
- number?: number;
- title?: string;
url?: string;
- updatedAt?: string;
- isDraft?: boolean;
- repository?: { owner?: { login?: string }; name?: string };
- author?: { login?: string } | null;
reviewDecision?: string | null;
- reviewRequests?: { totalCount?: number; nodes?: { requestedReviewer?: { login?: string; name?: string } | null }[] };
- reviewThreads?: { totalCount?: number; nodes?: { isResolved?: boolean }[] };
+ reviewRequests?: { totalCount?: number; nodes?: { requestedReviewer?: { login?: string; name?: string } | null }[] } | null;
+ reviewThreads?: { totalCount?: number; nodes?: { isResolved?: boolean }[] } | null;
}
+type OwnPullStatus = Required>;
+
const REVIEW_THREADS_QUERY = `query($owner:String!,$repo:String!,$number:Int!){
repository(owner:$owner,name:$repo){
pullRequest(number:$number){
@@ -338,53 +328,53 @@ export class GitHubApi {
}
/**
- * The token's own open pull requests, most recently updated first, with who each is waiting on.
- * Review threads and requested reviewers live on GraphQL only.
+ * The token's own open pull requests, most recently updated first, with who each is waiting on where
+ * GitHub lets the token see it. Review threads and requested reviewers live on GraphQL only, which
+ * refuses them in repositories the App is not installed on; those pull requests come without them.
*/
async ownPulls(token: string): Promise {
- let json: { data?: { search?: { nodes?: (RawOwnPull | null)[] } }; errors?: { message?: string }[] };
+ const pulls = await this.searchPulls(token, OWN_PULLS_QUERY);
+ if (pulls.length === 0) {
+ return pulls;
+ }
+ let statuses: Map;
try {
- json = await this.json(token, '/graphql', {
- method: 'POST',
- body: { query: OWN_PULLS_GRAPHQL, variables: { q: `${OWN_PULLS_QUERY} sort:updated-desc` } },
- });
+ statuses = await this.ownPullStatuses(token);
} catch (err) {
- if (err instanceof GitHubApiError && err.status === 422) {
- console.warn(`Searching pull requests failed: ${err.message}`);
- return [];
- }
- throw err;
+ console.warn(`Reading who pull requests wait on failed: ${err instanceof Error ? err.message : String(err)}`);
+ statuses = new Map();
}
- // GraphQL answers a refused or partly failed query with 200 and errors, e.g. when rate limited.
- if (!json.data?.search || (json.errors?.length ?? 0) > 0) {
- throw new Error(`Searching pull requests failed: ${json.errors?.map(e => e.message).join('; ') || 'no data'}`);
+ return pulls.map(pull => ({ ...pull, ...statuses.get(pull.url) }));
+ }
+
+ /** By pull request URL. A pull request GraphQL refuses is a null node with an error of its own, and is left out. */
+ private async ownPullStatuses(token: string): Promise