Skip to content

Commit 4c91e47

Browse files
Add claude code action (#737)
Patch adapted from anthropics/claude-code-action@13742ea Adds a GitHub Actions workflow that integrates claude-code-action with SAP AI Core via a LiteLLM proxy. Since the upstream action does not support LiteLLM natively, we clone it at a pinned commit and apply a local patch.
1 parent 263aa72 commit 4c91e47

3 files changed

Lines changed: 289 additions & 0 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: "Setup Claude Code Action"
2+
description: "Clone upstream claude-code-action, apply SAP AI Core LiteLLM patch, and prepare the action for use"
3+
4+
inputs:
5+
upstream_repo:
6+
description: "Upstream claude-code-action repository"
7+
required: false
8+
default: "anthropics/claude-code-action"
9+
upstream_ref:
10+
description: "Commit SHA to pin the upstream clone to"
11+
required: false
12+
default: "e58dfa55559035499a4982426bb73605e8b5ad8e"
13+
checkout_path:
14+
description: "Path to clone the action into"
15+
required: false
16+
default: ".claude-code-action"
17+
18+
runs:
19+
using: "composite"
20+
steps:
21+
- name: Clone upstream claude-code-action
22+
shell: bash
23+
run: |
24+
git clone --depth 1 "https://github.com/${{ inputs.upstream_repo }}.git" "${{ inputs.checkout_path }}"
25+
cd "${{ inputs.checkout_path }}"
26+
git fetch --depth 1 origin "${{ inputs.upstream_ref }}"
27+
git checkout "${{ inputs.upstream_ref }}"
28+
29+
- name: Apply LiteLLM proxy patch
30+
shell: bash
31+
run: |
32+
cd "${{ inputs.checkout_path }}"
33+
git apply "${{ github.workspace }}/.github/patches/claude-code-action/litellm-proxy.patch"
34+
35+
- name: Install Bun
36+
shell: bash
37+
run: |
38+
curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.13"
39+
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
diff --git a/action.yml b/action.yml
2+
index b6d0f05..e3e2145 100644
3+
--- a/action.yml
4+
+++ b/action.yml
5+
@@ -85,6 +85,14 @@ inputs:
6+
description: "Use Microsoft Foundry with OIDC authentication instead of direct Anthropic API"
7+
required: false
8+
default: "false"
9+
+ use_litellm:
10+
+ description: "Use LiteLLM to route requests through SAP AI Core (or other LiteLLM-supported providers)"
11+
+ required: false
12+
+ default: "false"
13+
+ litellm_model:
14+
+ description: "LiteLLM model identifier (e.g., 'sap/anthropic--claude-4.6-sonnet')"
15+
+ required: false
16+
+ default: "sap/anthropic--claude-4.6-sonnet"
17+
18+
claude_args:
19+
description: "Additional arguments to pass directly to Claude CLI"
20+
@@ -173,10 +181,11 @@ runs:
21+
steps:
22+
- name: Install Bun
23+
if: inputs.path_to_bun_executable == ''
24+
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # https://github.com/oven-sh/setup-bun/releases/tag/v2.2.0
25+
- with:
26+
- bun-version: 1.3.6
27+
- token: ${{ inputs.github_token || github.token }}
28+
+ shell: bash
29+
+ run: |
30+
+ # Install Bun via official install script (avoids github.com API token issues on GHES)
31+
+ curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.13"
32+
+ echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
33+
34+
- name: Setup Custom Bun Path
35+
if: inputs.path_to_bun_executable != ''
36+
@@ -237,6 +246,58 @@ runs:
37+
echo "/usr/bin" >> "$GITHUB_PATH"
38+
echo "/bin" >> "$GITHUB_PATH"
39+
40+
+ - name: Setup Python for LiteLLM
41+
+ if: inputs.use_litellm == 'true'
42+
+ uses: actions/setup-python@v5
43+
+ with:
44+
+ python-version: "3.12"
45+
+
46+
+ - name: Install LiteLLM dependencies
47+
+ if: inputs.use_litellm == 'true'
48+
+ shell: bash
49+
+ run: |
50+
+ pip install litellm==1.83.10 fastapi==0.136.0 uvicorn==0.44.0 --quiet
51+
+
52+
+ - name: Start LiteLLM Proxy
53+
+ if: inputs.use_litellm == 'true'
54+
+ id: litellm
55+
+ shell: bash
56+
+ env:
57+
+ LITELLM_MODEL: ${{ inputs.litellm_model }}
58+
+ run: |
59+
+ LITELLM_PORT=$(python3 -c "import socket; s=socket.socket(); s.bind(('',0)); print(s.getsockname()[1]); s.close()")
60+
+ echo "Starting LiteLLM proxy on port ${LITELLM_PORT}..."
61+
+
62+
+ LITELLM_PROXY_PORT=${LITELLM_PORT} python3 "${GITHUB_ACTION_PATH}/scripts/litellm-proxy.py" &
63+
+ LITELLM_PID=$!
64+
+ echo "LITELLM_PID=${LITELLM_PID}" >> "$GITHUB_ENV"
65+
+ echo "LITELLM_PORT=${LITELLM_PORT}" >> "$GITHUB_ENV"
66+
+
67+
+ for i in $(seq 1 30); do
68+
+ if curl -sf --connect-timeout 2 --max-time 3 "http://localhost:${LITELLM_PORT}/health/readiness" > /dev/null 2>&1; then
69+
+ echo "LiteLLM proxy is ready on port ${LITELLM_PORT}"
70+
+ break
71+
+ fi
72+
+ if ! kill -0 $LITELLM_PID 2>/dev/null; then
73+
+ echo "ERROR: LiteLLM proxy process died"
74+
+ exit 1
75+
+ fi
76+
+ sleep 2
77+
+ done
78+
+
79+
+ if ! curl -sf --connect-timeout 2 --max-time 3 "http://localhost:${LITELLM_PORT}/health/readiness" > /dev/null 2>&1; then
80+
+ echo "ERROR: LiteLLM proxy failed to start after 60 seconds"
81+
+ exit 1
82+
+ fi
83+
+
84+
+ # Set env vars for Claude Code CLI to use the proxy
85+
+ echo "ANTHROPIC_BASE_URL=http://localhost:${LITELLM_PORT}" >> "$GITHUB_ENV"
86+
+ echo "ANTHROPIC_MODEL=${{ inputs.litellm_model }}" >> "$GITHUB_ENV"
87+
+
88+
+ # Set a dummy API key — the proxy handles auth via AICORE_SERVICE_KEY
89+
+ LITELLM_PROXY_API_KEY="litellm-proxy-key"
90+
+ echo "LITELLM_PROXY_API_KEY=${LITELLM_PROXY_API_KEY}" >> "$GITHUB_ENV"
91+
+
92+
- name: Run Claude Code Action
93+
id: run
94+
shell: bash
95+
@@ -292,13 +353,14 @@ runs:
96+
NODE_VERSION: ${{ env.NODE_VERSION }}
97+
98+
# Provider configuration
99+
- ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
100+
+ ANTHROPIC_API_KEY: ${{ inputs.use_litellm == 'true' && env.LITELLM_PROXY_API_KEY || inputs.anthropic_api_key }}
101+
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }}
102+
ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }}
103+
ANTHROPIC_CUSTOM_HEADERS: ${{ env.ANTHROPIC_CUSTOM_HEADERS }}
104+
CLAUDE_CODE_USE_BEDROCK: ${{ inputs.use_bedrock == 'true' && '1' || '' }}
105+
CLAUDE_CODE_USE_VERTEX: ${{ inputs.use_vertex == 'true' && '1' || '' }}
106+
CLAUDE_CODE_USE_FOUNDRY: ${{ inputs.use_foundry == 'true' && '1' || '' }}
107+
+ CLAUDE_CODE_USE_LITELLM: ${{ inputs.use_litellm == 'true' && '1' || '' }}
108+
109+
# AWS configuration
110+
AWS_REGION: ${{ env.AWS_REGION }}
111+
@@ -335,6 +397,18 @@ runs:
112+
MCP_TOOL_TIMEOUT: ${{ env.MCP_TOOL_TIMEOUT }}
113+
MAX_MCP_OUTPUT_TOKENS: ${{ env.MAX_MCP_OUTPUT_TOKENS }}
114+
115+
+ # SAP AI Core configuration (for LiteLLM)
116+
+ AICORE_SERVICE_KEY: ${{ env.AICORE_SERVICE_KEY }}
117+
+ AICORE_AUTH_URL: ${{ env.AICORE_AUTH_URL }}
118+
+ AICORE_CLIENT_ID: ${{ env.AICORE_CLIENT_ID }}
119+
+ AICORE_CLIENT_SECRET: ${{ env.AICORE_CLIENT_SECRET }}
120+
+ AICORE_BASE_URL: ${{ env.AICORE_BASE_URL }}
121+
+ AICORE_RESOURCE_GROUP: ${{ env.AICORE_RESOURCE_GROUP }}
122+
+
123+
+ # SAP compliance configuration
124+
+ DISABLE_TELEMETRY: "1"
125+
+ DISABLE_ERROR_REPORTING: "1"
126+
+
127+
# Telemetry configuration
128+
CLAUDE_CODE_ENABLE_TELEMETRY: ${{ env.CLAUDE_CODE_ENABLE_TELEMETRY }}
129+
OTEL_METRICS_EXPORTER: ${{ env.OTEL_METRICS_EXPORTER }}
130+
@@ -372,6 +446,23 @@ runs:
131+
echo "DYLD_FRAMEWORK_PATH="
132+
} >> "$GITHUB_ENV"
133+
134+
+ - name: Stop LiteLLM Proxy
135+
+ if: always() && inputs.use_litellm == 'true'
136+
+ shell: bash
137+
+ run: |
138+
+ if [ -n "${LITELLM_PID}" ] && kill -0 "${LITELLM_PID}" 2>/dev/null; then
139+
+ echo "Stopping LiteLLM proxy (PID: ${LITELLM_PID})..."
140+
+ kill "${LITELLM_PID}" 2>/dev/null || true
141+
+ for _ in $(seq 1 20); do
142+
+ kill -0 "${LITELLM_PID}" 2>/dev/null || break
143+
+ sleep 0.5
144+
+ done
145+
+ kill -9 "${LITELLM_PID}" 2>/dev/null || true
146+
+ echo "LiteLLM proxy stopped."
147+
+ else
148+
+ echo "LiteLLM proxy already stopped."
149+
+ fi
150+
+
151+
- name: Cleanup SSH signing key
152+
if: always() && inputs.ssh_signing_key != ''
153+
shell: bash
154+
diff --git a/base-action/src/validate-env.ts b/base-action/src/validate-env.ts
155+
index 1f28da3..4eae3f2 100644
156+
--- a/base-action/src/validate-env.ts
157+
+++ b/base-action/src/validate-env.ts
158+
@@ -1,25 +1,26 @@
159+
/**
160+
* Validates the environment variables required for running Claude Code
161+
- * based on the selected provider (Anthropic API, AWS Bedrock, Google Vertex AI, or Microsoft Foundry)
162+
+ * based on the selected provider (Anthropic API, AWS Bedrock, Google Vertex AI, Microsoft Foundry, or LiteLLM/SAP AI Core)
163+
*/
164+
export function validateEnvironmentVariables() {
165+
const useBedrock = process.env.CLAUDE_CODE_USE_BEDROCK === "1";
166+
const useVertex = process.env.CLAUDE_CODE_USE_VERTEX === "1";
167+
const useFoundry = process.env.CLAUDE_CODE_USE_FOUNDRY === "1";
168+
+ const useLiteLLM = process.env.CLAUDE_CODE_USE_LITELLM === "1";
169+
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
170+
const claudeCodeOAuthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
171+
172+
const errors: string[] = [];
173+
174+
// Check for mutual exclusivity between providers
175+
- const activeProviders = [useBedrock, useVertex, useFoundry].filter(Boolean);
176+
+ const activeProviders = [useBedrock, useVertex, useFoundry, useLiteLLM].filter(Boolean);
177+
if (activeProviders.length > 1) {
178+
errors.push(
179+
- "Cannot use multiple providers simultaneously. Please set only one of: CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY.",
180+
+ "Cannot use multiple providers simultaneously. Please set only one of: CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, CLAUDE_CODE_USE_FOUNDRY, or CLAUDE_CODE_USE_LITELLM.",
181+
);
182+
}
183+
184+
- if (!useBedrock && !useVertex && !useFoundry) {
185+
+ if (!useBedrock && !useVertex && !useFoundry && !useLiteLLM) {
186+
if (!anthropicApiKey && !claudeCodeOAuthToken) {
187+
errors.push(
188+
"Either ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN is required when using direct Anthropic API.",
189+
@@ -66,6 +67,20 @@ export function validateEnvironmentVariables() {
190+
"Either ANTHROPIC_FOUNDRY_RESOURCE or ANTHROPIC_FOUNDRY_BASE_URL is required when using Microsoft Foundry.",
191+
);
192+
}
193+
+ } else if (useLiteLLM) {
194+
+ const baseUrl = process.env.ANTHROPIC_BASE_URL;
195+
+ const apiKey = process.env.ANTHROPIC_API_KEY;
196+
+
197+
+ if (!baseUrl) {
198+
+ errors.push(
199+
+ "ANTHROPIC_BASE_URL is required when using LiteLLM. This should be set automatically by the LiteLLM proxy startup step.",
200+
+ );
201+
+ }
202+
+ if (!apiKey) {
203+
+ errors.push(
204+
+ "ANTHROPIC_API_KEY is required when using LiteLLM. This should be set automatically by the LiteLLM proxy startup step.",
205+
+ );
206+
+ }
207+
}
208+
209+
if (errors.length > 0) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Claude Code on Issues and PRs
2+
3+
on:
4+
issue_comment:
5+
types: [created]
6+
issues:
7+
types: [opened, assigned]
8+
pull_request_review_comment:
9+
types: [created]
10+
pull_request_review:
11+
types: [submitted]
12+
13+
jobs:
14+
claude:
15+
if: github.event.sender.type == 'User'
16+
runs-on: ubuntu-latest
17+
permissions:
18+
contents: write
19+
pull-requests: write
20+
issues: write
21+
id-token: write
22+
steps:
23+
- uses: actions/checkout@v4
24+
with:
25+
fetch-depth: 1
26+
27+
- uses: ./.github/actions/setup-claude-code-action
28+
29+
- uses: ./.claude-code-action
30+
with:
31+
trigger_phrase: "@claude"
32+
use_litellm: "true"
33+
litellm_model: "sap/anthropic--claude-4.6-sonnet"
34+
github_token: ${{ secrets.GITHUB_TOKEN }}
35+
show_full_output: "true"
36+
env:
37+
AICORE_RESOURCE_GROUP: ${{ secrets.AICORE_RESOURCE_GROUP }}
38+
AICORE_BASE_URL: ${{ secrets.AICORE_BASE_URL }}
39+
AICORE_AUTH_URL: ${{ secrets.AICORE_AUTH_URL }}
40+
AICORE_CLIENT_ID: ${{ secrets.AICORE_CLIENT_ID }}
41+
AICORE_CLIENT_SECRET: ${{ secrets.AICORE_CLIENT_SECRET }}

0 commit comments

Comments
 (0)