feat: merge prepare and run steps into unified entry point

Consolidates the two-step architecture (prepare → run) into a single
step to eliminate file-based and output-based communication.

Changes:
- Add base-action/src/lib.ts with exports for main action import
- Modify run-claude-sdk.ts to accept prompt string directly and return result
- Add generatePromptContent() that returns prompt without file I/O
- Update mode prepare() to return promptContent in result
- Create src/entrypoints/run.ts as unified entry point
- Update action.yml to use single Run Claude Code step
- Update output references from steps.prepare to steps.claude-code

Benefits:
- No file I/O for prompt - data stays in memory
- No step output parsing - direct function returns
- Simpler debugging - single entry point
- Faster execution - no subprocess overhead
- Type safety - TypeScript across the boundary

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ashwin Bhat
2025-12-23 11:32:00 -08:00
parent e5b07416ea
commit e8e8fdc051
11 changed files with 484 additions and 139 deletions

View File

@@ -120,10 +120,10 @@ outputs:
value: ${{ steps.claude-code.outputs.execution_file }} value: ${{ steps.claude-code.outputs.execution_file }}
branch_name: branch_name:
description: "The branch created by Claude Code for this execution" description: "The branch created by Claude Code for this execution"
value: ${{ steps.prepare.outputs.CLAUDE_BRANCH }} value: ${{ steps.claude-code.outputs.CLAUDE_BRANCH }}
github_token: github_token:
description: "The GitHub token used by the action (Claude App token if available)" description: "The GitHub token used by the action (Claude App token if available)"
value: ${{ steps.prepare.outputs.github_token }} value: ${{ steps.claude-code.outputs.GITHUB_TOKEN }}
structured_output: 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" 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.claude-code.outputs.structured_output }}
@@ -152,44 +152,15 @@ runs:
echo "$BUN_DIR" >> "$GITHUB_PATH" echo "$BUN_DIR" >> "$GITHUB_PATH"
- name: Install Dependencies - name: Install Dependencies
shell: bash
run: |
cd ${GITHUB_ACTION_PATH}
bun install
- name: Prepare action
id: prepare
shell: bash
run: |
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/prepare.ts
env:
MODE: ${{ inputs.mode }}
PROMPT: ${{ inputs.prompt }}
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
LABEL_TRIGGER: ${{ inputs.label_trigger }}
BASE_BRANCH: ${{ inputs.base_branch }}
BRANCH_PREFIX: ${{ inputs.branch_prefix }}
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }}
GITHUB_RUN_ID: ${{ github.run_id }}
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
BOT_ID: ${{ inputs.bot_id }}
BOT_NAME: ${{ inputs.bot_name }}
TRACK_PROGRESS: ${{ inputs.track_progress }}
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
CLAUDE_ARGS: ${{ inputs.claude_args }}
ALL_INPUTS: ${{ toJson(inputs) }}
- name: Install Base Action Dependencies
if: steps.prepare.outputs.contains_trigger == 'true'
shell: bash shell: bash
env: env:
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }} PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
run: | run: |
# Install main action dependencies
cd ${GITHUB_ACTION_PATH}
bun install
# Install base-action dependencies
echo "Installing base-action dependencies..." echo "Installing base-action dependencies..."
cd ${GITHUB_ACTION_PATH}/base-action cd ${GITHUB_ACTION_PATH}/base-action
bun install bun install
@@ -223,31 +194,46 @@ runs:
echo "$CLAUDE_DIR" >> "$GITHUB_PATH" echo "$CLAUDE_DIR" >> "$GITHUB_PATH"
fi fi
# Unified step: prepare + setup + run Claude
- name: Run Claude Code - name: Run Claude Code
id: claude-code id: claude-code
if: steps.prepare.outputs.contains_trigger == 'true'
shell: bash shell: bash
run: | run: |
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/run.ts
# Run the base-action
bun run ${GITHUB_ACTION_PATH}/base-action/src/index.ts
env: env:
# Base-action inputs # Action configuration
CLAUDE_CODE_ACTION: "1" CLAUDE_CODE_ACTION: "1"
INPUT_PROMPT_FILE: ${{ runner.temp }}/claude-prompts/claude-prompt.txt MODE: ${{ inputs.mode }}
PROMPT: ${{ inputs.prompt }}
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
LABEL_TRIGGER: ${{ inputs.label_trigger }}
BASE_BRANCH: ${{ inputs.base_branch }}
BRANCH_PREFIX: ${{ inputs.branch_prefix }}
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }}
GITHUB_RUN_ID: ${{ github.run_id }}
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
BOT_ID: ${{ inputs.bot_id }}
BOT_NAME: ${{ inputs.bot_name }}
TRACK_PROGRESS: ${{ inputs.track_progress }}
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
CLAUDE_ARGS: ${{ inputs.claude_args }}
ALL_INPUTS: ${{ toJson(inputs) }}
# Base-action inputs
INPUT_SETTINGS: ${{ inputs.settings }} INPUT_SETTINGS: ${{ inputs.settings }}
INPUT_CLAUDE_ARGS: ${{ steps.prepare.outputs.claude_args }} INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }}
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands 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_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }} INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }} INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }}
INPUT_PLUGINS: ${{ inputs.plugins }} INPUT_PLUGINS: ${{ inputs.plugins }}
INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }} INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }}
# Model configuration
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
GH_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
NODE_VERSION: ${{ env.NODE_VERSION }} NODE_VERSION: ${{ env.NODE_VERSION }}
DETAILED_PERMISSION_MESSAGES: "1" DETAILED_PERMISSION_MESSAGES: "1"
@@ -287,33 +273,33 @@ runs:
ANTHROPIC_DEFAULT_OPUS_MODEL: ${{ env.ANTHROPIC_DEFAULT_OPUS_MODEL }} ANTHROPIC_DEFAULT_OPUS_MODEL: ${{ env.ANTHROPIC_DEFAULT_OPUS_MODEL }}
- name: Update comment with job link - name: Update comment with job link
if: steps.prepare.outputs.contains_trigger == 'true' && steps.prepare.outputs.claude_comment_id && always() if: steps.claude-code.outputs.contains_trigger == 'true' && steps.claude-code.outputs.claude_comment_id && always()
shell: bash shell: bash
run: | run: |
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/update-comment-link.ts bun run ${GITHUB_ACTION_PATH}/src/entrypoints/update-comment-link.ts
env: env:
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
CLAUDE_COMMENT_ID: ${{ steps.prepare.outputs.claude_comment_id }} CLAUDE_COMMENT_ID: ${{ steps.claude-code.outputs.claude_comment_id }}
GITHUB_RUN_ID: ${{ github.run_id }} GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ steps.claude-code.outputs.GITHUB_TOKEN }}
GH_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }} GH_TOKEN: ${{ steps.claude-code.outputs.GITHUB_TOKEN }}
GITHUB_EVENT_NAME: ${{ github.event_name }} GITHUB_EVENT_NAME: ${{ github.event_name }}
TRIGGER_COMMENT_ID: ${{ github.event.comment.id }} TRIGGER_COMMENT_ID: ${{ github.event.comment.id }}
CLAUDE_BRANCH: ${{ steps.prepare.outputs.CLAUDE_BRANCH }} CLAUDE_BRANCH: ${{ steps.claude-code.outputs.CLAUDE_BRANCH }}
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review_comment' }} 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 }} BASE_BRANCH: ${{ steps.claude-code.outputs.BASE_BRANCH }}
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }} CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }} 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 || '' }} 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_SUCCESS: ${{ steps.claude-code.outcome == 'success' }}
PREPARE_ERROR: ${{ steps.prepare.outputs.prepare_error || '' }} PREPARE_ERROR: ${{ steps.claude-code.outputs.prepare_error || '' }}
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }} USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }} USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
TRACK_PROGRESS: ${{ inputs.track_progress }} TRACK_PROGRESS: ${{ inputs.track_progress }}
- name: Display Claude Code Report - name: Display Claude Code Report
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != '' if: steps.claude-code.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''
shell: bash shell: bash
run: | run: |
# Try to format the turns, but if it fails, dump the raw JSON # Try to format the turns, but if it fails, dump the raw JSON
@@ -330,12 +316,12 @@ runs:
fi fi
- name: Revoke app token - name: Revoke app token
if: always() && inputs.github_token == '' && steps.prepare.outputs.skipped_due_to_workflow_validation_mismatch != 'true' if: always() && inputs.github_token == '' && steps.claude-code.outputs.skipped_due_to_workflow_validation_mismatch != 'true'
shell: bash shell: bash
run: | run: |
curl -L \ curl -L \
-X DELETE \ -X DELETE \
-H "Accept: application/vnd.github+json" \ -H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ steps.prepare.outputs.GITHUB_TOKEN }}" \ -H "Authorization: Bearer ${{ steps.claude-code.outputs.GITHUB_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \ -H "X-GitHub-Api-Version: 2022-11-28" \
${GITHUB_API_URL:-https://api.github.com}/installation/token ${GITHUB_API_URL:-https://api.github.com}/installation/token

13
base-action/src/lib.ts Normal file
View File

@@ -0,0 +1,13 @@
/**
* Library exports for the base-action.
* These functions can be imported directly by the main action
* to avoid file/output-based communication between steps.
*/
export { runClaudeWithSdk, runClaudeWithSdkFromFile } from "./run-claude-sdk";
export type { RunClaudeResult, PromptInput } from "./run-claude-sdk";
export { setupClaudeCodeSettings } from "./setup-claude-code-settings";
export { installPlugins } from "./install-plugins";
export { parseSdkOptions } from "./parse-sdk-options";
export type { ClaudeOptions } from "./run-claude";
export type { ParsedSdkOptions } from "./parse-sdk-options";

View File

@@ -9,6 +9,18 @@ import type { ParsedSdkOptions } from "./parse-sdk-options";
const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`; const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`;
/**
* Result of running Claude via SDK
*/
export type RunClaudeResult = {
success: boolean;
executionFile: string;
conclusion: "success" | "failure";
structuredOutput?: string;
sessionId?: string;
error?: string;
};
/** /**
* Sanitizes SDK output to match CLI sanitization behavior * Sanitizes SDK output to match CLI sanitization behavior
*/ */
@@ -57,13 +69,31 @@ function sanitizeSdkOutput(
} }
/** /**
* Run Claude using the Agent SDK * Input for runClaudeWithSdk - either a prompt string or file path
*/
export type PromptInput =
| { type: "string"; prompt: string }
| { type: "file"; promptPath: string };
/**
* Run Claude using the Agent SDK.
*
* @param promptInput - Either a direct prompt string or path to prompt file
* @param parsedOptions - Parsed SDK options
* @param options - Additional options
* @param options.setOutputs - Whether to set GitHub Action outputs (default: true for backwards compat)
* @returns Result of the execution
*/ */
export async function runClaudeWithSdk( export async function runClaudeWithSdk(
promptPath: string, promptInput: PromptInput,
{ sdkOptions, showFullOutput, hasJsonSchema }: ParsedSdkOptions, { sdkOptions, showFullOutput, hasJsonSchema }: ParsedSdkOptions,
): Promise<void> { { setOutputs = true }: { setOutputs?: boolean } = {},
const prompt = await readFile(promptPath, "utf-8"); ): Promise<RunClaudeResult> {
// Get prompt from string or file
const prompt =
promptInput.type === "string"
? promptInput.prompt
: await readFile(promptInput.promptPath, "utf-8");
if (!showFullOutput) { if (!showFullOutput) {
console.log( console.log(
@@ -74,7 +104,13 @@ export async function runClaudeWithSdk(
); );
} }
console.log(`Running Claude with prompt from file: ${promptPath}`); if (promptInput.type === "file") {
console.log(
`Running Claude with prompt from file: ${promptInput.promptPath}`,
);
} else {
console.log(`Running Claude with prompt string (${prompt.length} chars)`);
}
// Log SDK options without env (which could contain sensitive data) // Log SDK options without env (which could contain sensitive data)
const { env, ...optionsToLog } = sdkOptions; const { env, ...optionsToLog } = sdkOptions;
console.log("SDK options:", JSON.stringify(optionsToLog, null, 2)); console.log("SDK options:", JSON.stringify(optionsToLog, null, 2));
@@ -97,27 +133,47 @@ export async function runClaudeWithSdk(
} }
} catch (error) { } catch (error) {
console.error("SDK execution error:", error); console.error("SDK execution error:", error);
core.setOutput("conclusion", "failure"); if (setOutputs) {
process.exit(1); core.setOutput("conclusion", "failure");
}
return {
success: false,
executionFile: EXECUTION_FILE,
conclusion: "failure",
error: String(error),
};
} }
// Write execution file // Write execution file
try { try {
await writeFile(EXECUTION_FILE, JSON.stringify(messages, null, 2)); await writeFile(EXECUTION_FILE, JSON.stringify(messages, null, 2));
console.log(`Log saved to ${EXECUTION_FILE}`); console.log(`Log saved to ${EXECUTION_FILE}`);
core.setOutput("execution_file", EXECUTION_FILE); if (setOutputs) {
core.setOutput("execution_file", EXECUTION_FILE);
}
} catch (error) { } catch (error) {
core.warning(`Failed to write execution file: ${error}`); core.warning(`Failed to write execution file: ${error}`);
} }
if (!resultMessage) { if (!resultMessage) {
core.setOutput("conclusion", "failure"); if (setOutputs) {
core.setOutput("conclusion", "failure");
}
core.error("No result message received from Claude"); core.error("No result message received from Claude");
process.exit(1); return {
success: false,
executionFile: EXECUTION_FILE,
conclusion: "failure",
error: "No result message received from Claude",
};
} }
const isSuccess = resultMessage.subtype === "success"; const isSuccess = resultMessage.subtype === "success";
core.setOutput("conclusion", isSuccess ? "success" : "failure"); if (setOutputs) {
core.setOutput("conclusion", isSuccess ? "success" : "failure");
}
let structuredOutput: string | undefined;
// Handle structured output // Handle structured output
if (hasJsonSchema) { if (hasJsonSchema) {
@@ -126,26 +182,64 @@ export async function runClaudeWithSdk(
"structured_output" in resultMessage && "structured_output" in resultMessage &&
resultMessage.structured_output resultMessage.structured_output
) { ) {
const structuredOutputJson = JSON.stringify( structuredOutput = JSON.stringify(resultMessage.structured_output);
resultMessage.structured_output, if (setOutputs) {
); core.setOutput("structured_output", structuredOutput);
core.setOutput("structured_output", structuredOutputJson); }
core.info( core.info(
`Set structured_output with ${Object.keys(resultMessage.structured_output as object).length} field(s)`, `Set structured_output with ${Object.keys(resultMessage.structured_output as object).length} field(s)`,
); );
} else { } else {
core.setFailed( const errorMsg = `--json-schema was provided but Claude did not return structured_output. Result subtype: ${resultMessage.subtype}`;
`--json-schema was provided but Claude did not return structured_output. Result subtype: ${resultMessage.subtype}`, if (setOutputs) {
); core.setFailed(errorMsg);
core.setOutput("conclusion", "failure"); core.setOutput("conclusion", "failure");
process.exit(1); }
return {
success: false,
executionFile: EXECUTION_FILE,
conclusion: "failure",
error: errorMsg,
};
} }
} }
if (!isSuccess) { if (!isSuccess) {
if ("errors" in resultMessage && resultMessage.errors) { const errors =
core.error(`Execution failed: ${resultMessage.errors.join(", ")}`); "errors" in resultMessage && resultMessage.errors
} ? resultMessage.errors.join(", ")
: "Unknown error";
core.error(`Execution failed: ${errors}`);
return {
success: false,
executionFile: EXECUTION_FILE,
conclusion: "failure",
error: errors,
};
}
return {
success: true,
executionFile: EXECUTION_FILE,
conclusion: "success",
structuredOutput,
};
}
/**
* Wrapper for backwards compatibility - reads prompt from file path and exits on failure
*/
export async function runClaudeWithSdkFromFile(
promptPath: string,
parsedOptions: ParsedSdkOptions,
): Promise<void> {
const result = await runClaudeWithSdk(
{ type: "file", promptPath },
parsedOptions,
{ setOutputs: true },
);
if (!result.success) {
process.exit(1); process.exit(1);
} }
} }

View File

@@ -5,7 +5,7 @@ import { unlink, writeFile, stat, readFile } from "fs/promises";
import { createWriteStream } from "fs"; import { createWriteStream } from "fs";
import { spawn } from "child_process"; import { spawn } from "child_process";
import { parse as parseShellArgs } from "shell-quote"; import { parse as parseShellArgs } from "shell-quote";
import { runClaudeWithSdk } from "./run-claude-sdk"; import { runClaudeWithSdkFromFile } from "./run-claude-sdk";
import { parseSdkOptions } from "./parse-sdk-options"; import { parseSdkOptions } from "./parse-sdk-options";
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -205,7 +205,7 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
if (useAgentSdk) { if (useAgentSdk) {
const parsedOptions = parseSdkOptions(options); const parsedOptions = parseSdkOptions(options);
return runClaudeWithSdk(promptPath, parsedOptions); return runClaudeWithSdkFromFile(promptPath, parsedOptions);
} }
const config = prepareRunConfig(promptPath, options); const config = prepareRunConfig(promptPath, options);

View File

@@ -841,6 +841,78 @@ f. If you are unable to complete certain steps, such as running a linter or test
return promptContent; return promptContent;
} }
/**
* Result of generating prompt content
*/
export type PromptResult = {
promptContent: string;
allowedTools: string;
disallowedTools: string;
};
/**
* Generate prompt content and tool configurations.
* This function can be used directly without side effects (no file writes, no env vars).
*/
export function generatePromptContent(
mode: Mode,
modeContext: ModeContext,
githubData: FetchDataResult,
context: ParsedGitHubContext,
): PromptResult {
// Prepare the context for prompt generation
let claudeCommentId: string = "";
if (mode.name === "tag") {
if (!modeContext.commentId) {
throw new Error(
`${mode.name} mode requires a comment ID for prompt generation`,
);
}
claudeCommentId = modeContext.commentId.toString();
}
const preparedContext = prepareContext(
context,
claudeCommentId,
modeContext.baseBranch,
modeContext.claudeBranch,
);
// Generate the prompt directly
const promptContent = generatePrompt(
preparedContext,
githubData,
context.inputs.useCommitSigning,
mode,
);
// Get mode-specific tools
const modeAllowedTools = mode.getAllowedTools();
const modeDisallowedTools = mode.getDisallowedTools();
const hasActionsReadPermission = false;
const allowedTools = buildAllowedToolsString(
modeAllowedTools,
hasActionsReadPermission,
context.inputs.useCommitSigning,
);
const disallowedTools = buildDisallowedToolsString(
modeDisallowedTools,
modeAllowedTools,
);
return {
promptContent,
allowedTools,
disallowedTools,
};
}
/**
* Create prompt and write to file.
* This is the legacy function that writes files and sets environment variables.
* For the unified step, use generatePromptContent() instead.
*/
export async function createPrompt( export async function createPrompt(
mode: Mode, mode: Mode,
modeContext: ModeContext, modeContext: ModeContext,
@@ -848,66 +920,30 @@ export async function createPrompt(
context: ParsedGitHubContext, context: ParsedGitHubContext,
) { ) {
try { try {
// Prepare the context for prompt generation const result = generatePromptContent(
let claudeCommentId: string = "";
if (mode.name === "tag") {
if (!modeContext.commentId) {
throw new Error(
`${mode.name} mode requires a comment ID for prompt generation`,
);
}
claudeCommentId = modeContext.commentId.toString();
}
const preparedContext = prepareContext(
context,
claudeCommentId,
modeContext.baseBranch,
modeContext.claudeBranch,
);
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
recursive: true,
});
// Generate the prompt directly
const promptContent = generatePrompt(
preparedContext,
githubData,
context.inputs.useCommitSigning,
mode, mode,
modeContext,
githubData,
context,
); );
// Log the final prompt to console // Log the final prompt to console
console.log("===== FINAL PROMPT ====="); console.log("===== FINAL PROMPT =====");
console.log(promptContent); console.log(result.promptContent);
console.log("======================="); console.log("=======================");
// Write the prompt file // Write the prompt file
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
recursive: true,
});
await writeFile( await writeFile(
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`, `${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
promptContent, result.promptContent,
); );
// Set allowed tools // Set environment variables
const hasActionsReadPermission = false; core.exportVariable("ALLOWED_TOOLS", result.allowedTools);
core.exportVariable("DISALLOWED_TOOLS", result.disallowedTools);
// Get mode-specific tools
const modeAllowedTools = mode.getAllowedTools();
const modeDisallowedTools = mode.getDisallowedTools();
const allAllowedTools = buildAllowedToolsString(
modeAllowedTools,
hasActionsReadPermission,
context.inputs.useCommitSigning,
);
const allDisallowedTools = buildDisallowedToolsString(
modeDisallowedTools,
modeAllowedTools,
);
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);
core.exportVariable("DISALLOWED_TOOLS", allDisallowedTools);
} catch (error) { } catch (error) {
core.setFailed(`Create prompt failed with error: ${error}`); core.setFailed(`Create prompt failed with error: ${error}`);
process.exit(1); process.exit(1);

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

@@ -0,0 +1,186 @@
#!/usr/bin/env bun
/**
* Unified entry point for the Claude action.
*
* This combines the prepare and run phases into a single step,
* passing data directly in-memory instead of via files and outputs.
*/
import * as core from "@actions/core";
import { setupGitHubToken } from "../github/token";
import { checkWritePermissions } from "../github/validation/permissions";
import { createOctokit } from "../github/api/client";
import { parseGitHubContext, isEntityContext } from "../github/context";
import { getMode } from "../modes/registry";
import { prepare } from "../prepare";
import { collectActionInputsPresence } from "./collect-inputs";
import {
runClaudeWithSdk,
setupClaudeCodeSettings,
installPlugins,
parseSdkOptions,
} from "../../base-action/src/lib";
async function run() {
try {
// ============================================
// PHASE 1: PREPARE
// ============================================
collectActionInputsPresence();
// Parse GitHub context first to enable mode detection
const context = parseGitHubContext();
// Auto-detect mode based on context
const mode = getMode(context);
// Setup GitHub token
const githubToken = await setupGitHubToken();
const octokit = createOctokit(githubToken);
// Check write permissions (only for entity contexts)
if (isEntityContext(context)) {
const githubTokenProvided = !!process.env.OVERRIDE_GITHUB_TOKEN;
const hasWritePermissions = await checkWritePermissions(
octokit.rest,
context,
context.inputs.allowedNonWriteUsers,
githubTokenProvided,
);
if (!hasWritePermissions) {
throw new Error(
"Actor does not have write permissions to the repository",
);
}
}
// Check trigger conditions
const containsTrigger = mode.shouldTrigger(context);
// Debug logging
console.log(`Mode: ${mode.name}`);
console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`);
console.log(`Trigger result: ${containsTrigger}`);
// Set output for action.yml to check
core.setOutput("contains_trigger", containsTrigger.toString());
if (!containsTrigger) {
console.log("No trigger found, skipping remaining steps");
core.setOutput("GITHUB_TOKEN", githubToken);
return;
}
// Run mode.prepare() - returns prompt, commentId, branchInfo, etc.
const prepareResult = await prepare({
context,
octokit,
mode,
githubToken,
});
// Set outputs that may be needed by subsequent steps
core.setOutput("GITHUB_TOKEN", githubToken);
if (prepareResult.commentId) {
core.setOutput("claude_comment_id", prepareResult.commentId.toString());
}
core.setOutput(
"CLAUDE_BRANCH",
prepareResult.branchInfo.claudeBranch || "",
);
core.setOutput("BASE_BRANCH", prepareResult.branchInfo.baseBranch);
// Get system prompt from mode if available
let appendSystemPrompt: string | undefined;
if (mode.getSystemPrompt) {
const modeContext = mode.prepareContext(context, {
commentId: prepareResult.commentId,
baseBranch: prepareResult.branchInfo.baseBranch,
claudeBranch: prepareResult.branchInfo.claudeBranch,
});
appendSystemPrompt = mode.getSystemPrompt(modeContext);
}
// ============================================
// PHASE 2: SETUP
// ============================================
// Setup Claude Code settings
await setupClaudeCodeSettings(
process.env.INPUT_SETTINGS,
undefined, // homeDir
);
// Install Claude Code plugins if specified
await installPlugins(
process.env.INPUT_PLUGIN_MARKETPLACES,
process.env.INPUT_PLUGINS,
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
);
// ============================================
// PHASE 3: EXECUTE
// ============================================
// Get prompt content from prepare result
const promptContent = prepareResult.promptContent;
if (!promptContent) {
throw new Error("No prompt content generated by prepare phase");
}
console.log("===== PROMPT CONTENT =====");
console.log(`Prompt length: ${promptContent.length} chars`);
console.log("==========================");
// Build SDK options from environment and prepare result
const sdkOptions = parseSdkOptions({
claudeArgs: process.env.INPUT_CLAUDE_ARGS,
allowedTools:
prepareResult.allowedTools || process.env.INPUT_ALLOWED_TOOLS,
disallowedTools:
prepareResult.disallowedTools || process.env.INPUT_DISALLOWED_TOOLS,
maxTurns: process.env.INPUT_MAX_TURNS,
mcpConfig: process.env.INPUT_MCP_CONFIG,
systemPrompt: process.env.INPUT_SYSTEM_PROMPT,
appendSystemPrompt:
appendSystemPrompt || process.env.INPUT_APPEND_SYSTEM_PROMPT,
claudeEnv: process.env.INPUT_CLAUDE_ENV,
fallbackModel: process.env.INPUT_FALLBACK_MODEL,
model: process.env.ANTHROPIC_MODEL,
pathToClaudeCodeExecutable:
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT,
});
// Run Claude with prompt string directly
const execResult = await runClaudeWithSdk(
{ type: "string", prompt: promptContent },
sdkOptions,
{ setOutputs: true },
);
// Set additional outputs
core.setOutput("execution_file", execResult.executionFile);
core.setOutput("conclusion", execResult.conclusion);
if (execResult.structuredOutput) {
core.setOutput("structured_output", execResult.structuredOutput);
}
if (!execResult.success) {
core.setFailed(`Claude execution failed: ${execResult.error}`);
process.exit(1);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
core.setFailed(`Run step failed with error: ${errorMessage}`);
core.setOutput("prepare_error", errorMessage);
core.setOutput("conclusion", "failure");
process.exit(1);
}
}
if (import.meta.main) {
run();
}

View File

@@ -95,16 +95,15 @@ export const agentMode: Mode = {
} }
} }
// Create prompt directory // Generate prompt content - use the user's prompt directly
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
recursive: true,
});
// Write the prompt file - use the user's prompt directly
const promptContent = const promptContent =
context.inputs.prompt || context.inputs.prompt ||
`Repository: ${context.repository.owner}/${context.repository.repo}`; `Repository: ${context.repository.owner}/${context.repository.repo}`;
// Also write file for backwards compatibility with current flow
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
recursive: true,
});
await writeFile( await writeFile(
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`, `${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
promptContent, promptContent,
@@ -162,6 +161,8 @@ export const agentMode: Mode = {
claudeBranch: claudeBranch, claudeBranch: claudeBranch,
}, },
mcpConfig: ourMcpConfig, mcpConfig: ourMcpConfig,
promptContent,
// Agent mode doesn't use the same allowed/disallowed tools mechanism as tag mode
}; };
}, },

View File

@@ -10,7 +10,11 @@ import {
fetchGitHubData, fetchGitHubData,
extractTriggerTimestamp, extractTriggerTimestamp,
} from "../../github/data/fetcher"; } from "../../github/data/fetcher";
import { createPrompt, generateDefaultPrompt } from "../../create-prompt"; import {
createPrompt,
generateDefaultPrompt,
generatePromptContent,
} from "../../create-prompt";
import { isEntityContext } from "../../github/context"; import { isEntityContext } from "../../github/context";
import type { PreparedContext } from "../../create-prompt/types"; import type { PreparedContext } from "../../create-prompt/types";
import type { FetchDataResult } from "../../github/data/fetcher"; import type { FetchDataResult } from "../../github/data/fetcher";
@@ -104,13 +108,22 @@ export const tagMode: Mode = {
} }
} }
// Create prompt file // Create prompt
const modeContext = this.prepareContext(context, { const modeContext = this.prepareContext(context, {
commentId, commentId,
baseBranch: branchInfo.baseBranch, baseBranch: branchInfo.baseBranch,
claudeBranch: branchInfo.claudeBranch, claudeBranch: branchInfo.claudeBranch,
}); });
// Generate prompt content - returns data instead of writing file
const promptResult = generatePromptContent(
tagMode,
modeContext,
githubData,
context,
);
// Also write file for backwards compatibility with current flow
await createPrompt(tagMode, modeContext, githubData, context); await createPrompt(tagMode, modeContext, githubData, context);
const userClaudeArgs = process.env.CLAUDE_ARGS || ""; const userClaudeArgs = process.env.CLAUDE_ARGS || "";
@@ -188,6 +201,9 @@ export const tagMode: Mode = {
commentId, commentId,
branchInfo, branchInfo,
mcpConfig: ourMcpConfig, mcpConfig: ourMcpConfig,
promptContent: promptResult.promptContent,
allowedTools: promptResult.allowedTools,
disallowedTools: promptResult.disallowedTools,
}; };
}, },

View File

@@ -97,4 +97,10 @@ export type ModeResult = {
currentBranch: string; currentBranch: string;
}; };
mcpConfig: string; mcpConfig: string;
/** Generated prompt content for Claude */
promptContent?: string;
/** Comma-separated list of allowed tools */
allowedTools?: string;
/** Comma-separated list of disallowed tools */
disallowedTools?: string;
}; };

View File

@@ -10,6 +10,12 @@ export type PrepareResult = {
currentBranch: string; currentBranch: string;
}; };
mcpConfig: string; mcpConfig: string;
/** Generated prompt content for Claude */
promptContent?: string;
/** Comma-separated list of allowed tools */
allowedTools?: string;
/** Comma-separated list of disallowed tools */
disallowedTools?: string;
}; };
export type PrepareOptions = { export type PrepareOptions = {

View File

@@ -177,6 +177,7 @@ describe("Agent Mode", () => {
claudeBranch: undefined, claudeBranch: undefined,
}, },
mcpConfig: expect.any(String), mcpConfig: expect.any(String),
promptContent: expect.any(String),
}); });
// Clean up // Clean up