mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-07-27 22:38:30 +08:00
Add workload identity federation support to base-action (#1378)
* Add workload identity federation support to base-action Move the workload identity module into base-action so the standalone action can fetch and refresh the GitHub OIDC identity token itself, and expose the same federation inputs as the outer action. Switch the base-action test workflows from the anthropic_api_key secret to the federation repo variables and grant them id-token: write. * Verify MCP test tool invocation instead of init connection status MCP servers can connect asynchronously, so the init event may report a server as pending. Check that the server is registered at init, then assert the test tool was actually called and returned its response. Also pass the MCP config through claude_args --mcp-config, replacing the removed mcp_config input.
This commit is contained in:
parent
7f37f2e373
commit
420335da51
8
.github/workflows/ci-all.yml
vendored
8
.github/workflows/ci-all.yml
vendored
@ -11,6 +11,9 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# Lets the test workflows mint the GitHub OIDC token they exchange for a
|
||||
# Claude API access token (workload identity federation). See docs/setup.md.
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
@ -18,20 +21,15 @@ jobs:
|
||||
|
||||
test-base-action:
|
||||
uses: ./.github/workflows/test-base-action.yml
|
||||
secrets: inherit # Required for ANTHROPIC_API_KEY
|
||||
|
||||
test-custom-executables:
|
||||
uses: ./.github/workflows/test-custom-executables.yml
|
||||
secrets: inherit
|
||||
|
||||
test-mcp-servers:
|
||||
uses: ./.github/workflows/test-mcp-servers.yml
|
||||
secrets: inherit
|
||||
|
||||
test-settings:
|
||||
uses: ./.github/workflows/test-settings.yml
|
||||
secrets: inherit
|
||||
|
||||
test-structured-output:
|
||||
uses: ./.github/workflows/test-structured-output.yml
|
||||
secrets: inherit
|
||||
|
||||
15
.github/workflows/test-base-action.yml
vendored
15
.github/workflows/test-base-action.yml
vendored
@ -10,6 +10,13 @@ on:
|
||||
default: "List the files in the current directory starting with 'package'"
|
||||
workflow_call:
|
||||
|
||||
# The Claude API is authenticated via workload identity federation: id-token
|
||||
# lets the action mint the GitHub OIDC token it exchanges for a short-lived
|
||||
# access token. See docs/setup.md.
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
test-inline-prompt:
|
||||
runs-on: ubuntu-latest
|
||||
@ -21,7 +28,9 @@ jobs:
|
||||
uses: ./base-action
|
||||
with:
|
||||
prompt: ${{ github.event.inputs.test_prompt || 'List the files in the current directory starting with "package"' }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
allowed_tools: "LS,Read"
|
||||
|
||||
- name: Verify inline prompt output
|
||||
@ -78,7 +87,9 @@ jobs:
|
||||
uses: ./base-action
|
||||
with:
|
||||
prompt_file: "test-prompt.txt"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
allowed_tools: "LS,Read"
|
||||
|
||||
- name: Verify prompt file output
|
||||
|
||||
11
.github/workflows/test-custom-executables.yml
vendored
11
.github/workflows/test-custom-executables.yml
vendored
@ -5,6 +5,13 @@ on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
# The Claude API is authenticated via workload identity federation: id-token
|
||||
# lets the action mint the GitHub OIDC token it exchanges for a short-lived
|
||||
# access token. See docs/setup.md.
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
test-custom-executables:
|
||||
runs-on: ubuntu-latest
|
||||
@ -47,7 +54,9 @@ jobs:
|
||||
with:
|
||||
prompt: |
|
||||
List the files in the current directory starting with "package"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
path_to_claude_code_executable: /home/runner/.local/bin/claude
|
||||
path_to_bun_executable: /home/runner/.bun/bin/bun
|
||||
allowed_tools: "LS,Read"
|
||||
|
||||
76
.github/workflows/test-mcp-servers.yml
vendored
76
.github/workflows/test-mcp-servers.yml
vendored
@ -5,6 +5,13 @@ on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
# The Claude API is authenticated via workload identity federation: id-token
|
||||
# lets the action mint the GitHub OIDC token it exchanges for a short-lived
|
||||
# access token. See docs/setup.md.
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
test-mcp-integration:
|
||||
runs-on: ubuntu-latest
|
||||
@ -25,8 +32,11 @@ jobs:
|
||||
uses: ./base-action
|
||||
id: claude-test
|
||||
with:
|
||||
prompt: "List all available tools"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: "Call the test_tool tool and report its response."
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
claude_args: --allowedTools mcp__test-server__test_tool
|
||||
env:
|
||||
# Change to test directory so it finds .mcp.json
|
||||
CLAUDE_WORKING_DIR: ${{ github.workspace }}/base-action/test/mcp-test
|
||||
@ -50,21 +60,29 @@ jobs:
|
||||
if jq -e '.[] | select(.type == "system" and .subtype == "init") | .mcp_servers' "$OUTPUT_FILE" > /dev/null; then
|
||||
echo "✓ Found mcp_servers in output"
|
||||
|
||||
# Check if test-server is connected
|
||||
if jq -e '.[] | select(.type == "system" and .subtype == "init") | .mcp_servers[] | select(.name == "test-server" and .status == "connected")' "$OUTPUT_FILE" > /dev/null; then
|
||||
echo "✓ test-server is connected"
|
||||
# MCP servers can connect asynchronously, so the init event may
|
||||
# report the server as pending — check registration there, then
|
||||
# verify the tool actually ran.
|
||||
if jq -e '.[] | select(.type == "system" and .subtype == "init") | .mcp_servers[] | select(.name == "test-server")' "$OUTPUT_FILE" > /dev/null; then
|
||||
echo "✓ test-server is registered"
|
||||
else
|
||||
echo "✗ test-server not found or not connected"
|
||||
echo "✗ test-server not found"
|
||||
jq '.[] | select(.type == "system" and .subtype == "init") | .mcp_servers' "$OUTPUT_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if mcp tools are available
|
||||
if jq -e '.[] | select(.type == "system" and .subtype == "init") | .tools[] | select(. == "mcp__test-server__test_tool")' "$OUTPUT_FILE" > /dev/null; then
|
||||
echo "✓ MCP test tool found"
|
||||
if jq -e '.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use" and .name == "mcp__test-server__test_tool")' "$OUTPUT_FILE" > /dev/null; then
|
||||
echo "✓ MCP test tool was called"
|
||||
else
|
||||
echo "✗ MCP test tool not found"
|
||||
jq '.[] | select(.type == "system" and .subtype == "init") | .tools' "$OUTPUT_FILE"
|
||||
echo "✗ MCP test tool was not called"
|
||||
jq '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | .name]' "$OUTPUT_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if jq -e '.[] | select(.type == "user") | .message.content[]? | select(.type == "tool_result") | select(.content | tostring | contains("Test tool response"))' "$OUTPUT_FILE" > /dev/null; then
|
||||
echo "✓ MCP test tool returned its response"
|
||||
else
|
||||
echo "✗ MCP test tool response not found"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
@ -106,9 +124,13 @@ jobs:
|
||||
uses: ./base-action
|
||||
id: claude-config-test
|
||||
with:
|
||||
prompt: "List all available tools"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
mcp_config: '{"mcpServers":{"test-server":{"type":"stdio","command":"bun","args":["simple-mcp-server.ts"],"env":{}}}}'
|
||||
prompt: "Call the test_tool tool and report its response."
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
claude_args: |
|
||||
--allowedTools mcp__test-server__test_tool
|
||||
--mcp-config '{"mcpServers":{"test-server":{"type":"stdio","command":"bun","args":["simple-mcp-server.ts"],"env":{}}}}'
|
||||
env:
|
||||
# Change to test directory so bun can find the MCP server script
|
||||
CLAUDE_WORKING_DIR: ${{ github.workspace }}/base-action/test/mcp-test
|
||||
@ -132,21 +154,29 @@ jobs:
|
||||
if jq -e '.[] | select(.type == "system" and .subtype == "init") | .mcp_servers' "$OUTPUT_FILE" > /dev/null; then
|
||||
echo "✓ Found mcp_servers in output"
|
||||
|
||||
# Check if test-server is connected
|
||||
if jq -e '.[] | select(.type == "system" and .subtype == "init") | .mcp_servers[] | select(.name == "test-server" and .status == "connected")' "$OUTPUT_FILE" > /dev/null; then
|
||||
echo "✓ test-server is connected"
|
||||
# MCP servers can connect asynchronously, so the init event may
|
||||
# report the server as pending — check registration there, then
|
||||
# verify the tool actually ran.
|
||||
if jq -e '.[] | select(.type == "system" and .subtype == "init") | .mcp_servers[] | select(.name == "test-server")' "$OUTPUT_FILE" > /dev/null; then
|
||||
echo "✓ test-server is registered"
|
||||
else
|
||||
echo "✗ test-server not found or not connected"
|
||||
echo "✗ test-server not found"
|
||||
jq '.[] | select(.type == "system" and .subtype == "init") | .mcp_servers' "$OUTPUT_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if mcp tools are available
|
||||
if jq -e '.[] | select(.type == "system" and .subtype == "init") | .tools[] | select(. == "mcp__test-server__test_tool")' "$OUTPUT_FILE" > /dev/null; then
|
||||
echo "✓ MCP test tool found"
|
||||
if jq -e '.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use" and .name == "mcp__test-server__test_tool")' "$OUTPUT_FILE" > /dev/null; then
|
||||
echo "✓ MCP test tool was called"
|
||||
else
|
||||
echo "✗ MCP test tool not found"
|
||||
jq '.[] | select(.type == "system" and .subtype == "init") | .tools' "$OUTPUT_FILE"
|
||||
echo "✗ MCP test tool was not called"
|
||||
jq '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | .name]' "$OUTPUT_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if jq -e '.[] | select(.type == "user") | .message.content[]? | select(.type == "tool_result") | select(.content | tostring | contains("Test tool response"))' "$OUTPUT_FILE" > /dev/null; then
|
||||
echo "✓ MCP test tool returned its response"
|
||||
else
|
||||
echo "✗ MCP test tool response not found"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
|
||||
23
.github/workflows/test-settings.yml
vendored
23
.github/workflows/test-settings.yml
vendored
@ -5,6 +5,13 @@ on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
# The Claude API is authenticated via workload identity federation: id-token
|
||||
# lets the action mint the GitHub OIDC token it exchanges for a short-lived
|
||||
# access token. See docs/setup.md.
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
test-settings-inline-allow:
|
||||
runs-on: ubuntu-latest
|
||||
@ -17,7 +24,9 @@ jobs:
|
||||
with:
|
||||
prompt: |
|
||||
Use Bash to echo "Hello from settings test"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
settings: |
|
||||
{
|
||||
"permissions": {
|
||||
@ -66,7 +75,9 @@ jobs:
|
||||
with:
|
||||
prompt: |
|
||||
Run the command `echo $HOME` to check the home directory path
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
settings: |
|
||||
{
|
||||
"permissions": {
|
||||
@ -108,7 +119,9 @@ jobs:
|
||||
with:
|
||||
prompt: |
|
||||
Use Bash to echo "Hello from settings file test"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
settings: "test-settings.json"
|
||||
|
||||
- name: Verify echo worked
|
||||
@ -162,7 +175,9 @@ jobs:
|
||||
with:
|
||||
prompt: |
|
||||
Run the command `echo $HOME` to check the home directory path
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
settings: "test-settings.json"
|
||||
|
||||
- name: Verify echo was denied
|
||||
|
||||
24
.github/workflows/test-structured-output.yml
vendored
24
.github/workflows/test-structured-output.yml
vendored
@ -5,8 +5,12 @@ on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
# The Claude API is authenticated via workload identity federation: id-token
|
||||
# lets the action mint the GitHub OIDC token it exchanges for a short-lived
|
||||
# access token. See docs/setup.md.
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
test-basic-types:
|
||||
@ -28,7 +32,9 @@ jobs:
|
||||
- number_field: 42
|
||||
- boolean_true: true
|
||||
- boolean_false: false
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
claude_args: |
|
||||
--allowedTools Bash
|
||||
--json-schema '{"type":"object","properties":{"text_field":{"type":"string"},"number_field":{"type":"number"},"boolean_true":{"type":"boolean"},"boolean_false":{"type":"boolean"}},"required":["text_field","number_field","boolean_true","boolean_false"]}'
|
||||
@ -86,7 +92,9 @@ jobs:
|
||||
- items: ["apple", "banana", "cherry"]
|
||||
- config: {"key": "value", "count": 3}
|
||||
- empty_array: []
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
claude_args: |
|
||||
--allowedTools Bash
|
||||
--json-schema '{"type":"object","properties":{"items":{"type":"array","items":{"type":"string"}},"config":{"type":"object"},"empty_array":{"type":"array"}},"required":["items","config","empty_array"]}'
|
||||
@ -138,7 +146,9 @@ jobs:
|
||||
- empty_string: ""
|
||||
- negative: -5
|
||||
- decimal: 3.14
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
claude_args: |
|
||||
--allowedTools Bash
|
||||
--json-schema '{"type":"object","properties":{"zero":{"type":"number"},"empty_string":{"type":"string"},"negative":{"type":"number"},"decimal":{"type":"number"}},"required":["zero","empty_string","negative","decimal"]}'
|
||||
@ -192,7 +202,9 @@ jobs:
|
||||
prompt: |
|
||||
Run: echo "test"
|
||||
Return EXACTLY: {test-result: "passed", item_count: 10}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
claude_args: |
|
||||
--allowedTools Bash
|
||||
--json-schema '{"type":"object","properties":{"test-result":{"type":"string"},"item_count":{"type":"number"}},"required":["test-result","item_count"]}'
|
||||
@ -230,7 +242,9 @@ jobs:
|
||||
uses: ./base-action
|
||||
with:
|
||||
prompt: "Run: echo 'complete'. Return: {done: true}"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
claude_args: |
|
||||
--allowedTools Bash
|
||||
--json-schema '{"type":"object","properties":{"done":{"type":"boolean"}},"required":["done"]}'
|
||||
|
||||
@ -93,26 +93,29 @@ Add the following to your workflow file:
|
||||
|
||||
### Workload Identity Federation
|
||||
|
||||
Instead of a static API key or OAuth token, you can authenticate via [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation) by setting the federation environment variables on the step. Fetch an OIDC identity token from your provider, write it to a file, and point the action at it:
|
||||
Instead of a static API key or OAuth token, you can authenticate via [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation): the action fetches the workflow's GitHub OIDC token and the Claude Code CLI exchanges it for a short-lived access token. Requires the `id-token: write` permission on the job:
|
||||
|
||||
```yaml
|
||||
- name: Run Claude Code with workload identity federation
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Run Claude Code with workload identity federation
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
with:
|
||||
prompt: "Your prompt here"
|
||||
env:
|
||||
ANTHROPIC_FEDERATION_RULE_ID: fdrl_xxxxxxxxxxxx
|
||||
ANTHROPIC_ORGANIZATION_ID: 00000000-0000-0000-0000-000000000000
|
||||
ANTHROPIC_SERVICE_ACCOUNT_ID: svac_xxxxxxxxxxxx
|
||||
ANTHROPIC_IDENTITY_TOKEN_FILE: /path/to/identity-token
|
||||
anthropic_federation_rule_id: fdrl_xxxxxxxxxxxx
|
||||
anthropic_organization_id: 00000000-0000-0000-0000-000000000000
|
||||
anthropic_service_account_id: svac_xxxxxxxxxxxx
|
||||
```
|
||||
|
||||
Note: the base action does not fetch or refresh the identity token itself — you are responsible for providing a valid token file. [`anthropics/claude-code-action`](https://github.com/anthropics/claude-code-action) handles fetching and refreshing the GitHub Actions OIDC token automatically via its `anthropic_federation_rule_id` input.
|
||||
Do not set `anthropic_api_key` or `claude_code_oauth_token` alongside the federation inputs — a static credential takes precedence and federation will not be used.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Description | Required | Default |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------- |
|
||||
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------- |
|
||||
| `prompt` | The prompt to send to Claude Code | No\* | '' |
|
||||
| `prompt_file` | Path to a file containing the prompt to send to Claude Code | No\* | '' |
|
||||
| `allowed_tools` | Comma-separated list of allowed tools for Claude Code to use | No | '' |
|
||||
@ -128,6 +131,11 @@ Note: the base action does not fetch or refresh the identity token itself — yo
|
||||
| `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' |
|
||||
| `anthropic_api_key` | Anthropic API key (required for direct Anthropic API) | No | '' |
|
||||
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No | '' |
|
||||
| `anthropic_federation_rule_id` | Workload identity federation rule ID (fdrl\_...). Requires `id-token: write` permission | No | '' |
|
||||
| `anthropic_organization_id` | Anthropic organization UUID used for workload identity federation | No | '' |
|
||||
| `anthropic_service_account_id` | Service account ID (svac\_...) the federated token acts as (optional) | No | '' |
|
||||
| `anthropic_workspace_id` | Workspace ID (wrkspc\_...) for federation. Optional when the rule targets a single workspace | No | '' |
|
||||
| `anthropic_oidc_audience` | Audience to request on the GitHub OIDC token. Defaults to https://api.anthropic.com | No | '' |
|
||||
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | 'false' |
|
||||
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | 'false' |
|
||||
| `use_node_cache` | Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files) | No | 'false' |
|
||||
|
||||
@ -34,6 +34,26 @@ inputs:
|
||||
description: "Claude Code OAuth token (alternative to anthropic_api_key)"
|
||||
required: false
|
||||
default: ""
|
||||
anthropic_federation_rule_id:
|
||||
description: "Workload identity federation rule ID (fdrl_...). When set with anthropic_organization_id, the action authenticates to the Claude API by exchanging the workflow's GitHub OIDC token instead of using a static API key. Requires `id-token: write` permission."
|
||||
required: false
|
||||
default: ""
|
||||
anthropic_organization_id:
|
||||
description: "Anthropic organization UUID used for workload identity federation"
|
||||
required: false
|
||||
default: ""
|
||||
anthropic_service_account_id:
|
||||
description: "Service account ID (svac_...) the federated token acts as (optional, used with workload identity federation)"
|
||||
required: false
|
||||
default: ""
|
||||
anthropic_workspace_id:
|
||||
description: "Workspace ID (wrkspc_...) for workload identity federation. Optional when the federation rule targets a single workspace."
|
||||
required: false
|
||||
default: ""
|
||||
anthropic_oidc_audience:
|
||||
description: "Audience to request on the GitHub OIDC token used for workload identity federation. Defaults to https://api.anthropic.com."
|
||||
required: false
|
||||
default: ""
|
||||
use_bedrock:
|
||||
description: "Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API"
|
||||
required: false
|
||||
@ -175,6 +195,11 @@ runs:
|
||||
# Provider configuration
|
||||
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }}
|
||||
ANTHROPIC_FEDERATION_RULE_ID: ${{ inputs.anthropic_federation_rule_id }}
|
||||
ANTHROPIC_ORGANIZATION_ID: ${{ inputs.anthropic_organization_id }}
|
||||
ANTHROPIC_SERVICE_ACCOUNT_ID: ${{ inputs.anthropic_service_account_id }}
|
||||
ANTHROPIC_WORKSPACE_ID: ${{ inputs.anthropic_workspace_id }}
|
||||
ANTHROPIC_OIDC_AUDIENCE: ${{ inputs.anthropic_oidc_audience }}
|
||||
ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }}
|
||||
ANTHROPIC_CUSTOM_HEADERS: ${{ env.ANTHROPIC_CUSTOM_HEADERS }}
|
||||
# Only set provider flags if explicitly true, since any value (including "false") is truthy
|
||||
|
||||
@ -7,9 +7,16 @@ import { setupClaudeCodeSettings } from "./setup-claude-code-settings";
|
||||
import { validateEnvironmentVariables } from "./validate-env";
|
||||
import { installPlugins } from "./install-plugins";
|
||||
import { setExecutionFileOutputIfPresent } from "./execution-file";
|
||||
import { setupWorkloadIdentity } from "./workload-identity";
|
||||
import type { WorkloadIdentityHandle } from "./workload-identity";
|
||||
|
||||
async function run() {
|
||||
let workloadIdentity: WorkloadIdentityHandle | undefined;
|
||||
try {
|
||||
// When workload identity federation is configured, fetch the GitHub OIDC
|
||||
// identity token and expose it to the CLI before validating auth env vars.
|
||||
workloadIdentity = await setupWorkloadIdentity();
|
||||
|
||||
validateEnvironmentVariables();
|
||||
|
||||
// The composite action's "Install Claude Code" step writes the binary to
|
||||
@ -67,6 +74,9 @@ async function run() {
|
||||
core.setFailed(`Action failed with error: ${error}`);
|
||||
core.setOutput("conclusion", "failure");
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// Stop refreshing the workload identity token file so the process can exit
|
||||
workloadIdentity?.stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
47
base-action/src/retry.ts
Normal file
47
base-action/src/retry.ts
Normal file
@ -0,0 +1,47 @@
|
||||
export type RetryOptions = {
|
||||
maxAttempts?: number;
|
||||
initialDelayMs?: number;
|
||||
maxDelayMs?: number;
|
||||
backoffFactor?: number;
|
||||
shouldRetry?: (error: Error) => boolean;
|
||||
};
|
||||
|
||||
export async function retryWithBackoff<T>(
|
||||
operation: () => Promise<T>,
|
||||
options: RetryOptions = {},
|
||||
): Promise<T> {
|
||||
const {
|
||||
maxAttempts = 3,
|
||||
initialDelayMs = 5000,
|
||||
maxDelayMs = 20000,
|
||||
backoffFactor = 2,
|
||||
shouldRetry,
|
||||
} = options;
|
||||
|
||||
let delayMs = initialDelayMs;
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
console.log(`Attempt ${attempt} of ${maxAttempts}...`);
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
console.error(`Attempt ${attempt} failed:`, lastError.message);
|
||||
|
||||
if (shouldRetry && !shouldRetry(lastError)) {
|
||||
console.error("Error is not retryable, giving up immediately");
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
console.log(`Retrying in ${delayMs / 1000} seconds...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
delayMs = Math.min(delayMs * backoffFactor, maxDelayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`Operation failed after ${maxAttempts} attempts`);
|
||||
throw lastError;
|
||||
}
|
||||
@ -17,7 +17,7 @@
|
||||
import * as core from "@actions/core";
|
||||
import { mkdirSync, writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { retryWithBackoff } from "../utils/retry";
|
||||
import { retryWithBackoff } from "./retry";
|
||||
|
||||
/** How often the GitHub OIDC identity token file is rewritten. */
|
||||
const REFRESH_INTERVAL_MS = 4 * 60 * 1000;
|
||||
@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
|
||||
import { retryWithBackoff } from "../src/utils/retry";
|
||||
import { retryWithBackoff } from "../src/retry";
|
||||
|
||||
describe("retryWithBackoff", () => {
|
||||
let originalConsoleLog: typeof console.log;
|
||||
@ -8,7 +8,7 @@ import { join } from "path";
|
||||
import {
|
||||
isWorkloadIdentityConfigured,
|
||||
setupWorkloadIdentity,
|
||||
} from "../src/auth/workload-identity";
|
||||
} from "../src/workload-identity";
|
||||
|
||||
describe("workload identity federation", () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
@ -29,13 +29,13 @@ import { prepareAgentMode } from "../modes/agent";
|
||||
import { checkContainsTrigger } from "../github/validation/trigger";
|
||||
import { restoreConfigFromBase } from "../github/operations/restore-config";
|
||||
import { validateBranchName } from "../github/operations/branch";
|
||||
import { setupWorkloadIdentity } from "../auth/workload-identity";
|
||||
import type { WorkloadIdentityHandle } from "../auth/workload-identity";
|
||||
import { collectActionInputsPresence } from "./collect-inputs";
|
||||
import { updateCommentLink } from "./update-comment-link";
|
||||
import { formatTurnsFromData } from "./format-turns";
|
||||
import type { Turn } from "./format-turns";
|
||||
// Base-action imports (used directly instead of subprocess)
|
||||
import { setupWorkloadIdentity } from "../../base-action/src/workload-identity";
|
||||
import type { WorkloadIdentityHandle } from "../../base-action/src/workload-identity";
|
||||
import { validateEnvironmentVariables } from "../../base-action/src/validate-env";
|
||||
import { setupClaudeCodeSettings } from "../../base-action/src/setup-claude-code-settings";
|
||||
import { installPlugins } from "../../base-action/src/install-plugins";
|
||||
|
||||
@ -1,47 +1,4 @@
|
||||
export type RetryOptions = {
|
||||
maxAttempts?: number;
|
||||
initialDelayMs?: number;
|
||||
maxDelayMs?: number;
|
||||
backoffFactor?: number;
|
||||
shouldRetry?: (error: Error) => boolean;
|
||||
};
|
||||
|
||||
export async function retryWithBackoff<T>(
|
||||
operation: () => Promise<T>,
|
||||
options: RetryOptions = {},
|
||||
): Promise<T> {
|
||||
const {
|
||||
maxAttempts = 3,
|
||||
initialDelayMs = 5000,
|
||||
maxDelayMs = 20000,
|
||||
backoffFactor = 2,
|
||||
shouldRetry,
|
||||
} = options;
|
||||
|
||||
let delayMs = initialDelayMs;
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
console.log(`Attempt ${attempt} of ${maxAttempts}...`);
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
console.error(`Attempt ${attempt} failed:`, lastError.message);
|
||||
|
||||
if (shouldRetry && !shouldRetry(lastError)) {
|
||||
console.error("Error is not retryable, giving up immediately");
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
console.log(`Retrying in ${delayMs / 1000} seconds...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
delayMs = Math.min(delayMs * backoffFactor, maxDelayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`Operation failed after ${maxAttempts} attempts`);
|
||||
throw lastError;
|
||||
}
|
||||
export {
|
||||
retryWithBackoff,
|
||||
type RetryOptions,
|
||||
} from "../../base-action/src/retry";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user