Skip to content

Commit 511a2b2

Browse files
committed
Merge upstream changes and resolve conflicts
2 parents 2a39af4 + d5fa1ef commit 511a2b2

8 files changed

Lines changed: 137 additions & 46 deletions

File tree

.github/workflows/test-mcp-servers.yml

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,23 +96,13 @@ jobs:
9696
run: |
9797
echo "=== Environment Variables (--mcp-config test) ==="
9898
echo "HOME: $HOME"
99-
echo "XDG_CONFIG_HOME: ${XDG_CONFIG_HOME:-not set}"
100-
echo "CLAUDE_CONFIG_DIR: ${CLAUDE_CONFIG_DIR:-not set}"
10199
echo ""
102100
echo "=== Expected Config Paths ==="
103101
echo "GitHub action writes to: $HOME/.claude/settings.json"
104-
if [ -n "${XDG_CONFIG_HOME:-}" ]; then
105-
echo "Claude might read from: $XDG_CONFIG_HOME/claude/settings.json"
106-
else
107-
echo "Claude should read from: $HOME/.claude/settings.json"
108-
fi
102+
echo "Claude should read from: $HOME/.claude/settings.json"
109103
echo ""
110104
echo "=== Actual File System ==="
111105
ls -la $HOME/.claude/ || echo "No $HOME/.claude directory"
112-
if [ -n "${XDG_CONFIG_HOME:-}" ]; then
113-
ls -la $XDG_CONFIG_HOME/ || echo "No $XDG_CONFIG_HOME directory"
114-
ls -la $XDG_CONFIG_HOME/claude/ || echo "No $XDG_CONFIG_HOME/claude directory"
115-
fi
116106
117107
- name: Run Claude Code with --mcp-config flag
118108
uses: ./

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ Add the following to your workflow file:
9595
claude_refresh_token: ${{ secrets.CLAUDE_REFRESH_TOKEN }}
9696
claude_expires_at: ${{ secrets.CLAUDE_EXPIRES_AT }}
9797
secrets_admin_pat: ${{ secrets.SECRETS_ADMIN_PAT}}
98+
99+
# Using fallback model for handling API errors
100+
- name: Run Claude Code with fallback model
101+
uses: grll/claude-code-base-action@beta
102+
with:
103+
prompt: "Review and fix TypeScript errors"
104+
model: "claude-opus-4-20250514"
105+
fallback_model: "claude-sonnet-4-20250514"
106+
allowed_tools: "Bash(git:*),View,GlobTool,GrepTool,BatchTool"
107+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
98108
```
99109
100110
## Inputs
@@ -112,6 +122,7 @@ Add the following to your workflow file:
112122
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML multiline format) | No | '' |
113123
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | 'claude-4-0-sonnet-20250219' |
114124
| `anthropic_model` | DEPRECATED: Use 'model' instead | No | 'claude-4-0-sonnet-20250219' |
125+
| `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' |
115126
| `timeout_minutes` | Timeout in minutes for Claude Code execution | No | '10' |
116127
| `anthropic_api_key` | Anthropic API key (required for direct Anthropic API) | No | '' |
117128
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | 'false' |

action.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ inputs:
4444
anthropic_model:
4545
description: "DEPRECATED: Use 'model' instead. Model to use (provider-specific format required for Bedrock/Vertex)"
4646
required: false
47+
fallback_model:
48+
description: "Enable automatic fallback to specified model when default model is unavailable"
49+
required: false
4750
claude_env:
4851
description: "Custom environment variables to pass to Claude Code execution (YAML multiline format)"
4952
required: false
@@ -124,7 +127,7 @@ runs:
124127
125128
- name: Install Claude Code
126129
shell: bash
127-
run: npm install -g @anthropic-ai/claude-code@1.0.31
130+
run: npm install -g @anthropic-ai/claude-code@1.0.40
128131

129132
- name: Run Claude Code Action
130133
shell: bash
@@ -150,6 +153,7 @@ runs:
150153
INPUT_APPEND_SYSTEM_PROMPT: ${{ inputs.append_system_prompt }}
151154
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
152155
INPUT_CLAUDE_ENV: ${{ inputs.claude_env }}
156+
INPUT_FALLBACK_MODEL: ${{ inputs.fallback_model }}
153157

154158
# Provider configuration
155159
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}

examples/issue-triage.yml

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,20 @@ jobs:
2323
mkdir -p /tmp/mcp-config
2424
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
2525
{
26-
"github": {
27-
"command": "docker",
28-
"args": [
29-
"run",
30-
"-i",
31-
"--rm",
32-
"-e",
33-
"GITHUB_PERSONAL_ACCESS_TOKEN",
34-
"ghcr.io/github/github-mcp-server:sha-7aced2b"
35-
],
36-
"env": {
37-
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
26+
"mcpServers": {
27+
"github": {
28+
"command": "docker",
29+
"args": [
30+
"run",
31+
"-i",
32+
"--rm",
33+
"-e",
34+
"GITHUB_PERSONAL_ACCESS_TOKEN",
35+
"ghcr.io/github/github-mcp-server:sha-7aced2b"
36+
],
37+
"env": {
38+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
39+
}
3840
}
3941
}
4042
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ async function run() {
3636
systemPrompt: process.env.INPUT_SYSTEM_PROMPT,
3737
appendSystemPrompt: process.env.INPUT_APPEND_SYSTEM_PROMPT,
3838
claudeEnv: process.env.INPUT_CLAUDE_ENV,
39+
fallbackModel: process.env.INPUT_FALLBACK_MODEL,
3940
});
4041
} catch (error) {
4142
core.setFailed(`Action failed with error: ${error}`);

src/run-claude.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export type ClaudeOptions = {
1919
systemPrompt?: string;
2020
appendSystemPrompt?: string;
2121
claudeEnv?: string;
22+
fallbackModel?: string;
23+
timeoutMinutes?: string;
2224
};
2325

2426
type PreparedConfig = {
@@ -89,6 +91,17 @@ export function prepareRunConfig(
8991
if (options.appendSystemPrompt) {
9092
claudeArgs.push("--append-system-prompt", options.appendSystemPrompt);
9193
}
94+
if (options.fallbackModel) {
95+
claudeArgs.push("--fallback-model", options.fallbackModel);
96+
}
97+
if (options.timeoutMinutes) {
98+
const timeoutMinutesNum = parseInt(options.timeoutMinutes, 10);
99+
if (isNaN(timeoutMinutesNum) || timeoutMinutesNum <= 0) {
100+
throw new Error(
101+
`timeoutMinutes must be a positive number, got: ${options.timeoutMinutes}`,
102+
);
103+
}
104+
}
92105

93106
// Parse custom environment variables
94107
const customEnv = parseCustomEnvVars(options.claudeEnv);
@@ -205,8 +218,18 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
205218
});
206219

207220
// Wait for Claude to finish with timeout
208-
const timeoutMs =
209-
parseInt(process.env.INPUT_TIMEOUT_MINUTES || "10") * 60 * 1000;
221+
let timeoutMs = 10 * 60 * 1000; // Default 10 minutes
222+
if (options.timeoutMinutes) {
223+
timeoutMs = parseInt(options.timeoutMinutes, 10) * 60 * 1000;
224+
} else if (process.env.INPUT_TIMEOUT_MINUTES) {
225+
const envTimeout = parseInt(process.env.INPUT_TIMEOUT_MINUTES, 10);
226+
if (isNaN(envTimeout) || envTimeout <= 0) {
227+
throw new Error(
228+
`INPUT_TIMEOUT_MINUTES must be a positive number, got: ${process.env.INPUT_TIMEOUT_MINUTES}`,
229+
);
230+
}
231+
timeoutMs = envTimeout * 60 * 1000;
232+
}
210233
const exitCode = await new Promise<number>((resolve) => {
211234
let resolved = false;
212235

src/setup-claude-code-settings.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,14 @@
11
import { $ } from "bun";
22
import { homedir } from "os";
3-
import { join } from "path";
4-
5-
/**
6-
* Get Claude's config directory using XDG path when available
7-
* Priority order:
8-
* 1. XDG_CONFIG_HOME/claude (XDG Base Directory spec)
9-
* 2. ~/.claude (legacy fallback)
10-
*/
11-
export function getClaudeConfigHomeDir(): string {
12-
if (process.env.XDG_CONFIG_HOME) {
13-
return join(process.env.XDG_CONFIG_HOME, "claude");
14-
}
15-
16-
return join(homedir(), ".claude");
17-
}
183

194
export async function setupClaudeCodeSettings() {
20-
const configDir = getClaudeConfigHomeDir();
21-
const settingsPath = join(configDir, "settings.json");
5+
const home = homedir();
6+
const settingsPath = `${home}/.claude/settings.json`;
227
console.log(`Setting up Claude settings at: ${settingsPath}`);
238

24-
// Ensure config directory exists
25-
console.log(`Creating config directory...`);
26-
await $`mkdir -p ${configDir}`.quiet();
9+
// Ensure .claude directory exists
10+
console.log(`Creating .claude directory...`);
11+
await $`mkdir -p ${home}/.claude`.quiet();
2712

2813
let settings: Record<string, unknown> = {};
2914
try {

test/run-claude.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ describe("prepareRunConfig", () => {
8686
);
8787
});
8888

89+
test("should include fallback model in command arguments", () => {
90+
const options: ClaudeOptions = {
91+
fallbackModel: "claude-sonnet-4-20250514",
92+
};
93+
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
94+
95+
expect(prepared.claudeArgs).toContain("--fallback-model");
96+
expect(prepared.claudeArgs).toContain("claude-sonnet-4-20250514");
97+
});
98+
8999
test("should use provided prompt path", () => {
90100
const options: ClaudeOptions = {};
91101
const prepared = prepareRunConfig("/custom/prompt/path.txt", options);
@@ -103,6 +113,7 @@ describe("prepareRunConfig", () => {
103113
expect(prepared.claudeArgs).not.toContain("--mcp-config");
104114
expect(prepared.claudeArgs).not.toContain("--system-prompt");
105115
expect(prepared.claudeArgs).not.toContain("--append-system-prompt");
116+
expect(prepared.claudeArgs).not.toContain("--fallback-model");
106117
});
107118

108119
test("should preserve order of claude arguments", () => {
@@ -124,6 +135,40 @@ describe("prepareRunConfig", () => {
124135
]);
125136
});
126137

138+
test("should preserve order with all options including fallback model", () => {
139+
const options: ClaudeOptions = {
140+
allowedTools: "Bash,Read",
141+
disallowedTools: "Write",
142+
maxTurns: "3",
143+
mcpConfig: "/path/to/config.json",
144+
systemPrompt: "You are a helpful assistant",
145+
appendSystemPrompt: "Be concise",
146+
fallbackModel: "claude-sonnet-4-20250514",
147+
};
148+
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
149+
150+
expect(prepared.claudeArgs).toEqual([
151+
"-p",
152+
"--verbose",
153+
"--output-format",
154+
"stream-json",
155+
"--allowedTools",
156+
"Bash,Read",
157+
"--disallowedTools",
158+
"Write",
159+
"--max-turns",
160+
"3",
161+
"--mcp-config",
162+
"/path/to/config.json",
163+
"--system-prompt",
164+
"You are a helpful assistant",
165+
"--append-system-prompt",
166+
"Be concise",
167+
"--fallback-model",
168+
"claude-sonnet-4-20250514",
169+
]);
170+
});
171+
127172
describe("maxTurns validation", () => {
128173
test("should accept valid maxTurns value", () => {
129174
const options: ClaudeOptions = { maxTurns: "5" };
@@ -154,6 +199,36 @@ describe("prepareRunConfig", () => {
154199
});
155200
});
156201

202+
describe("timeoutMinutes validation", () => {
203+
test("should accept valid timeoutMinutes value", () => {
204+
const options: ClaudeOptions = { timeoutMinutes: "15" };
205+
expect(() =>
206+
prepareRunConfig("/tmp/test-prompt.txt", options),
207+
).not.toThrow();
208+
});
209+
210+
test("should throw error for non-numeric timeoutMinutes", () => {
211+
const options: ClaudeOptions = { timeoutMinutes: "abc" };
212+
expect(() => prepareRunConfig("/tmp/test-prompt.txt", options)).toThrow(
213+
"timeoutMinutes must be a positive number, got: abc",
214+
);
215+
});
216+
217+
test("should throw error for negative timeoutMinutes", () => {
218+
const options: ClaudeOptions = { timeoutMinutes: "-5" };
219+
expect(() => prepareRunConfig("/tmp/test-prompt.txt", options)).toThrow(
220+
"timeoutMinutes must be a positive number, got: -5",
221+
);
222+
});
223+
224+
test("should throw error for zero timeoutMinutes", () => {
225+
const options: ClaudeOptions = { timeoutMinutes: "0" };
226+
expect(() => prepareRunConfig("/tmp/test-prompt.txt", options)).toThrow(
227+
"timeoutMinutes must be a positive number, got: 0",
228+
);
229+
});
230+
});
231+
157232
describe("custom environment variables", () => {
158233
test("should parse empty claudeEnv correctly", () => {
159234
const options: ClaudeOptions = { claudeEnv: "" };

0 commit comments

Comments
 (0)