mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-11-10 17:10:00 +08:00
@@ -9,6 +9,8 @@
|
||||
import { $ } from "bun";
|
||||
import { execFileSync } from "child_process";
|
||||
import type { ParsedGitHubContext } from "../context";
|
||||
import { isPullRequestEvent } from "../context";
|
||||
import { retryWithBackoff } from "../../utils/retry";
|
||||
import type { GitHubPullRequest } from "../types";
|
||||
import type { Octokits } from "../api/client";
|
||||
import type { FetchDataResult } from "../data/fetcher";
|
||||
@@ -156,6 +158,7 @@ export async function setupBranch(
|
||||
octokits: Octokits,
|
||||
githubData: FetchDataResult,
|
||||
context: ParsedGitHubContext,
|
||||
triggerTime?: string,
|
||||
): Promise<BranchInfo> {
|
||||
const { owner, repo } = context.repository;
|
||||
const entityNumber = context.entityNumber;
|
||||
@@ -177,6 +180,7 @@ export async function setupBranch(
|
||||
console.log("This is an open PR, checking out PR branch...");
|
||||
|
||||
const branchName = prData.headRefName;
|
||||
const headSha = prData.headRefOid;
|
||||
|
||||
// Determine optimal fetch depth based on PR commit count, with a minimum
|
||||
// of 20. Only applied to a checkout that is already shallow — see
|
||||
@@ -195,25 +199,56 @@ export async function setupBranch(
|
||||
|
||||
// Validate branch names before use to prevent command injection
|
||||
validateBranchName(branchName);
|
||||
if (!/^[0-9a-f]{40}$/.test(headSha)) {
|
||||
throw new Error(
|
||||
`Unexpected head SHA for PR #${entityNumber}: ${headSha}`,
|
||||
);
|
||||
}
|
||||
|
||||
// For cross-repository (fork) PRs, fetch via the pull ref since the
|
||||
// branch only exists on the fork's remote, not on origin.
|
||||
if (prData.isCrossRepository) {
|
||||
console.log(
|
||||
`PR #${entityNumber} is from a fork, fetching via refs/pull/${entityNumber}/head...`,
|
||||
if (isPullRequestEvent(context)) {
|
||||
const eventSha = context.payload.pull_request.head.sha;
|
||||
if (eventSha !== headSha) {
|
||||
throw new Error(
|
||||
`PR #${entityNumber} was updated after the trigger (${eventSha} -> ${headSha}). Push again or mention Claude to run on the latest version.`,
|
||||
);
|
||||
}
|
||||
} else if (triggerTime) {
|
||||
if (!prData.headRepository) {
|
||||
throw new Error(
|
||||
`PR #${entityNumber}: head repository is not available, cannot pin the PR version`,
|
||||
);
|
||||
}
|
||||
const pushedAt = await headPushedAt(
|
||||
octokits,
|
||||
{
|
||||
owner: prData.headRepository.owner.login,
|
||||
repo: prData.headRepository.name,
|
||||
},
|
||||
branchName,
|
||||
headSha,
|
||||
);
|
||||
if (new Date(pushedAt).getTime() >= new Date(triggerTime).getTime()) {
|
||||
throw new Error(
|
||||
`PR #${entityNumber} was updated after the trigger (${pushedAt} >= ${triggerTime}). Mention Claude again to run on the latest version.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const localBranch = prData.isCrossRepository
|
||||
? `pr-${entityNumber}`
|
||||
: branchName;
|
||||
console.log(`PR #${entityNumber}: pinning ${localBranch} to ${headSha}`);
|
||||
execGit(["fetch", "origin", ...depthArgs, headSha]);
|
||||
execGit(["checkout", "-B", localBranch, headSha, "--"]);
|
||||
|
||||
const checkedOut = execFileSync("git", ["rev-parse", "HEAD"], {
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
if (checkedOut !== headSha) {
|
||||
throw new Error(
|
||||
`PR #${entityNumber}: checked out ${checkedOut}, expected ${headSha}`,
|
||||
);
|
||||
execGit([
|
||||
"fetch",
|
||||
"origin",
|
||||
...depthArgs,
|
||||
`pull/${entityNumber}/head:${branchName}`,
|
||||
]);
|
||||
} else {
|
||||
// Execute git commands to checkout PR branch (dynamic depth based on PR size)
|
||||
// Using execFileSync instead of shell template literals for security
|
||||
execGit(["fetch", "origin", ...depthArgs, branchName]);
|
||||
}
|
||||
execGit(["checkout", branchName, "--"]);
|
||||
|
||||
console.log(`Successfully checked out PR branch for PR #${entityNumber}`);
|
||||
|
||||
@@ -223,7 +258,7 @@ export async function setupBranch(
|
||||
|
||||
return {
|
||||
baseBranch,
|
||||
currentBranch: branchName,
|
||||
currentBranch: localBranch,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -350,3 +385,26 @@ export async function setupBranch(
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function headPushedAt(
|
||||
octokits: Octokits,
|
||||
headRepo: { owner: string; repo: string },
|
||||
branchName: string,
|
||||
headSha: string,
|
||||
): Promise<string> {
|
||||
const { data } = await retryWithBackoff(() =>
|
||||
octokits.rest.request("GET /repos/{owner}/{repo}/activity", {
|
||||
owner: headRepo.owner,
|
||||
repo: headRepo.repo,
|
||||
ref: `refs/heads/${branchName}`,
|
||||
per_page: 1,
|
||||
}),
|
||||
);
|
||||
const latest = (data as Array<{ after: string; timestamp: string }>)[0];
|
||||
if (!latest || latest.after !== headSha) {
|
||||
throw new Error(
|
||||
`Could not determine when ${headSha} was pushed to ${headRepo.owner}/${headRepo.repo}:${branchName}`,
|
||||
);
|
||||
}
|
||||
return latest.timestamp;
|
||||
}
|
||||
|
||||
+30
-2
@@ -14,7 +14,12 @@ import {
|
||||
extractOriginalBody,
|
||||
} from "../../github/data/fetcher";
|
||||
import { createPrompt } from "../../create-prompt";
|
||||
import { isEntityContext } from "../../github/context";
|
||||
import {
|
||||
isEntityContext,
|
||||
isIssueCommentEvent,
|
||||
isPullRequestReviewCommentEvent,
|
||||
} from "../../github/context";
|
||||
import { updateClaudeComment } from "../../github/operations/comments/update-claude-comment";
|
||||
import type { GitHubContext } from "../../github/context";
|
||||
import type { Octokits } from "../../github/api/client";
|
||||
import { parseAllowedTools } from "../agent/parse-tools";
|
||||
@@ -64,7 +69,30 @@ export async function prepareTagMode({
|
||||
});
|
||||
|
||||
// Setup branch
|
||||
const branchInfo = await setupBranch(octokit, githubData, context);
|
||||
const editedAt =
|
||||
context.eventAction === "edited" &&
|
||||
(isIssueCommentEvent(context) || isPullRequestReviewCommentEvent(context))
|
||||
? context.payload.comment.updated_at
|
||||
: undefined;
|
||||
let branchInfo;
|
||||
try {
|
||||
branchInfo = await setupBranch(
|
||||
octokit,
|
||||
githubData,
|
||||
context,
|
||||
editedAt || triggerTime,
|
||||
);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await updateClaudeComment(octokit.rest, {
|
||||
owner: context.repository.owner,
|
||||
repo: context.repository.repo,
|
||||
commentId,
|
||||
body: message,
|
||||
isPullRequestReviewComment: isPullRequestReviewCommentEvent(context),
|
||||
}).catch((e) => console.error(`Failed to update tracking comment: ${e}`));
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Configure git authentication
|
||||
// SSH signing takes precedence if provided
|
||||
|
||||
Reference in New Issue
Block a user