refactor: make GitHub PR status more reliable and up to date (#613)
* fix: avoid incorrect PR status on default branch Hide sidebar PR status when current branch is the repo default branch. Tighten PR fallback matching to avoid picking unrelated PRs with the same branch name. * fix: make sidebar PR status more reliable Resolve PRs more reliably across remotes and repo networks. Persist sidebar PR status across tab reloads while keeping updates fresh. Reduce noisy GitHub errors and correct blocked styling in PR view. * fix: correct blocked PR styling in sidebar Stop treating missing merge permissions as a blocked PR state in the sidebar. Align sidebar PR status colors and labels with the main PR view. * refactor: make GitHub PR status updates feel more current Refresh PR state on focus, visibility, and repo changes Add follow-up PR refreshes after create, merge, ready, and update actions Document GitHub PR resolution, consumers, polling, and triggers
This commit is contained in:
committed by
GitHub
parent
180cd57661
commit
d1650343b1
@@ -9579,6 +9579,9 @@ async function main(options = {}) {
|
||||
};
|
||||
};
|
||||
|
||||
const isGitHubAuthInvalid = (error) => error?.status === 401 || error?.status === 403;
|
||||
const isGitHubResourceUnavailable = (error) => error?.status === 403 || error?.status === 404;
|
||||
|
||||
app.get('/api/github/auth/status', async (_req, res) => {
|
||||
try {
|
||||
const { getGitHubAuth, getOctokitOrNull, clearGitHubAuth, getGitHubAuthAccounts } = await getGitHubLibraries();
|
||||
@@ -9597,7 +9600,7 @@ async function main(options = {}) {
|
||||
try {
|
||||
user = await getGitHubUserSummary(octokit);
|
||||
} catch (error) {
|
||||
if (error?.status === 401) {
|
||||
if (isGitHubAuthInvalid(error)) {
|
||||
clearGitHubAuth();
|
||||
return res.json({ connected: false, accounts: getGitHubAuthAccounts() });
|
||||
}
|
||||
@@ -9733,7 +9736,7 @@ async function main(options = {}) {
|
||||
try {
|
||||
user = await getGitHubUserSummary(octokit);
|
||||
} catch (error) {
|
||||
if (error?.status === 401) {
|
||||
if (isGitHubAuthInvalid(error)) {
|
||||
clearGitHubAuth();
|
||||
return res.json({ connected: false, accounts: getGitHubAuthAccounts() });
|
||||
}
|
||||
@@ -9773,7 +9776,7 @@ async function main(options = {}) {
|
||||
try {
|
||||
user = await getGitHubUserSummary(octokit);
|
||||
} catch (error) {
|
||||
if (error?.status === 401) {
|
||||
if (isGitHubAuthInvalid(error)) {
|
||||
clearGitHubAuth();
|
||||
return res.status(401).json({ error: 'GitHub token expired or revoked' });
|
||||
}
|
||||
@@ -9803,94 +9806,20 @@ async function main(options = {}) {
|
||||
return res.json({ connected: false });
|
||||
}
|
||||
|
||||
const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js');
|
||||
const { repo } = await resolveGitHubRepoFromDirectory(directory, remote);
|
||||
if (!repo) {
|
||||
return res.json({ connected: true, repo: null, branch, pr: null, checks: null, canMerge: false });
|
||||
const { resolveGitHubPrStatus } = await import('./lib/github/pr-status.js');
|
||||
const resolvedStatus = await resolveGitHubPrStatus({
|
||||
octokit,
|
||||
directory,
|
||||
branch,
|
||||
remoteName: remote,
|
||||
});
|
||||
const searchRepo = resolvedStatus.repo;
|
||||
const first = resolvedStatus.pr;
|
||||
if (!searchRepo) {
|
||||
return res.json({ connected: true, repo: null, branch, pr: null, checks: null, canMerge: false, defaultBranch: null, resolvedRemoteName: null });
|
||||
}
|
||||
|
||||
let originRepo = null;
|
||||
if (remote !== 'origin') {
|
||||
const originResolved = await resolveGitHubRepoFromDirectory(directory, 'origin').catch(() => ({ repo: null }));
|
||||
originRepo = originResolved?.repo || null;
|
||||
}
|
||||
|
||||
const candidateHeadOwners = [];
|
||||
const pushHeadOwner = (owner) => {
|
||||
if (typeof owner !== 'string') return;
|
||||
const normalized = owner.trim();
|
||||
if (!normalized) return;
|
||||
if (candidateHeadOwners.includes(normalized)) return;
|
||||
candidateHeadOwners.push(normalized);
|
||||
};
|
||||
|
||||
// First, use branch tracking remote owner (where branch is usually pushed).
|
||||
const { getStatus } = await import('./lib/git/index.js');
|
||||
const status = await getStatus(directory).catch(() => null);
|
||||
if (status?.tracking) {
|
||||
const trackingRemote = status.tracking.split('/')[0];
|
||||
if (trackingRemote) {
|
||||
const trackingResolved = await resolveGitHubRepoFromDirectory(directory, trackingRemote).catch(() => ({ repo: null }));
|
||||
pushHeadOwner(trackingResolved?.repo?.owner);
|
||||
}
|
||||
}
|
||||
|
||||
// Then same-repo and origin fallback owners.
|
||||
pushHeadOwner(repo.owner);
|
||||
pushHeadOwner(originRepo?.owner);
|
||||
|
||||
const listByHead = async (targetRepo, state, headOwner) => {
|
||||
const resp = await octokit.rest.pulls.list({
|
||||
owner: targetRepo.owner,
|
||||
repo: targetRepo.repo,
|
||||
state,
|
||||
head: `${headOwner}:${branch}`,
|
||||
per_page: 10,
|
||||
});
|
||||
return Array.isArray(resp?.data) ? resp.data[0] : null;
|
||||
};
|
||||
|
||||
const listByHeadRef = async (targetRepo, state) => {
|
||||
const resp = await octokit.rest.pulls.list({
|
||||
owner: targetRepo.owner,
|
||||
repo: targetRepo.repo,
|
||||
state,
|
||||
per_page: 100,
|
||||
});
|
||||
const matches = Array.isArray(resp?.data)
|
||||
? resp.data.filter((pr) => pr?.head?.ref === branch)
|
||||
: [];
|
||||
return matches[0] ?? null;
|
||||
};
|
||||
|
||||
const tryFindPr = async (targetRepo) => {
|
||||
let found = null;
|
||||
for (const owner of candidateHeadOwners) {
|
||||
found = await listByHead(targetRepo, 'open', owner);
|
||||
if (found) return found;
|
||||
found = await listByHead(targetRepo, 'closed', owner);
|
||||
if (found) return found;
|
||||
}
|
||||
found = await listByHeadRef(targetRepo, 'open');
|
||||
if (found) return found;
|
||||
return listByHeadRef(targetRepo, 'closed');
|
||||
};
|
||||
|
||||
// Try requested remote target repo first, then origin target repo fallback for fork flows.
|
||||
let searchRepo = repo;
|
||||
let first = await tryFindPr(searchRepo);
|
||||
if (!first && originRepo) {
|
||||
const isDifferentRepo = originRepo.owner !== repo.owner || originRepo.repo !== repo.repo;
|
||||
if (isDifferentRepo) {
|
||||
const originMatch = await tryFindPr(originRepo);
|
||||
if (originMatch) {
|
||||
first = originMatch;
|
||||
searchRepo = originRepo;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!first) {
|
||||
return res.json({ connected: true, repo: searchRepo, branch, pr: null, checks: null, canMerge: false });
|
||||
return res.json({ connected: true, repo: searchRepo, branch, pr: null, checks: null, canMerge: false, defaultBranch: resolvedStatus.defaultBranch ?? null, resolvedRemoteName: resolvedStatus.resolvedRemoteName ?? null });
|
||||
}
|
||||
|
||||
// Enrich with mergeability fields
|
||||
@@ -10006,6 +9935,8 @@ async function main(options = {}) {
|
||||
},
|
||||
checks,
|
||||
canMerge,
|
||||
defaultBranch: resolvedStatus.defaultBranch ?? null,
|
||||
resolvedRemoteName: resolvedStatus.resolvedRemoteName ?? null,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error?.status === 401) {
|
||||
@@ -10013,6 +9944,18 @@ async function main(options = {}) {
|
||||
clearGitHubAuth();
|
||||
return res.json({ connected: false });
|
||||
}
|
||||
if (isGitHubResourceUnavailable(error)) {
|
||||
return res.json({
|
||||
connected: true,
|
||||
repo: null,
|
||||
branch: typeof req.query?.branch === 'string' ? req.query.branch.trim() : '',
|
||||
pr: null,
|
||||
checks: null,
|
||||
canMerge: false,
|
||||
defaultBranch: null,
|
||||
resolvedRemoteName: null,
|
||||
});
|
||||
}
|
||||
console.error('Failed to load GitHub PR status:', error);
|
||||
return res.status(500).json({ error: error.message || 'Failed to load GitHub PR status' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user