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:
|
permissions:
|
||||||
contents: read
|
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:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
@ -18,20 +21,15 @@ jobs:
|
|||||||
|
|
||||||
test-base-action:
|
test-base-action:
|
||||||
uses: ./.github/workflows/test-base-action.yml
|
uses: ./.github/workflows/test-base-action.yml
|
||||||
secrets: inherit # Required for ANTHROPIC_API_KEY
|
|
||||||
|
|
||||||
test-custom-executables:
|
test-custom-executables:
|
||||||
uses: ./.github/workflows/test-custom-executables.yml
|
uses: ./.github/workflows/test-custom-executables.yml
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
test-mcp-servers:
|
test-mcp-servers:
|
||||||
uses: ./.github/workflows/test-mcp-servers.yml
|
uses: ./.github/workflows/test-mcp-servers.yml
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
test-settings:
|
test-settings:
|
||||||
uses: ./.github/workflows/test-settings.yml
|
uses: ./.github/workflows/test-settings.yml
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
test-structured-output:
|
test-structured-output:
|
||||||
uses: ./.github/workflows/test-structured-output.yml
|
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'"
|
default: "List the files in the current directory starting with 'package'"
|
||||||
workflow_call:
|
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:
|
jobs:
|
||||||
test-inline-prompt:
|
test-inline-prompt:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -21,7 +28,9 @@ jobs:
|
|||||||
uses: ./base-action
|
uses: ./base-action
|
||||||
with:
|
with:
|
||||||
prompt: ${{ github.event.inputs.test_prompt || 'List the files in the current directory starting with "package"' }}
|
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"
|
allowed_tools: "LS,Read"
|
||||||
|
|
||||||
- name: Verify inline prompt output
|
- name: Verify inline prompt output
|
||||||
@ -78,7 +87,9 @@ jobs:
|
|||||||
uses: ./base-action
|
uses: ./base-action
|
||||||
with:
|
with:
|
||||||
prompt_file: "test-prompt.txt"
|
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"
|
allowed_tools: "LS,Read"
|
||||||
|
|
||||||
- name: Verify prompt file output
|
- 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_dispatch:
|
||||||
workflow_call:
|
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:
|
jobs:
|
||||||
test-custom-executables:
|
test-custom-executables:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -47,7 +54,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
prompt: |
|
prompt: |
|
||||||
List the files in the current directory starting with "package"
|
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_claude_code_executable: /home/runner/.local/bin/claude
|
||||||
path_to_bun_executable: /home/runner/.bun/bin/bun
|
path_to_bun_executable: /home/runner/.bun/bin/bun
|
||||||
allowed_tools: "LS,Read"
|
allowed_tools: "LS,Read"
|
||||||
|
|||||||
80
.github/workflows/test-mcp-servers.yml
vendored
80
.github/workflows/test-mcp-servers.yml
vendored
@ -5,6 +5,13 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
workflow_call:
|
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:
|
jobs:
|
||||||
test-mcp-integration:
|
test-mcp-integration:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -25,8 +32,11 @@ jobs:
|
|||||||
uses: ./base-action
|
uses: ./base-action
|
||||||
id: claude-test
|
id: claude-test
|
||||||
with:
|
with:
|
||||||
prompt: "List all available tools"
|
prompt: "Call the test_tool tool and report its response."
|
||||||
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 mcp__test-server__test_tool
|
||||||
env:
|
env:
|
||||||
# Change to test directory so it finds .mcp.json
|
# Change to test directory so it finds .mcp.json
|
||||||
CLAUDE_WORKING_DIR: ${{ github.workspace }}/base-action/test/mcp-test
|
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
|
if jq -e '.[] | select(.type == "system" and .subtype == "init") | .mcp_servers' "$OUTPUT_FILE" > /dev/null; then
|
||||||
echo "✓ Found mcp_servers in output"
|
echo "✓ Found mcp_servers in output"
|
||||||
|
|
||||||
# Check if test-server is connected
|
# MCP servers can connect asynchronously, so the init event may
|
||||||
if jq -e '.[] | select(.type == "system" and .subtype == "init") | .mcp_servers[] | select(.name == "test-server" and .status == "connected")' "$OUTPUT_FILE" > /dev/null; then
|
# report the server as pending — check registration there, then
|
||||||
echo "✓ test-server is connected"
|
# 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
|
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"
|
jq '.[] | select(.type == "system" and .subtype == "init") | .mcp_servers' "$OUTPUT_FILE"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if mcp tools are available
|
if jq -e '.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use" and .name == "mcp__test-server__test_tool")' "$OUTPUT_FILE" > /dev/null; then
|
||||||
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 was called"
|
||||||
echo "✓ MCP test tool found"
|
|
||||||
else
|
else
|
||||||
echo "✗ MCP test tool not found"
|
echo "✗ MCP test tool was not called"
|
||||||
jq '.[] | select(.type == "system" and .subtype == "init") | .tools' "$OUTPUT_FILE"
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@ -106,9 +124,13 @@ jobs:
|
|||||||
uses: ./base-action
|
uses: ./base-action
|
||||||
id: claude-config-test
|
id: claude-config-test
|
||||||
with:
|
with:
|
||||||
prompt: "List all available tools"
|
prompt: "Call the test_tool tool and report its response."
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||||
mcp_config: '{"mcpServers":{"test-server":{"type":"stdio","command":"bun","args":["simple-mcp-server.ts"],"env":{}}}}'
|
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:
|
env:
|
||||||
# Change to test directory so bun can find the MCP server script
|
# Change to test directory so bun can find the MCP server script
|
||||||
CLAUDE_WORKING_DIR: ${{ github.workspace }}/base-action/test/mcp-test
|
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
|
if jq -e '.[] | select(.type == "system" and .subtype == "init") | .mcp_servers' "$OUTPUT_FILE" > /dev/null; then
|
||||||
echo "✓ Found mcp_servers in output"
|
echo "✓ Found mcp_servers in output"
|
||||||
|
|
||||||
# Check if test-server is connected
|
# MCP servers can connect asynchronously, so the init event may
|
||||||
if jq -e '.[] | select(.type == "system" and .subtype == "init") | .mcp_servers[] | select(.name == "test-server" and .status == "connected")' "$OUTPUT_FILE" > /dev/null; then
|
# report the server as pending — check registration there, then
|
||||||
echo "✓ test-server is connected"
|
# 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
|
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"
|
jq '.[] | select(.type == "system" and .subtype == "init") | .mcp_servers' "$OUTPUT_FILE"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if mcp tools are available
|
if jq -e '.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use" and .name == "mcp__test-server__test_tool")' "$OUTPUT_FILE" > /dev/null; then
|
||||||
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 was called"
|
||||||
echo "✓ MCP test tool found"
|
|
||||||
else
|
else
|
||||||
echo "✗ MCP test tool not found"
|
echo "✗ MCP test tool was not called"
|
||||||
jq '.[] | select(.type == "system" and .subtype == "init") | .tools' "$OUTPUT_FILE"
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
|||||||
23
.github/workflows/test-settings.yml
vendored
23
.github/workflows/test-settings.yml
vendored
@ -5,6 +5,13 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
workflow_call:
|
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:
|
jobs:
|
||||||
test-settings-inline-allow:
|
test-settings-inline-allow:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -17,7 +24,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
prompt: |
|
prompt: |
|
||||||
Use Bash to echo "Hello from settings test"
|
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: |
|
settings: |
|
||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
@ -66,7 +75,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
prompt: |
|
prompt: |
|
||||||
Run the command `echo $HOME` to check the home directory path
|
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: |
|
settings: |
|
||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
@ -108,7 +119,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
prompt: |
|
prompt: |
|
||||||
Use Bash to echo "Hello from settings file test"
|
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"
|
settings: "test-settings.json"
|
||||||
|
|
||||||
- name: Verify echo worked
|
- name: Verify echo worked
|
||||||
@ -162,7 +175,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
prompt: |
|
prompt: |
|
||||||
Run the command `echo $HOME` to check the home directory path
|
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"
|
settings: "test-settings.json"
|
||||||
|
|
||||||
- name: Verify echo was denied
|
- 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_dispatch:
|
||||||
workflow_call:
|
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:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-basic-types:
|
test-basic-types:
|
||||||
@ -28,7 +32,9 @@ jobs:
|
|||||||
- number_field: 42
|
- number_field: 42
|
||||||
- boolean_true: true
|
- boolean_true: true
|
||||||
- boolean_false: false
|
- 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: |
|
claude_args: |
|
||||||
--allowedTools Bash
|
--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"]}'
|
--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"]
|
- items: ["apple", "banana", "cherry"]
|
||||||
- config: {"key": "value", "count": 3}
|
- config: {"key": "value", "count": 3}
|
||||||
- empty_array: []
|
- 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: |
|
claude_args: |
|
||||||
--allowedTools Bash
|
--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"]}'
|
--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: ""
|
- empty_string: ""
|
||||||
- negative: -5
|
- negative: -5
|
||||||
- decimal: 3.14
|
- 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: |
|
claude_args: |
|
||||||
--allowedTools Bash
|
--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"]}'
|
--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: |
|
prompt: |
|
||||||
Run: echo "test"
|
Run: echo "test"
|
||||||
Return EXACTLY: {test-result: "passed", item_count: 10}
|
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: |
|
claude_args: |
|
||||||
--allowedTools Bash
|
--allowedTools Bash
|
||||||
--json-schema '{"type":"object","properties":{"test-result":{"type":"string"},"item_count":{"type":"number"}},"required":["test-result","item_count"]}'
|
--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
|
uses: ./base-action
|
||||||
with:
|
with:
|
||||||
prompt: "Run: echo 'complete'. Return: {done: true}"
|
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: |
|
claude_args: |
|
||||||
--allowedTools Bash
|
--allowedTools Bash
|
||||||
--json-schema '{"type":"object","properties":{"done":{"type":"boolean"}},"required":["done"]}'
|
--json-schema '{"type":"object","properties":{"done":{"type":"boolean"}},"required":["done"]}'
|
||||||
|
|||||||
@ -93,45 +93,53 @@ Add the following to your workflow file:
|
|||||||
|
|
||||||
### Workload Identity Federation
|
### 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
|
```yaml
|
||||||
- name: Run Claude Code with workload identity federation
|
permissions:
|
||||||
uses: anthropics/claude-code-base-action@beta
|
contents: read
|
||||||
with:
|
id-token: write
|
||||||
prompt: "Your prompt here"
|
|
||||||
env:
|
steps:
|
||||||
ANTHROPIC_FEDERATION_RULE_ID: fdrl_xxxxxxxxxxxx
|
- name: Run Claude Code with workload identity federation
|
||||||
ANTHROPIC_ORGANIZATION_ID: 00000000-0000-0000-0000-000000000000
|
uses: anthropics/claude-code-base-action@beta
|
||||||
ANTHROPIC_SERVICE_ACCOUNT_ID: svac_xxxxxxxxxxxx
|
with:
|
||||||
ANTHROPIC_IDENTITY_TOKEN_FILE: /path/to/identity-token
|
prompt: "Your prompt here"
|
||||||
|
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
|
## Inputs
|
||||||
|
|
||||||
| Input | Description | Required | Default |
|
| Input | Description | Required | Default |
|
||||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------- |
|
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------- |
|
||||||
| `prompt` | The prompt to send to Claude Code | No\* | '' |
|
| `prompt` | The prompt to send to Claude Code | No\* | '' |
|
||||||
| `prompt_file` | Path to a file containing 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 | '' |
|
| `allowed_tools` | Comma-separated list of allowed tools for Claude Code to use | No | '' |
|
||||||
| `disallowed_tools` | Comma-separated list of disallowed tools that Claude Code cannot use | No | '' |
|
| `disallowed_tools` | Comma-separated list of disallowed tools that Claude Code cannot use | No | '' |
|
||||||
| `max_turns` | Maximum number of conversation turns (default: no limit) | No | '' |
|
| `max_turns` | Maximum number of conversation turns (default: no limit) | No | '' |
|
||||||
| `mcp_config` | Path to the MCP configuration JSON file, or MCP configuration JSON string | No | '' |
|
| `mcp_config` | Path to the MCP configuration JSON file, or MCP configuration JSON string | No | '' |
|
||||||
| `settings` | Path to Claude Code settings JSON file, or settings JSON string | No | '' |
|
| `settings` | Path to Claude Code settings JSON file, or settings JSON string | No | '' |
|
||||||
| `system_prompt` | Override system prompt | No | '' |
|
| `system_prompt` | Override system prompt | No | '' |
|
||||||
| `append_system_prompt` | Append to system prompt | No | '' |
|
| `append_system_prompt` | Append to system prompt | No | '' |
|
||||||
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML multiline format) | No | '' |
|
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML multiline format) | No | '' |
|
||||||
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | 'claude-4-0-sonnet-20250219' |
|
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | 'claude-4-0-sonnet-20250219' |
|
||||||
| `anthropic_model` | DEPRECATED: Use 'model' instead | No | 'claude-4-0-sonnet-20250219' |
|
| `anthropic_model` | DEPRECATED: Use 'model' instead | No | 'claude-4-0-sonnet-20250219' |
|
||||||
| `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' |
|
| `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 | '' |
|
| `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 | '' |
|
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No | '' |
|
||||||
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | 'false' |
|
| `anthropic_federation_rule_id` | Workload identity federation rule ID (fdrl\_...). Requires `id-token: write` permission | No | '' |
|
||||||
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | 'false' |
|
| `anthropic_organization_id` | Anthropic organization UUID used for workload identity federation | No | '' |
|
||||||
| `use_node_cache` | Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files) | No | 'false' |
|
| `anthropic_service_account_id` | Service account ID (svac\_...) the federated token acts as (optional) | No | '' |
|
||||||
| `show_full_output` | Show full JSON output (⚠️ May expose secrets - see [security docs](../docs/security.md#️-full-output-security-warning)) | No | 'false'\*\* |
|
| `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' |
|
||||||
|
| `show_full_output` | Show full JSON output (⚠️ May expose secrets - see [security docs](../docs/security.md#️-full-output-security-warning)) | No | 'false'\*\* |
|
||||||
|
|
||||||
\*Either `prompt` or `prompt_file` must be provided, but not both.
|
\*Either `prompt` or `prompt_file` must be provided, but not both.
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,26 @@ inputs:
|
|||||||
description: "Claude Code OAuth token (alternative to anthropic_api_key)"
|
description: "Claude Code OAuth token (alternative to anthropic_api_key)"
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
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:
|
use_bedrock:
|
||||||
description: "Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API"
|
description: "Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API"
|
||||||
required: false
|
required: false
|
||||||
@ -175,6 +195,11 @@ runs:
|
|||||||
# Provider configuration
|
# Provider configuration
|
||||||
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }}
|
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_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }}
|
||||||
ANTHROPIC_CUSTOM_HEADERS: ${{ env.ANTHROPIC_CUSTOM_HEADERS }}
|
ANTHROPIC_CUSTOM_HEADERS: ${{ env.ANTHROPIC_CUSTOM_HEADERS }}
|
||||||
# Only set provider flags if explicitly true, since any value (including "false") is truthy
|
# 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 { validateEnvironmentVariables } from "./validate-env";
|
||||||
import { installPlugins } from "./install-plugins";
|
import { installPlugins } from "./install-plugins";
|
||||||
import { setExecutionFileOutputIfPresent } from "./execution-file";
|
import { setExecutionFileOutputIfPresent } from "./execution-file";
|
||||||
|
import { setupWorkloadIdentity } from "./workload-identity";
|
||||||
|
import type { WorkloadIdentityHandle } from "./workload-identity";
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
|
let workloadIdentity: WorkloadIdentityHandle | undefined;
|
||||||
try {
|
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();
|
validateEnvironmentVariables();
|
||||||
|
|
||||||
// The composite action's "Install Claude Code" step writes the binary to
|
// 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.setFailed(`Action failed with error: ${error}`);
|
||||||
core.setOutput("conclusion", "failure");
|
core.setOutput("conclusion", "failure");
|
||||||
process.exit(1);
|
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 * as core from "@actions/core";
|
||||||
import { mkdirSync, writeFileSync } from "fs";
|
import { mkdirSync, writeFileSync } from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { retryWithBackoff } from "../utils/retry";
|
import { retryWithBackoff } from "./retry";
|
||||||
|
|
||||||
/** How often the GitHub OIDC identity token file is rewritten. */
|
/** How often the GitHub OIDC identity token file is rewritten. */
|
||||||
const REFRESH_INTERVAL_MS = 4 * 60 * 1000;
|
const REFRESH_INTERVAL_MS = 4 * 60 * 1000;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
|
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
|
||||||
import { retryWithBackoff } from "../src/utils/retry";
|
import { retryWithBackoff } from "../src/retry";
|
||||||
|
|
||||||
describe("retryWithBackoff", () => {
|
describe("retryWithBackoff", () => {
|
||||||
let originalConsoleLog: typeof console.log;
|
let originalConsoleLog: typeof console.log;
|
||||||
@ -8,7 +8,7 @@ import { join } from "path";
|
|||||||
import {
|
import {
|
||||||
isWorkloadIdentityConfigured,
|
isWorkloadIdentityConfigured,
|
||||||
setupWorkloadIdentity,
|
setupWorkloadIdentity,
|
||||||
} from "../src/auth/workload-identity";
|
} from "../src/workload-identity";
|
||||||
|
|
||||||
describe("workload identity federation", () => {
|
describe("workload identity federation", () => {
|
||||||
let originalEnv: NodeJS.ProcessEnv;
|
let originalEnv: NodeJS.ProcessEnv;
|
||||||
@ -29,13 +29,13 @@ import { prepareAgentMode } from "../modes/agent";
|
|||||||
import { checkContainsTrigger } from "../github/validation/trigger";
|
import { checkContainsTrigger } from "../github/validation/trigger";
|
||||||
import { restoreConfigFromBase } from "../github/operations/restore-config";
|
import { restoreConfigFromBase } from "../github/operations/restore-config";
|
||||||
import { validateBranchName } from "../github/operations/branch";
|
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 { collectActionInputsPresence } from "./collect-inputs";
|
||||||
import { updateCommentLink } from "./update-comment-link";
|
import { updateCommentLink } from "./update-comment-link";
|
||||||
import { formatTurnsFromData } from "./format-turns";
|
import { formatTurnsFromData } from "./format-turns";
|
||||||
import type { Turn } from "./format-turns";
|
import type { Turn } from "./format-turns";
|
||||||
// Base-action imports (used directly instead of subprocess)
|
// 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 { validateEnvironmentVariables } from "../../base-action/src/validate-env";
|
||||||
import { setupClaudeCodeSettings } from "../../base-action/src/setup-claude-code-settings";
|
import { setupClaudeCodeSettings } from "../../base-action/src/setup-claude-code-settings";
|
||||||
import { installPlugins } from "../../base-action/src/install-plugins";
|
import { installPlugins } from "../../base-action/src/install-plugins";
|
||||||
|
|||||||
@ -1,47 +1,4 @@
|
|||||||
export type RetryOptions = {
|
export {
|
||||||
maxAttempts?: number;
|
retryWithBackoff,
|
||||||
initialDelayMs?: number;
|
type RetryOptions,
|
||||||
maxDelayMs?: number;
|
} from "../../base-action/src/retry";
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user