diff --git a/package.json b/package.json index f9a7c40..8997440 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,6 @@ "@actions/github": "^6.0.1", "@anthropic-ai/sdk": "^0.30.0", "@modelcontextprotocol/sdk": "^1.11.0", - "@octokit/graphql": "^8.2.2", - "@octokit/rest": "^21.1.1", "@octokit/webhooks-types": "^7.6.1", "node-fetch": "^3.3.2", "zod": "^3.24.4" diff --git a/src/entrypoints/prepare.ts b/src/entrypoints/prepare.ts index dbcb75b..14918e2 100644 --- a/src/entrypoints/prepare.ts +++ b/src/entrypoints/prepare.ts @@ -15,7 +15,7 @@ import { setupBranch } from "../github/operations/branch"; import { updateTrackingComment } from "../github/operations/comments/update-with-branch"; import { prepareMcpConfig } from "../mcp/install-mcp-server"; import { createPrompt } from "../create-prompt"; -import { createOctokit } from "../github/api/client"; +import { createClient } from "../github/api/client"; import { fetchGitHubData } from "../github/data/fetcher"; import { parseGitHubContext } from "../github/context"; @@ -23,14 +23,14 @@ async function run() { try { // Step 1: Setup GitHub token const githubToken = await setupGitHubToken(); - const octokit = createOctokit(githubToken); + const client = createClient(githubToken); // Step 2: Parse GitHub context (once for all operations) const context = parseGitHubContext(); // Step 3: Check write permissions const hasWritePermissions = await checkWritePermissions( - octokit.rest, + client.api, context, ); if (!hasWritePermissions) { @@ -52,22 +52,22 @@ async function run() { } // Step 5: Check if actor is human - await checkHumanActor(octokit.rest, context); + await checkHumanActor(client.api, context); // Step 6: Create initial tracking comment - const commentId = await createInitialComment(octokit.rest, context); + const commentId = await createInitialComment(client.api, context); core.setOutput("claude_comment_id", commentId.toString()); // Step 7: Fetch GitHub data (once for both branch setup and prompt creation) const githubData = await fetchGitHubData({ - octokits: octokit, + client: client, repository: `${context.repository.owner}/${context.repository.repo}`, prNumber: context.entityNumber.toString(), isPR: context.isPR, }); // Step 8: Setup branch - const branchInfo = await setupBranch(octokit, githubData, context); + const branchInfo = await setupBranch(client, githubData, context); core.setOutput("BASE_BRANCH", branchInfo.baseBranch); if (branchInfo.claudeBranch) { core.setOutput("CLAUDE_BRANCH", branchInfo.claudeBranch); @@ -76,7 +76,7 @@ async function run() { // Step 9: Update initial comment with branch link (only for issues that created a new branch) if (branchInfo.claudeBranch) { await updateTrackingComment( - octokit, + client, context, commentId, branchInfo.claudeBranch, diff --git a/src/entrypoints/update-comment-link.ts b/src/entrypoints/update-comment-link.ts index 3477c10..3ab66d6 100644 --- a/src/entrypoints/update-comment-link.ts +++ b/src/entrypoints/update-comment-link.ts @@ -1,6 +1,6 @@ #!/usr/bin/env bun -import { createOctokit } from "../github/api/client"; +import { createClient } from "../github/api/client"; import * as fs from "fs/promises"; import { updateCommentBody, @@ -23,7 +23,7 @@ async function run() { const context = parseGitHubContext(); const { owner, repo } = context.repository; - const octokit = createOctokit(githubToken); + const client = createClient(githubToken); const serverUrl = GITHUB_SERVER_URL; const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; @@ -37,12 +37,8 @@ async function run() { if (isPullRequestReviewCommentEvent(context)) { // For PR review comments, use the pulls API console.log(`Fetching PR review comment ${commentId}`); - const { data: prComment } = await octokit.rest.pulls.getReviewComment({ - owner, - repo, - comment_id: commentId, - }); - comment = prComment; + const response = await client.api.customRequest("GET", `/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`); + comment = response.data; isPRReviewComment = true; console.log("Successfully fetched as PR review comment"); } @@ -50,12 +46,8 @@ async function run() { // For all other event types, use the issues API if (!comment) { console.log(`Fetching issue comment ${commentId}`); - const { data: issueComment } = await octokit.rest.issues.getComment({ - owner, - repo, - comment_id: commentId, - }); - comment = issueComment; + const response = await client.api.customRequest("GET", `/api/v1/repos/${owner}/${repo}/issues/comments/${commentId}`); + comment = response.data; isPRReviewComment = false; console.log("Successfully fetched as issue comment"); } @@ -69,14 +61,10 @@ async function run() { // Try to get the PR info to understand the comment structure try { - const { data: pr } = await octokit.rest.pulls.get({ - owner, - repo, - pull_number: context.entityNumber, - }); - console.log(`PR state: ${pr.state}`); - console.log(`PR comments count: ${pr.comments}`); - console.log(`PR review comments count: ${pr.review_comments}`); + const pr = await client.api.getPullRequest(owner, repo, context.entityNumber); + console.log(`PR state: ${pr.data.state}`); + console.log(`PR comments count: ${pr.data.comments}`); + console.log(`PR review comments count: ${pr.data.review_comments}`); } catch { console.error("Could not fetch PR info for debugging"); } @@ -88,7 +76,7 @@ async function run() { // Check if we need to add branch link for new branches const { shouldDeleteBranch, branchLink } = await checkAndDeleteEmptyBranch( - octokit, + client, owner, repo, claudeBranch, @@ -107,158 +95,60 @@ async function run() { const containsPRUrl = currentBody.match(prUrlPattern); if (!containsPRUrl) { - // Check if we're in a Gitea environment - const isGitea = - process.env.GITHUB_API_URL && - !process.env.GITHUB_API_URL.includes("api.github.com"); + // Use direct SHA comparison for all Git platforms + console.log("Using SHA comparison for PR link check"); - if (isGitea) { - // Gitea doesn't support the /compare endpoint, use direct SHA comparison - console.log( - "Detected Gitea environment, using SHA comparison for PR link check", - ); + try { + // Get the branch info to see if it exists and has commits + const branchResponse = await client.api.getBranch(owner, repo, claudeBranch); - try { - // Get the branch info to see if it exists and has commits - const branchResponse = await octokit.rest.repos.getBranch({ - owner, - repo, - branch: claudeBranch, - }); + // Get base branch info for comparison + const baseResponse = await client.api.getBranch(owner, repo, baseBranch); - // Get base branch info for comparison - const baseResponse = await octokit.rest.repos.getBranch({ - owner, - repo, - branch: baseBranch, - }); + const branchSha = branchResponse.data.commit.sha; + const baseSha = baseResponse.data.commit.sha; - const branchSha = branchResponse.data.commit.sha; - const baseSha = baseResponse.data.commit.sha; - - // If SHAs are different, assume there are changes and add PR link - if (branchSha !== baseSha) { - console.log( - `Branch ${claudeBranch} appears to have changes (different SHA from base)`, - ); - const entityType = context.isPR ? "PR" : "Issue"; - const prTitle = encodeURIComponent( - `${entityType} #${context.entityNumber}: Changes from Claude`, - ); - const prBody = encodeURIComponent( - `This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`, - ); - const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`; - prLink = `\n[Create a PR](${prUrl})`; - } else { - console.log( - `Branch ${claudeBranch} has same SHA as base, no PR link needed`, - ); - } - } catch (error: any) { - console.error("Error checking branch in Gitea:", error); - - // Handle 404 specifically - branch doesn't exist - if (error.status === 404) { - console.log( - `Branch ${claudeBranch} does not exist yet - no PR link needed`, - ); - // Don't add PR link since branch doesn't exist - prLink = ""; - } else { - // For other errors, add PR link to be safe - console.log( - "Adding PR link as fallback for Gitea due to non-404 error", - ); - const entityType = context.isPR ? "PR" : "Issue"; - const prTitle = encodeURIComponent( - `${entityType} #${context.entityNumber}: Changes from Claude`, - ); - const prBody = encodeURIComponent( - `This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`, - ); - const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`; - prLink = `\n[Create a PR](${prUrl})`; - } + // If SHAs are different, assume there are changes and add PR link + if (branchSha !== baseSha) { + console.log( + `Branch ${claudeBranch} appears to have changes (different SHA from base)`, + ); + const entityType = context.isPR ? "PR" : "Issue"; + const prTitle = encodeURIComponent( + `${entityType} #${context.entityNumber}: Changes from Claude`, + ); + const prBody = encodeURIComponent( + `This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`, + ); + const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`; + prLink = `\n[Create a PR](${prUrl})`; + } else { + console.log( + `Branch ${claudeBranch} has same SHA as base, no PR link needed`, + ); } - } else { - // GitHub environment - use the comparison API - try { - const { data: comparison } = - await octokit.rest.repos.compareCommitsWithBasehead({ - owner, - repo, - basehead: `${baseBranch}...${claudeBranch}`, - }); + } catch (error: any) { + console.error("Error checking branch:", error); - // If there are changes (commits or file changes), add the PR URL - if ( - comparison.total_commits > 0 || - (comparison.files && comparison.files.length > 0) - ) { - const entityType = context.isPR ? "PR" : "Issue"; - const prTitle = encodeURIComponent( - `${entityType} #${context.entityNumber}: Changes from Claude`, - ); - const prBody = encodeURIComponent( - `This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`, - ); - const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`; - prLink = `\n[Create a PR](${prUrl})`; - } - } catch (error) { - console.error("Error checking for changes in branch:", error); - - // Fallback to SHA comparison even on GitHub if API fails - try { - console.log( - "GitHub comparison API failed, falling back to SHA comparison", - ); - - const branchResponse = await octokit.rest.repos.getBranch({ - owner, - repo, - branch: claudeBranch, - }); - - const baseResponse = await octokit.rest.repos.getBranch({ - owner, - repo, - branch: baseBranch, - }); - - const branchSha = branchResponse.data.commit.sha; - const baseSha = baseResponse.data.commit.sha; - - // If SHAs are different, assume there are changes and add PR link - if (branchSha !== baseSha) { - const entityType = context.isPR ? "PR" : "Issue"; - const prTitle = encodeURIComponent( - `${entityType} #${context.entityNumber}: Changes from Claude`, - ); - const prBody = encodeURIComponent( - `This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`, - ); - const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`; - prLink = `\n[Create a PR](${prUrl})`; - } - } catch (fallbackError) { - console.error( - "Fallback branch comparison also failed:", - fallbackError, - ); - // If all checks fail, still add PR link to be safe - console.log("Adding PR link as final fallback"); - const entityType = context.isPR ? "PR" : "Issue"; - const prTitle = encodeURIComponent( - `${entityType} #${context.entityNumber}: Changes from Claude`, - ); - const prBody = encodeURIComponent( - `This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`, - ); - const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`; - prLink = `\n[Create a PR](${prUrl})`; - } + // Handle 404 specifically - branch doesn't exist + if (error.status === 404) { + console.log( + `Branch ${claudeBranch} does not exist yet - no PR link needed`, + ); + // Don't add PR link since branch doesn't exist + prLink = ""; + } else { + // For other errors, add PR link to be safe + console.log("Adding PR link as fallback due to non-404 error"); + const entityType = context.isPR ? "PR" : "Issue"; + const prTitle = encodeURIComponent( + `${entityType} #${context.entityNumber}: Changes from Claude`, + ); + const prBody = encodeURIComponent( + `This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`, + ); + const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`; + prLink = `\n[Create a PR](${prUrl})`; } } } @@ -333,19 +223,11 @@ async function run() { // Update the comment using the appropriate API try { if (isPRReviewComment) { - await octokit.rest.pulls.updateReviewComment({ - owner, - repo, - comment_id: commentId, + await client.api.customRequest("PATCH", `/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`, { body: updatedBody, }); } else { - await octokit.rest.issues.updateComment({ - owner, - repo, - comment_id: commentId, - body: updatedBody, - }); + await client.api.updateIssueComment(owner, repo, commentId, updatedBody); } console.log( `✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} with job link`, diff --git a/src/github/api/client.ts b/src/github/api/client.ts index 5437058..00655a8 100644 --- a/src/github/api/client.ts +++ b/src/github/api/client.ts @@ -1,23 +1,11 @@ -import { Octokit } from "@octokit/rest"; -import { graphql } from "@octokit/graphql"; -import { GITHUB_API_URL } from "./config"; +import { GiteaApiClient, createGiteaClient } from "./gitea-client"; -export type Octokits = { - rest: Octokit; - graphql: typeof graphql; +export type GitHubClient = { + api: GiteaApiClient; }; -export function createOctokit(token: string): Octokits { +export function createClient(token: string): GitHubClient { return { - rest: new Octokit({ - auth: token, - baseUrl: GITHUB_API_URL, - }), - graphql: graphql.defaults({ - baseUrl: GITHUB_API_URL, - headers: { - authorization: `token ${token}`, - }, - }), + api: createGiteaClient(token), }; } diff --git a/src/github/api/gitea-client.ts b/src/github/api/gitea-client.ts new file mode 100644 index 0000000..5b5b57a --- /dev/null +++ b/src/github/api/gitea-client.ts @@ -0,0 +1,218 @@ +import fetch from "node-fetch"; +import { GITHUB_API_URL } from "./config"; + +export interface GiteaApiResponse { + status: number; + data: T; + headers: Record; +} + +export interface GiteaApiError extends Error { + status: number; + response?: { + data: any; + status: number; + headers: Record; + }; +} + +export class GiteaApiClient { + private baseUrl: string; + private token: string; + + constructor(token: string, baseUrl: string = GITHUB_API_URL) { + this.token = token; + this.baseUrl = baseUrl.replace(/\/+$/, ""); // Remove trailing slashes + } + + private async request( + method: string, + endpoint: string, + body?: any + ): Promise> { + const url = `${this.baseUrl}${endpoint}`; + + const headers: Record = { + "Content-Type": "application/json", + "Authorization": `token ${this.token}`, + }; + + const options: any = { + method, + headers, + }; + + if (body && (method === "POST" || method === "PUT" || method === "PATCH")) { + options.body = JSON.stringify(body); + } + + try { + const response = await fetch(url, options); + const responseData: any = await response.json(); + + if (!response.ok) { + const error = new Error( + `HTTP ${response.status}: ${responseData.message || response.statusText}` + ) as GiteaApiError; + error.status = response.status; + error.response = { + data: responseData, + status: response.status, + headers: Object.fromEntries(response.headers.entries()), + }; + throw error; + } + + return { + status: response.status, + data: responseData as T, + headers: Object.fromEntries(response.headers.entries()), + }; + } catch (error) { + if (error instanceof Error && "status" in error) { + throw error; + } + throw new Error(`Request failed: ${error}`); + } + } + + // Repository operations + async getRepo(owner: string, repo: string) { + return this.request("GET", `/api/v1/repos/${owner}/${repo}`); + } + + async getBranch(owner: string, repo: string, branch: string) { + return this.request("GET", `/api/v1/repos/${owner}/${repo}/branches/${encodeURIComponent(branch)}`); + } + + async createBranch(owner: string, repo: string, newBranch: string, fromBranch: string) { + return this.request("POST", `/api/v1/repos/${owner}/${repo}/branches`, { + new_branch_name: newBranch, + old_branch_name: fromBranch, + }); + } + + async listBranches(owner: string, repo: string) { + return this.request("GET", `/api/v1/repos/${owner}/${repo}/branches`); + } + + // Issue operations + async getIssue(owner: string, repo: string, issueNumber: number) { + return this.request("GET", `/api/v1/repos/${owner}/${repo}/issues/${issueNumber}`); + } + + async listIssueComments(owner: string, repo: string, issueNumber: number) { + return this.request("GET", `/api/v1/repos/${owner}/${repo}/issues/${issueNumber}/comments`); + } + + async createIssueComment(owner: string, repo: string, issueNumber: number, body: string) { + return this.request("POST", `/api/v1/repos/${owner}/${repo}/issues/${issueNumber}/comments`, { + body, + }); + } + + async updateIssueComment(owner: string, repo: string, commentId: number, body: string) { + return this.request("PATCH", `/api/v1/repos/${owner}/${repo}/issues/comments/${commentId}`, { + body, + }); + } + + // Pull request operations + async getPullRequest(owner: string, repo: string, prNumber: number) { + return this.request("GET", `/api/v1/repos/${owner}/${repo}/pulls/${prNumber}`); + } + + async listPullRequestFiles(owner: string, repo: string, prNumber: number) { + return this.request("GET", `/api/v1/repos/${owner}/${repo}/pulls/${prNumber}/files`); + } + + async listPullRequestComments(owner: string, repo: string, prNumber: number) { + return this.request("GET", `/api/v1/repos/${owner}/${repo}/pulls/${prNumber}/comments`); + } + + async createPullRequestComment(owner: string, repo: string, prNumber: number, body: string) { + return this.request("POST", `/api/v1/repos/${owner}/${repo}/pulls/${prNumber}/comments`, { + body, + }); + } + + // File operations + async getFileContents(owner: string, repo: string, path: string, ref?: string) { + let endpoint = `/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`; + if (ref) { + endpoint += `?ref=${encodeURIComponent(ref)}`; + } + return this.request("GET", endpoint); + } + + async createFile( + owner: string, + repo: string, + path: string, + content: string, + message: string, + branch?: string + ) { + const body: any = { + message, + content: Buffer.from(content).toString("base64"), + }; + + if (branch) { + body.branch = branch; + } + + return this.request("POST", `/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`, body); + } + + async updateFile( + owner: string, + repo: string, + path: string, + content: string, + message: string, + sha: string, + branch?: string + ) { + const body: any = { + message, + content: Buffer.from(content).toString("base64"), + sha, + }; + + if (branch) { + body.branch = branch; + } + + return this.request("PUT", `/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`, body); + } + + async deleteFile( + owner: string, + repo: string, + path: string, + message: string, + sha: string, + branch?: string + ) { + const body: any = { + message, + sha, + }; + + if (branch) { + body.branch = branch; + } + + return this.request("DELETE", `/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`, body); + } + + // Generic request method for other operations + async customRequest(method: string, endpoint: string, body?: any): Promise> { + return this.request(method, endpoint, body); + } +} + +export function createGiteaClient(token: string): GiteaApiClient { + return new GiteaApiClient(token); +} \ No newline at end of file diff --git a/src/github/data/fetcher.ts b/src/github/data/fetcher.ts index 7696257..5954da5 100644 --- a/src/github/data/fetcher.ts +++ b/src/github/data/fetcher.ts @@ -5,16 +5,13 @@ import type { GitHubComment, GitHubFile, GitHubReview, - PullRequestQueryResponse, - IssueQueryResponse, } from "../types"; -import { PR_QUERY, ISSUE_QUERY } from "../api/queries/github"; -import type { Octokits } from "../api/client"; +import type { GitHubClient } from "../api/client"; import { downloadCommentImages } from "../utils/image-downloader"; import type { CommentWithImages } from "../utils/image-downloader"; type FetchDataParams = { - octokits: Octokits; + client: GitHubClient; repository: string; prNumber: string; isPR: boolean; @@ -34,7 +31,7 @@ export type FetchDataResult = { }; export async function fetchGitHubData({ - octokits, + client, repository, prNumber, isPR, @@ -44,163 +41,102 @@ export async function fetchGitHubData({ throw new Error("Invalid repository format. Expected 'owner/repo'."); } - // Check if we're in a Gitea environment (no GraphQL support) - const isGitea = - process.env.GITHUB_API_URL && - !process.env.GITHUB_API_URL.includes("api.github.com"); - let contextData: GitHubPullRequest | GitHubIssue | null = null; let comments: GitHubComment[] = []; let changedFiles: GitHubFile[] = []; let reviewData: { nodes: GitHubReview[] } | null = null; try { - if (isGitea) { - // Use REST API for Gitea compatibility - if (isPR) { - console.log( - `Fetching PR #${prNumber} data using REST API (Gitea mode)`, - ); - const prResponse = await octokits.rest.pulls.get({ + // Use REST API for all requests (works with both GitHub and Gitea) + if (isPR) { + console.log(`Fetching PR #${prNumber} data using REST API`); + const prResponse = await client.api.getPullRequest(owner, repo, parseInt(prNumber)); + + contextData = { + title: prResponse.data.title, + body: prResponse.data.body || "", + author: { login: prResponse.data.user?.login || "" }, + baseRefName: prResponse.data.base.ref, + headRefName: prResponse.data.head.ref, + headRefOid: prResponse.data.head.sha, + createdAt: prResponse.data.created_at, + additions: prResponse.data.additions || 0, + deletions: prResponse.data.deletions || 0, + state: prResponse.data.state.toUpperCase(), + commits: { totalCount: 0, nodes: [] }, + files: { nodes: [] }, + comments: { nodes: [] }, + reviews: { nodes: [] }, + }; + + // Fetch comments separately + try { + const commentsResponse = await client.api.listIssueComments( owner, repo, - pull_number: parseInt(prNumber), - }); - - contextData = { - title: prResponse.data.title, - body: prResponse.data.body || "", - author: { login: prResponse.data.user?.login || "" }, - baseRefName: prResponse.data.base.ref, - headRefName: prResponse.data.head.ref, - headRefOid: prResponse.data.head.sha, - createdAt: prResponse.data.created_at, - additions: prResponse.data.additions || 0, - deletions: prResponse.data.deletions || 0, - state: prResponse.data.state.toUpperCase(), - commits: { totalCount: 0, nodes: [] }, - files: { nodes: [] }, - comments: { nodes: [] }, - reviews: { nodes: [] }, - }; - - // Fetch comments separately - try { - const commentsResponse = await octokits.rest.issues.listComments({ - owner, - repo, - issue_number: parseInt(prNumber), - }); - comments = commentsResponse.data.map((comment) => ({ - id: comment.id.toString(), - databaseId: comment.id.toString(), - body: comment.body || "", - author: { login: comment.user?.login || "" }, - createdAt: comment.created_at, - })); - } catch (error) { - console.warn("Failed to fetch PR comments:", error); - comments = []; // Ensure we have an empty array - } - - // Try to fetch files - try { - const filesResponse = await octokits.rest.pulls.listFiles({ - owner, - repo, - pull_number: parseInt(prNumber), - }); - changedFiles = filesResponse.data.map((file) => ({ - path: file.filename, - additions: file.additions || 0, - deletions: file.deletions || 0, - changeType: file.status || "modified", - })); - } catch (error) { - console.warn("Failed to fetch PR files:", error); - changedFiles = []; // Ensure we have an empty array - } - - reviewData = { nodes: [] }; // Simplified for Gitea - } else { - console.log( - `Fetching issue #${prNumber} data using REST API (Gitea mode)`, + parseInt(prNumber) ); - const issueResponse = await octokits.rest.issues.get({ - owner, - repo, - issue_number: parseInt(prNumber), - }); - - contextData = { - title: issueResponse.data.title, - body: issueResponse.data.body || "", - author: { login: issueResponse.data.user?.login || "" }, - createdAt: issueResponse.data.created_at, - state: issueResponse.data.state.toUpperCase(), - comments: { nodes: [] }, - }; - - // Fetch comments - try { - const commentsResponse = await octokits.rest.issues.listComments({ - owner, - repo, - issue_number: parseInt(prNumber), - }); - comments = commentsResponse.data.map((comment) => ({ - id: comment.id.toString(), - databaseId: comment.id.toString(), - body: comment.body || "", - author: { login: comment.user?.login || "" }, - createdAt: comment.created_at, - })); - } catch (error) { - console.warn("Failed to fetch issue comments:", error); - comments = []; // Ensure we have an empty array - } + comments = commentsResponse.data.map((comment: any) => ({ + id: comment.id.toString(), + databaseId: comment.id.toString(), + body: comment.body || "", + author: { login: comment.user?.login || "" }, + createdAt: comment.created_at, + })); + } catch (error) { + console.warn("Failed to fetch PR comments:", error); + comments = []; // Ensure we have an empty array } + + // Try to fetch files + try { + const filesResponse = await client.api.listPullRequestFiles( + owner, + repo, + parseInt(prNumber) + ); + changedFiles = filesResponse.data.map((file: any) => ({ + path: file.filename, + additions: file.additions || 0, + deletions: file.deletions || 0, + changeType: file.status || "modified", + })); + } catch (error) { + console.warn("Failed to fetch PR files:", error); + changedFiles = []; // Ensure we have an empty array + } + + reviewData = { nodes: [] }; // Simplified for Gitea } else { - // Use GraphQL for GitHub - if (isPR) { - const prResult = await octokits.graphql( - PR_QUERY, - { - owner, - repo, - number: parseInt(prNumber), - }, + console.log(`Fetching issue #${prNumber} data using REST API`); + const issueResponse = await client.api.getIssue(owner, repo, parseInt(prNumber)); + + contextData = { + title: issueResponse.data.title, + body: issueResponse.data.body || "", + author: { login: issueResponse.data.user?.login || "" }, + createdAt: issueResponse.data.created_at, + state: issueResponse.data.state.toUpperCase(), + comments: { nodes: [] }, + }; + + // Fetch comments + try { + const commentsResponse = await client.api.listIssueComments( + owner, + repo, + parseInt(prNumber) ); - - if (prResult.repository.pullRequest) { - const pullRequest = prResult.repository.pullRequest; - contextData = pullRequest; - changedFiles = pullRequest.files.nodes || []; - comments = pullRequest.comments?.nodes || []; - reviewData = pullRequest.reviews || []; - - console.log(`Successfully fetched PR #${prNumber} data`); - } else { - throw new Error(`PR #${prNumber} not found`); - } - } else { - const issueResult = await octokits.graphql( - ISSUE_QUERY, - { - owner, - repo, - number: parseInt(prNumber), - }, - ); - - if (issueResult.repository.issue) { - contextData = issueResult.repository.issue; - comments = contextData?.comments?.nodes || []; - - console.log(`Successfully fetched issue #${prNumber} data`); - } else { - throw new Error(`Issue #${prNumber} not found`); - } + comments = commentsResponse.data.map((comment: any) => ({ + id: comment.id.toString(), + databaseId: comment.id.toString(), + body: comment.body || "", + author: { login: comment.user?.login || "" }, + createdAt: comment.created_at, + })); + } catch (error) { + console.warn("Failed to fetch issue comments:", error); + comments = []; // Ensure we have an empty array } } } catch (error) { @@ -208,6 +144,7 @@ export async function fetchGitHubData({ throw new Error(`Failed to fetch ${isPR ? "PR" : "issue"} data`); } + // Compute SHAs for changed files let changedFilesWithSHA: GitHubFileWithSHA[] = []; if (isPR && changedFiles.length > 0) { @@ -288,7 +225,7 @@ export async function fetchGitHubData({ ]; const imageUrlMap = await downloadCommentImages( - octokits, + client, owner, repo, allComments, diff --git a/src/github/operations/branch-cleanup.ts b/src/github/operations/branch-cleanup.ts index 69e36a5..ec05e58 100644 --- a/src/github/operations/branch-cleanup.ts +++ b/src/github/operations/branch-cleanup.ts @@ -1,8 +1,8 @@ -import type { Octokits } from "../api/client"; +import type { GitHubClient } from "../api/client"; import { GITHUB_SERVER_URL } from "../api/config"; export async function checkAndDeleteEmptyBranch( - octokit: Octokits, + client: GitHubClient, owner: string, repo: string, claudeBranch: string | undefined, @@ -12,155 +12,57 @@ export async function checkAndDeleteEmptyBranch( let shouldDeleteBranch = false; if (claudeBranch) { - // Check if we're in a Gitea environment - const isGitea = - process.env.GITHUB_API_URL && - !process.env.GITHUB_API_URL.includes("api.github.com"); + // Use direct SHA comparison for both GitHub and Gitea + console.log("Using SHA comparison for branch check"); - if (isGitea) { - // Gitea doesn't support the /compare endpoint, use direct SHA comparison - console.log( - "Detected Gitea environment, using SHA comparison for branch check", - ); + try { + // Get the branch info to see if it exists and has commits + const branchResponse = await client.api.getBranch(owner, repo, claudeBranch); - try { - // Get the branch info to see if it exists and has commits - const branchResponse = await octokit.rest.repos.getBranch({ - owner, - repo, - branch: claudeBranch, - }); + // Get base branch info for comparison + const baseResponse = await client.api.getBranch(owner, repo, baseBranch); - // Get base branch info for comparison - const baseResponse = await octokit.rest.repos.getBranch({ - owner, - repo, - branch: baseBranch, - }); + const branchSha = branchResponse.data.commit.sha; + const baseSha = baseResponse.data.commit.sha; - const branchSha = branchResponse.data.commit.sha; - const baseSha = baseResponse.data.commit.sha; - - // If SHAs are different, assume there are commits - if (branchSha !== baseSha) { - console.log( - `Branch ${claudeBranch} appears to have commits (different SHA from base)`, - ); - const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; - branchLink = `\n[View branch](${branchUrl})`; - } else { - console.log( - `Branch ${claudeBranch} has same SHA as base, marking for deletion`, - ); - shouldDeleteBranch = true; - } - } catch (error: any) { - console.error("Error checking branch in Gitea:", error); - - // Handle 404 specifically - branch doesn't exist - if (error.status === 404) { - console.log( - `Branch ${claudeBranch} does not exist yet - this is normal during workflow`, - ); - // Don't add branch link since branch doesn't exist - branchLink = ""; - } else { - // For other errors, assume the branch has commits to be safe - console.log("Assuming branch exists due to non-404 error"); - const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; - branchLink = `\n[View branch](${branchUrl})`; - } + // If SHAs are different, assume there are commits + if (branchSha !== baseSha) { + console.log( + `Branch ${claudeBranch} appears to have commits (different SHA from base)`, + ); + const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; + branchLink = `\n[View branch](${branchUrl})`; + } else { + console.log( + `Branch ${claudeBranch} has same SHA as base, marking for deletion`, + ); + shouldDeleteBranch = true; } - } else { - // GitHub environment - use the comparison API - try { - const { data: comparison } = - await octokit.rest.repos.compareCommitsWithBasehead({ - owner, - repo, - basehead: `${baseBranch}...${claudeBranch}`, - }); + } catch (error: any) { + console.error("Error checking branch:", error); - // If there are no commits, mark branch for deletion - if (comparison.total_commits === 0) { - console.log( - `Branch ${claudeBranch} has no commits from Claude, will delete it`, - ); - shouldDeleteBranch = true; - } else { - // Only add branch link if there are commits - const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; - branchLink = `\n[View branch](${branchUrl})`; - } - } catch (error) { - console.error("Error checking for commits on Claude branch:", error); - - // Fallback to SHA comparison even on GitHub if API fails - try { - console.log( - "GitHub comparison API failed, falling back to SHA comparison", - ); - - const branchResponse = await octokit.rest.repos.getBranch({ - owner, - repo, - branch: claudeBranch, - }); - - const baseResponse = await octokit.rest.repos.getBranch({ - owner, - repo, - branch: baseBranch, - }); - - const branchSha = branchResponse.data.commit.sha; - const baseSha = baseResponse.data.commit.sha; - - if (branchSha !== baseSha) { - const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; - branchLink = `\n[View branch](${branchUrl})`; - } else { - shouldDeleteBranch = true; - } - } catch (fallbackError) { - console.error( - "Fallback branch comparison also failed:", - fallbackError, - ); - // If all checks fail, assume the branch has commits to be safe - const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; - branchLink = `\n[View branch](${branchUrl})`; - } + // Handle 404 specifically - branch doesn't exist + if (error.status === 404) { + console.log( + `Branch ${claudeBranch} does not exist yet - this is normal during workflow`, + ); + // Don't add branch link since branch doesn't exist + branchLink = ""; + } else { + // For other errors, assume the branch has commits to be safe + console.log("Assuming branch exists due to non-404 error"); + const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; + branchLink = `\n[View branch](${branchUrl})`; } } } // Delete the branch if it has no commits if (shouldDeleteBranch && claudeBranch) { - // Check if we're in a Gitea environment for deletion too - const isGitea = - process.env.GITHUB_API_URL && - !process.env.GITHUB_API_URL.includes("api.github.com"); - - if (isGitea) { - console.log( - `Skipping branch deletion for Gitea - not reliably supported: ${claudeBranch}`, - ); - // Don't attempt deletion in Gitea as it's not reliably supported - } else { - try { - await octokit.rest.git.deleteRef({ - owner, - repo, - ref: `heads/${claudeBranch}`, - }); - console.log(`✅ Deleted empty branch: ${claudeBranch}`); - } catch (deleteError: any) { - console.error(`Failed to delete branch ${claudeBranch}:`, deleteError); - console.log(`Delete error status: ${deleteError.status}`); - // Continue even if deletion fails - this is not critical - } - } + console.log( + `Skipping branch deletion - not reliably supported across all Git platforms: ${claudeBranch}`, + ); + // Skip deletion to avoid compatibility issues } return { shouldDeleteBranch, branchLink }; diff --git a/src/github/operations/branch.ts b/src/github/operations/branch.ts index 4cd2df1..1364ab0 100644 --- a/src/github/operations/branch.ts +++ b/src/github/operations/branch.ts @@ -10,7 +10,7 @@ import { $ } from "bun"; import * as core from "@actions/core"; import type { ParsedGitHubContext } from "../context"; import type { GitHubPullRequest } from "../types"; -import type { Octokits } from "../api/client"; +import type { GitHubClient } from "../api/client"; import type { FetchDataResult } from "../data/fetcher"; export type BranchInfo = { @@ -20,7 +20,7 @@ export type BranchInfo = { }; export async function setupBranch( - octokits: Octokits, + client: GitHubClient, githubData: FetchDataResult, context: ParsedGitHubContext, ): Promise { @@ -70,10 +70,7 @@ export async function setupBranch( sourceBranch = baseBranch; } else { // No base branch provided, fetch the default branch to use as source - const repoResponse = await octokits.rest.repos.get({ - owner, - repo, - }); + const repoResponse = await client.api.getRepo(owner, repo); sourceBranch = repoResponse.data.default_branch; } @@ -93,85 +90,29 @@ export async function setupBranch( const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`; try { - // Get the SHA of the source branch - // For Gitea, try using the branches endpoint instead of git/refs - let currentSHA: string; - + // Get the SHA of the source branch using Gitea's branches endpoint + console.log(`Getting branch info for: ${sourceBranch}`); + try { - // First try the GitHub-compatible git.getRef approach - const sourceBranchRef = await octokits.rest.git.getRef({ - owner, - repo, - ref: `heads/${sourceBranch}`, - }); - currentSHA = sourceBranchRef.data.object.sha; - } catch (gitRefError: any) { - // If git/refs fails (like in Gitea), use the branches endpoint - console.log( - `git/refs failed, trying branches endpoint: ${gitRefError.message}`, - ); - const branchResponse = await octokits.rest.repos.getBranch({ - owner, - repo, - branch: sourceBranch, - }); - // GitHub and Gitea both use commit.sha - currentSHA = branchResponse.data.commit.sha; + const branchResponse = await client.api.getBranch(owner, repo, sourceBranch); + const currentSHA = branchResponse.data.commit.sha; + console.log(`Current SHA: ${currentSHA}`); + } catch (branchError: any) { + console.log(`Failed to get branch info: ${branchError.message}`); } - console.log(`Current SHA: ${currentSHA}`); + // Create branch using Gitea's branch creation API + console.log(`Creating branch: ${newBranch} from: ${sourceBranch}`); - // Try to create branch using the appropriate method for each platform - const isGitea = - process.env.GITHUB_API_URL && - !process.env.GITHUB_API_URL.includes("api.github.com"); - - if (isGitea) { - // Gitea supports POST /repos/{owner}/{repo}/branches + try { + await client.api.createBranch(owner, repo, newBranch, sourceBranch); + console.log(`Successfully created branch via Gitea API: ${newBranch}`); + } catch (createBranchError: any) { + console.log(`Branch creation failed: ${createBranchError.message}`); + console.log(`Error status: ${createBranchError.status}`); console.log( - `Detected Gitea environment, using branches API for: ${newBranch}`, + `Branch ${newBranch} will be created when files are pushed via MCP server`, ); - - try { - // Use the raw Gitea API since Octokit might not have the createBranch method - await octokits.rest.request("POST /repos/{owner}/{repo}/branches", { - owner, - repo, - new_branch_name: newBranch, - old_branch_name: sourceBranch, - }); - console.log( - `Successfully created branch via Gitea branches API: ${newBranch}`, - ); - } catch (createBranchError: any) { - console.log( - `Gitea branch creation failed: ${createBranchError.message}`, - ); - console.log(`Error status: ${createBranchError.status}`); - console.log( - `Branch ${newBranch} will be created when files are pushed via MCP server`, - ); - } - } else { - // GitHub environment - use git.createRef - try { - await octokits.rest.git.createRef({ - owner, - repo, - ref: `refs/heads/${newBranch}`, - sha: currentSHA, - }); - - console.log( - `Successfully created branch via GitHub git.createRef: ${newBranch}`, - ); - } catch (createRefError: any) { - console.log(`GitHub git.createRef failed: ${createRefError.message}`); - console.log(`Error status: ${createRefError.status}`); - console.log( - `Branch ${newBranch} will be created when files are pushed`, - ); - } } console.log(`Branch setup completed for: ${newBranch}`); diff --git a/src/github/operations/comments/create-initial.ts b/src/github/operations/comments/create-initial.ts index c4c0449..7d2ef18 100644 --- a/src/github/operations/comments/create-initial.ts +++ b/src/github/operations/comments/create-initial.ts @@ -11,10 +11,10 @@ import { isPullRequestReviewCommentEvent, type ParsedGitHubContext, } from "../../context"; -import type { Octokit } from "@octokit/rest"; +import type { GiteaApiClient } from "../../api/gitea-client"; export async function createInitialComment( - octokit: Octokit, + api: GiteaApiClient, context: ParsedGitHubContext, ) { const { owner, repo } = context.repository; @@ -27,21 +27,12 @@ export async function createInitialComment( // Only use createReplyForReviewComment if it's a PR review comment AND we have a comment_id if (isPullRequestReviewCommentEvent(context)) { - response = await octokit.rest.pulls.createReplyForReviewComment({ - owner, - repo, - pull_number: context.entityNumber, - comment_id: context.payload.comment.id, + response = await api.customRequest("POST", `/api/v1/repos/${owner}/${repo}/pulls/${context.entityNumber}/comments/${context.payload.comment.id}/replies`, { body: initialBody, }); } else { // For all other cases (issues, issue comments, or missing comment_id) - response = await octokit.rest.issues.createComment({ - owner, - repo, - issue_number: context.entityNumber, - body: initialBody, - }); + response = await api.createIssueComment(owner, repo, context.entityNumber, initialBody); } // Output the comment ID for downstream steps using GITHUB_OUTPUT @@ -54,12 +45,7 @@ export async function createInitialComment( // Always fall back to regular issue comment if anything fails try { - const response = await octokit.rest.issues.createComment({ - owner, - repo, - issue_number: context.entityNumber, - body: initialBody, - }); + const response = await api.createIssueComment(owner, repo, context.entityNumber, initialBody); const githubOutput = process.env.GITHUB_OUTPUT!; appendFileSync(githubOutput, `claude_comment_id=${response.data.id}\n`); diff --git a/src/github/operations/comments/update-with-branch.ts b/src/github/operations/comments/update-with-branch.ts index 6709bac..af3ca16 100644 --- a/src/github/operations/comments/update-with-branch.ts +++ b/src/github/operations/comments/update-with-branch.ts @@ -10,14 +10,14 @@ import { createBranchLink, createCommentBody, } from "./common"; -import { type Octokits } from "../../api/client"; +import { type GitHubClient } from "../../api/client"; import { isPullRequestReviewCommentEvent, type ParsedGitHubContext, } from "../../context"; export async function updateTrackingComment( - octokit: Octokits, + client: GitHubClient, context: ParsedGitHubContext, commentId: number, branch?: string, @@ -38,21 +38,13 @@ export async function updateTrackingComment( try { if (isPullRequestReviewCommentEvent(context)) { // For PR review comments (inline comments), use the pulls API - await octokit.rest.pulls.updateReviewComment({ - owner, - repo, - comment_id: commentId, + await client.api.customRequest("PATCH", `/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`, { body: updatedBody, }); console.log(`✅ Updated PR review comment ${commentId} with branch link`); } else { // For all other comments, use the issues API - await octokit.rest.issues.updateComment({ - owner, - repo, - comment_id: commentId, - body: updatedBody, - }); + await client.api.updateIssueComment(owner, repo, commentId, updatedBody); console.log(`✅ Updated issue comment ${commentId} with branch link`); } } catch (error) { diff --git a/src/github/utils/image-downloader.ts b/src/github/utils/image-downloader.ts index f450fd1..a4a635c 100644 --- a/src/github/utils/image-downloader.ts +++ b/src/github/utils/image-downloader.ts @@ -1,12 +1,4 @@ -import fs from "fs/promises"; -import path from "path"; -import type { Octokits } from "../api/client"; -import { GITHUB_SERVER_URL } from "../api/config"; - -const IMAGE_REGEX = new RegExp( - `!\\[[^\\]]*\\]\\((${GITHUB_SERVER_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\/user-attachments\\/assets\\/[^)]+)\\)`, - "g", -); +import type { GitHubClient } from "../api/client"; type IssueComment = { type: "issue_comment"; @@ -47,254 +39,13 @@ export type CommentWithImages = | PullRequestBody; export async function downloadCommentImages( - octokits: Octokits, - owner: string, - repo: string, - comments: CommentWithImages[], + _client: GitHubClient, + _owner: string, + _repo: string, + _comments: CommentWithImages[], ): Promise> { - const urlToPathMap = new Map(); - const downloadsDir = "/tmp/github-images"; - - await fs.mkdir(downloadsDir, { recursive: true }); - - const commentsWithImages: Array<{ - comment: CommentWithImages; - urls: string[]; - }> = []; - - for (const comment of comments) { - const imageMatches = [...comment.body.matchAll(IMAGE_REGEX)]; - const urls = imageMatches.map((match) => match[1] as string); - - if (urls.length > 0) { - commentsWithImages.push({ comment, urls }); - const id = - comment.type === "issue_body" - ? comment.issueNumber - : comment.type === "pr_body" - ? comment.pullNumber - : comment.id; - console.log(`Found ${urls.length} image(s) in ${comment.type} ${id}`); - } - } - - // Process each comment with images - for (const { comment, urls } of commentsWithImages) { - try { - let bodyHtml: string | undefined; - - // Get the HTML version based on comment type - // Try with full+json mediaType first (GitHub), fallback to regular API (Gitea) - switch (comment.type) { - case "issue_comment": { - try { - const response = await octokits.rest.issues.getComment({ - owner, - repo, - comment_id: parseInt(comment.id), - mediaType: { - format: "full+json", - }, - }); - bodyHtml = response.data.body_html; - } catch (error: any) { - console.log( - "Full+json format not supported, trying regular API for issue comment", - ); - // Fallback for Gitea - use regular API without mediaType - const response = await octokits.rest.issues.getComment({ - owner, - repo, - comment_id: parseInt(comment.id), - }); - // Gitea might not have body_html, use body instead - bodyHtml = (response.data as any).body_html || response.data.body; - } - break; - } - case "review_comment": { - try { - const response = await octokits.rest.pulls.getReviewComment({ - owner, - repo, - comment_id: parseInt(comment.id), - mediaType: { - format: "full+json", - }, - }); - bodyHtml = response.data.body_html; - } catch (error: any) { - console.log( - "Full+json format not supported, trying regular API for review comment", - ); - // Fallback for Gitea - const response = await octokits.rest.pulls.getReviewComment({ - owner, - repo, - comment_id: parseInt(comment.id), - }); - bodyHtml = (response.data as any).body_html || response.data.body; - } - break; - } - case "review_body": { - try { - const response = await octokits.rest.pulls.getReview({ - owner, - repo, - pull_number: parseInt(comment.pullNumber), - review_id: parseInt(comment.id), - mediaType: { - format: "full+json", - }, - }); - bodyHtml = response.data.body_html; - } catch (error: any) { - console.log( - "Full+json format not supported, trying regular API for review", - ); - // Fallback for Gitea - const response = await octokits.rest.pulls.getReview({ - owner, - repo, - pull_number: parseInt(comment.pullNumber), - review_id: parseInt(comment.id), - }); - bodyHtml = (response.data as any).body_html || response.data.body; - } - break; - } - case "issue_body": { - try { - const response = await octokits.rest.issues.get({ - owner, - repo, - issue_number: parseInt(comment.issueNumber), - mediaType: { - format: "full+json", - }, - }); - bodyHtml = response.data.body_html; - } catch (error: any) { - console.log( - "Full+json format not supported, trying regular API for issue", - ); - // Fallback for Gitea - const response = await octokits.rest.issues.get({ - owner, - repo, - issue_number: parseInt(comment.issueNumber), - }); - bodyHtml = (response.data as any).body_html || response.data.body; - } - break; - } - case "pr_body": { - try { - const response = await octokits.rest.pulls.get({ - owner, - repo, - pull_number: parseInt(comment.pullNumber), - mediaType: { - format: "full+json", - }, - }); - // Type here seems to be wrong - bodyHtml = (response.data as any).body_html; - } catch (error: any) { - console.log( - "Full+json format not supported, trying regular API for PR", - ); - // Fallback for Gitea - const response = await octokits.rest.pulls.get({ - owner, - repo, - pull_number: parseInt(comment.pullNumber), - }); - bodyHtml = (response.data as any).body_html || response.data.body; - } - break; - } - } - if (!bodyHtml) { - const id = - comment.type === "issue_body" - ? comment.issueNumber - : comment.type === "pr_body" - ? comment.pullNumber - : comment.id; - console.warn(`No HTML body found for ${comment.type} ${id}`); - continue; - } - - // Extract signed URLs from HTML - const signedUrlRegex = - /https:\/\/private-user-images\.githubusercontent\.com\/[^"]+\?jwt=[^"]+/g; - const signedUrls = bodyHtml.match(signedUrlRegex) || []; - - // Download each image - for (let i = 0; i < Math.min(signedUrls.length, urls.length); i++) { - const signedUrl = signedUrls[i]; - const originalUrl = urls[i]; - - if (!signedUrl || !originalUrl) { - continue; - } - - // Check if we've already downloaded this URL - if (urlToPathMap.has(originalUrl)) { - continue; - } - - const fileExtension = getImageExtension(originalUrl); - const filename = `image-${Date.now()}-${i}${fileExtension}`; - const localPath = path.join(downloadsDir, filename); - - try { - console.log(`Downloading ${originalUrl}...`); - - const imageResponse = await fetch(signedUrl); - if (!imageResponse.ok) { - throw new Error( - `HTTP ${imageResponse.status}: ${imageResponse.statusText}`, - ); - } - - const arrayBuffer = await imageResponse.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - - await fs.writeFile(localPath, buffer); - console.log(`✓ Saved: ${localPath}`); - - urlToPathMap.set(originalUrl, localPath); - } catch (error) { - console.error(`✗ Failed to download ${originalUrl}:`, error); - } - } - } catch (error) { - const id = - comment.type === "issue_body" - ? comment.issueNumber - : comment.type === "pr_body" - ? comment.pullNumber - : comment.id; - console.error( - `Failed to process images for ${comment.type} ${id}:`, - error, - ); - } - } - - return urlToPathMap; -} - -function getImageExtension(url: string): string { - const urlParts = url.split("/"); - const filename = urlParts[urlParts.length - 1]; - if (!filename) { - throw new Error("Invalid URL: No filename found"); - } - - const match = filename.match(/\.(png|jpg|jpeg|gif|webp|svg)$/i); - return match ? match[0] : ".png"; + // Temporarily simplified - return empty map to avoid Octokit dependencies + // TODO: Implement image downloading with direct Gitea API calls if needed + console.log("Image downloading temporarily disabled during Octokit migration"); + return new Map(); } diff --git a/src/github/validation/actor.ts b/src/github/validation/actor.ts index e8e8375..787c513 100644 --- a/src/github/validation/actor.ts +++ b/src/github/validation/actor.ts @@ -5,11 +5,11 @@ * Prevents automated tools or bots from triggering Claude */ -import type { Octokit } from "@octokit/rest"; +import type { GiteaApiClient } from "../api/gitea-client"; import type { ParsedGitHubContext } from "../context"; export async function checkHumanActor( - octokit: Octokit, + api: GiteaApiClient, githubContext: ParsedGitHubContext, ) { // Check if we're in a Gitea environment @@ -26,9 +26,8 @@ export async function checkHumanActor( try { // Fetch user information from GitHub API - const { data: userData } = await octokit.users.getByUsername({ - username: githubContext.actor, - }); + const response = await api.customRequest("GET", `/api/v1/users/${githubContext.actor}`); + const userData = response.data; const actorType = userData.type; diff --git a/src/github/validation/permissions.ts b/src/github/validation/permissions.ts index ca9696b..b219736 100644 --- a/src/github/validation/permissions.ts +++ b/src/github/validation/permissions.ts @@ -1,15 +1,15 @@ import * as core from "@actions/core"; import type { ParsedGitHubContext } from "../context"; -import type { Octokit } from "@octokit/rest"; +import type { GiteaApiClient } from "../api/gitea-client"; /** * Check if the actor has write permissions to the repository - * @param octokit - The Octokit REST client + * @param api - The Gitea API client * @param context - The GitHub context * @returns true if the actor has write permissions, false otherwise */ export async function checkWritePermissions( - octokit: Octokit, + api: GiteaApiClient, context: ParsedGitHubContext, ): Promise { const { repository, actor } = context; @@ -28,11 +28,7 @@ export async function checkWritePermissions( core.info(`Checking permissions for actor: ${actor}`); // Check permissions directly using the permission endpoint - const response = await octokit.repos.getCollaboratorPermissionLevel({ - owner: repository.owner, - repo: repository.repo, - username: actor, - }); + const response = await api.customRequest("GET", `/api/v1/repos/${repository.owner}/${repository.repo}/collaborators/${actor}/permission`); const permissionLevel = response.data.permission; core.info(`Permission level retrieved: ${permissionLevel}`);