refactor: unify action into single composite step with run.ts entrypoint

Consolidate the prepare and base-action phases into a single composite
step that runs src/entrypoints/run.ts. This simplifies the action.yml
from multiple steps to one execution step, while keeping the same
behavior.

Key changes:
- Add src/entrypoints/run.ts as unified entrypoint
- Simplify action.yml to single 'Run Claude Code Action' step
- Pass all inputs via environment variables
- Update base-action to accept inputs via env vars
- Support agent mode auto-detection from prompt input
This commit is contained in:
Ashwin Bhat 2026-02-03 18:31:12 -08:00
parent 6867bb3ab0
commit 3d8ead909f
No known key found for this signature in database
16 changed files with 709 additions and 413 deletions

View File

@ -137,19 +137,19 @@ inputs:
outputs:
execution_file:
description: "Path to the Claude Code execution output file"
value: ${{ steps.claude-code.outputs.execution_file }}
value: ${{ steps.run.outputs.execution_file }}
branch_name:
description: "The branch created by Claude Code for this execution"
value: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
value: ${{ steps.run.outputs.branch_name }}
github_token:
description: "The GitHub token used by the action (Claude App token if available)"
value: ${{ steps.prepare.outputs.github_token }}
value: ${{ steps.run.outputs.github_token }}
structured_output:
description: "JSON string containing all structured output fields when --json-schema is provided in claude_args. Use fromJSON() to parse: fromJSON(steps.id.outputs.structured_output).field_name"
value: ${{ steps.claude-code.outputs.structured_output }}
value: ${{ steps.run.outputs.structured_output }}
session_id:
description: "The Claude Code session ID that can be used with --resume to continue this conversation"
value: ${{ steps.claude-code.outputs.session_id }}
value: ${{ steps.run.outputs.session_id }}
runs:
using: "composite"
@ -178,12 +178,13 @@ runs:
cd ${GITHUB_ACTION_PATH}
bun install
- name: Prepare action
id: prepare
- name: Run Claude Code Action
id: run
shell: bash
run: |
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/prepare.ts
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/run.ts
env:
# Prepare inputs
MODE: ${{ inputs.mode }}
PROMPT: ${{ inputs.prompt }}
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
@ -210,73 +211,19 @@ runs:
CLAUDE_ARGS: ${{ inputs.claude_args }}
ALL_INPUTS: ${{ toJson(inputs) }}
- name: Install Base Action Dependencies
if: steps.prepare.outputs.contains_trigger == 'true'
shell: bash
env:
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
run: |
echo "Installing base-action dependencies..."
cd ${GITHUB_ACTION_PATH}/base-action
bun install
echo "Base-action dependencies installed"
cd -
# Install Claude Code if no custom executable is provided
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
CLAUDE_CODE_VERSION="2.1.31"
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
for attempt in 1 2 3; do
echo "Installation attempt $attempt..."
if command -v timeout &> /dev/null; then
# Use --foreground to kill entire process group on timeout, --kill-after to send SIGKILL if SIGTERM fails
timeout --foreground --kill-after=10 120 bash -c "curl -fsSL https://claude.ai/install.sh | bash -s -- $CLAUDE_CODE_VERSION" && break
else
curl -fsSL https://claude.ai/install.sh | bash -s -- "$CLAUDE_CODE_VERSION" && break
fi
if [ $attempt -eq 3 ]; then
echo "Failed to install Claude Code after 3 attempts"
exit 1
fi
echo "Installation failed, retrying..."
sleep 5
done
echo "Claude Code installed successfully"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
else
echo "Using custom Claude Code executable: $PATH_TO_CLAUDE_CODE_EXECUTABLE"
# Add the directory containing the custom executable to PATH
CLAUDE_DIR=$(dirname "$PATH_TO_CLAUDE_CODE_EXECUTABLE")
echo "$CLAUDE_DIR" >> "$GITHUB_PATH"
fi
- name: Run Claude Code
id: claude-code
if: steps.prepare.outputs.contains_trigger == 'true'
shell: bash
run: |
# Run the base-action
bun run ${GITHUB_ACTION_PATH}/base-action/src/index.ts
env:
# Base-action inputs
CLAUDE_CODE_ACTION: "1"
INPUT_PROMPT_FILE: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
INPUT_SETTINGS: ${{ inputs.settings }}
INPUT_CLAUDE_ARGS: ${{ steps.prepare.outputs.claude_args }}
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }}
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }}
INPUT_PLUGINS: ${{ inputs.plugins }}
INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }}
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
# Model configuration
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
GH_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
NODE_VERSION: ${{ env.NODE_VERSION }}
DETAILED_PERMISSION_MESSAGES: "1"
# Provider configuration
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
@ -323,63 +270,3 @@ runs:
OTEL_METRIC_EXPORT_INTERVAL: ${{ env.OTEL_METRIC_EXPORT_INTERVAL }}
OTEL_LOGS_EXPORT_INTERVAL: ${{ env.OTEL_LOGS_EXPORT_INTERVAL }}
OTEL_RESOURCE_ATTRIBUTES: ${{ env.OTEL_RESOURCE_ATTRIBUTES }}
- name: Update comment with job link
if: steps.prepare.outputs.contains_trigger == 'true' && steps.prepare.outputs.claude_comment_id && always()
shell: bash
run: |
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/update-comment-link.ts
env:
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
CLAUDE_COMMENT_ID: ${{ steps.prepare.outputs.claude_comment_id }}
GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
GH_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
TRIGGER_COMMENT_ID: ${{ github.event.comment.id }}
CLAUDE_BRANCH: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review_comment' }}
BASE_BRANCH: ${{ steps.prepare.outputs.BASE_BRANCH }}
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
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' }}
PREPARE_ERROR: ${{ steps.prepare.outputs.prepare_error || '' }}
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
TRACK_PROGRESS: ${{ inputs.track_progress }}
- name: Display Claude Code Report
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''
shell: bash
run: |
# Try to format the turns, but if it fails, dump the raw JSON
if bun run ${{ github.action_path }}/src/entrypoints/format-turns.ts "${{ steps.claude-code.outputs.execution_file }}" >> $GITHUB_STEP_SUMMARY 2>/dev/null; then
echo "Successfully formatted Claude Code report"
else
echo "## Claude Code Report (Raw Output)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Failed to format output (please report). Here's the raw JSON:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
cat "${{ steps.claude-code.outputs.execution_file }}" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
- name: Cleanup SSH signing key
if: always() && inputs.ssh_signing_key != ''
shell: bash
run: |
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/cleanup-ssh-signing.ts
- name: Revoke app token
if: always() && inputs.github_token == '' && steps.prepare.outputs.skipped_due_to_workflow_validation_mismatch != 'true'
shell: bash
run: |
curl -L \
-X DELETE \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ steps.prepare.outputs.GITHUB_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
${GITHUB_API_URL:-https://api.github.com}/installation/token

View File

@ -28,7 +28,7 @@ async function run() {
promptFile: process.env.INPUT_PROMPT_FILE || "",
});
await runClaude(promptConfig.path, {
const result = await runClaude(promptConfig.path, {
claudeArgs: process.env.INPUT_CLAUDE_ARGS,
allowedTools: process.env.INPUT_ALLOWED_TOOLS,
disallowedTools: process.env.INPUT_DISALLOWED_TOOLS,
@ -42,6 +42,18 @@ async function run() {
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT,
});
// Set outputs for the standalone base-action
core.setOutput("conclusion", result.conclusion);
if (result.executionFile) {
core.setOutput("execution_file", result.executionFile);
}
if (result.sessionId) {
core.setOutput("session_id", result.sessionId);
}
if (result.structuredOutput) {
core.setOutput("structured_output", result.structuredOutput);
}
} catch (error) {
core.setFailed(`Action failed with error: ${error}`);
core.setOutput("conclusion", "failure");

View File

@ -9,6 +9,13 @@ import type {
} from "@anthropic-ai/claude-agent-sdk";
import type { ParsedSdkOptions } from "./parse-sdk-options";
export type ClaudeRunResult = {
executionFile?: string;
sessionId?: string;
conclusion: "success" | "failure";
structuredOutput?: string;
};
const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`;
/** Filename for the user request file, written by prompt generation */
@ -129,7 +136,7 @@ function sanitizeSdkOutput(
export async function runClaudeWithSdk(
promptPath: string,
{ sdkOptions, showFullOutput, hasJsonSchema }: ParsedSdkOptions,
): Promise<void> {
): Promise<ClaudeRunResult> {
// Create prompt configuration - may be a string or multi-block message
const prompt = await createPromptConfig(promptPath, showFullOutput);
@ -165,36 +172,38 @@ export async function runClaudeWithSdk(
}
} catch (error) {
console.error("SDK execution error:", error);
core.setOutput("conclusion", "failure");
process.exit(1);
throw new Error(`SDK execution error: ${error}`);
}
const result: ClaudeRunResult = {
conclusion: "failure",
};
// Write execution file
try {
await writeFile(EXECUTION_FILE, JSON.stringify(messages, null, 2));
console.log(`Log saved to ${EXECUTION_FILE}`);
core.setOutput("execution_file", EXECUTION_FILE);
result.executionFile = EXECUTION_FILE;
} catch (error) {
core.warning(`Failed to write execution file: ${error}`);
}
// Extract and set session_id from system.init message
// Extract session_id from system.init message
const initMessage = messages.find(
(m) => m.type === "system" && "subtype" in m && m.subtype === "init",
);
if (initMessage && "session_id" in initMessage && initMessage.session_id) {
core.setOutput("session_id", initMessage.session_id);
core.info(`Set session_id: ${initMessage.session_id}`);
result.sessionId = initMessage.session_id as string;
core.info(`Set session_id: ${result.sessionId}`);
}
if (!resultMessage) {
core.setOutput("conclusion", "failure");
core.error("No result message received from Claude");
process.exit(1);
throw new Error("No result message received from Claude");
}
const isSuccess = resultMessage.subtype === "success";
core.setOutput("conclusion", isSuccess ? "success" : "failure");
result.conclusion = isSuccess ? "success" : "failure";
// Handle structured output
if (hasJsonSchema) {
@ -203,10 +212,7 @@ export async function runClaudeWithSdk(
"structured_output" in resultMessage &&
resultMessage.structured_output
) {
const structuredOutputJson = JSON.stringify(
resultMessage.structured_output,
);
core.setOutput("structured_output", structuredOutputJson);
result.structuredOutput = JSON.stringify(resultMessage.structured_output);
core.info(
`Set structured_output with ${Object.keys(resultMessage.structured_output as object).length} field(s)`,
);
@ -214,8 +220,10 @@ export async function runClaudeWithSdk(
core.setFailed(
`--json-schema was provided but Claude did not return structured_output. Result subtype: ${resultMessage.subtype}`,
);
core.setOutput("conclusion", "failure");
process.exit(1);
result.conclusion = "failure";
throw new Error(
`--json-schema was provided but Claude did not return structured_output. Result subtype: ${resultMessage.subtype}`,
);
}
}
@ -223,6 +231,14 @@ export async function runClaudeWithSdk(
if ("errors" in resultMessage && resultMessage.errors) {
core.error(`Execution failed: ${resultMessage.errors.join(", ")}`);
}
process.exit(1);
throw new Error(
`Claude execution failed: ${
"errors" in resultMessage && resultMessage.errors
? resultMessage.errors.join(", ")
: "unknown error"
}`,
);
}
return result;
}

View File

@ -1,4 +1,5 @@
import { runClaudeWithSdk } from "./run-claude-sdk";
import type { ClaudeRunResult } from "./run-claude-sdk";
import { parseSdkOptions } from "./parse-sdk-options";
export type ClaudeOptions = {
@ -15,7 +16,10 @@ export type ClaudeOptions = {
showFullOutput?: string;
};
export async function runClaude(promptPath: string, options: ClaudeOptions) {
export async function runClaude(
promptPath: string,
options: ClaudeOptions,
): Promise<ClaudeRunResult> {
const parsedOptions = parseSdkOptions(options);
return runClaudeWithSdk(promptPath, parsedOptions);
}

View File

@ -1,6 +1,4 @@
import * as core from "@actions/core";
export function collectActionInputsPresence(): void {
export function collectActionInputsPresence(): string {
const inputDefaults: Record<string, string> = {
trigger_phrase: "@claude",
assignee_trigger: "",
@ -32,8 +30,7 @@ export function collectActionInputsPresence(): void {
const allInputsJson = process.env.ALL_INPUTS;
if (!allInputsJson) {
console.log("ALL_INPUTS environment variable not found");
core.setOutput("action_inputs_present", JSON.stringify({}));
return;
return JSON.stringify({});
}
let allInputs: Record<string, string>;
@ -41,8 +38,7 @@ export function collectActionInputsPresence(): void {
allInputs = JSON.parse(allInputsJson);
} catch (e) {
console.error("Failed to parse ALL_INPUTS JSON:", e);
core.setOutput("action_inputs_present", JSON.stringify({}));
return;
return JSON.stringify({});
}
const presentInputs: Record<string, boolean> = {};
@ -54,5 +50,5 @@ export function collectActionInputsPresence(): void {
presentInputs[name] = isSet;
}
core.setOutput("action_inputs_present", JSON.stringify(presentInputs));
return JSON.stringify(presentInputs);
}

361
src/entrypoints/run.ts Normal file
View File

@ -0,0 +1,361 @@
#!/usr/bin/env bun
/**
* Unified entrypoint for the Claude Code Action.
* Merges all previously separate action.yml steps (prepare, install, run, cleanup)
* into a single TypeScript orchestrator.
*/
import * as core from "@actions/core";
import { spawn } from "child_process";
import { appendFile } from "fs/promises";
import { existsSync, readFileSync } from "fs";
import { setupGitHubToken, WorkflowValidationSkipError } from "../github/token";
import { checkWritePermissions } from "../github/validation/permissions";
import { createOctokit } from "../github/api/client";
import type { Octokits } from "../github/api/client";
import { parseGitHubContext, isEntityContext } from "../github/context";
import type { GitHubContext } from "../github/context";
import { getMode } from "../modes/registry";
import { prepare } from "../prepare";
import { collectActionInputsPresence } from "./collect-inputs";
import { updateCommentLink } from "./update-comment-link";
import { formatTurnsFromData } from "./format-turns";
import type { Turn } from "./format-turns";
import { cleanupSshSigning } from "../github/operations/git-config";
import { GITHUB_API_URL } from "../github/api/config";
// Base-action imports (used directly instead of subprocess)
import { validateEnvironmentVariables } from "../../base-action/src/validate-env";
import { setupClaudeCodeSettings } from "../../base-action/src/setup-claude-code-settings";
import { installPlugins } from "../../base-action/src/install-plugins";
import { preparePrompt } from "../../base-action/src/prepare-prompt";
import { runClaude } from "../../base-action/src/run-claude";
import type { ClaudeRunResult } from "../../base-action/src/run-claude-sdk";
/**
* Install Claude Code CLI, handling retry logic and custom executable paths.
*/
async function installClaudeCode(): Promise<void> {
const customExecutable = process.env.PATH_TO_CLAUDE_CODE_EXECUTABLE;
if (customExecutable) {
console.log(`Using custom Claude Code executable: ${customExecutable}`);
const claudeDir = customExecutable.substring(
0,
customExecutable.lastIndexOf("/"),
);
// Add to PATH by appending to GITHUB_PATH
const githubPath = process.env.GITHUB_PATH;
if (githubPath) {
await appendFile(githubPath, `${claudeDir}\n`);
}
// Also add to current process PATH
process.env.PATH = `${claudeDir}:${process.env.PATH}`;
return;
}
const claudeCodeVersion = "2.1.31";
console.log(`Installing Claude Code v${claudeCodeVersion}...`);
for (let attempt = 1; attempt <= 3; attempt++) {
console.log(`Installation attempt ${attempt}...`);
try {
await new Promise<void>((resolve, reject) => {
const child = spawn(
"bash",
[
"-c",
`curl -fsSL https://claude.ai/install.sh | bash -s -- ${claudeCodeVersion}`,
],
{ stdio: "inherit" },
);
child.on("close", (code) => {
if (code === 0) resolve();
else reject(new Error(`Install failed with exit code ${code}`));
});
child.on("error", reject);
});
console.log("Claude Code installed successfully");
// Add to PATH
const homeBin = `${process.env.HOME}/.local/bin`;
const githubPath = process.env.GITHUB_PATH;
if (githubPath) {
await appendFile(githubPath, `${homeBin}\n`);
}
process.env.PATH = `${homeBin}:${process.env.PATH}`;
return;
} catch (error) {
if (attempt === 3) {
throw new Error(
`Failed to install Claude Code after 3 attempts: ${error}`,
);
}
console.log("Installation failed, retrying...");
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
/**
* Write the step summary from Claude's execution output file.
*/
async function writeStepSummary(executionFile: string): Promise<void> {
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
if (!summaryFile) return;
try {
const fileContent = readFileSync(executionFile, "utf-8");
const data: Turn[] = JSON.parse(fileContent);
const markdown = formatTurnsFromData(data);
await appendFile(summaryFile, markdown);
console.log("Successfully formatted Claude Code report");
} catch (error) {
console.error(`Failed to format output: ${error}`);
// Fall back to raw JSON
try {
let fallback = "## Claude Code Report (Raw Output)\n\n";
fallback +=
"Failed to format output (please report). Here's the raw JSON:\n\n";
fallback += "```json\n";
fallback += readFileSync(executionFile, "utf-8");
fallback += "\n```\n";
await appendFile(summaryFile, fallback);
} catch {
console.error("Failed to write raw output to step summary");
}
}
}
/**
* Revoke the GitHub App installation token.
*/
async function revokeAppToken(githubToken: string): Promise<void> {
try {
const apiUrl = GITHUB_API_URL;
const response = await fetch(`${apiUrl}/installation/token`, {
method: "DELETE",
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${githubToken}`,
"X-GitHub-Api-Version": "2022-11-28",
},
});
if (response.ok) {
console.log("App token revoked successfully");
} else {
console.error(
`Failed to revoke app token: ${response.status} ${response.statusText}`,
);
}
} catch (error) {
console.error("Error revoking app token:", error);
}
}
async function run() {
let githubToken: string | undefined;
let commentId: number | undefined;
let claudeBranch: string | undefined;
let baseBranch: string | undefined;
let executionFile: string | undefined;
let claudeSuccess = false;
let prepareSuccess = true;
let prepareError: string | undefined;
let context: GitHubContext | undefined;
let octokit: Octokits | undefined;
let useSshSigning = false;
let useOverrideToken = false;
let skippedDueToWorkflowValidation = false;
try {
// Phase 1: Prepare
const actionInputsPresent = collectActionInputsPresence();
context = parseGitHubContext();
const mode = getMode(context);
try {
githubToken = await setupGitHubToken();
} catch (error) {
if (error instanceof WorkflowValidationSkipError) {
skippedDueToWorkflowValidation = true;
core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
console.log("Exiting due to workflow validation skip");
return;
}
throw error;
}
useOverrideToken = !!process.env.OVERRIDE_GITHUB_TOKEN;
octokit = createOctokit(githubToken);
// Set GITHUB_TOKEN and GH_TOKEN in process env for downstream usage
process.env.GITHUB_TOKEN = githubToken;
process.env.GH_TOKEN = githubToken;
// Check write permissions (only for entity contexts)
if (isEntityContext(context)) {
const hasWritePermissions = await checkWritePermissions(
octokit.rest,
context,
context.inputs.allowedNonWriteUsers,
useOverrideToken,
);
if (!hasWritePermissions) {
throw new Error(
"Actor does not have write permissions to the repository",
);
}
}
// Check trigger conditions
const containsTrigger = mode.shouldTrigger(context);
console.log(`Mode: ${mode.name}`);
console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`);
console.log(`Trigger result: ${containsTrigger}`);
if (!containsTrigger) {
console.log("No trigger found, skipping remaining steps");
core.setOutput("github_token", githubToken);
return;
}
// Run prepare
const prepareResult = await prepare({
context,
octokit,
mode,
githubToken,
});
commentId = prepareResult.commentId;
claudeBranch = prepareResult.branchInfo.claudeBranch;
baseBranch = prepareResult.branchInfo.baseBranch;
useSshSigning = !!context.inputs.sshSigningKey;
// Set system prompt if available
if (mode.getSystemPrompt) {
const modeContext = mode.prepareContext(context, {
commentId: prepareResult.commentId,
baseBranch: prepareResult.branchInfo.baseBranch,
claudeBranch: prepareResult.branchInfo.claudeBranch,
});
const systemPrompt = mode.getSystemPrompt(modeContext);
if (systemPrompt) {
core.exportVariable("APPEND_SYSTEM_PROMPT", systemPrompt);
}
}
// Phase 2: Install Claude Code CLI
await installClaudeCode();
// Phase 3: Run Claude (import base-action directly)
// Set env vars needed by the base-action code
process.env.INPUT_ACTION_INPUTS_PRESENT = actionInputsPresent;
process.env.CLAUDE_CODE_ACTION = "1";
process.env.DETAILED_PERMISSION_MESSAGES = "1";
validateEnvironmentVariables();
await setupClaudeCodeSettings(process.env.INPUT_SETTINGS);
await installPlugins(
process.env.INPUT_PLUGIN_MARKETPLACES,
process.env.INPUT_PLUGINS,
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
);
const promptFile =
process.env.INPUT_PROMPT_FILE ||
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`;
const promptConfig = await preparePrompt({
prompt: "",
promptFile,
});
const claudeResult: ClaudeRunResult = await runClaude(promptConfig.path, {
claudeArgs: prepareResult.claudeArgs,
appendSystemPrompt: process.env.APPEND_SYSTEM_PROMPT,
model: process.env.ANTHROPIC_MODEL,
pathToClaudeCodeExecutable:
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT,
});
claudeSuccess = claudeResult.conclusion === "success";
executionFile = claudeResult.executionFile;
// Set action-level outputs
if (claudeResult.executionFile) {
core.setOutput("execution_file", claudeResult.executionFile);
}
if (claudeResult.sessionId) {
core.setOutput("session_id", claudeResult.sessionId);
}
if (claudeResult.structuredOutput) {
core.setOutput("structured_output", claudeResult.structuredOutput);
}
core.setOutput("conclusion", claudeResult.conclusion);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
prepareSuccess = false;
prepareError = errorMessage;
core.setFailed(`Action failed with error: ${errorMessage}`);
} finally {
// Phase 4: Cleanup (always runs)
// Update tracking comment
if (
commentId &&
context &&
isEntityContext(context) &&
githubToken &&
octokit
) {
try {
await updateCommentLink({
commentId,
githubToken,
claudeBranch,
baseBranch: baseBranch || "main",
triggerUsername: context.actor,
context,
octokit,
claudeSuccess,
outputFile: executionFile,
prepareSuccess,
prepareError,
useCommitSigning: context.inputs.useCommitSigning,
});
} catch (error) {
console.error("Error updating comment with job link:", error);
}
}
// Write step summary
if (executionFile && existsSync(executionFile)) {
await writeStepSummary(executionFile);
}
// Cleanup SSH signing key
if (useSshSigning) {
try {
await cleanupSshSigning();
} catch (error) {
console.error("Failed to cleanup SSH signing key:", error);
}
}
// Revoke app token (only if we're using the app token, not an override)
if (githubToken && !useOverrideToken && !skippedDueToWorkflowValidation) {
await revokeAppToken(githubToken);
}
// Set remaining action-level outputs
core.setOutput("branch_name", claudeBranch);
core.setOutput("github_token", githubToken);
}
}
if (import.meta.main) {
run();
}

View File

@ -1,6 +1,7 @@
#!/usr/bin/env bun
import { createOctokit } from "../github/api/client";
import type { Octokits } from "../github/api/client";
import * as fs from "fs/promises";
import {
updateCommentBody,
@ -11,229 +12,257 @@ import {
isPullRequestReviewCommentEvent,
isEntityContext,
} from "../github/context";
import type { ParsedGitHubContext } from "../github/context";
import { GITHUB_SERVER_URL } from "../github/api/config";
import { checkAndCommitOrDeleteBranch } from "../github/operations/branch-cleanup";
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
export type UpdateCommentLinkParams = {
commentId: number;
githubToken: string;
claudeBranch?: string;
baseBranch: string;
triggerUsername?: string;
context: ParsedGitHubContext;
octokit: Octokits;
claudeSuccess: boolean;
outputFile?: string;
prepareSuccess: boolean;
prepareError?: string;
useCommitSigning: boolean;
};
export async function updateCommentLink(
params: UpdateCommentLinkParams,
): Promise<void> {
const {
commentId,
claudeBranch,
baseBranch,
triggerUsername,
context,
octokit,
useCommitSigning,
} = params;
const { owner, repo } = context.repository;
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
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");
}
throw finalError;
}
const currentBody = comment.body ?? "";
// Check if we need to add branch link for new branches
const { shouldDeleteBranch, branchLink } = await checkAndCommitOrDeleteBranch(
octokit,
owner,
repo,
claudeBranch,
baseBranch,
useCommitSigning,
);
// Check if we need to add PR URL when we have a new branch
let prLink = "";
// If claudeBranch is set, it means we created a new branch (for issues or closed/merged PRs)
if (claudeBranch && !shouldDeleteBranch) {
// Check if comment already contains a PR URL
const serverUrlPattern = serverUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const prUrlPattern = new RegExp(
`${serverUrlPattern}\\/.+\\/compare\\/${baseBranch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\.\\.`,
);
const containsPRUrl = currentBody.match(prUrlPattern);
if (!containsPRUrl) {
// Check if there are changes to the branch compared to the default branch
try {
const { data: comparison } =
await octokit.rest.repos.compareCommitsWithBasehead({
owner,
repo,
basehead: `${baseBranch}...${claudeBranch}`,
});
// If there are changes (commits or file changes), add the PR URL
if (
comparison.total_commits > 0 ||
(comparison.files && comparison.files.length > 0)
) {
const entityType = context.isPR ? "PR" : "Issue";
const prTitle = encodeURIComponent(
`${entityType} #${context.entityNumber}: Changes from Claude`,
);
const prBody = encodeURIComponent(
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
);
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
prLink = `\n[Create a PR](${prUrl})`;
}
} catch (error) {
console.error("Error checking for changes in branch:", error);
// Don't fail the entire update if we can't check for changes
}
}
}
// Check if action failed and read output file for execution details
let executionDetails: {
total_cost_usd?: number;
duration_ms?: number;
duration_api_ms?: number;
} | null = null;
let actionFailed = false;
let errorDetails: string | undefined;
if (!params.prepareSuccess && params.prepareError) {
actionFailed = true;
errorDetails = params.prepareError;
} else {
// Check for existence of output file and parse it if available
try {
if (params.outputFile) {
const fileContent = await fs.readFile(params.outputFile, "utf8");
const outputData = JSON.parse(fileContent);
// Output file is an array, get the last element which contains execution details
if (Array.isArray(outputData) && outputData.length > 0) {
const lastElement = outputData[outputData.length - 1];
if (
lastElement.type === "result" &&
"total_cost_usd" in lastElement &&
"duration_ms" in lastElement
) {
executionDetails = {
total_cost_usd: lastElement.total_cost_usd,
duration_ms: lastElement.duration_ms,
duration_api_ms: lastElement.duration_api_ms,
};
}
}
}
actionFailed = !params.claudeSuccess;
} catch (error) {
console.error("Error reading output file:", error);
actionFailed = !params.claudeSuccess;
}
}
// Prepare input for updateCommentBody function
const commentInput: CommentUpdateInput = {
currentBody,
actionFailed,
executionDetails,
jobUrl,
branchLink,
prLink,
branchName: shouldDeleteBranch || !branchLink ? undefined : claudeBranch,
triggerUsername,
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;
}
}
async function run() {
try {
const commentId = parseInt(process.env.CLAUDE_COMMENT_ID!);
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 context = parseGitHubContext();
// This script is only called for entity-based events
if (!isEntityContext(context)) {
throw new Error("update-comment-link requires an entity context");
}
const { owner, repo } = context.repository;
const githubToken = process.env.GITHUB_TOKEN!;
const octokit = createOctokit(githubToken);
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
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");
}
throw finalError;
}
const currentBody = comment.body ?? "";
// Check if we need to add branch link for new branches
const useCommitSigning = process.env.USE_COMMIT_SIGNING === "true";
const { shouldDeleteBranch, branchLink } =
await checkAndCommitOrDeleteBranch(
octokit,
owner,
repo,
claudeBranch,
baseBranch,
useCommitSigning,
);
// Check if we need to add PR URL when we have a new branch
let prLink = "";
// If claudeBranch is set, it means we created a new branch (for issues or closed/merged PRs)
if (claudeBranch && !shouldDeleteBranch) {
// Check if comment already contains a PR URL
const serverUrlPattern = serverUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const prUrlPattern = new RegExp(
`${serverUrlPattern}\\/.+\\/compare\\/${baseBranch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\.\\.`,
);
const containsPRUrl = currentBody.match(prUrlPattern);
if (!containsPRUrl) {
// Check if there are changes to the branch compared to the default branch
try {
const { data: comparison } =
await octokit.rest.repos.compareCommitsWithBasehead({
owner,
repo,
basehead: `${baseBranch}...${claudeBranch}`,
});
// If there are changes (commits or file changes), add the PR URL
if (
comparison.total_commits > 0 ||
(comparison.files && comparison.files.length > 0)
) {
const entityType = context.isPR ? "PR" : "Issue";
const prTitle = encodeURIComponent(
`${entityType} #${context.entityNumber}: Changes from Claude`,
);
const prBody = encodeURIComponent(
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
);
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
prLink = `\n[Create a PR](${prUrl})`;
}
} catch (error) {
console.error("Error checking for changes in branch:", error);
// Don't fail the entire update if we can't check for changes
}
}
}
// Check if action failed and read output file for execution details
let executionDetails: {
total_cost_usd?: number;
duration_ms?: number;
duration_api_ms?: number;
} | null = null;
let actionFailed = false;
let errorDetails: string | undefined;
// First check if prepare step failed
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR;
if (!prepareSuccess && prepareError) {
actionFailed = true;
errorDetails = prepareError;
} else {
// Check for existence of output file and parse it if available
try {
const outputFile = process.env.OUTPUT_FILE;
if (outputFile) {
const fileContent = await fs.readFile(outputFile, "utf8");
const outputData = JSON.parse(fileContent);
// Output file is an array, get the last element which contains execution details
if (Array.isArray(outputData) && outputData.length > 0) {
const lastElement = outputData[outputData.length - 1];
if (
lastElement.type === "result" &&
"total_cost_usd" in lastElement &&
"duration_ms" in lastElement
) {
executionDetails = {
total_cost_usd: lastElement.total_cost_usd,
duration_ms: lastElement.duration_ms,
duration_api_ms: lastElement.duration_api_ms,
};
}
}
}
// Check if the Claude action failed
const claudeSuccess = process.env.CLAUDE_SUCCESS !== "false";
actionFailed = !claudeSuccess;
} catch (error) {
console.error("Error reading output file:", error);
// If we can't read the file, check for any failure markers
actionFailed = process.env.CLAUDE_SUCCESS === "false";
}
}
// Prepare input for updateCommentBody function
const commentInput: CommentUpdateInput = {
currentBody,
actionFailed,
executionDetails,
jobUrl,
branchLink,
prLink,
branchName: shouldDeleteBranch || !branchLink ? undefined : claudeBranch,
triggerUsername,
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;
}
await updateCommentLink({
commentId: parseInt(process.env.CLAUDE_COMMENT_ID!),
githubToken,
claudeBranch: process.env.CLAUDE_BRANCH,
baseBranch: process.env.BASE_BRANCH || "main",
triggerUsername: process.env.TRIGGER_USERNAME,
context,
octokit,
claudeSuccess: process.env.CLAUDE_SUCCESS !== "false",
outputFile: process.env.OUTPUT_FILE,
prepareSuccess: process.env.PREPARE_SUCCESS !== "false",
prepareError: process.env.PREPARE_ERROR,
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
});
process.exit(0);
} catch (error) {
@ -242,4 +271,6 @@ async function run() {
}
}
run();
if (import.meta.main) {
run();
}

View File

@ -8,7 +8,6 @@
import { $ } from "bun";
import { execFileSync } from "child_process";
import * as core from "@actions/core";
import type { ParsedGitHubContext } from "../context";
import type { GitHubPullRequest } from "../types";
import type { Octokits } from "../api/client";
@ -265,9 +264,6 @@ export async function setupBranch(
execGit(["fetch", "origin", sourceBranch, "--depth=1"]);
execGit(["checkout", sourceBranch, "--"]);
// Set outputs for GitHub Actions
core.setOutput("CLAUDE_BRANCH", newBranch);
core.setOutput("BASE_BRANCH", sourceBranch);
return {
baseBranch: sourceBranch,
claudeBranch: newBranch,
@ -294,9 +290,6 @@ export async function setupBranch(
`Successfully created and checked out local branch: ${newBranch}`,
);
// Set outputs for GitHub Actions
core.setOutput("CLAUDE_BRANCH", newBranch);
core.setOutput("BASE_BRANCH", sourceBranch);
return {
baseBranch: sourceBranch,
claudeBranch: newBranch,

View File

@ -3,6 +3,13 @@
import * as core from "@actions/core";
import { retryWithBackoff } from "../utils/retry";
export class WorkflowValidationSkipError extends Error {
constructor(message: string) {
super(message);
this.name = "WorkflowValidationSkipError";
}
}
async function getOidcToken(): Promise<string> {
try {
const oidcToken = await core.getIDToken("claude-code-github-action");
@ -96,8 +103,7 @@ async function exchangeForAppToken(
console.log(
"Action skipped due to workflow validation error. This is expected when adding Claude Code workflows to new repositories or on PRs with workflow changes. If you're seeing this, your workflow will begin working once you merge your PR.",
);
core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
process.exit(0);
throw new WorkflowValidationSkipError(message);
}
console.error(
@ -120,36 +126,26 @@ async function exchangeForAppToken(
}
export async function setupGitHubToken(): Promise<string> {
try {
// Check if GitHub token was provided as override
const providedToken = process.env.OVERRIDE_GITHUB_TOKEN;
// Check if GitHub token was provided as override
const providedToken = process.env.OVERRIDE_GITHUB_TOKEN;
if (providedToken) {
console.log("Using provided GITHUB_TOKEN for authentication");
core.setOutput("GITHUB_TOKEN", providedToken);
return providedToken;
}
console.log("Requesting OIDC token...");
const oidcToken = await retryWithBackoff(() => getOidcToken());
console.log("OIDC token successfully obtained");
const permissions = parseAdditionalPermissions();
console.log("Exchanging OIDC token for app token...");
const appToken = await retryWithBackoff(() =>
exchangeForAppToken(oidcToken, permissions),
);
console.log("App token successfully obtained");
console.log("Using GITHUB_TOKEN from OIDC");
core.setOutput("GITHUB_TOKEN", appToken);
return appToken;
} catch (error) {
// Only set failed if we get here - workflow validation errors will exit(0) before this
core.setFailed(
`Failed to setup GitHub token: ${error}\n\nIf you instead wish to use this action with a custom GitHub token or custom GitHub app, provide a \`github_token\` in the \`uses\` section of the app in your workflow yml file.`,
);
process.exit(1);
if (providedToken) {
console.log("Using provided GITHUB_TOKEN for authentication");
return providedToken;
}
console.log("Requesting OIDC token...");
const oidcToken = await retryWithBackoff(() => getOidcToken());
console.log("OIDC token successfully obtained");
const permissions = parseAdditionalPermissions();
console.log("Exchanging OIDC token for app token...");
const appToken = await retryWithBackoff(() =>
exchangeForAppToken(oidcToken, permissions),
);
console.log("App token successfully obtained");
console.log("Using GITHUB_TOKEN from OIDC");
return appToken;
}

View File

@ -183,8 +183,6 @@ export const agentMode: Mode = {
// Append user's claude_args (which may have more --mcp-config flags)
claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim();
core.setOutput("claude_args", claudeArgs);
return {
commentId: undefined,
branchInfo: {
@ -193,6 +191,7 @@ export const agentMode: Mode = {
claudeBranch: claudeBranch,
},
mcpConfig: ourMcpConfig,
claudeArgs,
};
},

View File

@ -1,4 +1,3 @@
import * as core from "@actions/core";
import type { Mode, ModeOptions, ModeResult } from "../types";
import { checkContainsTrigger } from "../../github/validation/trigger";
import { checkHumanActor } from "../../github/validation/actor";
@ -211,12 +210,11 @@ export const tagMode: Mode = {
claudeArgs += ` ${userClaudeArgs}`;
}
core.setOutput("claude_args", claudeArgs.trim());
return {
commentId,
branchInfo,
mcpConfig: ourMcpConfig,
claudeArgs: claudeArgs.trim(),
};
},

View File

@ -97,4 +97,5 @@ export type ModeResult = {
currentBranch: string;
};
mcpConfig: string;
claudeArgs: string;
};

View File

@ -10,6 +10,7 @@ export type PrepareResult = {
currentBranch: string;
};
mcpConfig: string;
claudeArgs: string;
};
export type PrepareOptions = {

View File

@ -31,6 +31,7 @@ describe("generatePrompt", () => {
claudeBranch: undefined,
},
mcpConfig: "{}",
claudeArgs: "",
}),
};
@ -52,6 +53,7 @@ describe("generatePrompt", () => {
claudeBranch: undefined,
},
mcpConfig: "{}",
claudeArgs: "",
}),
};

View File

@ -163,10 +163,8 @@ describe("Agent Mode", () => {
});
// Verify claude_args includes user args (no MCP config in agent mode without allowed tools)
const callArgs = setOutputSpy.mock.calls[0];
expect(callArgs[0]).toBe("claude_args");
expect(callArgs[1]).toBe("--model claude-sonnet-4 --max-turns 10");
expect(callArgs[1]).not.toContain("--mcp-config");
expect(result.claudeArgs).toBe("--model claude-sonnet-4 --max-turns 10");
expect(result.claudeArgs).not.toContain("--mcp-config");
// Verify return structure - should use "main" as fallback when no env vars set
expect(result).toEqual({
@ -177,6 +175,7 @@ describe("Agent Mode", () => {
claudeBranch: undefined,
},
mcpConfig: expect.any(String),
claudeArgs: "--model claude-sonnet-4 --max-turns 10",
});
// Clean up
@ -269,7 +268,7 @@ describe("Agent Mode", () => {
},
},
} as any;
await agentMode.prepare({
const result = await agentMode.prepare({
context: contextWithPrompts,
octokit: mockOctokit,
githubToken: "test-token",
@ -279,9 +278,7 @@ describe("Agent Mode", () => {
// but we can verify the method completes without errors
// With our conditional MCP logic, agent mode with no allowed tools
// should not include any MCP config
const callArgs = setOutputSpy.mock.calls[0];
expect(callArgs[0]).toBe("claude_args");
// Should be empty or just whitespace when no MCP servers are included
expect(callArgs[1]).not.toContain("--mcp-config");
expect(result.claudeArgs).not.toContain("--mcp-config");
});
});

View File

@ -29,6 +29,7 @@ describe("pull_request_target event support", () => {
claudeBranch: undefined,
},
mcpConfig: "{}",
claudeArgs: "",
}),
};
@ -313,6 +314,7 @@ describe("pull_request_target event support", () => {
claudeBranch: undefined,
},
mcpConfig: "{}",
claudeArgs: "",
}),
};