mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-24 15:34:13 +08:00
Compare commits
1 Commits
claude/iss
...
ashwin/tri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e35c991da9 |
@@ -160,7 +160,6 @@ runs:
|
|||||||
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' }}
|
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' }}
|
||||||
BASE_BRANCH: ${{ steps.prepare.outputs.BASE_BRANCH }}
|
BASE_BRANCH: ${{ steps.prepare.outputs.BASE_BRANCH }}
|
||||||
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
|
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
|
||||||
CLAUDE_CANCELLED: ${{ cancelled() }}
|
|
||||||
OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }}
|
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 || '' }}
|
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' }}
|
PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }}
|
||||||
|
|||||||
@@ -146,12 +146,8 @@ async function run() {
|
|||||||
duration_api_ms?: number;
|
duration_api_ms?: number;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
let actionFailed = false;
|
let actionFailed = false;
|
||||||
let actionCancelled = false;
|
|
||||||
let errorDetails: string | undefined;
|
let errorDetails: string | undefined;
|
||||||
|
|
||||||
// Check if the workflow was cancelled
|
|
||||||
const isCancelled = process.env.CLAUDE_CANCELLED === "true";
|
|
||||||
|
|
||||||
// First check if prepare step failed
|
// First check if prepare step failed
|
||||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||||
const prepareError = process.env.PREPARE_ERROR;
|
const prepareError = process.env.PREPARE_ERROR;
|
||||||
@@ -159,18 +155,8 @@ async function run() {
|
|||||||
if (!prepareSuccess && prepareError) {
|
if (!prepareSuccess && prepareError) {
|
||||||
actionFailed = true;
|
actionFailed = true;
|
||||||
errorDetails = prepareError;
|
errorDetails = prepareError;
|
||||||
} else if (isCancelled) {
|
|
||||||
// If the workflow was cancelled, set the cancelled flag
|
|
||||||
actionCancelled = true;
|
|
||||||
} else {
|
} else {
|
||||||
// Check if the Claude action failed
|
// Check for existence of output file and parse it if available
|
||||||
// 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 {
|
try {
|
||||||
const outputFile = process.env.OUTPUT_FILE;
|
const outputFile = process.env.OUTPUT_FILE;
|
||||||
if (outputFile) {
|
if (outputFile) {
|
||||||
@@ -193,10 +179,14 @@ async function run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the Claude action failed
|
||||||
|
const claudeSuccess = process.env.CLAUDE_SUCCESS !== "false";
|
||||||
|
actionFailed = !claudeSuccess;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error reading output file:", error);
|
console.error("Error reading output file:", error);
|
||||||
// Error reading output file doesn't change the action status
|
// If we can't read the file, check for any failure markers
|
||||||
// We already determined actionFailed based on CLAUDE_SUCCESS
|
actionFailed = process.env.CLAUDE_SUCCESS === "false";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +194,6 @@ async function run() {
|
|||||||
const commentInput: CommentUpdateInput = {
|
const commentInput: CommentUpdateInput = {
|
||||||
currentBody,
|
currentBody,
|
||||||
actionFailed,
|
actionFailed,
|
||||||
actionCancelled,
|
|
||||||
executionDetails,
|
executionDetails,
|
||||||
jobUrl,
|
jobUrl,
|
||||||
branchLink,
|
branchLink,
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
import type { Octokits } from "../api/client";
|
|
||||||
import { ISSUE_QUERY, PR_QUERY } from "../api/queries/github";
|
|
||||||
import type {
|
import type {
|
||||||
|
GitHubPullRequest,
|
||||||
|
GitHubIssue,
|
||||||
GitHubComment,
|
GitHubComment,
|
||||||
GitHubFile,
|
GitHubFile,
|
||||||
GitHubIssue,
|
|
||||||
GitHubPullRequest,
|
|
||||||
GitHubReview,
|
GitHubReview,
|
||||||
IssueQueryResponse,
|
|
||||||
PullRequestQueryResponse,
|
PullRequestQueryResponse,
|
||||||
|
IssueQueryResponse,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import type { CommentWithImages } from "../utils/image-downloader";
|
import { PR_QUERY, ISSUE_QUERY } from "../api/queries/github";
|
||||||
|
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";
|
||||||
|
|
||||||
type FetchDataParams = {
|
type FetchDataParams = {
|
||||||
octokits: Octokits;
|
octokits: Octokits;
|
||||||
@@ -101,14 +101,6 @@ export async function fetchGitHubData({
|
|||||||
let changedFilesWithSHA: GitHubFileWithSHA[] = [];
|
let changedFilesWithSHA: GitHubFileWithSHA[] = [];
|
||||||
if (isPR && changedFiles.length > 0) {
|
if (isPR && changedFiles.length > 0) {
|
||||||
changedFilesWithSHA = changedFiles.map((file) => {
|
changedFilesWithSHA = changedFiles.map((file) => {
|
||||||
// Don't compute SHA for deleted files
|
|
||||||
if (file.changeType === "DELETED") {
|
|
||||||
return {
|
|
||||||
...file,
|
|
||||||
sha: "deleted",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use git hash-object to compute the SHA for the current file content
|
// Use git hash-object to compute the SHA for the current file content
|
||||||
const sha = execSync(`git hash-object "${file.path}"`, {
|
const sha = execSync(`git hash-object "${file.path}"`, {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export type ExecutionDetails = {
|
|||||||
export type CommentUpdateInput = {
|
export type CommentUpdateInput = {
|
||||||
currentBody: string;
|
currentBody: string;
|
||||||
actionFailed: boolean;
|
actionFailed: boolean;
|
||||||
actionCancelled?: boolean;
|
|
||||||
executionDetails: ExecutionDetails | null;
|
executionDetails: ExecutionDetails | null;
|
||||||
jobUrl: string;
|
jobUrl: string;
|
||||||
branchLink?: string;
|
branchLink?: string;
|
||||||
@@ -75,7 +74,6 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
|||||||
branchLink,
|
branchLink,
|
||||||
prLink,
|
prLink,
|
||||||
actionFailed,
|
actionFailed,
|
||||||
actionCancelled,
|
|
||||||
branchName,
|
branchName,
|
||||||
triggerUsername,
|
triggerUsername,
|
||||||
errorDetails,
|
errorDetails,
|
||||||
@@ -114,13 +112,7 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
|||||||
// Build the header
|
// Build the header
|
||||||
let header = "";
|
let header = "";
|
||||||
|
|
||||||
if (actionCancelled) {
|
if (actionFailed) {
|
||||||
header = "**Claude's task was cancelled";
|
|
||||||
if (durationStr) {
|
|
||||||
header += ` after ${durationStr}`;
|
|
||||||
}
|
|
||||||
header += "**";
|
|
||||||
} else if (actionFailed) {
|
|
||||||
header = "**Claude encountered an error";
|
header = "**Claude encountered an error";
|
||||||
if (durationStr) {
|
if (durationStr) {
|
||||||
header += ` after ${durationStr}`;
|
header += ` after ${durationStr}`;
|
||||||
|
|||||||
@@ -418,48 +418,4 @@ 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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
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