simplified code

This commit is contained in:
yiqingxiong 2026-01-11 22:02:14 +08:00
parent 3ba9f7c8c2
commit 86c6ed301f
10 changed files with 405 additions and 35 deletions

View File

@ -1,4 +1,8 @@
export const GITHUB_API_URL =
process.env.GITHUB_API_URL || "https://api.github.com";
process.env.GITHUB_API_URL || "https://api.github.com" ||
process.env.GITEA_API_URL;
export const GITHUB_SERVER_URL =
process.env.GITHUB_SERVER_URL || "https://github.com";
process.env.GITHUB_SERVER_URL || "https://github.com" ||
process.env.GITEA_SERVER_URL;
export const USE_REST_API = process.env.USE_REST_API === "true";

View File

@ -0,0 +1,279 @@
// Gitea REST API query functions
// These functions replace GraphQL queries with REST API calls
import type { Octokit } from "@octokit/rest";
// Type definitions for REST API responses
type RestFile = Awaited<ReturnType<Octokit["rest"]["pulls"]["listFiles"]>>["data"][number];
type RestComment = Awaited<ReturnType<Octokit["rest"]["issues"]["listComments"]>>["data"][number];
type RestCommit = Awaited<ReturnType<Octokit["rest"]["pulls"]["listCommits"]>>["data"][number];
type RestReview = Awaited<ReturnType<Octokit["rest"]["pulls"]["listReviews"]>>["data"][number];
type RestReviewComment = Awaited<ReturnType<Octokit["rest"]["pulls"]["listReviewComments"]>>["data"][number];
/**
* Fetch complete Pull Request data including commits, files, comments, and reviews
*/
export async function fetchPullRequest(
octokit: Octokit,
owner: string,
repo: string,
number: number,
) {
// Fetch all PR data in parallel for better performance
const [prData, prFiles, prComments, prCommits, prReviews] = await Promise.all([
// Basic PR information
octokit.rest.pulls.get({
owner,
repo,
pull_number: number,
}),
// Changed files
octokit.rest.pulls.listFiles({
owner,
repo,
pull_number: number,
per_page: 100,
}),
// Issue comments (PR general comments)
octokit.rest.issues.listComments({
owner,
repo,
issue_number: number,
per_page: 100,
}),
// PR commits
octokit.rest.pulls.listCommits({
owner,
repo,
pull_number: number,
per_page: 100,
}),
// PR reviews
octokit.rest.pulls.listReviews({
owner,
repo,
pull_number: number,
per_page: 100,
}),
]);
// Fetch review comments for each review using Gitea API
// Gitea endpoint: GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments
const reviewsWithComments = await Promise.all(
prReviews.data.map(async (review: RestReview) => {
try {
// Use Gitea-specific endpoint to get comments for each review
const response = await octokit.request(
"GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments",
{
owner,
repo,
pull_number: number,
review_id: review.id,
per_page: 100,
}
);
return {
...review,
comments: response.data,
};
} catch (error) {
// If fetching comments fails, return review with empty comments
// @ts-expect-error - console is available at runtime
console.warn(`Failed to fetch comments for review ${review.id}:`, error);
return {
...review,
comments: [],
};
}
})
);
// Transform REST API response to match GraphQL-like structure
return {
repository: {
pullRequest: {
title: prData.data.title,
body: prData.data.body || "",
author: {
login: prData.data.user?.login || "",
name: prData.data.user?.name || undefined,
},
baseRefName: prData.data.base.ref,
headRefName: prData.data.head.ref,
headRefOid: prData.data.head.sha,
createdAt: prData.data.created_at,
updatedAt: prData.data.updated_at,
lastEditedAt: prData.data.updated_at, // Gitea may not have separate lastEditedAt
additions: prData.data.additions || 0,
deletions: prData.data.deletions || 0,
state: prData.data.state.toUpperCase(), // "open" -> "OPEN"
commits: {
totalCount: prCommits.data.length,
nodes: prCommits.data.map((commit: RestCommit) => ({
commit: {
oid: commit.sha,
message: commit.commit.message,
author: {
name: commit.commit.author?.name || "",
email: commit.commit.author?.email || "",
},
},
})),
},
files: {
nodes: prFiles.data.map((file: RestFile) => ({
path: file.filename,
additions: file.additions,
deletions: file.deletions,
changeType: mapFileStatus(file.status),
})),
},
comments: {
nodes: prComments.data.map((comment: RestComment) => ({
id: `comment_${comment.id}`,
databaseId: comment.id,
body: comment.body || "",
author: {
login: comment.user?.login || "",
},
createdAt: comment.created_at,
updatedAt: comment.updated_at,
lastEditedAt: comment.updated_at,
isMinimized: false, // Gitea may not support this
})),
},
reviews: {
nodes: reviewsWithComments.map((review: RestReview & { comments: RestReviewComment[] }) => ({
id: `review_${review.id}`,
databaseId: review.id,
author: {
login: review.user?.login || "",
},
body: review.body || "",
state: review.state.toUpperCase(), // "APPROVED", "CHANGES_REQUESTED", etc.
submittedAt: review.submitted_at || review.created_at || "",
updatedAt: review.submitted_at || review.created_at || "",
lastEditedAt: review.submitted_at || review.created_at || "",
comments: {
nodes: review.comments.map((comment: RestReviewComment) => ({
id: `review_comment_${comment.id}`,
databaseId: comment.id,
body: comment.body || "",
path: comment.path,
line: comment.line || comment.original_line || null,
author: {
login: comment.user?.login || "",
},
createdAt: comment.created_at,
updatedAt: comment.updated_at,
lastEditedAt: comment.updated_at,
isMinimized: false,
})),
},
})),
},
},
},
};
}
/**
* Fetch complete Issue data including comments
*/
export async function fetchIssue(
octokit: Octokit,
owner: string,
repo: string,
number: number,
) {
// Fetch issue data and comments in parallel
const [issueData, issueComments] = await Promise.all([
octokit.rest.issues.get({
owner,
repo,
issue_number: number,
}),
octokit.rest.issues.listComments({
owner,
repo,
issue_number: number,
per_page: 100,
}),
]);
// Transform REST API response to match GraphQL-like structure
return {
repository: {
issue: {
title: issueData.data.title,
body: issueData.data.body || "",
author: {
login: issueData.data.user?.login || "",
},
createdAt: issueData.data.created_at,
updatedAt: issueData.data.updated_at,
lastEditedAt: issueData.data.updated_at,
state: issueData.data.state.toUpperCase(),
comments: {
nodes: issueComments.data.map((comment: RestComment) => ({
id: `comment_${comment.id}`,
databaseId: comment.id,
body: comment.body || "",
author: {
login: comment.user?.login || "",
},
createdAt: comment.created_at,
updatedAt: comment.updated_at,
lastEditedAt: comment.updated_at,
isMinimized: false,
})),
},
},
},
};
}
/**
* Fetch user display name
*/
export async function fetchUser(octokit: Octokit, login: string) {
try {
const userData = await octokit.rest.users.getByUsername({
username: login,
});
return {
user: {
name: userData.data.name || userData.data.full_name || null,
},
};
} catch (error) {
// Note: console is available at runtime in Node.js environment
// @ts-expect-error - console is not in lib but available at runtime
console.warn(`Failed to fetch user ${login}:`, error);
return {
user: {
name: null,
},
};
}
}
/**
* Map Gitea file status to GraphQL changeType format
*/
function mapFileStatus(status: string): string {
const statusMap: Record<string, string> = {
added: "ADDED",
modified: "MODIFIED",
removed: "DELETED",
renamed: "RENAMED",
copied: "COPIED",
changed: "MODIFIED",
};
return statusMap[status] || status.toUpperCase();
}

View File

@ -1,6 +1,12 @@
import { execFileSync } from "child_process";
import type { Octokits } from "../api/client";
import { USE_GITEA_API } from "../api/config";
import { ISSUE_QUERY, PR_QUERY, USER_QUERY } from "../api/queries/github";
import {
fetchPullRequest,
fetchIssue,
fetchUser,
} from "../api/queries/gitea";
import {
isIssueCommentEvent,
isPullRequestReviewEvent,
@ -183,7 +189,14 @@ export async function fetchGitHubData({
try {
if (isPR) {
// Fetch PR data with all comments and file information
const prResult = await octokits.graphql<PullRequestQueryResponse>(
const prResult = USE_GITEA_API
? await fetchPullRequest(
octokits.rest,
owner,
repo,
parseInt(prNumber),
)
: await octokits.graphql<PullRequestQueryResponse>(
PR_QUERY,
{
owner,
@ -200,7 +213,9 @@ export async function fetchGitHubData({
pullRequest.comments?.nodes || [],
triggerTime,
);
reviewData = pullRequest.reviews || [];
reviewData = USE_GITEA_API
? (pullRequest.reviews || null)
: (pullRequest.reviews || { nodes: [] });
console.log(`Successfully fetched PR #${prNumber} data`);
} else {
@ -208,7 +223,14 @@ export async function fetchGitHubData({
}
} else {
// Fetch issue data
const issueResult = await octokits.graphql<IssueQueryResponse>(
const issueResult = USE_GITEA_API
? await fetchIssue(
octokits.rest,
owner,
repo,
parseInt(prNumber),
)
: await octokits.graphql<IssueQueryResponse>(
ISSUE_QUERY,
{
owner,
@ -376,7 +398,9 @@ export async function fetchUserDisplayName(
login: string,
): Promise<string | null> {
try {
const result = await octokits.graphql<UserQueryResponse>(USER_QUERY, {
const result = USE_GITEA_API
? await fetchUser(octokits.rest, login)
: await octokits.graphql<UserQueryResponse>(USER_QUERY, {
login,
});
return result.user.name;

View File

@ -1,5 +1,6 @@
import type { Octokits } from "../api/client";
import { GITHUB_SERVER_URL } from "../api/config";
import { GITHUB_SERVER_URL, USE_GITEA_API} from "../api/config";
import { createBranchUrl } from "./comments/common";
import { $ } from "bun";
export async function checkAndCommitOrDeleteBranch(
@ -80,7 +81,9 @@ export async function checkAndCommitOrDeleteBranch(
);
// Set branch link since we now have commits
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
const branchUrl = USE_GITEA_API
? createBranchUrl(owner, repo, claudeBranch)
: `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
branchLink = `\n[View branch](${branchUrl})`;
} else {
console.log(
@ -91,7 +94,9 @@ export async function checkAndCommitOrDeleteBranch(
} catch (gitError) {
console.error("Error checking/committing changes:", gitError);
// If we can't check git status, assume the branch might have changes
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
const branchUrl = USE_GITEA_API
? createBranchUrl(owner, repo, claudeBranch)
: `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
branchLink = `\n[View branch](${branchUrl})`;
}
} else {
@ -102,13 +107,17 @@ export async function checkAndCommitOrDeleteBranch(
}
} else {
// Only add branch link if there are commits
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
const branchUrl = USE_GITEA_API
? createBranchUrl(owner, repo, claudeBranch)
: `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
branchLink = `\n[View branch](${branchUrl})`;
}
} catch (error) {
console.error("Error comparing commits on Claude branch:", error);
// If we can't compare but the branch exists remotely, include the branch link
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
const branchUrl = USE_GITEA_API
? createBranchUrl(owner, repo, claudeBranch)
: `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
branchLink = `\n[View branch](${branchUrl})`;
}
}

View File

@ -1,4 +1,5 @@
import { GITHUB_SERVER_URL } from "../api/config";
import { GITHUB_SERVER_URL, USE_GITEA_API} from "../api/config";
import { createBranchUrl } from "./comments/common";
export type ExecutionDetails = {
total_cost_usd?: number;
@ -157,10 +158,14 @@ export function updateCommentBody(input: CommentUpdateInput): string {
// If we don't have a URL yet but have a branch name, construct it
if (!branchUrl && finalBranchName) {
// Extract owner/repo from jobUrl
const repoMatch = jobUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\//);
// Extract owner/repo from jobUrl (works for both GitHub and Gitea URLs)
const repoMatch = USE_GITEA_API
? jobUrl.match(/\/([^\/]+)\/([^\/]+)\/(?:actions|tree|src)/)
: jobUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\//);
if (repoMatch) {
branchUrl = `${GITHUB_SERVER_URL}/${repoMatch[1]}/${repoMatch[2]}/tree/${finalBranchName}`;
branchUrl = USE_GITEA_API
? createBranchUrl(repoMatch[1], repoMatch[2], finalBranchName)
: `${GITHUB_SERVER_URL}/${repoMatch[1]}/${repoMatch[2]}/tree/${finalBranchName}`;
}
}

View File

@ -1,4 +1,4 @@
import { GITHUB_SERVER_URL } from "../../api/config";
import { GITHUB_SERVER_URL, USE_GITEA_API} from "../../api/config";
export const SPINNER_HTML =
'<img src="https://github.com/user-attachments/assets/5ac382c7-e004-429b-8e35-7feb3e8f9c6f" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />';
@ -17,10 +17,32 @@ export function createBranchLink(
repo: string,
branchName: string,
): string {
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${branchName}`;
const branchUrl = USE_GITEA_API
? createBranchUrl(owner, repo, branchName)
: `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${branchName}`;
return `\n[View branch](${branchUrl})`;
}
/**
* Get the branch URL path segment for the current platform
* Gitea uses /src/branch/ while GitHub uses /tree/
*/
export function getBranchPath(): string {
const isGitea = !GITHUB_SERVER_URL.includes("github.com");
return isGitea ? "src/branch" : "tree";
}
/**
* Create a branch URL for the current platform
*/
export function createBranchUrl(
owner: string,
repo: string,
branchName: string,
): string {
return `${GITHUB_SERVER_URL}/${owner}/${repo}/${getBranchPath()}/${branchName}`;
}
export function createCommentBody(
jobRunLink: string,
branchLink: string = "",

View File

@ -12,6 +12,16 @@ export async function checkHumanActor(
octokit: Octokit,
githubContext: ParsedGitHubContext,
) {
if (process.env.OVERRIDE_BOT_USERNAMES){
if (process.env.OVERRIDE_BOT_USERNAMES.split(",").includes(githubContext.actor)){
throw new Error(
`Workflow initiated by non-human actor: ${githubContext.actor}. Please add bot to OVERRIDE_BOT_USERNAMES`,
);
}
return;
}
// Fetch user information from GitHub API
const { data: userData } = await octokit.users.getByUsername({
username: githubContext.actor,

View File

@ -59,7 +59,7 @@ export async function checkWritePermissions(
const permissionLevel = response.data.permission;
core.info(`Permission level retrieved: ${permissionLevel}`);
if (permissionLevel === "admin" || permissionLevel === "write") {
if (permissionLevel === "owner" || permissionLevel === "admin" || permissionLevel === "write") {
core.info(`Actor has write access: ${permissionLevel}`);
return true;
} else {

View File

@ -71,6 +71,11 @@ server.tool(
repo: REPO_NAME!,
head_sha: headSha,
...(status && { status }),
}).catch((error: Error) => {
// Gitea Actions API might not be fully compatible
throw new Error(
`Failed to fetch workflow runs. This may indicate Gitea Actions API incompatibility: ${error.message}`,
);
});
// Process runs to create summary
@ -152,6 +157,11 @@ server.tool(
owner: REPO_OWNER!,
repo: REPO_NAME!,
run_id,
})
.catch((error: Error) => {
throw new Error(
`Failed to fetch workflow run details. Gitea Actions API may not be fully compatible: ${error.message}`,
);
});
const processedJobs = jobsData.jobs.map((job: any) => {
@ -219,6 +229,11 @@ server.tool(
owner: REPO_OWNER!,
repo: REPO_NAME!,
job_id,
})
.catch((error: Error) => {
throw new Error(
`Failed to download job logs. Gitea Actions API may not be fully compatible: ${error.message}`,
);
});
const logsText = response.data as unknown as string;

View File

@ -65,6 +65,7 @@ export function detectMode(context: GitHubContext): AutoDetectedMode {
const supportedActions = [
"opened",
"synchronize",
"synchronized",
"ready_for_review",
"reopened",
];
@ -112,6 +113,7 @@ function validateTrackProgressEvent(context: GitHubContext): void {
const validActions = [
"opened",
"synchronize",
"synchronized",
"ready_for_review",
"reopened",
];