mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 15:04:13 +08:00
Compare commits
4 Commits
v0.0.13
...
claude/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d100f7832 | ||
|
|
e0dcb85d34 | ||
|
|
be799cbe7b | ||
|
|
bd71ac0e8f |
26
.github/workflows/issue-triage.yml
vendored
26
.github/workflows/issue-triage.yml
vendored
@@ -23,18 +23,20 @@ jobs:
|
||||
mkdir -p /tmp/mcp-config
|
||||
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
|
||||
{
|
||||
"github": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"ghcr.io/github/github-mcp-server:sha-7aced2b"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"ghcr.io/github/github-mcp-server:sha-7aced2b"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ runs:
|
||||
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' }}
|
||||
BASE_BRANCH: ${{ steps.prepare.outputs.BASE_BRANCH }}
|
||||
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
|
||||
CLAUDE_CANCELLED: ${{ cancelled() }}
|
||||
OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }}
|
||||
TRIGGER_USERNAME: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.pull_request.user.login || github.event.sender.login || github.triggering_actor || github.actor || '' }}
|
||||
PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }}
|
||||
|
||||
@@ -146,8 +146,12 @@ async function run() {
|
||||
duration_api_ms?: number;
|
||||
} | null = null;
|
||||
let actionFailed = false;
|
||||
let actionCancelled = false;
|
||||
let errorDetails: string | undefined;
|
||||
|
||||
// Check if the workflow was cancelled
|
||||
const isCancelled = process.env.CLAUDE_CANCELLED === "true";
|
||||
|
||||
// First check if prepare step failed
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
@@ -155,8 +159,18 @@ async function run() {
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
errorDetails = prepareError;
|
||||
} else if (isCancelled) {
|
||||
// If the workflow was cancelled, set the cancelled flag
|
||||
actionCancelled = true;
|
||||
} else {
|
||||
// Check for existence of output file and parse it if available
|
||||
// Check if the Claude action failed
|
||||
// CLAUDE_SUCCESS is set to the result of: steps.claude-code.outputs.conclusion == 'success'
|
||||
// If the step didn't run or didn't set outputs.conclusion, CLAUDE_SUCCESS will be "false"
|
||||
// If the step succeeded, CLAUDE_SUCCESS will be "true"
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
|
||||
actionFailed = !claudeSuccess;
|
||||
|
||||
// Try to read execution details from output file
|
||||
try {
|
||||
const outputFile = process.env.OUTPUT_FILE;
|
||||
if (outputFile) {
|
||||
@@ -179,14 +193,10 @@ async function run() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the Claude action failed
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS !== "false";
|
||||
actionFailed = !claudeSuccess;
|
||||
} catch (error) {
|
||||
console.error("Error reading output file:", error);
|
||||
// If we can't read the file, check for any failure markers
|
||||
actionFailed = process.env.CLAUDE_SUCCESS === "false";
|
||||
// Error reading output file doesn't change the action status
|
||||
// We already determined actionFailed based on CLAUDE_SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +204,7 @@ async function run() {
|
||||
const commentInput: CommentUpdateInput = {
|
||||
currentBody,
|
||||
actionFailed,
|
||||
actionCancelled,
|
||||
executionDetails,
|
||||
jobUrl,
|
||||
branchLink,
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { execSync } from "child_process";
|
||||
import type { Octokits } from "../api/client";
|
||||
import { ISSUE_QUERY, PR_QUERY } from "../api/queries/github";
|
||||
import type {
|
||||
GitHubPullRequest,
|
||||
GitHubIssue,
|
||||
GitHubComment,
|
||||
GitHubFile,
|
||||
GitHubIssue,
|
||||
GitHubPullRequest,
|
||||
GitHubReview,
|
||||
PullRequestQueryResponse,
|
||||
IssueQueryResponse,
|
||||
PullRequestQueryResponse,
|
||||
} from "../types";
|
||||
import { PR_QUERY, ISSUE_QUERY } from "../api/queries/github";
|
||||
import type { Octokits } from "../api/client";
|
||||
import { downloadCommentImages } from "../utils/image-downloader";
|
||||
import type { CommentWithImages } from "../utils/image-downloader";
|
||||
import { downloadCommentImages } from "../utils/image-downloader";
|
||||
|
||||
type FetchDataParams = {
|
||||
octokits: Octokits;
|
||||
@@ -101,6 +101,14 @@ export async function fetchGitHubData({
|
||||
let changedFilesWithSHA: GitHubFileWithSHA[] = [];
|
||||
if (isPR && changedFiles.length > 0) {
|
||||
changedFilesWithSHA = changedFiles.map((file) => {
|
||||
// Don't compute SHA for deleted files
|
||||
if (file.changeType === "DELETED") {
|
||||
return {
|
||||
...file,
|
||||
sha: "deleted",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Use git hash-object to compute the SHA for the current file content
|
||||
const sha = execSync(`git hash-object "${file.path}"`, {
|
||||
|
||||
@@ -9,6 +9,7 @@ export type ExecutionDetails = {
|
||||
export type CommentUpdateInput = {
|
||||
currentBody: string;
|
||||
actionFailed: boolean;
|
||||
actionCancelled?: boolean;
|
||||
executionDetails: ExecutionDetails | null;
|
||||
jobUrl: string;
|
||||
branchLink?: string;
|
||||
@@ -74,6 +75,7 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
||||
branchLink,
|
||||
prLink,
|
||||
actionFailed,
|
||||
actionCancelled,
|
||||
branchName,
|
||||
triggerUsername,
|
||||
errorDetails,
|
||||
@@ -112,7 +114,13 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
||||
// Build the header
|
||||
let header = "";
|
||||
|
||||
if (actionFailed) {
|
||||
if (actionCancelled) {
|
||||
header = "**Claude's task was cancelled";
|
||||
if (durationStr) {
|
||||
header += ` after ${durationStr}`;
|
||||
}
|
||||
header += "**";
|
||||
} else if (actionFailed) {
|
||||
header = "**Claude encountered an error";
|
||||
if (durationStr) {
|
||||
header += ` after ${durationStr}`;
|
||||
|
||||
@@ -418,4 +418,48 @@ describe("updateCommentBody", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancellation handling", () => {
|
||||
it("shows cancellation message when actionCancelled is true", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
currentBody: "Claude Code is working…",
|
||||
actionCancelled: true,
|
||||
executionDetails: { duration_ms: 30000 }, // 30s
|
||||
triggerUsername: "test-user",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result).toContain("**Claude's task was cancelled after 30s**");
|
||||
expect(result).not.toContain("Claude encountered an error");
|
||||
expect(result).not.toContain("Claude finished");
|
||||
});
|
||||
|
||||
it("shows cancellation message without duration when duration is missing", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
currentBody: "Claude Code is working…",
|
||||
actionCancelled: true,
|
||||
triggerUsername: "test-user",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result).toContain("**Claude's task was cancelled**");
|
||||
expect(result).not.toContain("after");
|
||||
});
|
||||
|
||||
it("prioritizes cancellation over failure", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
currentBody: "Claude Code is working…",
|
||||
actionCancelled: true,
|
||||
actionFailed: true, // Both are true, cancellation should take precedence
|
||||
executionDetails: { duration_ms: 45000 },
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result).toContain("**Claude's task was cancelled after 45s**");
|
||||
expect(result).not.toContain("Claude encountered an error");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
160
test/update-comment-link-logic.test.ts
Normal file
160
test/update-comment-link-logic.test.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
||||
|
||||
describe("update-comment-link workflow status detection", () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = { ...process.env };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
test("should detect prepare step failure", () => {
|
||||
process.env.PREPARE_SUCCESS = "false";
|
||||
process.env.PREPARE_ERROR = "Failed to fetch issue data";
|
||||
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
|
||||
let actionFailed = false;
|
||||
let errorDetails: string | undefined;
|
||||
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
errorDetails = prepareError;
|
||||
}
|
||||
|
||||
expect(actionFailed).toBe(true);
|
||||
expect(errorDetails).toBe("Failed to fetch issue data");
|
||||
});
|
||||
|
||||
test("should detect claude-code step failure when prepare succeeds", () => {
|
||||
process.env.PREPARE_SUCCESS = "true";
|
||||
process.env.CLAUDE_SUCCESS = "false";
|
||||
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
|
||||
let actionFailed = false;
|
||||
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
} else {
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
|
||||
actionFailed = !claudeSuccess;
|
||||
}
|
||||
|
||||
expect(actionFailed).toBe(true);
|
||||
});
|
||||
|
||||
test("should detect success when both steps succeed", () => {
|
||||
process.env.PREPARE_SUCCESS = "true";
|
||||
process.env.CLAUDE_SUCCESS = "true";
|
||||
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
|
||||
let actionFailed = false;
|
||||
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
} else {
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
|
||||
actionFailed = !claudeSuccess;
|
||||
}
|
||||
|
||||
expect(actionFailed).toBe(false);
|
||||
});
|
||||
|
||||
test("should treat missing CLAUDE_SUCCESS env var as failure", () => {
|
||||
process.env.PREPARE_SUCCESS = "true";
|
||||
delete process.env.CLAUDE_SUCCESS;
|
||||
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
|
||||
let actionFailed = false;
|
||||
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
} else {
|
||||
// When CLAUDE_SUCCESS is undefined, it's not === "true", so claudeSuccess = false
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
|
||||
actionFailed = !claudeSuccess;
|
||||
}
|
||||
|
||||
expect(actionFailed).toBe(true);
|
||||
});
|
||||
|
||||
test("should handle undefined PREPARE_SUCCESS as success", () => {
|
||||
delete process.env.PREPARE_SUCCESS;
|
||||
delete process.env.PREPARE_ERROR;
|
||||
process.env.CLAUDE_SUCCESS = "true";
|
||||
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
|
||||
let actionFailed = false;
|
||||
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
} else {
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
|
||||
actionFailed = !claudeSuccess;
|
||||
}
|
||||
|
||||
expect(actionFailed).toBe(false);
|
||||
});
|
||||
|
||||
test("should detect cancellation when CLAUDE_CANCELLED is true", () => {
|
||||
process.env.PREPARE_SUCCESS = "true";
|
||||
process.env.CLAUDE_SUCCESS = "false";
|
||||
process.env.CLAUDE_CANCELLED = "true";
|
||||
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
const isCancelled = process.env.CLAUDE_CANCELLED === "true";
|
||||
|
||||
let actionFailed = false;
|
||||
let actionCancelled = false;
|
||||
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
} else if (isCancelled) {
|
||||
actionCancelled = true;
|
||||
} else {
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
|
||||
actionFailed = !claudeSuccess;
|
||||
}
|
||||
|
||||
expect(actionFailed).toBe(false);
|
||||
expect(actionCancelled).toBe(true);
|
||||
});
|
||||
|
||||
test("should not detect cancellation when CLAUDE_CANCELLED is false", () => {
|
||||
process.env.PREPARE_SUCCESS = "true";
|
||||
process.env.CLAUDE_SUCCESS = "false";
|
||||
process.env.CLAUDE_CANCELLED = "false";
|
||||
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
const isCancelled = process.env.CLAUDE_CANCELLED === "true";
|
||||
|
||||
let actionFailed = false;
|
||||
let actionCancelled = false;
|
||||
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
} else if (isCancelled) {
|
||||
actionCancelled = true;
|
||||
} else {
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
|
||||
actionFailed = !claudeSuccess;
|
||||
}
|
||||
|
||||
expect(actionFailed).toBe(true);
|
||||
expect(actionCancelled).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user