diff --git a/src/github/token.ts b/src/github/token.ts index 13863eb..234070c 100644 --- a/src/github/token.ts +++ b/src/github/token.ts @@ -1,47 +1,7 @@ #!/usr/bin/env bun import * as core from "@actions/core"; - -type RetryOptions = { - maxAttempts?: number; - initialDelayMs?: number; - maxDelayMs?: number; - backoffFactor?: number; -}; - -async function retryWithBackoff( - operation: () => Promise, - options: RetryOptions = {}, -): Promise { - const { - maxAttempts = 3, - initialDelayMs = 5000, - maxDelayMs = 20000, - backoffFactor = 2, - } = options; - - let delayMs = initialDelayMs; - let lastError: Error | undefined; - - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - console.log(`Attempt ${attempt} of ${maxAttempts}...`); - return await operation(); - } catch (error) { - lastError = error instanceof Error ? error : new Error(String(error)); - console.error(`Attempt ${attempt} failed:`, lastError.message); - - if (attempt < maxAttempts) { - console.log(`Retrying in ${delayMs / 1000} seconds...`); - await new Promise((resolve) => setTimeout(resolve, delayMs)); - delayMs = Math.min(delayMs * backoffFactor, maxDelayMs); - } - } - } - - console.error(`Operation failed after ${maxAttempts} attempts`); - throw lastError; -} +import { retryWithBackoff } from "../utils/retry"; async function getOidcToken(): Promise { try { diff --git a/src/mcp/github-file-ops-server.ts b/src/mcp/github-file-ops-server.ts index ef03178..e00c887 100644 --- a/src/mcp/github-file-ops-server.ts +++ b/src/mcp/github-file-ops-server.ts @@ -9,6 +9,7 @@ import fetch from "node-fetch"; import { GITHUB_API_URL } from "../github/api/config"; import { Octokit } from "@octokit/rest"; import { updateClaudeComment } from "../github/operations/comments/update-claude-comment"; +import { retryWithBackoff } from "../utils/retry"; type GitHubRef = { object: { @@ -233,26 +234,50 @@ server.tool( // 6. Update the reference to point to the new commit const updateRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`; - const updateRefResponse = await fetch(updateRefUrl, { - method: "PATCH", - headers: { - Accept: "application/vnd.github+json", - Authorization: `Bearer ${githubToken}`, - "X-GitHub-Api-Version": "2022-11-28", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - sha: newCommitData.sha, - force: false, - }), - }); - if (!updateRefResponse.ok) { - const errorText = await updateRefResponse.text(); - throw new Error( - `Failed to update reference: ${updateRefResponse.status} - ${errorText}`, - ); - } + // We're seeing intermittent 403 "Resource not accessible by integration" errors + // on certain repos when updating git references. These appear to be transient + // GitHub API issues that succeed on retry. + await retryWithBackoff( + async () => { + const updateRefResponse = await fetch(updateRefUrl, { + method: "PATCH", + headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${githubToken}`, + "X-GitHub-Api-Version": "2022-11-28", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + sha: newCommitData.sha, + force: false, + }), + }); + + if (!updateRefResponse.ok) { + const errorText = await updateRefResponse.text(); + const error = new Error( + `Failed to update reference: ${updateRefResponse.status} - ${errorText}`, + ); + + // Only retry on 403 errors - these are the intermittent failures we're targeting + if (updateRefResponse.status === 403) { + console.log("Received 403 error, will retry..."); + throw error; + } + + // For non-403 errors, fail immediately without retry + console.error("Non-retryable error:", updateRefResponse.status); + throw error; + } + }, + { + maxAttempts: 3, + initialDelayMs: 1000, // Start with 1 second delay + maxDelayMs: 5000, // Max 5 seconds delay + backoffFactor: 2, // Double the delay each time + }, + ); const simplifiedResult = { commit: { @@ -427,26 +452,50 @@ server.tool( // 6. Update the reference to point to the new commit const updateRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`; - const updateRefResponse = await fetch(updateRefUrl, { - method: "PATCH", - headers: { - Accept: "application/vnd.github+json", - Authorization: `Bearer ${githubToken}`, - "X-GitHub-Api-Version": "2022-11-28", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - sha: newCommitData.sha, - force: false, - }), - }); - if (!updateRefResponse.ok) { - const errorText = await updateRefResponse.text(); - throw new Error( - `Failed to update reference: ${updateRefResponse.status} - ${errorText}`, - ); - } + // We're seeing intermittent 403 "Resource not accessible by integration" errors + // on certain repos when updating git references. These appear to be transient + // GitHub API issues that succeed on retry. + await retryWithBackoff( + async () => { + const updateRefResponse = await fetch(updateRefUrl, { + method: "PATCH", + headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${githubToken}`, + "X-GitHub-Api-Version": "2022-11-28", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + sha: newCommitData.sha, + force: false, + }), + }); + + if (!updateRefResponse.ok) { + const errorText = await updateRefResponse.text(); + const error = new Error( + `Failed to update reference: ${updateRefResponse.status} - ${errorText}`, + ); + + // Only retry on 403 errors - these are the intermittent failures we're targeting + if (updateRefResponse.status === 403) { + console.log("Received 403 error, will retry..."); + throw error; + } + + // For non-403 errors, fail immediately without retry + console.error("Non-retryable error:", updateRefResponse.status); + throw error; + } + }, + { + maxAttempts: 3, + initialDelayMs: 1000, // Start with 1 second delay + maxDelayMs: 5000, // Max 5 seconds delay + backoffFactor: 2, // Double the delay each time + }, + ); const simplifiedResult = { commit: { diff --git a/src/utils/retry.ts b/src/utils/retry.ts new file mode 100644 index 0000000..bdcb541 --- /dev/null +++ b/src/utils/retry.ts @@ -0,0 +1,40 @@ +export type RetryOptions = { + maxAttempts?: number; + initialDelayMs?: number; + maxDelayMs?: number; + backoffFactor?: number; +}; + +export async function retryWithBackoff( + operation: () => Promise, + options: RetryOptions = {}, +): Promise { + const { + maxAttempts = 3, + initialDelayMs = 5000, + maxDelayMs = 20000, + backoffFactor = 2, + } = options; + + let delayMs = initialDelayMs; + let lastError: Error | undefined; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + console.log(`Attempt ${attempt} of ${maxAttempts}...`); + return await operation(); + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + console.error(`Attempt ${attempt} failed:`, lastError.message); + + if (attempt < maxAttempts) { + console.log(`Retrying in ${delayMs / 1000} seconds...`); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + delayMs = Math.min(delayMs * backoffFactor, maxDelayMs); + } + } + } + + console.error(`Operation failed after ${maxAttempts} attempts`); + throw lastError; +}