mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 15:04:13 +08:00
Compare commits
14 Commits
v1.0.22
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80591ffc11 | ||
|
|
f0c8eb2980 | ||
|
|
68a0348c20 | ||
|
|
dc06a34646 | ||
|
|
a3bb51dac1 | ||
|
|
6610520549 | ||
|
|
e2eb96f51d | ||
|
|
05c95aed79 | ||
|
|
bb4a3f68f7 | ||
|
|
2acd1f7011 | ||
|
|
469fc9c1a4 | ||
|
|
90da6b6e15 | ||
|
|
752ba96ea1 | ||
|
|
66bf95c07f |
4
.github/workflows/sync-base-action.yml
vendored
4
.github/workflows/sync-base-action.yml
vendored
@@ -94,5 +94,5 @@ jobs:
|
||||
echo "✅ Successfully synced \`base-action\` directory to [anthropics/claude-code-base-action](https://github.com/anthropics/claude-code-base-action)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Source commit**: [\`${GITHUB_SHA:0:7}\`](https://github.com/anthropics/claude-code-action/commit/${GITHUB_SHA})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Triggered by**: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Actor**: @${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Triggered by**: $GITHUB_EVENT_NAME" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Actor**: @$GITHUB_ACTOR" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
58
.github/workflows/test-base-action.yml
vendored
58
.github/workflows/test-base-action.yml
vendored
@@ -118,3 +118,61 @@ jobs:
|
||||
echo "❌ Execution log file not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
test-agent-sdk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Test with Agent SDK
|
||||
id: sdk-test
|
||||
uses: ./base-action
|
||||
env:
|
||||
USE_AGENT_SDK: "true"
|
||||
with:
|
||||
prompt: ${{ github.event.inputs.test_prompt || 'List the files in the current directory starting with "package"' }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
allowed_tools: "LS,Read"
|
||||
|
||||
- name: Verify SDK output
|
||||
run: |
|
||||
OUTPUT_FILE="${{ steps.sdk-test.outputs.execution_file }}"
|
||||
CONCLUSION="${{ steps.sdk-test.outputs.conclusion }}"
|
||||
|
||||
echo "Conclusion: $CONCLUSION"
|
||||
echo "Output file: $OUTPUT_FILE"
|
||||
|
||||
if [ "$CONCLUSION" = "success" ]; then
|
||||
echo "✅ Action completed successfully with Agent SDK"
|
||||
else
|
||||
echo "❌ Action failed with Agent SDK"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$OUTPUT_FILE" ]; then
|
||||
if [ -s "$OUTPUT_FILE" ]; then
|
||||
echo "✅ Execution log file created successfully with content"
|
||||
echo "Validating JSON format:"
|
||||
if jq . "$OUTPUT_FILE" > /dev/null 2>&1; then
|
||||
echo "✅ Output is valid JSON"
|
||||
# Verify SDK output contains total_cost_usd (SDK field name)
|
||||
if jq -e '.[] | select(.type == "result") | .total_cost_usd' "$OUTPUT_FILE" > /dev/null 2>&1; then
|
||||
echo "✅ SDK output contains total_cost_usd field"
|
||||
else
|
||||
echo "❌ SDK output missing total_cost_usd field"
|
||||
exit 1
|
||||
fi
|
||||
echo "Content preview:"
|
||||
head -c 500 "$OUTPUT_FILE"
|
||||
else
|
||||
echo "❌ Output is not valid JSON"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "❌ Execution log file is empty"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "❌ Execution log file not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
16
action.yml
16
action.yml
@@ -140,10 +140,12 @@ runs:
|
||||
- name: Setup Custom Bun Path
|
||||
if: inputs.path_to_bun_executable != ''
|
||||
shell: bash
|
||||
env:
|
||||
PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
|
||||
run: |
|
||||
echo "Using custom Bun executable: ${{ inputs.path_to_bun_executable }}"
|
||||
echo "Using custom Bun executable: $PATH_TO_BUN_EXECUTABLE"
|
||||
# Add the directory containing the custom executable to PATH
|
||||
BUN_DIR=$(dirname "${{ inputs.path_to_bun_executable }}")
|
||||
BUN_DIR=$(dirname "$PATH_TO_BUN_EXECUTABLE")
|
||||
echo "$BUN_DIR" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install Dependencies
|
||||
@@ -182,6 +184,8 @@ runs:
|
||||
- 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
|
||||
@@ -190,8 +194,8 @@ runs:
|
||||
cd -
|
||||
|
||||
# Install Claude Code if no custom executable is provided
|
||||
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
||||
CLAUDE_CODE_VERSION="2.0.55"
|
||||
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
||||
CLAUDE_CODE_VERSION="2.0.62"
|
||||
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
||||
for attempt in 1 2 3; do
|
||||
echo "Installation attempt $attempt..."
|
||||
@@ -210,9 +214,9 @@ runs:
|
||||
echo "Claude Code installed successfully"
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
else
|
||||
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
||||
echo "Using custom Claude Code executable: $PATH_TO_CLAUDE_CODE_EXECUTABLE"
|
||||
# Add the directory containing the custom executable to PATH
|
||||
CLAUDE_DIR=$(dirname "${{ inputs.path_to_claude_code_executable }}")
|
||||
CLAUDE_DIR=$(dirname "$PATH_TO_CLAUDE_CODE_EXECUTABLE")
|
||||
echo "$CLAUDE_DIR" >> "$GITHUB_PATH"
|
||||
fi
|
||||
|
||||
|
||||
@@ -101,10 +101,12 @@ runs:
|
||||
- name: Setup Custom Bun Path
|
||||
if: inputs.path_to_bun_executable != ''
|
||||
shell: bash
|
||||
env:
|
||||
PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
|
||||
run: |
|
||||
echo "Using custom Bun executable: ${{ inputs.path_to_bun_executable }}"
|
||||
echo "Using custom Bun executable: $PATH_TO_BUN_EXECUTABLE"
|
||||
# Add the directory containing the custom executable to PATH
|
||||
BUN_DIR=$(dirname "${{ inputs.path_to_bun_executable }}")
|
||||
BUN_DIR=$(dirname "$PATH_TO_BUN_EXECUTABLE")
|
||||
echo "$BUN_DIR" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install Dependencies
|
||||
@@ -115,9 +117,11 @@ runs:
|
||||
|
||||
- name: Install Claude Code
|
||||
shell: bash
|
||||
env:
|
||||
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
||||
run: |
|
||||
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
||||
CLAUDE_CODE_VERSION="2.0.55"
|
||||
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
||||
CLAUDE_CODE_VERSION="2.0.62"
|
||||
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
||||
for attempt in 1 2 3; do
|
||||
echo "Installation attempt $attempt..."
|
||||
@@ -135,9 +139,9 @@ runs:
|
||||
done
|
||||
echo "Claude Code installed successfully"
|
||||
else
|
||||
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
||||
echo "Using custom Claude Code executable: $PATH_TO_CLAUDE_CODE_EXECUTABLE"
|
||||
# Add the directory containing the custom executable to PATH
|
||||
CLAUDE_DIR=$(dirname "${{ inputs.path_to_claude_code_executable }}")
|
||||
CLAUDE_DIR=$(dirname "$PATH_TO_CLAUDE_CODE_EXECUTABLE")
|
||||
echo "$CLAUDE_DIR" >> "$GITHUB_PATH"
|
||||
fi
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"name": "@anthropic-ai/claude-code-base-action",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.52",
|
||||
"shell-quote": "^1.8.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -25,8 +26,40 @@
|
||||
|
||||
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.1.52", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^3.24.1" } }, "sha512-yF8N05+9NRbqYA/h39jQ726HTQFrdXXp7pEfDNKIJ2c4FdWvEjxBA/8ciZIebN6/PyvGDcbEp3yq2Co4rNpg6A=="],
|
||||
|
||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
|
||||
|
||||
"@types/node": ["@types/node@20.19.9", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw=="],
|
||||
@@ -50,5 +83,7 @@
|
||||
"undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.52",
|
||||
"shell-quote": "^1.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
149
base-action/src/parse-sdk-options.ts
Normal file
149
base-action/src/parse-sdk-options.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { parse as parseShellArgs } from "shell-quote";
|
||||
import type { ClaudeOptions } from "./run-claude";
|
||||
import type { Options as SdkOptions } from "@anthropic-ai/claude-agent-sdk";
|
||||
|
||||
/**
|
||||
* Result of parsing ClaudeOptions for SDK usage
|
||||
*/
|
||||
export type ParsedSdkOptions = {
|
||||
sdkOptions: SdkOptions;
|
||||
showFullOutput: boolean;
|
||||
hasJsonSchema: boolean;
|
||||
};
|
||||
|
||||
// Flags that should accumulate multiple values instead of overwriting
|
||||
const ACCUMULATING_FLAGS = new Set(["allowedTools", "disallowedTools"]);
|
||||
|
||||
/**
|
||||
* Parse claudeArgs string into extraArgs record for SDK pass-through
|
||||
* The SDK/CLI will handle --mcp-config, --json-schema, etc.
|
||||
* For allowedTools and disallowedTools, multiple occurrences are accumulated (comma-joined).
|
||||
*/
|
||||
function parseClaudeArgsToExtraArgs(
|
||||
claudeArgs?: string,
|
||||
): Record<string, string | null> {
|
||||
if (!claudeArgs?.trim()) return {};
|
||||
|
||||
const result: Record<string, string | null> = {};
|
||||
const args = parseShellArgs(claudeArgs).filter(
|
||||
(arg): arg is string => typeof arg === "string",
|
||||
);
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg?.startsWith("--")) {
|
||||
const flag = arg.slice(2);
|
||||
const nextArg = args[i + 1];
|
||||
|
||||
// Check if next arg is a value (not another flag)
|
||||
if (nextArg && !nextArg.startsWith("--")) {
|
||||
// For accumulating flags, join multiple values with commas
|
||||
if (ACCUMULATING_FLAGS.has(flag) && result[flag]) {
|
||||
result[flag] = `${result[flag]},${nextArg}`;
|
||||
} else {
|
||||
result[flag] = nextArg;
|
||||
}
|
||||
i++; // Skip the value
|
||||
} else {
|
||||
result[flag] = null; // Boolean flag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse ClaudeOptions into SDK-compatible options
|
||||
* Uses extraArgs for CLI pass-through instead of duplicating option parsing
|
||||
*/
|
||||
export function parseSdkOptions(options: ClaudeOptions): ParsedSdkOptions {
|
||||
// Determine output verbosity
|
||||
const isDebugMode = process.env.ACTIONS_STEP_DEBUG === "true";
|
||||
const showFullOutput = options.showFullOutput === "true" || isDebugMode;
|
||||
|
||||
// Parse claudeArgs into extraArgs for CLI pass-through
|
||||
const extraArgs = parseClaudeArgsToExtraArgs(options.claudeArgs);
|
||||
|
||||
// Detect if --json-schema is present (for hasJsonSchema flag)
|
||||
const hasJsonSchema = "json-schema" in extraArgs;
|
||||
|
||||
// Extract and merge allowedTools from both sources:
|
||||
// 1. From extraArgs (parsed from claudeArgs - contains tag mode's tools)
|
||||
// 2. From options.allowedTools (direct input - may be undefined)
|
||||
// This prevents duplicate flags being overwritten when claudeArgs contains --allowedTools
|
||||
const extraArgsAllowedTools = extraArgs["allowedTools"]
|
||||
? extraArgs["allowedTools"].split(",").map((t) => t.trim())
|
||||
: [];
|
||||
const directAllowedTools = options.allowedTools
|
||||
? options.allowedTools.split(",").map((t) => t.trim())
|
||||
: [];
|
||||
const mergedAllowedTools = [
|
||||
...new Set([...extraArgsAllowedTools, ...directAllowedTools]),
|
||||
];
|
||||
delete extraArgs["allowedTools"];
|
||||
|
||||
// Same for disallowedTools
|
||||
const extraArgsDisallowedTools = extraArgs["disallowedTools"]
|
||||
? extraArgs["disallowedTools"].split(",").map((t) => t.trim())
|
||||
: [];
|
||||
const directDisallowedTools = options.disallowedTools
|
||||
? options.disallowedTools.split(",").map((t) => t.trim())
|
||||
: [];
|
||||
const mergedDisallowedTools = [
|
||||
...new Set([...extraArgsDisallowedTools, ...directDisallowedTools]),
|
||||
];
|
||||
delete extraArgs["disallowedTools"];
|
||||
|
||||
// Build custom environment
|
||||
const env: Record<string, string | undefined> = { ...process.env };
|
||||
if (process.env.INPUT_ACTION_INPUTS_PRESENT) {
|
||||
env.GITHUB_ACTION_INPUTS = process.env.INPUT_ACTION_INPUTS_PRESENT;
|
||||
}
|
||||
|
||||
// Build system prompt option - default to claude_code preset
|
||||
let systemPrompt: SdkOptions["systemPrompt"];
|
||||
if (options.systemPrompt) {
|
||||
systemPrompt = options.systemPrompt;
|
||||
} else if (options.appendSystemPrompt) {
|
||||
systemPrompt = {
|
||||
type: "preset",
|
||||
preset: "claude_code",
|
||||
append: options.appendSystemPrompt,
|
||||
};
|
||||
} else {
|
||||
// Default to claude_code preset when no custom prompt is specified
|
||||
systemPrompt = {
|
||||
type: "preset",
|
||||
preset: "claude_code",
|
||||
};
|
||||
}
|
||||
|
||||
// Build SDK options - use merged tools from both direct options and claudeArgs
|
||||
const sdkOptions: SdkOptions = {
|
||||
// Direct options from ClaudeOptions inputs
|
||||
model: options.model,
|
||||
maxTurns: options.maxTurns ? parseInt(options.maxTurns, 10) : undefined,
|
||||
allowedTools:
|
||||
mergedAllowedTools.length > 0 ? mergedAllowedTools : undefined,
|
||||
disallowedTools:
|
||||
mergedDisallowedTools.length > 0 ? mergedDisallowedTools : undefined,
|
||||
systemPrompt,
|
||||
fallbackModel: options.fallbackModel,
|
||||
pathToClaudeCodeExecutable: options.pathToClaudeCodeExecutable,
|
||||
|
||||
// Pass through claudeArgs as extraArgs - CLI handles --mcp-config, --json-schema, etc.
|
||||
// Note: allowedTools and disallowedTools have been removed from extraArgs to prevent duplicates
|
||||
extraArgs,
|
||||
env,
|
||||
|
||||
// Load settings from all sources to pick up CLI-installed plugins, CLAUDE.md, etc.
|
||||
settingSources: ["user", "project", "local"],
|
||||
};
|
||||
|
||||
return {
|
||||
sdkOptions,
|
||||
showFullOutput,
|
||||
hasJsonSchema,
|
||||
};
|
||||
}
|
||||
151
base-action/src/run-claude-sdk.ts
Normal file
151
base-action/src/run-claude-sdk.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import * as core from "@actions/core";
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type {
|
||||
SDKMessage,
|
||||
SDKResultMessage,
|
||||
} from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { ParsedSdkOptions } from "./parse-sdk-options";
|
||||
|
||||
const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`;
|
||||
|
||||
/**
|
||||
* Sanitizes SDK output to match CLI sanitization behavior
|
||||
*/
|
||||
function sanitizeSdkOutput(
|
||||
message: SDKMessage,
|
||||
showFullOutput: boolean,
|
||||
): string | null {
|
||||
if (showFullOutput) {
|
||||
return JSON.stringify(message, null, 2);
|
||||
}
|
||||
|
||||
// System initialization - safe to show
|
||||
if (message.type === "system" && message.subtype === "init") {
|
||||
return JSON.stringify(
|
||||
{
|
||||
type: "system",
|
||||
subtype: "init",
|
||||
message: "Claude Code initialized",
|
||||
model: "model" in message ? message.model : "unknown",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
// Result messages - show sanitized summary
|
||||
if (message.type === "result") {
|
||||
const resultMsg = message as SDKResultMessage;
|
||||
return JSON.stringify(
|
||||
{
|
||||
type: "result",
|
||||
subtype: resultMsg.subtype,
|
||||
is_error: resultMsg.is_error,
|
||||
duration_ms: resultMsg.duration_ms,
|
||||
num_turns: resultMsg.num_turns,
|
||||
total_cost_usd: resultMsg.total_cost_usd,
|
||||
permission_denials: resultMsg.permission_denials,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
// Suppress other message types in non-full-output mode
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run Claude using the Agent SDK
|
||||
*/
|
||||
export async function runClaudeWithSdk(
|
||||
promptPath: string,
|
||||
{ sdkOptions, showFullOutput, hasJsonSchema }: ParsedSdkOptions,
|
||||
): Promise<void> {
|
||||
const prompt = await readFile(promptPath, "utf-8");
|
||||
|
||||
if (!showFullOutput) {
|
||||
console.log(
|
||||
"Running Claude Code via SDK (full output hidden for security)...",
|
||||
);
|
||||
console.log(
|
||||
"Rerun in debug mode or enable `show_full_output: true` in your workflow file for full output.",
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Running Claude with prompt from file: ${promptPath}`);
|
||||
// Log SDK options without env (which could contain sensitive data)
|
||||
const { env, ...optionsToLog } = sdkOptions;
|
||||
console.log("SDK options:", JSON.stringify(optionsToLog, null, 2));
|
||||
|
||||
const messages: SDKMessage[] = [];
|
||||
let resultMessage: SDKResultMessage | undefined;
|
||||
|
||||
try {
|
||||
for await (const message of query({ prompt, options: sdkOptions })) {
|
||||
messages.push(message);
|
||||
|
||||
const sanitized = sanitizeSdkOutput(message, showFullOutput);
|
||||
if (sanitized) {
|
||||
console.log(sanitized);
|
||||
}
|
||||
|
||||
if (message.type === "result") {
|
||||
resultMessage = message as SDKResultMessage;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("SDK execution error:", error);
|
||||
core.setOutput("conclusion", "failure");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 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);
|
||||
} catch (error) {
|
||||
core.warning(`Failed to write execution file: ${error}`);
|
||||
}
|
||||
|
||||
if (!resultMessage) {
|
||||
core.setOutput("conclusion", "failure");
|
||||
core.error("No result message received from Claude");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const isSuccess = resultMessage.subtype === "success";
|
||||
core.setOutput("conclusion", isSuccess ? "success" : "failure");
|
||||
|
||||
// Handle structured output
|
||||
if (hasJsonSchema) {
|
||||
if (
|
||||
isSuccess &&
|
||||
"structured_output" in resultMessage &&
|
||||
resultMessage.structured_output
|
||||
) {
|
||||
const structuredOutputJson = JSON.stringify(
|
||||
resultMessage.structured_output,
|
||||
);
|
||||
core.setOutput("structured_output", structuredOutputJson);
|
||||
core.info(
|
||||
`Set structured_output with ${Object.keys(resultMessage.structured_output as object).length} field(s)`,
|
||||
);
|
||||
} else {
|
||||
core.setFailed(
|
||||
`--json-schema was provided but Claude did not return structured_output. Result subtype: ${resultMessage.subtype}`,
|
||||
);
|
||||
core.setOutput("conclusion", "failure");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSuccess) {
|
||||
if ("errors" in resultMessage && resultMessage.errors) {
|
||||
core.error(`Execution failed: ${resultMessage.errors.join(", ")}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { unlink, writeFile, stat, readFile } from "fs/promises";
|
||||
import { createWriteStream } from "fs";
|
||||
import { spawn } from "child_process";
|
||||
import { parse as parseShellArgs } from "shell-quote";
|
||||
import { runClaudeWithSdk } from "./run-claude-sdk";
|
||||
import { parseSdkOptions } from "./parse-sdk-options";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -165,6 +167,17 @@ export async function parseAndSetStructuredOutputs(
|
||||
}
|
||||
|
||||
export async function runClaude(promptPath: string, options: ClaudeOptions) {
|
||||
// Feature flag: use SDK path when USE_AGENT_SDK=true
|
||||
const useAgentSdk = process.env.USE_AGENT_SDK === "true";
|
||||
console.log(
|
||||
`Using ${useAgentSdk ? "Agent SDK" : "CLI"} path (USE_AGENT_SDK=${process.env.USE_AGENT_SDK ?? "unset"})`,
|
||||
);
|
||||
|
||||
if (useAgentSdk) {
|
||||
const parsedOptions = parseSdkOptions(options);
|
||||
return runClaudeWithSdk(promptPath, parsedOptions);
|
||||
}
|
||||
|
||||
const config = prepareRunConfig(promptPath, options);
|
||||
|
||||
// Detect if --json-schema is present in claude args
|
||||
|
||||
163
base-action/test/parse-sdk-options.test.ts
Normal file
163
base-action/test/parse-sdk-options.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { parseSdkOptions } from "../src/parse-sdk-options";
|
||||
import type { ClaudeOptions } from "../src/run-claude";
|
||||
|
||||
describe("parseSdkOptions", () => {
|
||||
describe("allowedTools merging", () => {
|
||||
test("should extract allowedTools from claudeArgs", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--allowedTools "Edit,Read,Write"',
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual(["Edit", "Read", "Write"]);
|
||||
expect(result.sdkOptions.extraArgs?.["allowedTools"]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should extract allowedTools from claudeArgs with MCP tools", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs:
|
||||
'--allowedTools "Edit,Read,mcp__github_comment__update_claude_comment"',
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual([
|
||||
"Edit",
|
||||
"Read",
|
||||
"mcp__github_comment__update_claude_comment",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should accumulate multiple --allowedTools flags from claudeArgs", () => {
|
||||
// This simulates tag mode adding its tools, then user adding their own
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs:
|
||||
'--allowedTools "Edit,Read,mcp__github_comment__update_claude_comment" --model "claude-3" --allowedTools "Bash(npm install),mcp__github__get_issue"',
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual([
|
||||
"Edit",
|
||||
"Read",
|
||||
"mcp__github_comment__update_claude_comment",
|
||||
"Bash(npm install)",
|
||||
"mcp__github__get_issue",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should merge allowedTools from both claudeArgs and direct options", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--allowedTools "Edit,Read"',
|
||||
allowedTools: "Write,Glob",
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual([
|
||||
"Edit",
|
||||
"Read",
|
||||
"Write",
|
||||
"Glob",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should deduplicate allowedTools when merging", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--allowedTools "Edit,Read"',
|
||||
allowedTools: "Edit,Write",
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual(["Edit", "Read", "Write"]);
|
||||
});
|
||||
|
||||
test("should use only direct options when claudeArgs has no allowedTools", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--model "claude-3-5-sonnet"',
|
||||
allowedTools: "Edit,Read",
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual(["Edit", "Read"]);
|
||||
});
|
||||
|
||||
test("should return undefined allowedTools when neither source has it", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--model "claude-3-5-sonnet"',
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should remove allowedTools from extraArgs after extraction", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--allowedTools "Edit,Read" --model "claude-3-5-sonnet"',
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.extraArgs?.["allowedTools"]).toBeUndefined();
|
||||
expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude-3-5-sonnet");
|
||||
});
|
||||
});
|
||||
|
||||
describe("disallowedTools merging", () => {
|
||||
test("should extract disallowedTools from claudeArgs", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--disallowedTools "Bash,Write"',
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.disallowedTools).toEqual(["Bash", "Write"]);
|
||||
expect(result.sdkOptions.extraArgs?.["disallowedTools"]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should merge disallowedTools from both sources", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--disallowedTools "Bash"',
|
||||
disallowedTools: "Write",
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.disallowedTools).toEqual(["Bash", "Write"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("other extraArgs passthrough", () => {
|
||||
test("should pass through mcp-config in extraArgs", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: `--mcp-config '{"mcpServers":{}}' --allowedTools "Edit"`,
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.extraArgs?.["mcp-config"]).toBe(
|
||||
'{"mcpServers":{}}',
|
||||
);
|
||||
});
|
||||
|
||||
test("should pass through json-schema in extraArgs", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: `--json-schema '{"type":"object"}'`,
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.extraArgs?.["json-schema"]).toBe(
|
||||
'{"type":"object"}',
|
||||
);
|
||||
expect(result.hasJsonSchema).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
33
bun.lock
33
bun.lock
@@ -6,6 +6,7 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.52",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"@octokit/graphql": "^8.2.2",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
@@ -35,8 +36,40 @@
|
||||
|
||||
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.1.52", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^3.24.1" } }, "sha512-yF8N05+9NRbqYA/h39jQ726HTQFrdXXp7pEfDNKIJ2c4FdWvEjxBA/8ciZIebN6/PyvGDcbEp3yq2Co4rNpg6A=="],
|
||||
|
||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.16.0", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-8ofX7gkZcLj9H9rSd50mCgm3SSF8C7XoclxJuLoV0Cz3rEQ1tv9MZRYYvJtm9n1BiEQQMzSmE/w2AEkNacLYfg=="],
|
||||
|
||||
"@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="],
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.52",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"@octokit/graphql": "^8.2.2",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
|
||||
@@ -192,11 +192,6 @@ export function prepareContext(
|
||||
if (!isPR) {
|
||||
throw new Error("IS_PR must be true for pull_request_review event");
|
||||
}
|
||||
if (!commentBody) {
|
||||
throw new Error(
|
||||
"COMMENT_BODY is required for pull_request_review event",
|
||||
);
|
||||
}
|
||||
eventData = {
|
||||
eventName: "pull_request_review",
|
||||
isPR: true,
|
||||
@@ -464,6 +459,123 @@ export function generatePrompt(
|
||||
return mode.generatePrompt(context, githubData, useCommitSigning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a simplified prompt for tag mode (opt-in via USE_SIMPLE_PROMPT env var)
|
||||
* @internal
|
||||
*/
|
||||
function generateSimplePrompt(
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
useCommitSigning: boolean = false,
|
||||
): string {
|
||||
const {
|
||||
contextData,
|
||||
comments,
|
||||
changedFilesWithSHA,
|
||||
reviewData,
|
||||
imageUrlMap,
|
||||
} = githubData;
|
||||
const { eventData } = context;
|
||||
|
||||
const { triggerContext } = getEventTypeAndContext(context);
|
||||
|
||||
const formattedContext = formatContext(contextData, eventData.isPR);
|
||||
const formattedComments = formatComments(comments, imageUrlMap);
|
||||
const formattedReviewComments = eventData.isPR
|
||||
? formatReviewComments(reviewData, imageUrlMap)
|
||||
: "";
|
||||
const formattedChangedFiles = eventData.isPR
|
||||
? formatChangedFilesWithSHA(changedFilesWithSHA)
|
||||
: "";
|
||||
|
||||
const hasImages = imageUrlMap && imageUrlMap.size > 0;
|
||||
const imagesInfo = hasImages
|
||||
? `\n\n<images_info>
|
||||
Images from comments have been saved to disk. Paths are in the formatted content above. Use Read tool to view them.
|
||||
</images_info>`
|
||||
: "";
|
||||
|
||||
const formattedBody = contextData?.body
|
||||
? formatBody(contextData.body, imageUrlMap)
|
||||
: "No description provided";
|
||||
|
||||
const entityType = eventData.isPR ? "pull request" : "issue";
|
||||
const jobUrl = `${GITHUB_SERVER_URL}/${context.repository}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
||||
|
||||
let promptContent = `You were tagged on a GitHub ${entityType} via "${context.triggerPhrase}". Read the request and decide how to help.
|
||||
|
||||
<context>
|
||||
${formattedContext}
|
||||
</context>
|
||||
|
||||
<${eventData.isPR ? "pr" : "issue"}_body>
|
||||
${formattedBody}
|
||||
</${eventData.isPR ? "pr" : "issue"}_body>
|
||||
|
||||
<comments>
|
||||
${formattedComments || "No comments"}
|
||||
</comments>
|
||||
${
|
||||
eventData.isPR
|
||||
? `
|
||||
<review_comments>
|
||||
${formattedReviewComments || "No review comments"}
|
||||
</review_comments>
|
||||
|
||||
<changed_files>
|
||||
${formattedChangedFiles || "No files changed"}
|
||||
</changed_files>`
|
||||
: ""
|
||||
}${imagesInfo}
|
||||
|
||||
<metadata>
|
||||
repository: ${context.repository}
|
||||
${eventData.isPR && eventData.prNumber ? `pr_number: ${eventData.prNumber}` : ""}
|
||||
${!eventData.isPR && eventData.issueNumber ? `issue_number: ${eventData.issueNumber}` : ""}
|
||||
trigger: ${triggerContext}
|
||||
triggered_by: ${context.triggerUsername ?? "Unknown"}
|
||||
claude_comment_id: ${context.claudeCommentId}
|
||||
</metadata>
|
||||
${
|
||||
(eventData.eventName === "issue_comment" ||
|
||||
eventData.eventName === "pull_request_review_comment" ||
|
||||
eventData.eventName === "pull_request_review") &&
|
||||
eventData.commentBody
|
||||
? `
|
||||
<trigger_comment>
|
||||
${sanitizeContent(eventData.commentBody)}
|
||||
</trigger_comment>`
|
||||
: ""
|
||||
}
|
||||
|
||||
Your request is in <trigger_comment> above${eventData.eventName === "issues" ? ` (or the ${entityType} body for assigned/labeled events)` : ""}.
|
||||
|
||||
Decide what's being asked:
|
||||
1. **Question or code review** - Answer directly or provide feedback
|
||||
2. **Code change** - Implement the change, commit, and push
|
||||
|
||||
Communication:
|
||||
- Your ONLY visible output is your GitHub comment - update it with progress and results
|
||||
- Use mcp__github_comment__update_claude_comment to update (only "body" param needed)
|
||||
- Use checklist format for tasks: - [ ] incomplete, - [x] complete
|
||||
- Use ### headers (not #)
|
||||
${getCommitInstructions(eventData, githubData, context, useCommitSigning)}
|
||||
${
|
||||
eventData.claudeBranch
|
||||
? `
|
||||
When done with changes, provide a PR link:
|
||||
[Create a PR](${GITHUB_SERVER_URL}/${context.repository}/compare/${eventData.baseBranch}...${eventData.claudeBranch}?quick_pull=1&title=<url-encoded-title>&body=<url-encoded-body>)
|
||||
Use THREE dots (...) between branches. URL-encode all parameters.`
|
||||
: ""
|
||||
}
|
||||
|
||||
Always include at the bottom:
|
||||
- Job link: [View job run](${jobUrl})
|
||||
- Follow the repo's CLAUDE.md file for project-specific guidelines`;
|
||||
|
||||
return promptContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the default prompt for tag mode
|
||||
* @internal
|
||||
@@ -473,6 +585,10 @@ export function generateDefaultPrompt(
|
||||
githubData: FetchDataResult,
|
||||
useCommitSigning: boolean = false,
|
||||
): string {
|
||||
// Use simplified prompt if opted in
|
||||
if (process.env.USE_SIMPLE_PROMPT === "true") {
|
||||
return generateSimplePrompt(context, githubData, useCommitSigning);
|
||||
}
|
||||
const {
|
||||
contextData,
|
||||
comments,
|
||||
|
||||
@@ -23,7 +23,7 @@ type PullRequestReviewEvent = {
|
||||
eventName: "pull_request_review";
|
||||
isPR: true;
|
||||
prNumber: string;
|
||||
commentBody: string;
|
||||
commentBody?: string; // May be absent for approvals without comments
|
||||
claudeBranch?: string;
|
||||
baseBranch?: string;
|
||||
};
|
||||
|
||||
@@ -152,7 +152,7 @@ async function run() {
|
||||
|
||||
// Check if action failed and read output file for execution details
|
||||
let executionDetails: {
|
||||
cost_usd?: number;
|
||||
total_cost_usd?: number;
|
||||
duration_ms?: number;
|
||||
duration_api_ms?: number;
|
||||
} | null = null;
|
||||
@@ -179,11 +179,11 @@ async function run() {
|
||||
const lastElement = outputData[outputData.length - 1];
|
||||
if (
|
||||
lastElement.type === "result" &&
|
||||
"cost_usd" in lastElement &&
|
||||
"total_cost_usd" in lastElement &&
|
||||
"duration_ms" in lastElement
|
||||
) {
|
||||
executionDetails = {
|
||||
cost_usd: lastElement.cost_usd,
|
||||
total_cost_usd: lastElement.total_cost_usd,
|
||||
duration_ms: lastElement.duration_ms,
|
||||
duration_api_ms: lastElement.duration_api_ms,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { GITHUB_SERVER_URL } from "../api/config";
|
||||
|
||||
export type ExecutionDetails = {
|
||||
cost_usd?: number;
|
||||
total_cost_usd?: number;
|
||||
duration_ms?: number;
|
||||
duration_api_ms?: number;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
import { readFile, stat } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { join, resolve, sep } from "path";
|
||||
import { constants } from "fs";
|
||||
import fetch from "node-fetch";
|
||||
import { GITHUB_API_URL } from "../github/api/config";
|
||||
@@ -474,20 +474,21 @@ server.tool(
|
||||
throw new Error("GITHUB_TOKEN environment variable is required");
|
||||
}
|
||||
|
||||
// Convert absolute paths to relative if they match CWD
|
||||
// Normalize all paths and validate they're within the repository root
|
||||
const cwd = process.cwd();
|
||||
const processedPaths = paths.map((filePath) => {
|
||||
if (filePath.startsWith("/")) {
|
||||
if (filePath.startsWith(cwd)) {
|
||||
// Strip CWD from absolute path
|
||||
return filePath.slice(cwd.length + 1);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Path '${filePath}' must be relative to repository root or within current working directory`,
|
||||
);
|
||||
}
|
||||
// Normalize the path to resolve any .. or . sequences
|
||||
const normalizedPath = resolve(cwd, filePath);
|
||||
|
||||
// Validate the normalized path is within the current working directory
|
||||
if (!normalizedPath.startsWith(cwd + sep)) {
|
||||
throw new Error(
|
||||
`Path '${filePath}' resolves outside the repository root`,
|
||||
);
|
||||
}
|
||||
return filePath;
|
||||
|
||||
// Convert to relative path by stripping the cwd prefix
|
||||
return normalizedPath.slice(cwd.length + 1);
|
||||
});
|
||||
|
||||
// 1. Get the branch reference (create if doesn't exist)
|
||||
|
||||
@@ -258,7 +258,7 @@ describe("updateCommentBody", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
executionDetails: {
|
||||
cost_usd: 0.13382595,
|
||||
total_cost_usd: 0.13382595,
|
||||
duration_ms: 31033,
|
||||
duration_api_ms: 31034,
|
||||
},
|
||||
@@ -301,7 +301,7 @@ describe("updateCommentBody", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
executionDetails: {
|
||||
cost_usd: 0.25,
|
||||
total_cost_usd: 0.25,
|
||||
},
|
||||
triggerUsername: "testuser",
|
||||
};
|
||||
@@ -322,7 +322,7 @@ describe("updateCommentBody", () => {
|
||||
branchName: "claude-branch-123",
|
||||
prLink: "\n[Create a PR](https://github.com/owner/repo/pr-url)",
|
||||
executionDetails: {
|
||||
cost_usd: 0.01,
|
||||
total_cost_usd: 0.01,
|
||||
duration_ms: 65000, // 1 minute 5 seconds
|
||||
},
|
||||
triggerUsername: "trigger-user",
|
||||
|
||||
2
test/fixtures/sample-turns.json
vendored
2
test/fixtures/sample-turns.json
vendored
@@ -189,7 +189,7 @@
|
||||
},
|
||||
{
|
||||
"type": "result",
|
||||
"cost_usd": 0.0347,
|
||||
"total_cost_usd": 0.0347,
|
||||
"duration_ms": 18750,
|
||||
"result": "Successfully removed debug print statement from file and added review comment to document the change."
|
||||
}
|
||||
|
||||
@@ -401,6 +401,53 @@ export const mockPullRequestReviewContext: ParsedGitHubContext = {
|
||||
inputs: { ...defaultInputs, triggerPhrase: "@claude" },
|
||||
};
|
||||
|
||||
export const mockPullRequestReviewWithoutCommentContext: ParsedGitHubContext = {
|
||||
runId: "1234567890",
|
||||
eventName: "pull_request_review",
|
||||
eventAction: "dismissed",
|
||||
repository: defaultRepository,
|
||||
actor: "senior-developer",
|
||||
payload: {
|
||||
action: "submitted",
|
||||
review: {
|
||||
id: 11122233,
|
||||
body: null, // Simulating approval without comment
|
||||
user: {
|
||||
login: "senior-developer",
|
||||
id: 44444,
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/44444",
|
||||
html_url: "https://github.com/senior-developer",
|
||||
},
|
||||
state: "approved",
|
||||
html_url:
|
||||
"https://github.com/test-owner/test-repo/pull/321#pullrequestreview-11122233",
|
||||
submitted_at: "2024-01-15T15:30:00Z",
|
||||
},
|
||||
pull_request: {
|
||||
number: 321,
|
||||
title: "Refactor: Improve error handling in API layer",
|
||||
body: "This PR improves error handling across all API endpoints",
|
||||
user: {
|
||||
login: "backend-developer",
|
||||
id: 33333,
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/33333",
|
||||
html_url: "https://github.com/backend-developer",
|
||||
},
|
||||
},
|
||||
repository: {
|
||||
name: "test-repo",
|
||||
full_name: "test-owner/test-repo",
|
||||
private: false,
|
||||
owner: {
|
||||
login: "test-owner",
|
||||
},
|
||||
},
|
||||
} as PullRequestReviewEvent,
|
||||
entityNumber: 321,
|
||||
isPR: true,
|
||||
inputs: { ...defaultInputs, triggerPhrase: "@claude" },
|
||||
};
|
||||
|
||||
export const mockPullRequestReviewCommentContext: ParsedGitHubContext = {
|
||||
runId: "1234567890",
|
||||
eventName: "pull_request_review_comment",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
mockPullRequestCommentContext,
|
||||
mockPullRequestReviewContext,
|
||||
mockPullRequestReviewCommentContext,
|
||||
mockPullRequestReviewWithoutCommentContext,
|
||||
} from "./mockContext";
|
||||
|
||||
const BASE_ENV = {
|
||||
@@ -126,6 +127,24 @@ describe("parseEnvVarsWithContext", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("pull_request_review event without comment", () => {
|
||||
test("should parse pull_request_review event correctly", () => {
|
||||
process.env = BASE_ENV;
|
||||
const result = prepareContext(
|
||||
mockPullRequestReviewWithoutCommentContext,
|
||||
"12345",
|
||||
);
|
||||
|
||||
expect(result.eventData.eventName).toBe("pull_request_review");
|
||||
expect(result.eventData.isPR).toBe(true);
|
||||
expect(result.triggerUsername).toBe("senior-developer");
|
||||
if (result.eventData.eventName === "pull_request_review") {
|
||||
expect(result.eventData.prNumber).toBe("321");
|
||||
expect(result.eventData.commentBody).toBe("");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("pull_request_review_comment event", () => {
|
||||
test("should parse pull_request_review_comment event correctly", () => {
|
||||
process.env = BASE_ENV;
|
||||
|
||||
Reference in New Issue
Block a user