Attempt to make this work

This commit is contained in:
Mark Wylde
2025-05-30 21:47:12 +01:00
parent c77bb0e4b3
commit c0d1a3fc4c
13 changed files with 468 additions and 878 deletions

View File

@@ -14,8 +14,6 @@
"@actions/github": "^6.0.1", "@actions/github": "^6.0.1",
"@anthropic-ai/sdk": "^0.30.0", "@anthropic-ai/sdk": "^0.30.0",
"@modelcontextprotocol/sdk": "^1.11.0", "@modelcontextprotocol/sdk": "^1.11.0",
"@octokit/graphql": "^8.2.2",
"@octokit/rest": "^21.1.1",
"@octokit/webhooks-types": "^7.6.1", "@octokit/webhooks-types": "^7.6.1",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"zod": "^3.24.4" "zod": "^3.24.4"

View File

@@ -15,7 +15,7 @@ import { setupBranch } from "../github/operations/branch";
import { updateTrackingComment } from "../github/operations/comments/update-with-branch"; import { updateTrackingComment } from "../github/operations/comments/update-with-branch";
import { prepareMcpConfig } from "../mcp/install-mcp-server"; import { prepareMcpConfig } from "../mcp/install-mcp-server";
import { createPrompt } from "../create-prompt"; import { createPrompt } from "../create-prompt";
import { createOctokit } from "../github/api/client"; import { createClient } from "../github/api/client";
import { fetchGitHubData } from "../github/data/fetcher"; import { fetchGitHubData } from "../github/data/fetcher";
import { parseGitHubContext } from "../github/context"; import { parseGitHubContext } from "../github/context";
@@ -23,14 +23,14 @@ async function run() {
try { try {
// Step 1: Setup GitHub token // Step 1: Setup GitHub token
const githubToken = await setupGitHubToken(); const githubToken = await setupGitHubToken();
const octokit = createOctokit(githubToken); const client = createClient(githubToken);
// Step 2: Parse GitHub context (once for all operations) // Step 2: Parse GitHub context (once for all operations)
const context = parseGitHubContext(); const context = parseGitHubContext();
// Step 3: Check write permissions // Step 3: Check write permissions
const hasWritePermissions = await checkWritePermissions( const hasWritePermissions = await checkWritePermissions(
octokit.rest, client.api,
context, context,
); );
if (!hasWritePermissions) { if (!hasWritePermissions) {
@@ -52,22 +52,22 @@ async function run() {
} }
// Step 5: Check if actor is human // Step 5: Check if actor is human
await checkHumanActor(octokit.rest, context); await checkHumanActor(client.api, context);
// Step 6: Create initial tracking comment // 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()); core.setOutput("claude_comment_id", commentId.toString());
// Step 7: Fetch GitHub data (once for both branch setup and prompt creation) // Step 7: Fetch GitHub data (once for both branch setup and prompt creation)
const githubData = await fetchGitHubData({ const githubData = await fetchGitHubData({
octokits: octokit, client: client,
repository: `${context.repository.owner}/${context.repository.repo}`, repository: `${context.repository.owner}/${context.repository.repo}`,
prNumber: context.entityNumber.toString(), prNumber: context.entityNumber.toString(),
isPR: context.isPR, isPR: context.isPR,
}); });
// Step 8: Setup branch // Step 8: Setup branch
const branchInfo = await setupBranch(octokit, githubData, context); const branchInfo = await setupBranch(client, githubData, context);
core.setOutput("BASE_BRANCH", branchInfo.baseBranch); core.setOutput("BASE_BRANCH", branchInfo.baseBranch);
if (branchInfo.claudeBranch) { if (branchInfo.claudeBranch) {
core.setOutput("CLAUDE_BRANCH", 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) // Step 9: Update initial comment with branch link (only for issues that created a new branch)
if (branchInfo.claudeBranch) { if (branchInfo.claudeBranch) {
await updateTrackingComment( await updateTrackingComment(
octokit, client,
context, context,
commentId, commentId,
branchInfo.claudeBranch, branchInfo.claudeBranch,

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { createOctokit } from "../github/api/client"; import { createClient } from "../github/api/client";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import { import {
updateCommentBody, updateCommentBody,
@@ -23,7 +23,7 @@ async function run() {
const context = parseGitHubContext(); const context = parseGitHubContext();
const { owner, repo } = context.repository; const { owner, repo } = context.repository;
const octokit = createOctokit(githubToken); const client = createClient(githubToken);
const serverUrl = GITHUB_SERVER_URL; const serverUrl = GITHUB_SERVER_URL;
const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`;
@@ -37,12 +37,8 @@ async function run() {
if (isPullRequestReviewCommentEvent(context)) { if (isPullRequestReviewCommentEvent(context)) {
// For PR review comments, use the pulls API // For PR review comments, use the pulls API
console.log(`Fetching PR review comment ${commentId}`); console.log(`Fetching PR review comment ${commentId}`);
const { data: prComment } = await octokit.rest.pulls.getReviewComment({ const response = await client.api.customRequest("GET", `/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`);
owner, comment = response.data;
repo,
comment_id: commentId,
});
comment = prComment;
isPRReviewComment = true; isPRReviewComment = true;
console.log("Successfully fetched as PR review comment"); console.log("Successfully fetched as PR review comment");
} }
@@ -50,12 +46,8 @@ async function run() {
// For all other event types, use the issues API // For all other event types, use the issues API
if (!comment) { if (!comment) {
console.log(`Fetching issue comment ${commentId}`); console.log(`Fetching issue comment ${commentId}`);
const { data: issueComment } = await octokit.rest.issues.getComment({ const response = await client.api.customRequest("GET", `/api/v1/repos/${owner}/${repo}/issues/comments/${commentId}`);
owner, comment = response.data;
repo,
comment_id: commentId,
});
comment = issueComment;
isPRReviewComment = false; isPRReviewComment = false;
console.log("Successfully fetched as issue comment"); 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 to get the PR info to understand the comment structure
try { try {
const { data: pr } = await octokit.rest.pulls.get({ const pr = await client.api.getPullRequest(owner, repo, context.entityNumber);
owner, console.log(`PR state: ${pr.data.state}`);
repo, console.log(`PR comments count: ${pr.data.comments}`);
pull_number: context.entityNumber, console.log(`PR review comments count: ${pr.data.review_comments}`);
});
console.log(`PR state: ${pr.state}`);
console.log(`PR comments count: ${pr.comments}`);
console.log(`PR review comments count: ${pr.review_comments}`);
} catch { } catch {
console.error("Could not fetch PR info for debugging"); 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 // Check if we need to add branch link for new branches
const { shouldDeleteBranch, branchLink } = await checkAndDeleteEmptyBranch( const { shouldDeleteBranch, branchLink } = await checkAndDeleteEmptyBranch(
octokit, client,
owner, owner,
repo, repo,
claudeBranch, claudeBranch,
@@ -107,158 +95,60 @@ async function run() {
const containsPRUrl = currentBody.match(prUrlPattern); const containsPRUrl = currentBody.match(prUrlPattern);
if (!containsPRUrl) { if (!containsPRUrl) {
// Check if we're in a Gitea environment // Use direct SHA comparison for all Git platforms
const isGitea = console.log("Using SHA comparison for PR link check");
process.env.GITHUB_API_URL &&
!process.env.GITHUB_API_URL.includes("api.github.com");
if (isGitea) { try {
// Gitea doesn't support the /compare endpoint, use direct SHA comparison // Get the branch info to see if it exists and has commits
console.log( const branchResponse = await client.api.getBranch(owner, repo, claudeBranch);
"Detected Gitea environment, using SHA comparison for PR link check",
);
try { // Get base branch info for comparison
// Get the branch info to see if it exists and has commits const baseResponse = await client.api.getBranch(owner, repo, baseBranch);
const branchResponse = await octokit.rest.repos.getBranch({
owner,
repo,
branch: claudeBranch,
});
// Get base branch info for comparison const branchSha = branchResponse.data.commit.sha;
const baseResponse = await octokit.rest.repos.getBranch({ const baseSha = baseResponse.data.commit.sha;
owner,
repo,
branch: baseBranch,
});
const branchSha = branchResponse.data.commit.sha; // If SHAs are different, assume there are changes and add PR link
const baseSha = baseResponse.data.commit.sha; if (branchSha !== baseSha) {
console.log(
// If SHAs are different, assume there are changes and add PR link `Branch ${claudeBranch} appears to have changes (different SHA from base)`,
if (branchSha !== baseSha) { );
console.log( const entityType = context.isPR ? "PR" : "Issue";
`Branch ${claudeBranch} appears to have changes (different SHA from base)`, const prTitle = encodeURIComponent(
); `${entityType} #${context.entityNumber}: Changes from Claude`,
const entityType = context.isPR ? "PR" : "Issue"; );
const prTitle = encodeURIComponent( const prBody = encodeURIComponent(
`${entityType} #${context.entityNumber}: Changes from Claude`, `This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
); );
const prBody = encodeURIComponent( const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`, prLink = `\n[Create a PR](${prUrl})`;
); } else {
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`; console.log(
prLink = `\n[Create a PR](${prUrl})`; `Branch ${claudeBranch} has same SHA as base, no PR link needed`,
} 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})`;
}
} }
} else { } catch (error: any) {
// GitHub environment - use the comparison API console.error("Error checking branch:", error);
try {
const { data: comparison } =
await octokit.rest.repos.compareCommitsWithBasehead({
owner,
repo,
basehead: `${baseBranch}...${claudeBranch}`,
});
// If there are changes (commits or file changes), add the PR URL // Handle 404 specifically - branch doesn't exist
if ( if (error.status === 404) {
comparison.total_commits > 0 || console.log(
(comparison.files && comparison.files.length > 0) `Branch ${claudeBranch} does not exist yet - no PR link needed`,
) { );
const entityType = context.isPR ? "PR" : "Issue"; // Don't add PR link since branch doesn't exist
const prTitle = encodeURIComponent( prLink = "";
`${entityType} #${context.entityNumber}: Changes from Claude`, } else {
); // For other errors, add PR link to be safe
const prBody = encodeURIComponent( console.log("Adding PR link as fallback due to non-404 error");
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`, const entityType = context.isPR ? "PR" : "Issue";
); const prTitle = encodeURIComponent(
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`; `${entityType} #${context.entityNumber}: Changes from Claude`,
prLink = `\n[Create a PR](${prUrl})`; );
} const prBody = encodeURIComponent(
} catch (error) { `This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
console.error("Error checking for changes in branch:", error); );
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
// Fallback to SHA comparison even on GitHub if API fails prLink = `\n[Create a PR](${prUrl})`;
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})`;
}
} }
} }
} }
@@ -333,19 +223,11 @@ async function run() {
// Update the comment using the appropriate API // Update the comment using the appropriate API
try { try {
if (isPRReviewComment) { if (isPRReviewComment) {
await octokit.rest.pulls.updateReviewComment({ await client.api.customRequest("PATCH", `/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`, {
owner,
repo,
comment_id: commentId,
body: updatedBody, body: updatedBody,
}); });
} else { } else {
await octokit.rest.issues.updateComment({ await client.api.updateIssueComment(owner, repo, commentId, updatedBody);
owner,
repo,
comment_id: commentId,
body: updatedBody,
});
} }
console.log( console.log(
`✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} with job link`, `✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} with job link`,

View File

@@ -1,23 +1,11 @@
import { Octokit } from "@octokit/rest"; import { GiteaApiClient, createGiteaClient } from "./gitea-client";
import { graphql } from "@octokit/graphql";
import { GITHUB_API_URL } from "./config";
export type Octokits = { export type GitHubClient = {
rest: Octokit; api: GiteaApiClient;
graphql: typeof graphql;
}; };
export function createOctokit(token: string): Octokits { export function createClient(token: string): GitHubClient {
return { return {
rest: new Octokit({ api: createGiteaClient(token),
auth: token,
baseUrl: GITHUB_API_URL,
}),
graphql: graphql.defaults({
baseUrl: GITHUB_API_URL,
headers: {
authorization: `token ${token}`,
},
}),
}; };
} }

View File

@@ -0,0 +1,218 @@
import fetch from "node-fetch";
import { GITHUB_API_URL } from "./config";
export interface GiteaApiResponse<T = any> {
status: number;
data: T;
headers: Record<string, string>;
}
export interface GiteaApiError extends Error {
status: number;
response?: {
data: any;
status: number;
headers: Record<string, string>;
};
}
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<T = any>(
method: string,
endpoint: string,
body?: any
): Promise<GiteaApiResponse<T>> {
const url = `${this.baseUrl}${endpoint}`;
const headers: Record<string, string> = {
"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<T = any>(method: string, endpoint: string, body?: any): Promise<GiteaApiResponse<T>> {
return this.request<T>(method, endpoint, body);
}
}
export function createGiteaClient(token: string): GiteaApiClient {
return new GiteaApiClient(token);
}

View File

@@ -5,16 +5,13 @@ import type {
GitHubComment, GitHubComment,
GitHubFile, GitHubFile,
GitHubReview, GitHubReview,
PullRequestQueryResponse,
IssueQueryResponse,
} from "../types"; } from "../types";
import { PR_QUERY, ISSUE_QUERY } from "../api/queries/github"; import type { GitHubClient } from "../api/client";
import type { Octokits } from "../api/client";
import { downloadCommentImages } from "../utils/image-downloader"; import { downloadCommentImages } from "../utils/image-downloader";
import type { CommentWithImages } from "../utils/image-downloader"; import type { CommentWithImages } from "../utils/image-downloader";
type FetchDataParams = { type FetchDataParams = {
octokits: Octokits; client: GitHubClient;
repository: string; repository: string;
prNumber: string; prNumber: string;
isPR: boolean; isPR: boolean;
@@ -34,7 +31,7 @@ export type FetchDataResult = {
}; };
export async function fetchGitHubData({ export async function fetchGitHubData({
octokits, client,
repository, repository,
prNumber, prNumber,
isPR, isPR,
@@ -44,163 +41,102 @@ export async function fetchGitHubData({
throw new Error("Invalid repository format. Expected 'owner/repo'."); 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 contextData: GitHubPullRequest | GitHubIssue | null = null;
let comments: GitHubComment[] = []; let comments: GitHubComment[] = [];
let changedFiles: GitHubFile[] = []; let changedFiles: GitHubFile[] = [];
let reviewData: { nodes: GitHubReview[] } | null = null; let reviewData: { nodes: GitHubReview[] } | null = null;
try { try {
if (isGitea) { // Use REST API for all requests (works with both GitHub and Gitea)
// Use REST API for Gitea compatibility if (isPR) {
if (isPR) { console.log(`Fetching PR #${prNumber} data using REST API`);
console.log( const prResponse = await client.api.getPullRequest(owner, repo, parseInt(prNumber));
`Fetching PR #${prNumber} data using REST API (Gitea mode)`,
); contextData = {
const prResponse = await octokits.rest.pulls.get({ 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, owner,
repo, repo,
pull_number: parseInt(prNumber), 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)`,
); );
const issueResponse = await octokits.rest.issues.get({ comments = commentsResponse.data.map((comment: any) => ({
owner, id: comment.id.toString(),
repo, databaseId: comment.id.toString(),
issue_number: parseInt(prNumber), body: comment.body || "",
}); author: { login: comment.user?.login || "" },
createdAt: comment.created_at,
contextData = { }));
title: issueResponse.data.title, } catch (error) {
body: issueResponse.data.body || "", console.warn("Failed to fetch PR comments:", error);
author: { login: issueResponse.data.user?.login || "" }, comments = []; // Ensure we have an empty array
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
}
} }
// 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 { } else {
// Use GraphQL for GitHub console.log(`Fetching issue #${prNumber} data using REST API`);
if (isPR) { const issueResponse = await client.api.getIssue(owner, repo, parseInt(prNumber));
const prResult = await octokits.graphql<PullRequestQueryResponse>(
PR_QUERY, contextData = {
{ title: issueResponse.data.title,
owner, body: issueResponse.data.body || "",
repo, author: { login: issueResponse.data.user?.login || "" },
number: parseInt(prNumber), 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)
); );
comments = commentsResponse.data.map((comment: any) => ({
if (prResult.repository.pullRequest) { id: comment.id.toString(),
const pullRequest = prResult.repository.pullRequest; databaseId: comment.id.toString(),
contextData = pullRequest; body: comment.body || "",
changedFiles = pullRequest.files.nodes || []; author: { login: comment.user?.login || "" },
comments = pullRequest.comments?.nodes || []; createdAt: comment.created_at,
reviewData = pullRequest.reviews || []; }));
} catch (error) {
console.log(`Successfully fetched PR #${prNumber} data`); console.warn("Failed to fetch issue comments:", error);
} else { comments = []; // Ensure we have an empty array
throw new Error(`PR #${prNumber} not found`);
}
} else {
const issueResult = await octokits.graphql<IssueQueryResponse>(
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`);
}
} }
} }
} catch (error) { } catch (error) {
@@ -208,6 +144,7 @@ export async function fetchGitHubData({
throw new Error(`Failed to fetch ${isPR ? "PR" : "issue"} data`); throw new Error(`Failed to fetch ${isPR ? "PR" : "issue"} data`);
} }
// Compute SHAs for changed files // Compute SHAs for changed files
let changedFilesWithSHA: GitHubFileWithSHA[] = []; let changedFilesWithSHA: GitHubFileWithSHA[] = [];
if (isPR && changedFiles.length > 0) { if (isPR && changedFiles.length > 0) {
@@ -288,7 +225,7 @@ export async function fetchGitHubData({
]; ];
const imageUrlMap = await downloadCommentImages( const imageUrlMap = await downloadCommentImages(
octokits, client,
owner, owner,
repo, repo,
allComments, allComments,

View File

@@ -1,8 +1,8 @@
import type { Octokits } from "../api/client"; import type { GitHubClient } from "../api/client";
import { GITHUB_SERVER_URL } from "../api/config"; import { GITHUB_SERVER_URL } from "../api/config";
export async function checkAndDeleteEmptyBranch( export async function checkAndDeleteEmptyBranch(
octokit: Octokits, client: GitHubClient,
owner: string, owner: string,
repo: string, repo: string,
claudeBranch: string | undefined, claudeBranch: string | undefined,
@@ -12,155 +12,57 @@ export async function checkAndDeleteEmptyBranch(
let shouldDeleteBranch = false; let shouldDeleteBranch = false;
if (claudeBranch) { if (claudeBranch) {
// Check if we're in a Gitea environment // Use direct SHA comparison for both GitHub and Gitea
const isGitea = console.log("Using SHA comparison for branch check");
process.env.GITHUB_API_URL &&
!process.env.GITHUB_API_URL.includes("api.github.com");
if (isGitea) { try {
// Gitea doesn't support the /compare endpoint, use direct SHA comparison // Get the branch info to see if it exists and has commits
console.log( const branchResponse = await client.api.getBranch(owner, repo, claudeBranch);
"Detected Gitea environment, using SHA comparison for branch check",
);
try { // Get base branch info for comparison
// Get the branch info to see if it exists and has commits const baseResponse = await client.api.getBranch(owner, repo, baseBranch);
const branchResponse = await octokit.rest.repos.getBranch({
owner,
repo,
branch: claudeBranch,
});
// Get base branch info for comparison const branchSha = branchResponse.data.commit.sha;
const baseResponse = await octokit.rest.repos.getBranch({ const baseSha = baseResponse.data.commit.sha;
owner,
repo,
branch: baseBranch,
});
const branchSha = branchResponse.data.commit.sha; // If SHAs are different, assume there are commits
const baseSha = baseResponse.data.commit.sha; if (branchSha !== baseSha) {
console.log(
// If SHAs are different, assume there are commits `Branch ${claudeBranch} appears to have commits (different SHA from base)`,
if (branchSha !== baseSha) { );
console.log( const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
`Branch ${claudeBranch} appears to have commits (different SHA from base)`, branchLink = `\n[View branch](${branchUrl})`;
); } else {
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; console.log(
branchLink = `\n[View branch](${branchUrl})`; `Branch ${claudeBranch} has same SHA as base, marking for deletion`,
} else { );
console.log( shouldDeleteBranch = true;
`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})`;
}
} }
} else { } catch (error: any) {
// GitHub environment - use the comparison API console.error("Error checking branch:", error);
try {
const { data: comparison } =
await octokit.rest.repos.compareCommitsWithBasehead({
owner,
repo,
basehead: `${baseBranch}...${claudeBranch}`,
});
// If there are no commits, mark branch for deletion // Handle 404 specifically - branch doesn't exist
if (comparison.total_commits === 0) { if (error.status === 404) {
console.log( console.log(
`Branch ${claudeBranch} has no commits from Claude, will delete it`, `Branch ${claudeBranch} does not exist yet - this is normal during workflow`,
); );
shouldDeleteBranch = true; // Don't add branch link since branch doesn't exist
} else { branchLink = "";
// Only add branch link if there are commits } else {
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; // For other errors, assume the branch has commits to be safe
branchLink = `\n[View branch](${branchUrl})`; console.log("Assuming branch exists due to non-404 error");
} const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
} catch (error) { branchLink = `\n[View branch](${branchUrl})`;
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})`;
}
} }
} }
} }
// Delete the branch if it has no commits // Delete the branch if it has no commits
if (shouldDeleteBranch && claudeBranch) { if (shouldDeleteBranch && claudeBranch) {
// Check if we're in a Gitea environment for deletion too console.log(
const isGitea = `Skipping branch deletion - not reliably supported across all Git platforms: ${claudeBranch}`,
process.env.GITHUB_API_URL && );
!process.env.GITHUB_API_URL.includes("api.github.com"); // Skip deletion to avoid compatibility issues
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
}
}
} }
return { shouldDeleteBranch, branchLink }; return { shouldDeleteBranch, branchLink };

View File

@@ -10,7 +10,7 @@ import { $ } from "bun";
import * as core from "@actions/core"; import * as core from "@actions/core";
import type { ParsedGitHubContext } from "../context"; import type { ParsedGitHubContext } from "../context";
import type { GitHubPullRequest } from "../types"; import type { GitHubPullRequest } from "../types";
import type { Octokits } from "../api/client"; import type { GitHubClient } from "../api/client";
import type { FetchDataResult } from "../data/fetcher"; import type { FetchDataResult } from "../data/fetcher";
export type BranchInfo = { export type BranchInfo = {
@@ -20,7 +20,7 @@ export type BranchInfo = {
}; };
export async function setupBranch( export async function setupBranch(
octokits: Octokits, client: GitHubClient,
githubData: FetchDataResult, githubData: FetchDataResult,
context: ParsedGitHubContext, context: ParsedGitHubContext,
): Promise<BranchInfo> { ): Promise<BranchInfo> {
@@ -70,10 +70,7 @@ export async function setupBranch(
sourceBranch = baseBranch; sourceBranch = baseBranch;
} else { } else {
// No base branch provided, fetch the default branch to use as source // No base branch provided, fetch the default branch to use as source
const repoResponse = await octokits.rest.repos.get({ const repoResponse = await client.api.getRepo(owner, repo);
owner,
repo,
});
sourceBranch = repoResponse.data.default_branch; sourceBranch = repoResponse.data.default_branch;
} }
@@ -93,85 +90,29 @@ export async function setupBranch(
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`; const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
try { try {
// Get the SHA of the source branch // Get the SHA of the source branch using Gitea's branches endpoint
// For Gitea, try using the branches endpoint instead of git/refs console.log(`Getting branch info for: ${sourceBranch}`);
let currentSHA: string;
try { try {
// First try the GitHub-compatible git.getRef approach const branchResponse = await client.api.getBranch(owner, repo, sourceBranch);
const sourceBranchRef = await octokits.rest.git.getRef({ const currentSHA = branchResponse.data.commit.sha;
owner, console.log(`Current SHA: ${currentSHA}`);
repo, } catch (branchError: any) {
ref: `heads/${sourceBranch}`, console.log(`Failed to get branch info: ${branchError.message}`);
});
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;
} }
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 try {
const isGitea = await client.api.createBranch(owner, repo, newBranch, sourceBranch);
process.env.GITHUB_API_URL && console.log(`Successfully created branch via Gitea API: ${newBranch}`);
!process.env.GITHUB_API_URL.includes("api.github.com"); } catch (createBranchError: any) {
console.log(`Branch creation failed: ${createBranchError.message}`);
if (isGitea) { console.log(`Error status: ${createBranchError.status}`);
// Gitea supports POST /repos/{owner}/{repo}/branches
console.log( 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}`); console.log(`Branch setup completed for: ${newBranch}`);

View File

@@ -11,10 +11,10 @@ import {
isPullRequestReviewCommentEvent, isPullRequestReviewCommentEvent,
type ParsedGitHubContext, type ParsedGitHubContext,
} from "../../context"; } from "../../context";
import type { Octokit } from "@octokit/rest"; import type { GiteaApiClient } from "../../api/gitea-client";
export async function createInitialComment( export async function createInitialComment(
octokit: Octokit, api: GiteaApiClient,
context: ParsedGitHubContext, context: ParsedGitHubContext,
) { ) {
const { owner, repo } = context.repository; 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 // Only use createReplyForReviewComment if it's a PR review comment AND we have a comment_id
if (isPullRequestReviewCommentEvent(context)) { if (isPullRequestReviewCommentEvent(context)) {
response = await octokit.rest.pulls.createReplyForReviewComment({ response = await api.customRequest("POST", `/api/v1/repos/${owner}/${repo}/pulls/${context.entityNumber}/comments/${context.payload.comment.id}/replies`, {
owner,
repo,
pull_number: context.entityNumber,
comment_id: context.payload.comment.id,
body: initialBody, body: initialBody,
}); });
} else { } else {
// For all other cases (issues, issue comments, or missing comment_id) // For all other cases (issues, issue comments, or missing comment_id)
response = await octokit.rest.issues.createComment({ response = await api.createIssueComment(owner, repo, context.entityNumber, initialBody);
owner,
repo,
issue_number: context.entityNumber,
body: initialBody,
});
} }
// Output the comment ID for downstream steps using GITHUB_OUTPUT // 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 // Always fall back to regular issue comment if anything fails
try { try {
const response = await octokit.rest.issues.createComment({ const response = await api.createIssueComment(owner, repo, context.entityNumber, initialBody);
owner,
repo,
issue_number: context.entityNumber,
body: initialBody,
});
const githubOutput = process.env.GITHUB_OUTPUT!; const githubOutput = process.env.GITHUB_OUTPUT!;
appendFileSync(githubOutput, `claude_comment_id=${response.data.id}\n`); appendFileSync(githubOutput, `claude_comment_id=${response.data.id}\n`);

View File

@@ -10,14 +10,14 @@ import {
createBranchLink, createBranchLink,
createCommentBody, createCommentBody,
} from "./common"; } from "./common";
import { type Octokits } from "../../api/client"; import { type GitHubClient } from "../../api/client";
import { import {
isPullRequestReviewCommentEvent, isPullRequestReviewCommentEvent,
type ParsedGitHubContext, type ParsedGitHubContext,
} from "../../context"; } from "../../context";
export async function updateTrackingComment( export async function updateTrackingComment(
octokit: Octokits, client: GitHubClient,
context: ParsedGitHubContext, context: ParsedGitHubContext,
commentId: number, commentId: number,
branch?: string, branch?: string,
@@ -38,21 +38,13 @@ export async function updateTrackingComment(
try { try {
if (isPullRequestReviewCommentEvent(context)) { if (isPullRequestReviewCommentEvent(context)) {
// For PR review comments (inline comments), use the pulls API // For PR review comments (inline comments), use the pulls API
await octokit.rest.pulls.updateReviewComment({ await client.api.customRequest("PATCH", `/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`, {
owner,
repo,
comment_id: commentId,
body: updatedBody, body: updatedBody,
}); });
console.log(`✅ Updated PR review comment ${commentId} with branch link`); console.log(`✅ Updated PR review comment ${commentId} with branch link`);
} else { } else {
// For all other comments, use the issues API // For all other comments, use the issues API
await octokit.rest.issues.updateComment({ await client.api.updateIssueComment(owner, repo, commentId, updatedBody);
owner,
repo,
comment_id: commentId,
body: updatedBody,
});
console.log(`✅ Updated issue comment ${commentId} with branch link`); console.log(`✅ Updated issue comment ${commentId} with branch link`);
} }
} catch (error) { } catch (error) {

View File

@@ -1,12 +1,4 @@
import fs from "fs/promises"; import type { GitHubClient } from "../api/client";
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",
);
type IssueComment = { type IssueComment = {
type: "issue_comment"; type: "issue_comment";
@@ -47,254 +39,13 @@ export type CommentWithImages =
| PullRequestBody; | PullRequestBody;
export async function downloadCommentImages( export async function downloadCommentImages(
octokits: Octokits, _client: GitHubClient,
owner: string, _owner: string,
repo: string, _repo: string,
comments: CommentWithImages[], _comments: CommentWithImages[],
): Promise<Map<string, string>> { ): Promise<Map<string, string>> {
const urlToPathMap = new Map<string, string>(); // Temporarily simplified - return empty map to avoid Octokit dependencies
const downloadsDir = "/tmp/github-images"; // TODO: Implement image downloading with direct Gitea API calls if needed
console.log("Image downloading temporarily disabled during Octokit migration");
await fs.mkdir(downloadsDir, { recursive: true }); return new Map<string, string>();
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";
} }

View File

@@ -5,11 +5,11 @@
* Prevents automated tools or bots from triggering Claude * 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"; import type { ParsedGitHubContext } from "../context";
export async function checkHumanActor( export async function checkHumanActor(
octokit: Octokit, api: GiteaApiClient,
githubContext: ParsedGitHubContext, githubContext: ParsedGitHubContext,
) { ) {
// Check if we're in a Gitea environment // Check if we're in a Gitea environment
@@ -26,9 +26,8 @@ export async function checkHumanActor(
try { try {
// Fetch user information from GitHub API // Fetch user information from GitHub API
const { data: userData } = await octokit.users.getByUsername({ const response = await api.customRequest("GET", `/api/v1/users/${githubContext.actor}`);
username: githubContext.actor, const userData = response.data;
});
const actorType = userData.type; const actorType = userData.type;

View File

@@ -1,15 +1,15 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
import type { ParsedGitHubContext } from "../context"; 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 * 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 * @param context - The GitHub context
* @returns true if the actor has write permissions, false otherwise * @returns true if the actor has write permissions, false otherwise
*/ */
export async function checkWritePermissions( export async function checkWritePermissions(
octokit: Octokit, api: GiteaApiClient,
context: ParsedGitHubContext, context: ParsedGitHubContext,
): Promise<boolean> { ): Promise<boolean> {
const { repository, actor } = context; const { repository, actor } = context;
@@ -28,11 +28,7 @@ export async function checkWritePermissions(
core.info(`Checking permissions for actor: ${actor}`); core.info(`Checking permissions for actor: ${actor}`);
// Check permissions directly using the permission endpoint // Check permissions directly using the permission endpoint
const response = await octokit.repos.getCollaboratorPermissionLevel({ const response = await api.customRequest("GET", `/api/v1/repos/${repository.owner}/${repository.repo}/collaborators/${actor}/permission`);
owner: repository.owner,
repo: repository.repo,
username: actor,
});
const permissionLevel = response.data.permission; const permissionLevel = response.data.permission;
core.info(`Permission level retrieved: ${permissionLevel}`); core.info(`Permission level retrieved: ${permissionLevel}`);