diff --git a/action.yml b/action.yml index 80651fe..885bf11 100644 --- a/action.yml +++ b/action.yml @@ -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' }} diff --git a/src/entrypoints/update-comment-link.ts b/src/entrypoints/update-comment-link.ts index 668fe64..34c7e7b 100644 --- a/src/entrypoints/update-comment-link.ts +++ b/src/entrypoints/update-comment-link.ts @@ -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,6 +159,9 @@ 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 if the Claude action failed // CLAUDE_SUCCESS is set to the result of: steps.claude-code.outputs.conclusion == 'success' @@ -197,6 +204,7 @@ async function run() { const commentInput: CommentUpdateInput = { currentBody, actionFailed, + actionCancelled, executionDetails, jobUrl, branchLink, diff --git a/src/github/operations/comment-logic.ts b/src/github/operations/comment-logic.ts index 6a4551a..f118374 100644 --- a/src/github/operations/comment-logic.ts +++ b/src/github/operations/comment-logic.ts @@ -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}`; diff --git a/test/comment-logic.test.ts b/test/comment-logic.test.ts index 82fec08..168e8c9 100644 --- a/test/comment-logic.test.ts +++ b/test/comment-logic.test.ts @@ -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"); + }); + }); }); diff --git a/test/update-comment-link-logic.test.ts b/test/update-comment-link-logic.test.ts index 5df2790..8784287 100644 --- a/test/update-comment-link-logic.test.ts +++ b/test/update-comment-link-logic.test.ts @@ -14,18 +14,18 @@ describe("update-comment-link workflow status detection", () => { 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"); }); @@ -33,50 +33,50 @@ describe("update-comment-link workflow status detection", () => { 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 { @@ -84,7 +84,7 @@ describe("update-comment-link workflow status detection", () => { const claudeSuccess = process.env.CLAUDE_SUCCESS === "true"; actionFailed = !claudeSuccess; } - + expect(actionFailed).toBe(true); }); @@ -92,19 +92,69 @@ describe("update-comment-link workflow status detection", () => { 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); }); -}); \ No newline at end of file + + 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); + }); +});