Skip to content

Commit 365ecec

Browse files
authored
Merge pull request #2 from classdojo/andrew/multiple-outputs
support writing the review to other output locations
2 parents aec1c34 + d78ddfd commit 365ecec

File tree

8 files changed

+799
-99
lines changed

8 files changed

+799
-99
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: 31 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,31 @@ 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(
55+
process.env.OUTPUT_MODE || "pr_comment",
56+
);
57+
const commitSha = process.env.COMMIT_SHA;
58+
const outputManager = new OutputManager(
59+
outputModes,
60+
octokit.rest,
61+
context,
62+
commitSha,
63+
);
64+
const outputIdentifiers = await outputManager.createInitial(context);
65+
66+
// Output the identifiers for downstream steps
67+
core.setOutput(
68+
"output_identifiers",
69+
outputManager.serializeIdentifiers(outputIdentifiers),
70+
);
71+
72+
// Legacy support: output the primary identifier as claude_comment_id
73+
const primaryIdentifier =
74+
outputManager.getPrimaryIdentifier(outputIdentifiers);
75+
if (primaryIdentifier) {
76+
core.setOutput("claude_comment_id", primaryIdentifier);
77+
}
5578

5679
// Step 7: Fetch GitHub data (once for both branch setup and prompt creation)
5780
const githubData = await fetchGitHubData({
@@ -66,18 +89,19 @@ async function run() {
6689
const branchInfo = await setupBranch(octokit, githubData, context);
6790

6891
// Step 9: Update initial comment with branch link (only for issues that created a new branch)
69-
if (branchInfo.claudeBranch) {
92+
// Note: This only applies to pr_comment strategy, others don't support updates
93+
if (branchInfo.claudeBranch && outputIdentifiers.pr_comment) {
7094
await updateTrackingComment(
7195
octokit,
7296
context,
73-
commentId,
97+
parseInt(outputIdentifiers.pr_comment),
7498
branchInfo.claudeBranch,
7599
);
76100
}
77101

78102
// Step 10: Create prompt file
79103
await createPrompt(
80-
commentId,
104+
primaryIdentifier ? parseInt(primaryIdentifier) : 0,
81105
branchInfo.baseBranch,
82106
branchInfo.claudeBranch,
83107
githubData,
@@ -92,7 +116,7 @@ async function run() {
92116
repo: context.repository.repo,
93117
branch: branchInfo.currentBranch,
94118
additionalMcpConfig,
95-
claudeCommentId: commentId.toString(),
119+
claudeCommentId: primaryIdentifier || "0",
96120
allowedTools: context.inputs.allowedTools,
97121
});
98122
core.setOutput("mcp_config", mcpConfig);

src/entrypoints/update-comment-link.ts

Lines changed: 68 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -2,91 +2,87 @@
22

33
import { createOctokit } from "../github/api/client";
44
import * as fs from "fs/promises";
5-
import {
6-
updateCommentBody,
7-
type CommentUpdateInput,
8-
} from "../github/operations/comment-logic";
9-
import {
10-
parseGitHubContext,
11-
isPullRequestReviewCommentEvent,
12-
} from "../github/context";
5+
import { type ExecutionDetails } from "../github/operations/comment-logic";
6+
import { parseGitHubContext } from "../github/context";
137
import { GITHUB_SERVER_URL } from "../github/api/config";
148
import { checkAndDeleteEmptyBranch } from "../github/operations/branch-cleanup";
15-
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
9+
import { OutputManager, type OutputIdentifiers } from "../output-manager";
10+
import type { ReviewContent } from "../output-strategies/base";
1611

1712
async function run() {
1813
try {
19-
const commentId = parseInt(process.env.CLAUDE_COMMENT_ID!);
14+
// Legacy fallback for claude_comment_id
15+
const legacyCommentId = process.env.CLAUDE_COMMENT_ID;
16+
const outputIdentifiersJson = process.env.OUTPUT_IDENTIFIERS;
2017
const githubToken = process.env.GITHUB_TOKEN!;
2118
const claudeBranch = process.env.CLAUDE_BRANCH;
2219
const baseBranch = process.env.BASE_BRANCH || "main";
2320
const triggerUsername = process.env.TRIGGER_USERNAME;
21+
const outputModes = OutputManager.parseOutputModes(
22+
process.env.OUTPUT_MODE || "pr_comment",
23+
);
24+
const commitSha = process.env.COMMIT_SHA;
2425

2526
const context = parseGitHubContext();
2627
const { owner, repo } = context.repository;
2728
const octokit = createOctokit(githubToken);
2829

30+
// Parse output identifiers from prepare step or fall back to legacy
31+
let outputIdentifiers: OutputIdentifiers;
32+
if (outputIdentifiersJson) {
33+
outputIdentifiers = OutputManager.deserializeIdentifiers(
34+
outputIdentifiersJson,
35+
);
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(
45+
outputModes,
46+
octokit.rest,
47+
context,
48+
commitSha,
49+
);
50+
2951
const serverUrl = GITHUB_SERVER_URL;
3052
const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`;
3153

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
54+
// For legacy support, we still need to fetch the current body if we have a pr_comment identifier
55+
let currentBody = "";
56+
if (outputIdentifiers.pr_comment) {
7257
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");
58+
const commentId = parseInt(outputIdentifiers.pr_comment);
59+
// Try to fetch the current comment body for the update
60+
try {
61+
const { data: issueComment } = await octokit.rest.issues.getComment({
62+
owner,
63+
repo,
64+
comment_id: commentId,
65+
});
66+
currentBody = issueComment.body ?? "";
67+
} catch {
68+
// If issue comment fails, try PR review comment
69+
const { data: prComment } = await octokit.rest.pulls.getReviewComment(
70+
{
71+
owner,
72+
repo,
73+
comment_id: commentId,
74+
},
75+
);
76+
currentBody = prComment.body ?? "";
77+
}
78+
} catch (error) {
79+
console.warn(
80+
"Could not fetch current comment body, proceeding with empty body:",
81+
error,
82+
);
8383
}
84-
85-
throw finalError;
8684
}
8785

88-
const currentBody = comment.body ?? "";
89-
9086
// Check if we need to add branch link for new branches
9187
const { shouldDeleteBranch, branchLink } = await checkAndDeleteEmptyBranch(
9288
octokit,
@@ -140,11 +136,7 @@ async function run() {
140136
}
141137

142138
// 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;
139+
let executionDetails: ExecutionDetails | null = null;
148140
let actionFailed = false;
149141
let errorDetails: string | undefined;
150142

@@ -190,9 +182,10 @@ async function run() {
190182
}
191183
}
192184

193-
// Prepare input for updateCommentBody function
194-
const commentInput: CommentUpdateInput = {
195-
currentBody,
185+
// Prepare content for all output strategies
186+
const reviewContent: ReviewContent = {
187+
summary: actionFailed ? "Action failed" : "Action completed",
188+
body: currentBody,
196189
actionFailed,
197190
executionDetails,
198191
jobUrl,
@@ -203,26 +196,9 @@ async function run() {
203196
errorDetails,
204197
};
205198

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-
}
199+
// Use OutputManager to update all configured output strategies
200+
await outputManager.updateFinal(outputIdentifiers, context, reviewContent);
201+
console.log("✅ Updated all configured output strategies");
226202

227203
process.exit(0);
228204
} catch (error) {

0 commit comments

Comments
 (0)