Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ inputs:
description: "Custom environment variables to pass to Claude Code execution (YAML format)"
required: false
default: ""
output_mode:
description: "Where to post the review. Comma-separated list. Options: pr_comment, commit_comment, stdout"
required: false
default: "pr_comment"
commit_sha:
description: "Specific commit SHA to comment on for commit_comment mode. Defaults to PR HEAD or github.sha"
required: false

# Auth configuration
anthropic_api_key:
Expand Down Expand Up @@ -106,6 +113,8 @@ runs:
MCP_CONFIG: ${{ inputs.mcp_config }}
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
GITHUB_RUN_ID: ${{ github.run_id }}
OUTPUT_MODE: ${{ inputs.output_mode }}
COMMIT_SHA: ${{ inputs.commit_sha }}

- name: Run Claude Code
id: claude-code
Expand Down Expand Up @@ -158,6 +167,7 @@ runs:
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
CLAUDE_COMMENT_ID: ${{ steps.prepare.outputs.claude_comment_id }}
OUTPUT_IDENTIFIERS: ${{ steps.prepare.outputs.output_identifiers }}
GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
Expand All @@ -170,6 +180,8 @@ runs:
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_ERROR: ${{ steps.prepare.outputs.prepare_error || '' }}
OUTPUT_MODE: ${{ inputs.output_mode }}
COMMIT_SHA: ${{ inputs.commit_sha }}

- name: Display Claude Code Report
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''
Expand Down
38 changes: 31 additions & 7 deletions src/entrypoints/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { setupGitHubToken } from "../github/token";
import { checkTriggerAction } from "../github/validation/trigger";
import { checkHumanActor } from "../github/validation/actor";
import { checkWritePermissions } from "../github/validation/permissions";
import { createInitialComment } from "../github/operations/comments/create-initial";
import { setupBranch } from "../github/operations/branch";
import { updateTrackingComment } from "../github/operations/comments/update-with-branch";
import { OutputManager } from "../output-manager";
import { prepareMcpConfig } from "../mcp/install-mcp-server";
import { createPrompt } from "../create-prompt";
import { createOctokit } from "../github/api/client";
Expand Down Expand Up @@ -50,8 +50,31 @@ async function run() {
// Step 5: Check if actor is human
await checkHumanActor(octokit.rest, context);

// Step 6: Create initial tracking comment
const commentId = await createInitialComment(octokit.rest, context);
// Step 6: Setup output manager and create initial tracking
const outputModes = OutputManager.parseOutputModes(
process.env.OUTPUT_MODE || "pr_comment",
);
const commitSha = process.env.COMMIT_SHA;
const outputManager = new OutputManager(
outputModes,
octokit.rest,
context,
commitSha,
);
const outputIdentifiers = await outputManager.createInitial(context);

// Output the identifiers for downstream steps
core.setOutput(
"output_identifiers",
outputManager.serializeIdentifiers(outputIdentifiers),
);

// Legacy support: output the primary identifier as claude_comment_id
const primaryIdentifier =
outputManager.getPrimaryIdentifier(outputIdentifiers);
if (primaryIdentifier) {
core.setOutput("claude_comment_id", primaryIdentifier);
}

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

// Step 9: Update initial comment with branch link (only for issues that created a new branch)
if (branchInfo.claudeBranch) {
// Note: This only applies to pr_comment strategy, others don't support updates
if (branchInfo.claudeBranch && outputIdentifiers.pr_comment) {
await updateTrackingComment(
octokit,
context,
commentId,
parseInt(outputIdentifiers.pr_comment),
branchInfo.claudeBranch,
);
}

// Step 10: Create prompt file
await createPrompt(
commentId,
primaryIdentifier ? parseInt(primaryIdentifier) : 0,
branchInfo.baseBranch,
branchInfo.claudeBranch,
githubData,
Expand All @@ -92,7 +116,7 @@ async function run() {
repo: context.repository.repo,
branch: branchInfo.currentBranch,
additionalMcpConfig,
claudeCommentId: commentId.toString(),
claudeCommentId: primaryIdentifier || "0",
allowedTools: context.inputs.allowedTools,
});
core.setOutput("mcp_config", mcpConfig);
Expand Down
160 changes: 68 additions & 92 deletions src/entrypoints/update-comment-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,91 +2,87 @@

import { createOctokit } from "../github/api/client";
import * as fs from "fs/promises";
import {
updateCommentBody,
type CommentUpdateInput,
} from "../github/operations/comment-logic";
import {
parseGitHubContext,
isPullRequestReviewCommentEvent,
} from "../github/context";
import { type ExecutionDetails } from "../github/operations/comment-logic";
import { parseGitHubContext } from "../github/context";
import { GITHUB_SERVER_URL } from "../github/api/config";
import { checkAndDeleteEmptyBranch } from "../github/operations/branch-cleanup";
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
import { OutputManager, type OutputIdentifiers } from "../output-manager";
import type { ReviewContent } from "../output-strategies/base";

async function run() {
try {
const commentId = parseInt(process.env.CLAUDE_COMMENT_ID!);
// Legacy fallback for claude_comment_id
const legacyCommentId = process.env.CLAUDE_COMMENT_ID;
const outputIdentifiersJson = process.env.OUTPUT_IDENTIFIERS;
const githubToken = process.env.GITHUB_TOKEN!;
const claudeBranch = process.env.CLAUDE_BRANCH;
const baseBranch = process.env.BASE_BRANCH || "main";
const triggerUsername = process.env.TRIGGER_USERNAME;
const outputModes = OutputManager.parseOutputModes(
process.env.OUTPUT_MODE || "pr_comment",
);
const commitSha = process.env.COMMIT_SHA;

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

// Parse output identifiers from prepare step or fall back to legacy
let outputIdentifiers: OutputIdentifiers;
if (outputIdentifiersJson) {
outputIdentifiers = OutputManager.deserializeIdentifiers(
outputIdentifiersJson,
);
} else if (legacyCommentId) {
// Legacy fallback - assume pr_comment mode
outputIdentifiers = { pr_comment: legacyCommentId };
} else {
outputIdentifiers = {};
}

// Create output manager for final update
const outputManager = new OutputManager(
outputModes,
octokit.rest,
context,
commitSha,
);

const serverUrl = GITHUB_SERVER_URL;
const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`;

let comment;
let isPRReviewComment = false;

try {
// GitHub has separate ID namespaces for review comments and issue comments
// We need to use the correct API based on the event type
if (isPullRequestReviewCommentEvent(context)) {
// For PR review comments, use the pulls API
console.log(`Fetching PR review comment ${commentId}`);
const { data: prComment } = await octokit.rest.pulls.getReviewComment({
owner,
repo,
comment_id: commentId,
});
comment = prComment;
isPRReviewComment = true;
console.log("Successfully fetched as PR review comment");
}

// For all other event types, use the issues API
if (!comment) {
console.log(`Fetching issue comment ${commentId}`);
const { data: issueComment } = await octokit.rest.issues.getComment({
owner,
repo,
comment_id: commentId,
});
comment = issueComment;
isPRReviewComment = false;
console.log("Successfully fetched as issue comment");
}
} catch (finalError) {
// If all attempts fail, try to determine more information about the comment
console.error("Failed to fetch comment. Debug info:");
console.error(`Comment ID: ${commentId}`);
console.error(`Event name: ${context.eventName}`);
console.error(`Entity number: ${context.entityNumber}`);
console.error(`Repository: ${context.repository.full_name}`);

// Try to get the PR info to understand the comment structure
// For legacy support, we still need to fetch the current body if we have a pr_comment identifier
let currentBody = "";
if (outputIdentifiers.pr_comment) {
try {
const { data: pr } = await octokit.rest.pulls.get({
owner,
repo,
pull_number: context.entityNumber,
});
console.log(`PR state: ${pr.state}`);
console.log(`PR comments count: ${pr.comments}`);
console.log(`PR review comments count: ${pr.review_comments}`);
} catch {
console.error("Could not fetch PR info for debugging");
const commentId = parseInt(outputIdentifiers.pr_comment);
// Try to fetch the current comment body for the update
try {
const { data: issueComment } = await octokit.rest.issues.getComment({
owner,
repo,
comment_id: commentId,
});
currentBody = issueComment.body ?? "";
} catch {
// If issue comment fails, try PR review comment
const { data: prComment } = await octokit.rest.pulls.getReviewComment(
{
owner,
repo,
comment_id: commentId,
},
);
currentBody = prComment.body ?? "";
}
} catch (error) {
console.warn(
"Could not fetch current comment body, proceeding with empty body:",
error,
);
}

throw finalError;
}

const currentBody = comment.body ?? "";

// Check if we need to add branch link for new branches
const { shouldDeleteBranch, branchLink } = await checkAndDeleteEmptyBranch(
octokit,
Expand Down Expand Up @@ -140,11 +136,7 @@ async function run() {
}

// Check if action failed and read output file for execution details
let executionDetails: {
cost_usd?: number;
duration_ms?: number;
duration_api_ms?: number;
} | null = null;
let executionDetails: ExecutionDetails | null = null;
let actionFailed = false;
let errorDetails: string | undefined;

Expand Down Expand Up @@ -190,9 +182,10 @@ async function run() {
}
}

// Prepare input for updateCommentBody function
const commentInput: CommentUpdateInput = {
currentBody,
// Prepare content for all output strategies
const reviewContent: ReviewContent = {
summary: actionFailed ? "Action failed" : "Action completed",
body: currentBody,
actionFailed,
executionDetails,
jobUrl,
Expand All @@ -203,26 +196,9 @@ async function run() {
errorDetails,
};

const updatedBody = updateCommentBody(commentInput);

try {
await updateClaudeComment(octokit.rest, {
owner,
repo,
commentId,
body: updatedBody,
isPullRequestReviewComment: isPRReviewComment,
});
console.log(
`✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} with job link`,
);
} catch (updateError) {
console.error(
`Failed to update ${isPRReviewComment ? "PR review" : "issue"} comment:`,
updateError,
);
throw updateError;
}
// Use OutputManager to update all configured output strategies
await outputManager.updateFinal(outputIdentifiers, context, reviewContent);
console.log("✅ Updated all configured output strategies");

process.exit(0);
} catch (error) {
Expand Down
Loading