( token: string, owner: string, repo: string, pullNumber: number, mergeMethod: MergeMethod = 'merge', maxRetries = 3 )
| 514 | * Merge a PR with retries. |
| 515 | */ |
| 516 | export async function mergePullRequest( |
| 517 | token: string, |
| 518 | owner: string, |
| 519 | repo: string, |
| 520 | pullNumber: number, |
| 521 | mergeMethod: MergeMethod = 'merge', |
| 522 | maxRetries = 3 |
| 523 | ): Promise<{ merged: boolean; sha?: string; message?: string }> { |
| 524 | let lastError: Error | null = null; |
| 525 | |
| 526 | for (let attempt = 0; attempt < maxRetries; attempt++) { |
| 527 | try { |
| 528 | const res = await ghFetch(`/repos/${owner}/${repo}/pulls/${pullNumber}/merge`, token, { |
| 529 | method: 'PUT', |
| 530 | body: JSON.stringify({ merge_method: mergeMethod }), |
| 531 | headers: { 'Content-Type': 'application/json' }, |
| 532 | }); |
| 533 | |
| 534 | if (res.ok) { |
| 535 | const data = await res.json(); |
| 536 | return { merged: true, sha: data.sha, message: data.message }; |
| 537 | } |
| 538 | |
| 539 | const err = await res.json().catch(() => ({ message: res.statusText })); |
| 540 | |
| 541 | if (res.status === 405) { |
| 542 | if (attempt < maxRetries - 1) { await sleep(2000); continue; } |
| 543 | throw new Error(`Cannot merge PR #${pullNumber}: ${err.message || 'Method not allowed. Check branch protection rules.'}`); |
| 544 | } |
| 545 | if (res.status === 409) { |
| 546 | if (attempt < maxRetries - 1) { await sleep(1500); continue; } |
| 547 | throw new Error(`Merge conflict on PR #${pullNumber}: ${err.message}`); |
| 548 | } |
| 549 | if (res.status === 403) { |
| 550 | if (err.message?.includes('Resource not accessible')) |
| 551 | throw new Error(`PERMISSION_DENIED: ${err.message}`); |
| 552 | if (err.message?.includes('rate limit') || err.message?.includes('abuse')) |
| 553 | throw new Error(`RATE_LIMITED: ${err.message}`); |
| 554 | throw new Error(`Forbidden: ${err.message}`); |
| 555 | } |
| 556 | if (res.status === 401) { |
| 557 | throw new Error(`PERMISSION_DENIED: Token expired or invalid (401 Unauthorized)`); |
| 558 | } |
| 559 | throw new Error(`Merge failed (HTTP ${res.status}): ${err.message || res.statusText}`); |
| 560 | } catch (err) { |
| 561 | lastError = err instanceof Error ? err : new Error(String(err)); |
| 562 | if (lastError.message.includes('PERMISSION_DENIED') || lastError.message.includes('RATE_LIMITED')) { |
| 563 | throw lastError; |
| 564 | } |
| 565 | if (attempt < maxRetries - 1) { await sleep(1500); } |
| 566 | } |
| 567 | } |
| 568 | throw lastError || new Error(`Failed to merge PR #${pullNumber} after ${maxRetries} attempts`); |
| 569 | } |
| 570 | |
| 571 | /** |
| 572 | * Delete a branch after merge (cleanup). |
no test coverage detected