Skip to content

Commit 255d102

Browse files
committed
support writing the review to other output locations
1 parent aec1c34 commit 255d102

File tree

8 files changed

+715
-94
lines changed

8 files changed

+715
-94
lines changed

action.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ inputs:
4545
description: "Custom environment variables to pass to Claude Code execution (YAML format)"
4646
required: false
4747
default: ""
48+
output_mode:
49+
description: "Where to post the review. Comma-separated list. Options: pr_comment, commit_comment, stdout"
50+
required: false
51+
default: "pr_comment"
52+
commit_sha:
53+
description: "Specific commit SHA to comment on for commit_comment mode. Defaults to PR HEAD or github.sha"
54+
required: false
4855

4956
# Auth configuration
5057
anthropic_api_key:
@@ -106,6 +113,8 @@ runs:
106113
MCP_CONFIG: ${{ inputs.mcp_config }}
107114
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
108115
GITHUB_RUN_ID: ${{ github.run_id }}
116+
OUTPUT_MODE: ${{ inputs.output_mode }}
117+
COMMIT_SHA: ${{ inputs.commit_sha }}
109118

110119
- name: Run Claude Code
111120
id: claude-code
@@ -158,6 +167,7 @@ runs:
158167
REPOSITORY: ${{ github.repository }}
159168
PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
160169
CLAUDE_COMMENT_ID: ${{ steps.prepare.outputs.claude_comment_id }}
170+
OUTPUT_IDENTIFIERS: ${{ steps.prepare.outputs.output_identifiers }}
161171
GITHUB_RUN_ID: ${{ github.run_id }}
162172
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
163173
GITHUB_EVENT_NAME: ${{ github.event_name }}
@@ -170,6 +180,8 @@ runs:
170180
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 || '' }}
171181
PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }}
172182
PREPARE_ERROR: ${{ steps.prepare.outputs.prepare_error || '' }}
183+
OUTPUT_MODE: ${{ inputs.output_mode }}
184+
COMMIT_SHA: ${{ inputs.commit_sha }}
173185

174186
- name: Display Claude Code Report
175187
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''

src/entrypoints/prepare.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import { setupGitHubToken } from "../github/token";
1010
import { checkTriggerAction } from "../github/validation/trigger";
1111
import { checkHumanActor } from "../github/validation/actor";
1212
import { checkWritePermissions } from "../github/validation/permissions";
13-
import { createInitialComment } from "../github/operations/comments/create-initial";
1413
import { setupBranch } from "../github/operations/branch";
1514
import { updateTrackingComment } from "../github/operations/comments/update-with-branch";
15+
import { OutputManager } from "../output-manager";
1616
import { prepareMcpConfig } from "../mcp/install-mcp-server";
1717
import { createPrompt } from "../create-prompt";
1818
import { createOctokit } from "../github/api/client";
@@ -50,8 +50,20 @@ async function run() {
5050
// Step 5: Check if actor is human
5151
await checkHumanActor(octokit.rest, context);
5252

53-
// Step 6: Create initial tracking comment
54-
const commentId = await createInitialComment(octokit.rest, context);
53+
// Step 6: Setup output manager and create initial tracking
54+
const outputModes = OutputManager.parseOutputModes(process.env.OUTPUT_MODE || "pr_comment");
55+
const commitSha = process.env.COMMIT_SHA;
56+
const outputManager = new OutputManager(outputModes, octokit.rest, context, commitSha);
57+
const outputIdentifiers = await outputManager.createInitial(context);
58+
59+
// Output the identifiers for downstream steps
60+
core.setOutput("output_identifiers", outputManager.serializeIdentifiers(outputIdentifiers));
61+
62+
// Legacy support: output the primary identifier as claude_comment_id
63+
const primaryIdentifier = outputManager.getPrimaryIdentifier(outputIdentifiers);
64+
if (primaryIdentifier) {
65+
core.setOutput("claude_comment_id", primaryIdentifier);
66+
}
5567

5668
// Step 7: Fetch GitHub data (once for both branch setup and prompt creation)
5769
const githubData = await fetchGitHubData({
@@ -66,18 +78,19 @@ async function run() {
6678
const branchInfo = await setupBranch(octokit, githubData, context);
6779

6880
// Step 9: Update initial comment with branch link (only for issues that created a new branch)
69-
if (branchInfo.claudeBranch) {
81+
// Note: This only applies to pr_comment strategy, others don't support updates
82+
if (branchInfo.claudeBranch && outputIdentifiers.pr_comment) {
7083
await updateTrackingComment(
7184
octokit,
7285
context,
73-
commentId,
86+
parseInt(outputIdentifiers.pr_comment),
7487
branchInfo.claudeBranch,
7588
);
7689
}
7790

7891
// Step 10: Create prompt file
7992
await createPrompt(
80-
commentId,
93+
primaryIdentifier ? parseInt(primaryIdentifier) : 0,
8194
branchInfo.baseBranch,
8295
branchInfo.claudeBranch,
8396
githubData,
@@ -92,7 +105,7 @@ async function run() {
92105
repo: context.repository.repo,
93106
branch: branchInfo.currentBranch,
94107
additionalMcpConfig,
95-
claudeCommentId: commentId.toString(),
108+
claudeCommentId: primaryIdentifier || "0",
96109
allowedTools: context.inputs.allowedTools,
97110
});
98111
core.setOutput("mcp_config", mcpConfig);

src/entrypoints/update-comment-link.ts

Lines changed: 53 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -3,90 +3,76 @@
33
import { createOctokit } from "../github/api/client";
44
import * as fs from "fs/promises";
55
import {
6-
updateCommentBody,
7-
type CommentUpdateInput,
6+
type ExecutionDetails,
87
} from "../github/operations/comment-logic";
98
import {
109
parseGitHubContext,
11-
isPullRequestReviewCommentEvent,
1210
} from "../github/context";
1311
import { GITHUB_SERVER_URL } from "../github/api/config";
1412
import { checkAndDeleteEmptyBranch } from "../github/operations/branch-cleanup";
15-
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
13+
import { OutputManager, type OutputIdentifiers } from "../output-manager";
14+
import type { ReviewContent } from "../output-strategies/base";
1615

1716
async function run() {
1817
try {
19-
const commentId = parseInt(process.env.CLAUDE_COMMENT_ID!);
18+
// Legacy fallback for claude_comment_id
19+
const legacyCommentId = process.env.CLAUDE_COMMENT_ID;
20+
const outputIdentifiersJson = process.env.OUTPUT_IDENTIFIERS;
2021
const githubToken = process.env.GITHUB_TOKEN!;
2122
const claudeBranch = process.env.CLAUDE_BRANCH;
2223
const baseBranch = process.env.BASE_BRANCH || "main";
2324
const triggerUsername = process.env.TRIGGER_USERNAME;
25+
const outputModes = OutputManager.parseOutputModes(process.env.OUTPUT_MODE || "pr_comment");
26+
const commitSha = process.env.COMMIT_SHA;
2427

2528
const context = parseGitHubContext();
2629
const { owner, repo } = context.repository;
2730
const octokit = createOctokit(githubToken);
2831

32+
// Parse output identifiers from prepare step or fall back to legacy
33+
let outputIdentifiers: OutputIdentifiers;
34+
if (outputIdentifiersJson) {
35+
outputIdentifiers = OutputManager.deserializeIdentifiers(outputIdentifiersJson);
36+
} else if (legacyCommentId) {
37+
// Legacy fallback - assume pr_comment mode
38+
outputIdentifiers = { pr_comment: legacyCommentId };
39+
} else {
40+
outputIdentifiers = {};
41+
}
42+
43+
// Create output manager for final update
44+
const outputManager = new OutputManager(outputModes, octokit.rest, context, commitSha);
45+
2946
const serverUrl = GITHUB_SERVER_URL;
3047
const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`;
3148

32-
let comment;
33-
let isPRReviewComment = false;
34-
35-
try {
36-
// GitHub has separate ID namespaces for review comments and issue comments
37-
// We need to use the correct API based on the event type
38-
if (isPullRequestReviewCommentEvent(context)) {
39-
// For PR review comments, use the pulls API
40-
console.log(`Fetching PR review comment ${commentId}`);
41-
const { data: prComment } = await octokit.rest.pulls.getReviewComment({
42-
owner,
43-
repo,
44-
comment_id: commentId,
45-
});
46-
comment = prComment;
47-
isPRReviewComment = true;
48-
console.log("Successfully fetched as PR review comment");
49-
}
50-
51-
// For all other event types, use the issues API
52-
if (!comment) {
53-
console.log(`Fetching issue comment ${commentId}`);
54-
const { data: issueComment } = await octokit.rest.issues.getComment({
55-
owner,
56-
repo,
57-
comment_id: commentId,
58-
});
59-
comment = issueComment;
60-
isPRReviewComment = false;
61-
console.log("Successfully fetched as issue comment");
62-
}
63-
} catch (finalError) {
64-
// If all attempts fail, try to determine more information about the comment
65-
console.error("Failed to fetch comment. Debug info:");
66-
console.error(`Comment ID: ${commentId}`);
67-
console.error(`Event name: ${context.eventName}`);
68-
console.error(`Entity number: ${context.entityNumber}`);
69-
console.error(`Repository: ${context.repository.full_name}`);
70-
71-
// Try to get the PR info to understand the comment structure
49+
// For legacy support, we still need to fetch the current body if we have a pr_comment identifier
50+
let currentBody = "";
51+
if (outputIdentifiers.pr_comment) {
7252
try {
73-
const { data: pr } = await octokit.rest.pulls.get({
74-
owner,
75-
repo,
76-
pull_number: context.entityNumber,
77-
});
78-
console.log(`PR state: ${pr.state}`);
79-
console.log(`PR comments count: ${pr.comments}`);
80-
console.log(`PR review comments count: ${pr.review_comments}`);
81-
} catch {
82-
console.error("Could not fetch PR info for debugging");
53+
const commentId = parseInt(outputIdentifiers.pr_comment);
54+
// Try to fetch the current comment body for the update
55+
try {
56+
const { data: issueComment } = await octokit.rest.issues.getComment({
57+
owner,
58+
repo,
59+
comment_id: commentId,
60+
});
61+
currentBody = issueComment.body ?? "";
62+
} catch {
63+
// If issue comment fails, try PR review comment
64+
const { data: prComment } = await octokit.rest.pulls.getReviewComment({
65+
owner,
66+
repo,
67+
comment_id: commentId,
68+
});
69+
currentBody = prComment.body ?? "";
70+
}
71+
} catch (error) {
72+
console.warn("Could not fetch current comment body, proceeding with empty body:", error);
8373
}
84-
85-
throw finalError;
8674
}
8775

88-
const currentBody = comment.body ?? "";
89-
9076
// Check if we need to add branch link for new branches
9177
const { shouldDeleteBranch, branchLink } = await checkAndDeleteEmptyBranch(
9278
octokit,
@@ -140,11 +126,7 @@ async function run() {
140126
}
141127

142128
// Check if action failed and read output file for execution details
143-
let executionDetails: {
144-
cost_usd?: number;
145-
duration_ms?: number;
146-
duration_api_ms?: number;
147-
} | null = null;
129+
let executionDetails: ExecutionDetails | null = null;
148130
let actionFailed = false;
149131
let errorDetails: string | undefined;
150132

@@ -190,9 +172,10 @@ async function run() {
190172
}
191173
}
192174

193-
// Prepare input for updateCommentBody function
194-
const commentInput: CommentUpdateInput = {
195-
currentBody,
175+
// Prepare content for all output strategies
176+
const reviewContent: ReviewContent = {
177+
summary: actionFailed ? "Action failed" : "Action completed",
178+
body: currentBody,
196179
actionFailed,
197180
executionDetails,
198181
jobUrl,
@@ -203,26 +186,9 @@ async function run() {
203186
errorDetails,
204187
};
205188

206-
const updatedBody = updateCommentBody(commentInput);
207-
208-
try {
209-
await updateClaudeComment(octokit.rest, {
210-
owner,
211-
repo,
212-
commentId,
213-
body: updatedBody,
214-
isPullRequestReviewComment: isPRReviewComment,
215-
});
216-
console.log(
217-
`✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} with job link`,
218-
);
219-
} catch (updateError) {
220-
console.error(
221-
`Failed to update ${isPRReviewComment ? "PR review" : "issue"} comment:`,
222-
updateError,
223-
);
224-
throw updateError;
225-
}
189+
// Use OutputManager to update all configured output strategies
190+
await outputManager.updateFinal(outputIdentifiers, context, reviewContent);
191+
console.log("✅ Updated all configured output strategies");
226192

227193
process.exit(0);
228194
} catch (error) {

0 commit comments

Comments
 (0)