mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 23:14:13 +08:00
Compare commits
11 Commits
v0.0.59
...
ashwin/env
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8177a01b9e | ||
|
|
9f02f6f6d4 | ||
|
|
79cee96324 | ||
|
|
194fca8b05 | ||
|
|
0f913a6e0e | ||
|
|
68b7ca379c | ||
|
|
900322ca88 | ||
|
|
8f0a7fe9d3 | ||
|
|
db36412854 | ||
|
|
f05d669d5f | ||
|
|
e89411bb6f |
2
.github/workflows/issue-triage.yml
vendored
2
.github/workflows/issue-triage.yml
vendored
@@ -104,3 +104,5 @@ jobs:
|
|||||||
mcp_config: /tmp/mcp-config/mcp-servers.json
|
mcp_config: /tmp/mcp-config/mcp-servers.json
|
||||||
timeout_minutes: "5"
|
timeout_minutes: "5"
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ runs:
|
|||||||
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
||||||
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
|
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
|
||||||
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
|
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
|
||||||
|
ALL_INPUTS: ${{ toJson(inputs) }}
|
||||||
|
|
||||||
- name: Install Base Action Dependencies
|
- name: Install Base Action Dependencies
|
||||||
if: steps.prepare.outputs.contains_trigger == 'true'
|
if: steps.prepare.outputs.contains_trigger == 'true'
|
||||||
@@ -177,7 +178,7 @@ runs:
|
|||||||
echo "Base-action dependencies installed"
|
echo "Base-action dependencies installed"
|
||||||
cd -
|
cd -
|
||||||
# Install Claude Code globally
|
# Install Claude Code globally
|
||||||
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.83
|
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.86
|
||||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
- name: Setup Network Restrictions
|
- name: Setup Network Restrictions
|
||||||
@@ -212,6 +213,7 @@ runs:
|
|||||||
INPUT_CLAUDE_ENV: ${{ inputs.claude_env }}
|
INPUT_CLAUDE_ENV: ${{ inputs.claude_env }}
|
||||||
INPUT_FALLBACK_MODEL: ${{ inputs.fallback_model }}
|
INPUT_FALLBACK_MODEL: ${{ inputs.fallback_model }}
|
||||||
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 }}
|
||||||
|
|
||||||
# Model configuration
|
# Model configuration
|
||||||
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
|
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
|
||||||
@@ -286,7 +288,7 @@ runs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Revoke app token
|
- name: Revoke app token
|
||||||
if: always() && inputs.github_token == ''
|
if: always() && inputs.github_token == '' && steps.prepare.outputs.skipped_due_to_workflow_validation_mismatch != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
curl -L \
|
curl -L \
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ runs:
|
|||||||
|
|
||||||
- name: Install Claude Code
|
- name: Install Claude Code
|
||||||
shell: bash
|
shell: bash
|
||||||
run: curl -fsSL https://claude.ai/install.sh | bash -s 1.0.83
|
run: curl -fsSL https://claude.ai/install.sh | bash -s 1.0.86
|
||||||
|
|
||||||
- name: Run Claude Code Action
|
- name: Run Claude Code Action
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ export function prepareRunConfig(
|
|||||||
// Parse custom environment variables
|
// Parse custom environment variables
|
||||||
const customEnv = parseCustomEnvVars(options.claudeEnv);
|
const customEnv = parseCustomEnvVars(options.claudeEnv);
|
||||||
|
|
||||||
|
if (process.env.INPUT_ACTION_INPUTS_PRESENT) {
|
||||||
|
customEnv.GITHUB_ACTION_INPUTS = process.env.INPUT_ACTION_INPUTS_PRESENT;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
claudeArgs,
|
claudeArgs,
|
||||||
promptPath,
|
promptPath,
|
||||||
@@ -141,12 +145,6 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
|
|||||||
|
|
||||||
console.log(`Prompt file size: ${promptSize} bytes`);
|
console.log(`Prompt file size: ${promptSize} bytes`);
|
||||||
|
|
||||||
// Log custom environment variables if any
|
|
||||||
if (Object.keys(config.env).length > 0) {
|
|
||||||
const envKeys = Object.keys(config.env).join(", ");
|
|
||||||
console.log(`Custom environment variables: ${envKeys}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output to console
|
// Output to console
|
||||||
console.log(`Running Claude with prompt from file: ${config.promptPath}`);
|
console.log(`Running Claude with prompt from file: ${config.promptPath}`);
|
||||||
|
|
||||||
@@ -162,6 +160,11 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
|
|||||||
pipeStream.destroy();
|
pipeStream.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("yolo", process.env);
|
||||||
|
console.log("yolo running with", {
|
||||||
|
...process.env,
|
||||||
|
...config.env,
|
||||||
|
});
|
||||||
const claudeProcess = spawn("claude", config.claudeArgs, {
|
const claudeProcess = spawn("claude", config.claudeArgs, {
|
||||||
stdio: ["pipe", "pipe", "inherit"],
|
stdio: ["pipe", "pipe", "inherit"],
|
||||||
env: {
|
env: {
|
||||||
@@ -301,7 +304,10 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
|
|||||||
await writeFile("output.txt", output);
|
await writeFile("output.txt", output);
|
||||||
|
|
||||||
// Process output.txt into JSON and save to execution file
|
// Process output.txt into JSON and save to execution file
|
||||||
const { stdout: jsonOutput } = await execAsync("jq -s '.' output.txt");
|
// Increase maxBuffer from Node.js default of 1MB to 10MB to handle large Claude outputs
|
||||||
|
const { stdout: jsonOutput } = await execAsync("jq -s '.' output.txt", {
|
||||||
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
});
|
||||||
await writeFile(EXECUTION_FILE, jsonOutput);
|
await writeFile(EXECUTION_FILE, jsonOutput);
|
||||||
|
|
||||||
console.log(`Log saved to ${EXECUTION_FILE}`);
|
console.log(`Log saved to ${EXECUTION_FILE}`);
|
||||||
@@ -318,7 +324,10 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
|
|||||||
if (output) {
|
if (output) {
|
||||||
try {
|
try {
|
||||||
await writeFile("output.txt", output);
|
await writeFile("output.txt", output);
|
||||||
const { stdout: jsonOutput } = await execAsync("jq -s '.' output.txt");
|
// Increase maxBuffer from Node.js default of 1MB to 10MB to handle large Claude outputs
|
||||||
|
const { stdout: jsonOutput } = await execAsync("jq -s '.' output.txt", {
|
||||||
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
});
|
||||||
await writeFile(EXECUTION_FILE, jsonOutput);
|
await writeFile(EXECUTION_FILE, jsonOutput);
|
||||||
core.setOutput("execution_file", EXECUTION_FILE);
|
core.setOutput("execution_file", EXECUTION_FILE);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -836,7 +836,7 @@ export async function createPrompt(
|
|||||||
modeContext.claudeBranch,
|
modeContext.claudeBranch,
|
||||||
);
|
);
|
||||||
|
|
||||||
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, {
|
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -855,7 +855,7 @@ export async function createPrompt(
|
|||||||
|
|
||||||
// Write the prompt file
|
// Write the prompt file
|
||||||
await writeFile(
|
await writeFile(
|
||||||
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`,
|
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
|
||||||
promptContent,
|
promptContent,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
59
src/entrypoints/collect-inputs.ts
Normal file
59
src/entrypoints/collect-inputs.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
|
export function collectActionInputsPresence(): void {
|
||||||
|
const inputDefaults: Record<string, string> = {
|
||||||
|
trigger_phrase: "@claude",
|
||||||
|
assignee_trigger: "",
|
||||||
|
label_trigger: "claude",
|
||||||
|
base_branch: "",
|
||||||
|
branch_prefix: "claude/",
|
||||||
|
allowed_bots: "",
|
||||||
|
mode: "tag",
|
||||||
|
model: "",
|
||||||
|
anthropic_model: "",
|
||||||
|
fallback_model: "",
|
||||||
|
allowed_tools: "",
|
||||||
|
disallowed_tools: "",
|
||||||
|
custom_instructions: "",
|
||||||
|
direct_prompt: "",
|
||||||
|
override_prompt: "",
|
||||||
|
mcp_config: "",
|
||||||
|
additional_permissions: "",
|
||||||
|
claude_env: "",
|
||||||
|
settings: "",
|
||||||
|
anthropic_api_key: "",
|
||||||
|
claude_code_oauth_token: "",
|
||||||
|
github_token: "",
|
||||||
|
max_turns: "",
|
||||||
|
use_sticky_comment: "false",
|
||||||
|
use_commit_signing: "false",
|
||||||
|
experimental_allowed_domains: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const allInputsJson = process.env.ALL_INPUTS;
|
||||||
|
if (!allInputsJson) {
|
||||||
|
console.log("ALL_INPUTS environment variable not found");
|
||||||
|
core.setOutput("action_inputs_present", JSON.stringify({}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let allInputs: Record<string, string>;
|
||||||
|
try {
|
||||||
|
allInputs = JSON.parse(allInputsJson);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse ALL_INPUTS JSON:", e);
|
||||||
|
core.setOutput("action_inputs_present", JSON.stringify({}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentInputs: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
for (const [name, defaultValue] of Object.entries(inputDefaults)) {
|
||||||
|
const actualValue = allInputs[name] || "";
|
||||||
|
|
||||||
|
const isSet = actualValue !== defaultValue;
|
||||||
|
presentInputs[name] = isSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
core.setOutput("action_inputs_present", JSON.stringify(presentInputs));
|
||||||
|
}
|
||||||
@@ -13,9 +13,12 @@ import { parseGitHubContext, isEntityContext } from "../github/context";
|
|||||||
import { getMode, isValidMode, DEFAULT_MODE } from "../modes/registry";
|
import { getMode, isValidMode, DEFAULT_MODE } from "../modes/registry";
|
||||||
import type { ModeName } from "../modes/types";
|
import type { ModeName } from "../modes/types";
|
||||||
import { prepare } from "../prepare";
|
import { prepare } from "../prepare";
|
||||||
|
import { collectActionInputsPresence } from "./collect-inputs";
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
|
collectActionInputsPresence();
|
||||||
|
|
||||||
// Step 1: Get mode first to determine authentication method
|
// Step 1: Get mode first to determine authentication method
|
||||||
const modeInput = process.env.MODE || DEFAULT_MODE;
|
const modeInput = process.env.MODE || DEFAULT_MODE;
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,30 @@ async function exchangeForAppToken(oidcToken: string): Promise<string> {
|
|||||||
const responseJson = (await response.json()) as {
|
const responseJson = (await response.json()) as {
|
||||||
error?: {
|
error?: {
|
||||||
message?: string;
|
message?: string;
|
||||||
|
details?: {
|
||||||
|
error_code?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
type?: string;
|
||||||
|
message?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check for specific workflow validation error codes that should skip the action
|
||||||
|
const errorCode = responseJson.error?.details?.error_code;
|
||||||
|
|
||||||
|
if (errorCode === "workflow_not_found_on_default_branch") {
|
||||||
|
const message =
|
||||||
|
responseJson.message ??
|
||||||
|
responseJson.error?.message ??
|
||||||
|
"Workflow validation failed";
|
||||||
|
core.warning(`Skipping action due to workflow validation: ${message}`);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
console.error(
|
console.error(
|
||||||
`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson?.error?.message ?? "Unknown error"}`,
|
`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson?.error?.message ?? "Unknown error"}`,
|
||||||
);
|
);
|
||||||
@@ -77,6 +99,7 @@ export async function setupGitHubToken(): Promise<string> {
|
|||||||
core.setOutput("GITHUB_TOKEN", appToken);
|
core.setOutput("GITHUB_TOKEN", appToken);
|
||||||
return appToken;
|
return appToken;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Only set failed if we get here - workflow validation errors will exit(0) before this
|
||||||
core.setFailed(
|
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.`,
|
`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.`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { readFile } from "fs/promises";
|
import { readFile, stat } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
import { constants } from "fs";
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import { GITHUB_API_URL } from "../github/api/config";
|
import { GITHUB_API_URL } from "../github/api/config";
|
||||||
import { retryWithBackoff } from "../utils/retry";
|
import { retryWithBackoff } from "../utils/retry";
|
||||||
@@ -162,6 +163,34 @@ async function getOrCreateBranchRef(
|
|||||||
return baseSha;
|
return baseSha;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the appropriate Git file mode for a file
|
||||||
|
async function getFileMode(filePath: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const fileStat = await stat(filePath);
|
||||||
|
if (fileStat.isFile()) {
|
||||||
|
// Check if execute bit is set for user
|
||||||
|
if (fileStat.mode & constants.S_IXUSR) {
|
||||||
|
return "100755"; // Executable file
|
||||||
|
} else {
|
||||||
|
return "100644"; // Regular file
|
||||||
|
}
|
||||||
|
} else if (fileStat.isDirectory()) {
|
||||||
|
return "040000"; // Directory (tree)
|
||||||
|
} else if (fileStat.isSymbolicLink()) {
|
||||||
|
return "120000"; // Symbolic link
|
||||||
|
} else {
|
||||||
|
// Fallback for unknown file types
|
||||||
|
return "100644";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If we can't stat the file, default to regular file
|
||||||
|
console.warn(
|
||||||
|
`Could not determine file mode for ${filePath}, using default: ${error}`,
|
||||||
|
);
|
||||||
|
return "100644";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Commit files tool
|
// Commit files tool
|
||||||
server.tool(
|
server.tool(
|
||||||
"commit_files",
|
"commit_files",
|
||||||
@@ -223,6 +252,9 @@ server.tool(
|
|||||||
? filePath
|
? filePath
|
||||||
: join(REPO_DIR, filePath);
|
: join(REPO_DIR, filePath);
|
||||||
|
|
||||||
|
// Get the proper file mode based on file permissions
|
||||||
|
const fileMode = await getFileMode(fullPath);
|
||||||
|
|
||||||
// Check if file is binary (images, etc.)
|
// Check if file is binary (images, etc.)
|
||||||
const isBinaryFile =
|
const isBinaryFile =
|
||||||
/\.(png|jpg|jpeg|gif|webp|ico|pdf|zip|tar|gz|exe|bin|woff|woff2|ttf|eot)$/i.test(
|
/\.(png|jpg|jpeg|gif|webp|ico|pdf|zip|tar|gz|exe|bin|woff|woff2|ttf|eot)$/i.test(
|
||||||
@@ -261,7 +293,7 @@ server.tool(
|
|||||||
// Return tree entry with blob SHA
|
// Return tree entry with blob SHA
|
||||||
return {
|
return {
|
||||||
path: filePath,
|
path: filePath,
|
||||||
mode: "100644",
|
mode: fileMode,
|
||||||
type: "blob",
|
type: "blob",
|
||||||
sha: blobData.sha,
|
sha: blobData.sha,
|
||||||
};
|
};
|
||||||
@@ -270,7 +302,7 @@ server.tool(
|
|||||||
const content = await readFile(fullPath, "utf-8");
|
const content = await readFile(fullPath, "utf-8");
|
||||||
return {
|
return {
|
||||||
path: filePath,
|
path: filePath,
|
||||||
mode: "100644",
|
mode: fileMode,
|
||||||
type: "blob",
|
type: "blob",
|
||||||
content: content,
|
content: content,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const agentMode: Mode = {
|
|||||||
|
|
||||||
// TODO: handle by createPrompt (similar to tag and review modes)
|
// TODO: handle by createPrompt (similar to tag and review modes)
|
||||||
// Create prompt directory
|
// Create prompt directory
|
||||||
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, {
|
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
// Write the prompt file - the base action requires a prompt_file parameter,
|
// Write the prompt file - the base action requires a prompt_file parameter,
|
||||||
@@ -57,7 +57,7 @@ export const agentMode: Mode = {
|
|||||||
context.inputs.directPrompt ||
|
context.inputs.directPrompt ||
|
||||||
`Repository: ${context.repository.owner}/${context.repository.repo}`;
|
`Repository: ${context.repository.owner}/${context.repository.repo}`;
|
||||||
await writeFile(
|
await writeFile(
|
||||||
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`,
|
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
|
||||||
promptContent,
|
promptContent,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user