This commit is contained in:
Mark Wylde
2025-05-30 20:02:39 +01:00
parent 180a1b6680
commit fb6df649ed
33 changed files with 4709 additions and 901 deletions

View File

@@ -1,8 +1,14 @@
import type { Octokits } from "../api/client";
import { GITHUB_SERVER_URL } from "../api/config";
import type { GitHubClient } from "../api/client";
import { GITEA_SERVER_URL } from "../api/config";
import {
branchHasChanges,
fetchBranch,
branchExists,
remoteBranchExists,
} from "../utils/local-git";
export async function checkAndDeleteEmptyBranch(
octokit: Octokits,
client: GitHubClient,
owner: string,
repo: string,
claudeBranch: string | undefined,
@@ -12,47 +18,128 @@ export async function checkAndDeleteEmptyBranch(
let shouldDeleteBranch = false;
if (claudeBranch) {
// Check if Claude made any commits to the branch
try {
const { data: comparison } =
await octokit.rest.repos.compareCommitsWithBasehead({
owner,
repo,
basehead: `${baseBranch}...${claudeBranch}`,
});
// Check if we're using Gitea or GitHub
const giteaApiUrl = process.env.GITEA_API_URL?.trim();
const isGitea =
giteaApiUrl &&
giteaApiUrl !== "" &&
!giteaApiUrl.includes("api.github.com") &&
!giteaApiUrl.includes("github.com");
// 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`,
if (isGitea) {
// Use local git operations for Gitea
console.log("Using local git commands for branch check (Gitea mode)");
try {
// Fetch latest changes from remote
await fetchBranch(claudeBranch);
await fetchBranch(baseBranch);
// Check if branch exists and has changes
const { hasChanges, branchSha, baseSha } = await branchHasChanges(
claudeBranch,
baseBranch,
);
shouldDeleteBranch = true;
} else {
// Only add branch link if there are commits
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
if (branchSha && baseSha) {
if (hasChanges) {
console.log(
`Branch ${claudeBranch} appears to have commits (different SHA from base)`,
);
const branchUrl = `${GITEA_SERVER_URL}/${owner}/${repo}/src/branch/${claudeBranch}`;
branchLink = `\n[View branch](${branchUrl})`;
} else {
console.log(
`Branch ${claudeBranch} has same SHA as base, marking for deletion`,
);
shouldDeleteBranch = true;
}
} else {
// If we can't get SHAs, check if branch exists at all
const localExists = await branchExists(claudeBranch);
const remoteExists = await remoteBranchExists(claudeBranch);
if (localExists || remoteExists) {
console.log(
`Branch ${claudeBranch} exists but SHA comparison failed, assuming it has commits`,
);
const branchUrl = `${GITEA_SERVER_URL}/${owner}/${repo}/src/branch/${claudeBranch}`;
branchLink = `\n[View branch](${branchUrl})`;
} else {
console.log(
`Branch ${claudeBranch} does not exist yet - this is normal during workflow`,
);
branchLink = "";
}
}
} catch (error: any) {
console.error("Error checking branch with git commands:", error);
// For errors, assume the branch has commits to be safe
console.log("Assuming branch exists due to git command error");
const branchUrl = `${GITEA_SERVER_URL}/${owner}/${repo}/src/branch/${claudeBranch}`;
branchLink = `\n[View branch](${branchUrl})`;
}
} catch (error) {
console.error("Error checking for commits on Claude branch:", error);
// If we can't check, assume the branch has commits to be safe
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
branchLink = `\n[View branch](${branchUrl})`;
} else {
// Use API calls for GitHub
console.log("Using API calls for branch check (GitHub mode)");
try {
// Get the branch info to see if it exists and has commits
const branchResponse = await client.api.getBranch(
owner,
repo,
claudeBranch,
);
// Get base branch info for comparison
const baseResponse = await client.api.getBranch(
owner,
repo,
baseBranch,
);
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 = `${GITEA_SERVER_URL}/${owner}/${repo}/src/branch/${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:", 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 = `${GITEA_SERVER_URL}/${owner}/${repo}/src/branch/${claudeBranch}`;
branchLink = `\n[View branch](${branchUrl})`;
}
}
}
}
// Delete the branch if it has no commits
if (shouldDeleteBranch && claudeBranch) {
try {
await octokit.rest.git.deleteRef({
owner,
repo,
ref: `heads/${claudeBranch}`,
});
console.log(`✅ Deleted empty branch: ${claudeBranch}`);
} catch (deleteError) {
console.error(`Failed to delete branch ${claudeBranch}:`, deleteError);
// Continue even if deletion fails
}
console.log(
`Skipping branch deletion - not reliably supported across all Git platforms: ${claudeBranch}`,
);
// Skip deletion to avoid compatibility issues
}
return { shouldDeleteBranch, branchLink };

View File

@@ -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<BranchInfo> {
@@ -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,33 +90,62 @@ export async function setupBranch(
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
try {
// Get the SHA of the source branch
const sourceBranchRef = await octokits.rest.git.getRef({
owner,
repo,
ref: `heads/${sourceBranch}`,
});
const currentSHA = sourceBranchRef.data.object.sha;
console.log(`Current SHA: ${currentSHA}`);
// Create branch using GitHub API
await octokits.rest.git.createRef({
owner,
repo,
ref: `refs/heads/${newBranch}`,
sha: currentSHA,
});
// Checkout the new branch (shallow fetch for performance)
await $`git fetch origin --depth=1 ${newBranch}`;
await $`git checkout ${newBranch}`;
// Use local git operations instead of API since Gitea's API is unreliable
console.log(
`Successfully created and checked out new branch: ${newBranch}`,
`Setting up local git branch: ${newBranch} from: ${sourceBranch}`,
);
// Ensure we're in the repository directory
const repoDir = process.env.GITHUB_WORKSPACE || process.cwd();
console.log(`Working in directory: ${repoDir}`);
try {
// Check if we're in a git repository
console.log(`Checking if we're in a git repository...`);
await $`git status`;
// Ensure we have the latest version of the source branch
console.log(`Fetching latest ${sourceBranch}...`);
await $`git fetch origin ${sourceBranch}`;
// Checkout the source branch
console.log(`Checking out ${sourceBranch}...`);
await $`git checkout ${sourceBranch}`;
// Pull latest changes
console.log(`Pulling latest changes for ${sourceBranch}...`);
await $`git pull origin ${sourceBranch}`;
// Create and checkout the new branch
console.log(`Creating new branch: ${newBranch}`);
await $`git checkout -b ${newBranch}`;
// Verify the branch was created
const currentBranch = await $`git branch --show-current`;
const branchName = currentBranch.text().trim();
console.log(`Current branch after creation: ${branchName}`);
if (branchName === newBranch) {
console.log(
`✅ Successfully created and checked out branch: ${newBranch}`,
);
} else {
throw new Error(
`Branch creation failed. Expected ${newBranch}, got ${branchName}`,
);
}
} catch (gitError: any) {
console.error(`❌ Git operations failed:`, gitError);
console.error(`Error message: ${gitError.message || gitError}`);
// This is a critical failure - the branch MUST be created for Claude to work
throw new Error(
`Failed to create branch ${newBranch}: ${gitError.message || gitError}`,
);
}
console.log(`Branch setup completed for: ${newBranch}`);
// Set outputs for GitHub Actions
core.setOutput("CLAUDE_BRANCH", newBranch);
core.setOutput("BASE_BRANCH", sourceBranch);
@@ -129,7 +155,7 @@ export async function setupBranch(
currentBranch: newBranch,
};
} catch (error) {
console.error("Error creating branch:", error);
console.error("Error setting up branch:", error);
process.exit(1);
}
}

View File

@@ -1,4 +1,4 @@
import { GITHUB_SERVER_URL } from "../api/config";
import { GITEA_SERVER_URL } from "../api/config";
export type ExecutionDetails = {
cost_usd?: number;
@@ -160,7 +160,7 @@ export function updateCommentBody(input: CommentUpdateInput): string {
// Extract owner/repo from jobUrl
const repoMatch = jobUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\//);
if (repoMatch) {
branchUrl = `${GITHUB_SERVER_URL}/${repoMatch[1]}/${repoMatch[2]}/tree/${finalBranchName}`;
branchUrl = `${GITEA_SERVER_URL}/${repoMatch[1]}/${repoMatch[2]}/src/branch/${finalBranchName}`;
}
}

View File

@@ -1,14 +1,19 @@
import { GITHUB_SERVER_URL } from "../../api/config";
import { GITEA_SERVER_URL } from "../../api/config";
import { readFileSync } from "fs";
import { join } from "path";
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;" />';
function getSpinnerHtml(): string {
return `<img src="https://raw.githubusercontent.com/markwylde/claude-code-gitea-action/refs/heads/gitea/assets/spinner.gif" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />`;
}
export const SPINNER_HTML = getSpinnerHtml();
export function createJobRunLink(
owner: string,
repo: string,
runId: string,
): string {
const jobRunUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/actions/runs/${runId}`;
const jobRunUrl = `${GITEA_SERVER_URL}/${owner}/${repo}/actions/runs/${runId}`;
return `[View job run](${jobRunUrl})`;
}
@@ -17,7 +22,7 @@ export function createBranchLink(
repo: string,
branchName: string,
): string {
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${branchName}`;
const branchUrl = `${GITEA_SERVER_URL}/${owner}/${repo}/src/branch/${branchName}/`;
return `\n[View branch](${branchUrl})`;
}

View File

@@ -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;
@@ -25,23 +25,30 @@ export async function createInitialComment(
try {
let response;
console.log(
`Creating comment for ${context.isPR ? "PR" : "issue"} #${context.entityNumber}`,
);
console.log(`Repository: ${owner}/${repo}`);
// 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,
body: initialBody,
});
console.log(`Creating PR review comment reply`);
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({
console.log(`Creating issue comment via API`);
response = await api.createIssueComment(
owner,
repo,
issue_number: context.entityNumber,
body: initialBody,
});
context.entityNumber,
initialBody,
);
}
// Output the comment ID for downstream steps using GITHUB_OUTPUT
@@ -54,12 +61,12 @@ export async function createInitialComment(
// Always fall back to regular issue comment if anything fails
try {
const response = await octokit.rest.issues.createComment({
const response = await api.createIssueComment(
owner,
repo,
issue_number: context.entityNumber,
body: initialBody,
});
context.entityNumber,
initialBody,
);
const githubOutput = process.env.GITHUB_OUTPUT!;
appendFileSync(githubOutput, `claude_comment_id=${response.data.id}\n`);

View File

@@ -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,17 @@ 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,
body: updatedBody,
});
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) {