mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 06:54:13 +08:00
Compare commits
2 Commits
v0.0.46
...
claude/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d100f7832 | ||
|
|
e0dcb85d34 |
@@ -160,6 +160,7 @@ 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,8 +146,12 @@ 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;
|
||||||
@@ -155,8 +159,18 @@ 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 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 {
|
try {
|
||||||
const outputFile = process.env.OUTPUT_FILE;
|
const outputFile = process.env.OUTPUT_FILE;
|
||||||
if (outputFile) {
|
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) {
|
} catch (error) {
|
||||||
console.error("Error reading output file:", error);
|
console.error("Error reading output file:", error);
|
||||||
// If we can't read the file, check for any failure markers
|
// Error reading output file doesn't change the action status
|
||||||
actionFailed = process.env.CLAUDE_SUCCESS === "false";
|
// We already determined actionFailed based on CLAUDE_SUCCESS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +204,7 @@ async function run() {
|
|||||||
const commentInput: CommentUpdateInput = {
|
const commentInput: CommentUpdateInput = {
|
||||||
currentBody,
|
currentBody,
|
||||||
actionFailed,
|
actionFailed,
|
||||||
|
actionCancelled,
|
||||||
executionDetails,
|
executionDetails,
|
||||||
jobUrl,
|
jobUrl,
|
||||||
branchLink,
|
branchLink,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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;
|
||||||
@@ -74,6 +75,7 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
|||||||
branchLink,
|
branchLink,
|
||||||
prLink,
|
prLink,
|
||||||
actionFailed,
|
actionFailed,
|
||||||
|
actionCancelled,
|
||||||
branchName,
|
branchName,
|
||||||
triggerUsername,
|
triggerUsername,
|
||||||
errorDetails,
|
errorDetails,
|
||||||
@@ -112,7 +114,13 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
|||||||
// Build the header
|
// Build the header
|
||||||
let 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";
|
header = "**Claude encountered an error";
|
||||||
if (durationStr) {
|
if (durationStr) {
|
||||||
header += ` after ${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