Compare commits

..

11 Commits

Author SHA1 Message Date
inigo
606f6af1c6 docs: update structured outputs documentation 2025-11-18 15:36:45 -08:00
inigo
6bc261bb35 feat: add structured output support 2025-11-18 15:22:15 -08:00
inigo
84265a4271 update 2025-11-18 14:37:48 -08:00
inigo
9d3bab5bc7 test: add proper test coverage for parseAndSetStructuredOutputs
Fixed test coverage gap where tests were only parsing JSON manually
without actually invoking the parseAndSetStructuredOutputs function.

Changes:
- Export parseAndSetStructuredOutputs for testing
- Rewrite tests to use spyOn() to mock @actions/core functions
- Add tests that actually call the function and verify:
  - core.setOutput() called with correct JSON string
  - core.info() called with correct field count
  - Error thrown when result exists but structured_output undefined
  - Error thrown when no result message exists
  - Handles special characters in field names (hyphens, dots, @ symbols)
  - Handles arrays and nested objects correctly
  - File errors propagate correctly

All 8 tests now properly test the actual implementation with full
coverage of success and error paths.

Addresses review comment: https://github.com/anthropics/claude-code-action/pull/683#discussion_r2539770213

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 14:26:53 -08:00
inigo
bf8f85ca9d docs: fix incorrect field naming restrictions in base-action/action.yml
Fixed outdated documentation that incorrectly stated field naming
restrictions that don't exist in the implementation.

Changes:
- Removed incorrect claim about field naming requirements (letter/underscore start)
- Removed incorrect claim about special character sanitization
- Clarified that field names can use any valid JSON property name
- Updated access pattern to show fromJSON() usage
- Clarified 1MB limit applies to entire structured_output string, not per-field

The implementation simply does JSON.stringify(result.structured_output)
without any sanitization, so any valid JSON property name works (including
hyphens like "test-result", as validated by integration tests).

Addresses review comment: https://github.com/anthropics/claude-code-action/pull/683#discussion_r2539749593

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 14:22:44 -08:00
inigo
f551cdf070 fix: remove double error reporting in parseAndSetStructuredOutputs
Fixed error handling anti-pattern identified in PR review where the
function was calling core.setFailed() AND throwing errors, causing
confusion about error handling flow.

Changes:
- parseAndSetStructuredOutputs now just throws errors without calling
  core.setFailed() - follows single responsibility principle
- Caller (runClaude) catches errors and calls core.setFailed() once
- Removed unnecessary structuredOutputSuccess boolean flag
- Clearer error handling flow: function parses/throws, caller decides
  how to handle failures

Addresses review comment: https://github.com/anthropics/claude-code-action/pull/683#discussion_r2539741001

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 14:13:57 -08:00
inigo
ec3a934da7 docs: update structured output documentation for JSON-only approach
Updated documentation to reflect that structured outputs are now only
accessible via the single structured_output JSON string, not as
individual fields.

Changes:
- docs/usage.md: Updated "Accessing Structured Outputs" section
  - Show fromJSON() usage in GitHub Actions expressions
  - Show jq usage in bash
  - Explain composite action limitation
  - Remove outdated "Output Naming Rules" and size limit sections
- action.yml: Updated json_schema input description
- examples/test-failure-analysis.yml: Updated to use fromJSON() and jq

Users now access fields via:
  fromJSON(steps.<id>.outputs.structured_output).field_name
Or:
  echo '${{ steps.<id>.outputs.structured_output }}' | jq '.field_name'

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 14:07:00 -08:00
inigo
8cd2cc1236 refactor: remove individual field outputs, keep only structured_output JSON
Since GitHub Actions composite actions cannot expose dynamic outputs,
individual field outputs were not accessible anyway and only added
complexity and collision risk.

Simplified by:
- Removing individual core.setOutput() calls for each field
- Removing RESERVED_OUTPUTS check (no longer needed)
- Removing sanitizeOutputName, convertToString, MAX_OUTPUT_SIZE helpers
- Removing related unit tests for removed functionality

Users access all fields via single structured_output JSON string:
  fromJSON(steps.<id>.outputs.structured_output).field_name

Or with jq:
  echo '${{ steps.<id>.outputs.structured_output }}' | jq '.field_name'

All tests pass (462 tests).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 14:01:58 -08:00
inigo
dcee434ef2 fix: workaround GitHub Actions composite action output limitation
GitHub Actions composite actions cannot have dynamic outputs - all outputs
must be explicitly declared in action.yml. This is a known limitation.

Changes:
- Add structured_output JSON output to base-action/action.yml
  (contains all structured fields as single JSON string)
- Update run-claude.ts to set structured_output output
- Update tests to parse structured_output JSON with jq
- Add structured_output to RESERVED_OUTPUTS list

Users can now access structured outputs via:
  steps.<id>.outputs.structured_output | jq '.field_name'

Or in GitHub Actions expressions:
  fromJSON(steps.<id>.outputs.structured_output).field_name

Individual field outputs are still set for direct usage contexts,
but only the structured_output JSON is accessible via composite action.

Fixes #683 test failures

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 12:08:41 -08:00
inigo
e93583852d fix: address PR #683 review feedback
Critical fixes:
- Remove duplicate core.setFailed() call in parseAndSetStructuredOutputs
  (fixes double error reporting issue)
- Extract JSON schema handling to shared utility function
  (eliminates code duplication between agent/tag modes)

Changes:
- base-action/src/run-claude.ts: Remove redundant setFailed() before throw
- src/utils/json-schema.ts: New shared appendJsonSchemaArg() utility
- src/modes/agent/index.ts: Use shared JSON schema utility
- src/modes/tag/index.ts: Use shared JSON schema utility

All tests passing, types checked, code formatted.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 11:55:41 -08:00
inigo
e600a516c7 feat: add structured output support
Add support for Agent SDK structured outputs.

New input: json_schema - JSON schema for validated outputs
Auto-sets GitHub Action outputs for each field

Security:
- Reserved output protection (prevents shadowing)
- 1MB output size limits enforced
- Output key format validation
- Objects/arrays >1MB skipped (not truncated to invalid JSON)

Tests:
- 26 unit tests
- 5 integration tests
- 480 tests passing

Docs: https://docs.claude.com/en/docs/agent-sdk/structured-outputs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 11:48:03 -08:00
20 changed files with 395 additions and 312 deletions

View File

@@ -2,7 +2,7 @@ name: PR Review
on: on:
pull_request: pull_request:
types: [opened] types: [opened, synchronize, ready_for_review, reopened]
jobs: jobs:
review: review:

View File

@@ -30,10 +30,19 @@ jobs:
- number_field: 42 - number_field: 42
- boolean_true: true - boolean_true: true
- boolean_false: false - boolean_false: 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"]
}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
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"]}'
- name: Verify outputs - name: Verify outputs
run: | run: |
@@ -88,10 +97,21 @@ jobs:
- items: ["apple", "banana", "cherry"] - items: ["apple", "banana", "cherry"]
- config: {"key": "value", "count": 3} - config: {"key": "value", "count": 3}
- empty_array: [] - 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"]
}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
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"]}'
- name: Verify JSON stringification - name: Verify JSON stringification
run: | run: |
@@ -140,10 +160,19 @@ jobs:
- empty_string: "" - empty_string: ""
- negative: -5 - negative: -5
- decimal: 3.14 - decimal: 3.14
json_schema: |
{
"type": "object",
"properties": {
"zero": {"type": "number"},
"empty_string": {"type": "string"},
"negative": {"type": "number"},
"decimal": {"type": "number"}
},
"required": ["zero", "empty_string", "negative", "decimal"]
}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
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"]}'
- name: Verify edge cases - name: Verify edge cases
run: | run: |
@@ -194,10 +223,17 @@ 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}
json_schema: |
{
"type": "object",
"properties": {
"test-result": {"type": "string"},
"item_count": {"type": "number"}
},
"required": ["test-result", "item_count"]
}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
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"]}'
- name: Verify sanitized names work - name: Verify sanitized names work
run: | run: |
@@ -232,10 +268,16 @@ jobs:
uses: ./base-action uses: ./base-action
with: with:
prompt: "Run: echo 'complete'. Return: {done: true}" prompt: "Run: echo 'complete'. Return: {done: true}"
json_schema: |
{
"type": "object",
"properties": {
"done": {"type": "boolean"}
},
"required": ["done"]
}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: | claude_args: "--allowedTools Bash"
--allowedTools Bash
--json-schema '{"type":"object","properties":{"done":{"type":"boolean"}},"required":["done"]}'
- name: Verify execution file contains structured_output - name: Verify execution file contains structured_output
run: | run: |

View File

@@ -2,7 +2,7 @@
# Claude Code Action # Claude Code Action
A general-purpose [Claude Code](https://claude.ai/code) action for GitHub PRs and issues that can answer questions and implement code changes. This action intelligently detects when to activate based on your workflow context—whether responding to @claude mentions, issue assignments, or executing automation tasks with explicit prompts. It supports multiple authentication methods including Anthropic direct API, Amazon Bedrock, Google Vertex AI, and Microsoft Foundry. A general-purpose [Claude Code](https://claude.ai/code) action for GitHub PRs and issues that can answer questions and implement code changes. This action intelligently detects when to activate based on your workflow context—whether responding to @claude mentions, issue assignments, or executing automation tasks with explicit prompts. It supports multiple authentication methods including Anthropic direct API, Amazon Bedrock, and Google Vertex AI.
## Features ## Features
@@ -30,7 +30,7 @@ This command will guide you through setting up the GitHub app and required secre
**Note**: **Note**:
- You must be a repository admin to install the GitHub app and add secrets - You must be a repository admin to install the GitHub app and add secrets
- This quickstart method is only available for direct Anthropic API users. For AWS Bedrock, Google Vertex AI, or Microsoft Foundry setup, see [docs/cloud-providers.md](./docs/cloud-providers.md). - This quickstart method is only available for direct Anthropic API users. For AWS Bedrock or Google Vertex AI setup, see [docs/cloud-providers.md](./docs/cloud-providers.md).
## 📚 Solutions & Use Cases ## 📚 Solutions & Use Cases
@@ -57,7 +57,7 @@ Each solution includes complete working examples, configuration details, and exp
- [Custom Automations](./docs/custom-automations.md) - Examples of automated workflows and custom prompts - [Custom Automations](./docs/custom-automations.md) - Examples of automated workflows and custom prompts
- [Configuration](./docs/configuration.md) - MCP servers, permissions, environment variables, and advanced settings - [Configuration](./docs/configuration.md) - MCP servers, permissions, environment variables, and advanced settings
- [Experimental Features](./docs/experimental.md) - Execution modes and network restrictions - [Experimental Features](./docs/experimental.md) - Execution modes and network restrictions
- [Cloud Providers](./docs/cloud-providers.md) - AWS Bedrock, Google Vertex AI, and Microsoft Foundry setup - [Cloud Providers](./docs/cloud-providers.md) - AWS Bedrock and Google Vertex AI setup
- [Capabilities & Limitations](./docs/capabilities-and-limitations.md) - What Claude can and cannot do - [Capabilities & Limitations](./docs/capabilities-and-limitations.md) - What Claude can and cannot do
- [Security](./docs/security.md) - Access control, permissions, and commit signing - [Security](./docs/security.md) - Access control, permissions, and commit signing
- [FAQ](./docs/faq.md) - Common questions and troubleshooting - [FAQ](./docs/faq.md) - Common questions and troubleshooting

View File

@@ -44,7 +44,7 @@ inputs:
# Auth configuration # Auth configuration
anthropic_api_key: anthropic_api_key:
description: "Anthropic API key (required for direct API, not needed for Bedrock/Vertex/Foundry)" description: "Anthropic API key (required for direct API, not needed for Bedrock/Vertex)"
required: false required: false
claude_code_oauth_token: claude_code_oauth_token:
description: "Claude Code OAuth token (alternative to anthropic_api_key)" description: "Claude Code OAuth token (alternative to anthropic_api_key)"
@@ -60,10 +60,6 @@ inputs:
description: "Use Google Vertex AI with OIDC authentication instead of direct Anthropic API" description: "Use Google Vertex AI with OIDC authentication instead of direct Anthropic API"
required: false required: false
default: "false" default: "false"
use_foundry:
description: "Use Microsoft Foundry with OIDC authentication instead of direct Anthropic API"
required: false
default: "false"
claude_args: claude_args:
description: "Additional arguments to pass directly to Claude CLI" description: "Additional arguments to pass directly to Claude CLI"
@@ -93,6 +89,10 @@ inputs:
description: "Force tag mode with tracking comments for pull_request and issue events. Only applicable to pull_request (opened, synchronize, ready_for_review, reopened) and issue (opened, edited, labeled, assigned) events." description: "Force tag mode with tracking comments for pull_request and issue events. Only applicable to pull_request (opened, synchronize, ready_for_review, reopened) and issue (opened, edited, labeled, assigned) events."
required: false required: false
default: "false" default: "false"
experimental_allowed_domains:
description: "Restrict network access to these domains only (newline-separated). If not set, no restrictions are applied. Provider domains are auto-detected."
required: false
default: ""
path_to_claude_code_executable: path_to_claude_code_executable:
description: "Optional path to a custom Claude Code executable. If provided, skips automatic installation and uses this executable instead. WARNING: Using an older version may cause problems if the action begins taking advantage of new Claude Code features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment." description: "Optional path to a custom Claude Code executable. If provided, skips automatic installation and uses this executable instead. WARNING: Using an older version may cause problems if the action begins taking advantage of new Claude Code features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment."
required: false required: false
@@ -113,6 +113,10 @@ inputs:
description: "Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., 'https://github.com/user/marketplace1.git\nhttps://github.com/user/marketplace2.git')" description: "Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., 'https://github.com/user/marketplace1.git\nhttps://github.com/user/marketplace2.git')"
required: false required: false
default: "" default: ""
json_schema:
description: "JSON schema for structured output validation. When provided, Claude will return validated JSON matching this schema. All fields are available in the structured_output output as a JSON string (use fromJSON() or jq to access fields)."
required: false
default: ""
outputs: outputs:
execution_file: execution_file:
@@ -125,7 +129,7 @@ outputs:
description: "The GitHub token used by the action (Claude App token if available)" description: "The GitHub token used by the action (Claude App token if available)"
value: ${{ steps.prepare.outputs.github_token }} value: ${{ steps.prepare.outputs.github_token }}
structured_output: structured_output:
description: "JSON string containing all structured output fields when --json-schema is provided in claude_args. Use fromJSON() to parse: fromJSON(steps.id.outputs.structured_output).field_name" description: "JSON string containing all structured output fields when json_schema input is provided. Use fromJSON() to parse: fromJSON(steps.id.outputs.structured_output).field_name"
value: ${{ steps.claude-code.outputs.structured_output }} value: ${{ steps.claude-code.outputs.structured_output }}
runs: runs:
@@ -177,6 +181,7 @@ runs:
TRACK_PROGRESS: ${{ inputs.track_progress }} TRACK_PROGRESS: ${{ inputs.track_progress }}
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }} ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
CLAUDE_ARGS: ${{ inputs.claude_args }} CLAUDE_ARGS: ${{ inputs.claude_args }}
JSON_SCHEMA: ${{ inputs.json_schema }}
ALL_INPUTS: ${{ toJson(inputs) }} ALL_INPUTS: ${{ toJson(inputs) }}
- name: Install Base Action Dependencies - name: Install Base Action Dependencies
@@ -191,23 +196,8 @@ runs:
# Install Claude Code if no custom executable is provided # Install Claude Code if no custom executable is provided
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
CLAUDE_CODE_VERSION="2.0.50" echo "Installing Claude Code..."
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..." curl -fsSL https://claude.ai/install.sh | bash -s 2.0.42
for attempt in 1 2 3; do
echo "Installation attempt $attempt..."
if command -v timeout &> /dev/null; then
timeout 120 bash -c "curl -fsSL https://claude.ai/install.sh | bash -s -- $CLAUDE_CODE_VERSION" && break
else
curl -fsSL https://claude.ai/install.sh | bash -s -- "$CLAUDE_CODE_VERSION" && break
fi
if [ $attempt -eq 3 ]; then
echo "Failed to install Claude Code after 3 attempts"
exit 1
fi
echo "Installation failed, retrying..."
sleep 5
done
echo "Claude Code installed successfully"
echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "$HOME/.local/bin" >> "$GITHUB_PATH"
else else
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}" echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
@@ -216,6 +206,15 @@ runs:
echo "$CLAUDE_DIR" >> "$GITHUB_PATH" echo "$CLAUDE_DIR" >> "$GITHUB_PATH"
fi fi
- name: Setup Network Restrictions
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
shell: bash
run: |
chmod +x ${GITHUB_ACTION_PATH}/scripts/setup-network-restrictions.sh
${GITHUB_ACTION_PATH}/scripts/setup-network-restrictions.sh
env:
EXPERIMENTAL_ALLOWED_DOMAINS: ${{ inputs.experimental_allowed_domains }}
- name: Run Claude Code - name: Run Claude Code
id: claude-code id: claude-code
if: steps.prepare.outputs.contains_trigger == 'true' if: steps.prepare.outputs.contains_trigger == 'true'
@@ -237,6 +236,7 @@ runs:
INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }} INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }}
INPUT_PLUGINS: ${{ inputs.plugins }} INPUT_PLUGINS: ${{ inputs.plugins }}
INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }} INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }}
JSON_SCHEMA: ${{ inputs.json_schema }}
# Model configuration # Model configuration
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
@@ -250,14 +250,12 @@ runs:
ANTHROPIC_CUSTOM_HEADERS: ${{ env.ANTHROPIC_CUSTOM_HEADERS }} ANTHROPIC_CUSTOM_HEADERS: ${{ env.ANTHROPIC_CUSTOM_HEADERS }}
CLAUDE_CODE_USE_BEDROCK: ${{ inputs.use_bedrock == 'true' && '1' || '' }} CLAUDE_CODE_USE_BEDROCK: ${{ inputs.use_bedrock == 'true' && '1' || '' }}
CLAUDE_CODE_USE_VERTEX: ${{ inputs.use_vertex == 'true' && '1' || '' }} CLAUDE_CODE_USE_VERTEX: ${{ inputs.use_vertex == 'true' && '1' || '' }}
CLAUDE_CODE_USE_FOUNDRY: ${{ inputs.use_foundry == 'true' && '1' || '' }}
# AWS configuration # AWS configuration
AWS_REGION: ${{ env.AWS_REGION }} AWS_REGION: ${{ env.AWS_REGION }}
AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }}
AWS_SESSION_TOKEN: ${{ env.AWS_SESSION_TOKEN }} AWS_SESSION_TOKEN: ${{ env.AWS_SESSION_TOKEN }}
AWS_BEARER_TOKEN_BEDROCK: ${{ env.AWS_BEARER_TOKEN_BEDROCK }}
ANTHROPIC_BEDROCK_BASE_URL: ${{ env.ANTHROPIC_BEDROCK_BASE_URL || (env.AWS_REGION && format('https://bedrock-runtime.{0}.amazonaws.com', env.AWS_REGION)) }} ANTHROPIC_BEDROCK_BASE_URL: ${{ env.ANTHROPIC_BEDROCK_BASE_URL || (env.AWS_REGION && format('https://bedrock-runtime.{0}.amazonaws.com', env.AWS_REGION)) }}
# GCP configuration # GCP configuration
@@ -271,13 +269,6 @@ runs:
VERTEX_REGION_CLAUDE_3_5_SONNET: ${{ env.VERTEX_REGION_CLAUDE_3_5_SONNET }} VERTEX_REGION_CLAUDE_3_5_SONNET: ${{ env.VERTEX_REGION_CLAUDE_3_5_SONNET }}
VERTEX_REGION_CLAUDE_3_7_SONNET: ${{ env.VERTEX_REGION_CLAUDE_3_7_SONNET }} VERTEX_REGION_CLAUDE_3_7_SONNET: ${{ env.VERTEX_REGION_CLAUDE_3_7_SONNET }}
# Microsoft Foundry configuration
ANTHROPIC_FOUNDRY_RESOURCE: ${{ env.ANTHROPIC_FOUNDRY_RESOURCE }}
ANTHROPIC_FOUNDRY_BASE_URL: ${{ env.ANTHROPIC_FOUNDRY_BASE_URL }}
ANTHROPIC_DEFAULT_SONNET_MODEL: ${{ env.ANTHROPIC_DEFAULT_SONNET_MODEL }}
ANTHROPIC_DEFAULT_HAIKU_MODEL: ${{ env.ANTHROPIC_DEFAULT_HAIKU_MODEL }}
ANTHROPIC_DEFAULT_OPUS_MODEL: ${{ env.ANTHROPIC_DEFAULT_OPUS_MODEL }}
- name: Update comment with job link - name: Update comment with job link
if: steps.prepare.outputs.contains_trigger == 'true' && steps.prepare.outputs.claude_comment_id && always() if: steps.prepare.outputs.contains_trigger == 'true' && steps.prepare.outputs.claude_comment_id && always()
shell: bash shell: bash

View File

@@ -42,10 +42,6 @@ inputs:
description: "Use Google Vertex AI with OIDC authentication instead of direct Anthropic API" description: "Use Google Vertex AI with OIDC authentication instead of direct Anthropic API"
required: false required: false
default: "false" default: "false"
use_foundry:
description: "Use Microsoft Foundry with OIDC authentication instead of direct Anthropic API"
required: false
default: "false"
use_node_cache: use_node_cache:
description: "Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files)" description: "Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files)"
@@ -71,6 +67,14 @@ inputs:
description: "Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., 'https://github.com/user/marketplace1.git\nhttps://github.com/user/marketplace2.git')" description: "Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., 'https://github.com/user/marketplace1.git\nhttps://github.com/user/marketplace2.git')"
required: false required: false
default: "" default: ""
json_schema:
description: |
JSON schema for structured output validation. Claude must return JSON matching this schema
or the action will fail. All fields are returned in a single structured_output JSON string.
Access outputs via: fromJSON(steps.<step-id>.outputs.structured_output).<field_name>
required: false
default: ""
outputs: outputs:
conclusion: conclusion:
@@ -80,7 +84,7 @@ outputs:
description: "Path to the JSON file containing Claude Code execution log" description: "Path to the JSON file containing Claude Code execution log"
value: ${{ steps.run_claude.outputs.execution_file }} value: ${{ steps.run_claude.outputs.execution_file }}
structured_output: structured_output:
description: "JSON string containing all structured output fields when --json-schema is provided in claude_args (use fromJSON() or jq to parse)" description: "JSON string containing all structured output fields (use fromJSON() or jq to parse)"
value: ${{ steps.run_claude.outputs.structured_output }} value: ${{ steps.run_claude.outputs.structured_output }}
runs: runs:
@@ -117,23 +121,8 @@ runs:
shell: bash shell: bash
run: | run: |
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
CLAUDE_CODE_VERSION="2.0.50" echo "Installing Claude Code..."
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..." curl -fsSL https://claude.ai/install.sh | bash -s 2.0.45
for attempt in 1 2 3; do
echo "Installation attempt $attempt..."
if command -v timeout &> /dev/null; then
timeout 120 bash -c "curl -fsSL https://claude.ai/install.sh | bash -s -- $CLAUDE_CODE_VERSION" && break
else
curl -fsSL https://claude.ai/install.sh | bash -s -- "$CLAUDE_CODE_VERSION" && break
fi
if [ $attempt -eq 3 ]; then
echo "Failed to install Claude Code after 3 attempts"
exit 1
fi
echo "Installation failed, retrying..."
sleep 5
done
echo "Claude Code installed successfully"
else else
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}" echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
# Add the directory containing the custom executable to PATH # Add the directory containing the custom executable to PATH
@@ -163,6 +152,7 @@ runs:
INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }} INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }}
INPUT_PLUGINS: ${{ inputs.plugins }} INPUT_PLUGINS: ${{ inputs.plugins }}
INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }} INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }}
JSON_SCHEMA: ${{ inputs.json_schema }}
# Provider configuration # Provider configuration
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
@@ -172,14 +162,12 @@ runs:
# 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
CLAUDE_CODE_USE_BEDROCK: ${{ inputs.use_bedrock == 'true' && '1' || '' }} CLAUDE_CODE_USE_BEDROCK: ${{ inputs.use_bedrock == 'true' && '1' || '' }}
CLAUDE_CODE_USE_VERTEX: ${{ inputs.use_vertex == 'true' && '1' || '' }} CLAUDE_CODE_USE_VERTEX: ${{ inputs.use_vertex == 'true' && '1' || '' }}
CLAUDE_CODE_USE_FOUNDRY: ${{ inputs.use_foundry == 'true' && '1' || '' }}
# AWS configuration # AWS configuration
AWS_REGION: ${{ env.AWS_REGION }} AWS_REGION: ${{ env.AWS_REGION }}
AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }}
AWS_SESSION_TOKEN: ${{ env.AWS_SESSION_TOKEN }} AWS_SESSION_TOKEN: ${{ env.AWS_SESSION_TOKEN }}
AWS_BEARER_TOKEN_BEDROCK: ${{ env.AWS_BEARER_TOKEN_BEDROCK }}
ANTHROPIC_BEDROCK_BASE_URL: ${{ env.ANTHROPIC_BEDROCK_BASE_URL || (env.AWS_REGION && format('https://bedrock-runtime.{0}.amazonaws.com', env.AWS_REGION)) }} ANTHROPIC_BEDROCK_BASE_URL: ${{ env.ANTHROPIC_BEDROCK_BASE_URL || (env.AWS_REGION && format('https://bedrock-runtime.{0}.amazonaws.com', env.AWS_REGION)) }}
# GCP configuration # GCP configuration
@@ -187,10 +175,3 @@ runs:
CLOUD_ML_REGION: ${{ env.CLOUD_ML_REGION }} CLOUD_ML_REGION: ${{ env.CLOUD_ML_REGION }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ env.GOOGLE_APPLICATION_CREDENTIALS }} GOOGLE_APPLICATION_CREDENTIALS: ${{ env.GOOGLE_APPLICATION_CREDENTIALS }}
ANTHROPIC_VERTEX_BASE_URL: ${{ env.ANTHROPIC_VERTEX_BASE_URL }} ANTHROPIC_VERTEX_BASE_URL: ${{ env.ANTHROPIC_VERTEX_BASE_URL }}
# Microsoft Foundry configuration
ANTHROPIC_FOUNDRY_RESOURCE: ${{ env.ANTHROPIC_FOUNDRY_RESOURCE }}
ANTHROPIC_FOUNDRY_BASE_URL: ${{ env.ANTHROPIC_FOUNDRY_BASE_URL }}
ANTHROPIC_DEFAULT_SONNET_MODEL: ${{ env.ANTHROPIC_DEFAULT_SONNET_MODEL }}
ANTHROPIC_DEFAULT_HAIKU_MODEL: ${{ env.ANTHROPIC_DEFAULT_HAIKU_MODEL }}
ANTHROPIC_DEFAULT_OPUS_MODEL: ${{ env.ANTHROPIC_DEFAULT_OPUS_MODEL }}

View File

@@ -28,8 +28,22 @@ async function run() {
promptFile: process.env.INPUT_PROMPT_FILE || "", promptFile: process.env.INPUT_PROMPT_FILE || "",
}); });
// Build claudeArgs with JSON schema if provided
let claudeArgs = process.env.INPUT_CLAUDE_ARGS || "";
// Add allowed tools if specified
if (process.env.INPUT_ALLOWED_TOOLS) {
claudeArgs += ` --allowedTools "${process.env.INPUT_ALLOWED_TOOLS}"`;
}
// Add JSON schema if specified (no escaping - parseShellArgs handles it)
if (process.env.JSON_SCHEMA) {
// Wrap in single quotes for parseShellArgs
claudeArgs += ` --json-schema '${process.env.JSON_SCHEMA}'`;
}
await runClaude(promptConfig.path, { await runClaude(promptConfig.path, {
claudeArgs: process.env.INPUT_CLAUDE_ARGS, claudeArgs: claudeArgs.trim(),
allowedTools: process.env.INPUT_ALLOWED_TOOLS, allowedTools: process.env.INPUT_ALLOWED_TOOLS,
disallowedTools: process.env.INPUT_DISALLOWED_TOOLS, disallowedTools: process.env.INPUT_DISALLOWED_TOOLS,
maxTurns: process.env.INPUT_MAX_TURNS, maxTurns: process.env.INPUT_MAX_TURNS,

View File

@@ -124,7 +124,7 @@ export function prepareRunConfig(
/** /**
* Parses structured_output from execution file and sets GitHub Action outputs * Parses structured_output from execution file and sets GitHub Action outputs
* Only runs if --json-schema was explicitly provided in claude_args * Only runs if json_schema was explicitly provided by the user
* Exported for testing * Exported for testing
*/ */
export async function parseAndSetStructuredOutputs( export async function parseAndSetStructuredOutputs(
@@ -144,7 +144,7 @@ export async function parseAndSetStructuredOutputs(
if (!result?.structured_output) { if (!result?.structured_output) {
throw new Error( throw new Error(
`--json-schema was provided but Claude did not return structured_output.\n` + `json_schema was provided but Claude did not return structured_output.\n` +
`Found ${messages.length} messages. Result exists: ${!!result}\n`, `Found ${messages.length} messages. Result exists: ${!!result}\n`,
); );
} }
@@ -167,9 +167,6 @@ export async function parseAndSetStructuredOutputs(
export async function runClaude(promptPath: string, options: ClaudeOptions) { export async function runClaude(promptPath: string, options: ClaudeOptions) {
const config = prepareRunConfig(promptPath, options); const config = prepareRunConfig(promptPath, options);
// Detect if --json-schema is present in claude args
const hasJsonSchema = options.claudeArgs?.includes("--json-schema") ?? false;
// Create a named pipe // Create a named pipe
try { try {
await unlink(PIPE_PATH); await unlink(PIPE_PATH);
@@ -355,8 +352,8 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
core.setOutput("execution_file", EXECUTION_FILE); core.setOutput("execution_file", EXECUTION_FILE);
// Parse and set structured outputs only if user provided --json-schema in claude_args // Parse and set structured outputs only if user provided json_schema
if (hasJsonSchema) { if (process.env.JSON_SCHEMA) {
try { try {
await parseAndSetStructuredOutputs(EXECUTION_FILE); await parseAndSetStructuredOutputs(EXECUTION_FILE);
} catch (error) { } catch (error) {

View File

@@ -1,50 +1,39 @@
/** /**
* Validates the environment variables required for running Claude Code * Validates the environment variables required for running Claude Code
* based on the selected provider (Anthropic API, AWS Bedrock, Google Vertex AI, or Microsoft Foundry) * based on the selected provider (Anthropic API, AWS Bedrock, or Google Vertex AI)
*/ */
export function validateEnvironmentVariables() { export function validateEnvironmentVariables() {
const useBedrock = process.env.CLAUDE_CODE_USE_BEDROCK === "1"; const useBedrock = process.env.CLAUDE_CODE_USE_BEDROCK === "1";
const useVertex = process.env.CLAUDE_CODE_USE_VERTEX === "1"; const useVertex = process.env.CLAUDE_CODE_USE_VERTEX === "1";
const useFoundry = process.env.CLAUDE_CODE_USE_FOUNDRY === "1";
const anthropicApiKey = process.env.ANTHROPIC_API_KEY; const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
const claudeCodeOAuthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN; const claudeCodeOAuthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
const errors: string[] = []; const errors: string[] = [];
// Check for mutual exclusivity between providers if (useBedrock && useVertex) {
const activeProviders = [useBedrock, useVertex, useFoundry].filter(Boolean);
if (activeProviders.length > 1) {
errors.push( errors.push(
"Cannot use multiple providers simultaneously. Please set only one of: CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY.", "Cannot use both Bedrock and Vertex AI simultaneously. Please set only one provider.",
); );
} }
if (!useBedrock && !useVertex && !useFoundry) { if (!useBedrock && !useVertex) {
if (!anthropicApiKey && !claudeCodeOAuthToken) { if (!anthropicApiKey && !claudeCodeOAuthToken) {
errors.push( errors.push(
"Either ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN is required when using direct Anthropic API.", "Either ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN is required when using direct Anthropic API.",
); );
} }
} else if (useBedrock) { } else if (useBedrock) {
const awsRegion = process.env.AWS_REGION; const requiredBedrockVars = {
const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID; AWS_REGION: process.env.AWS_REGION,
const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID,
const awsBearerToken = process.env.AWS_BEARER_TOKEN_BEDROCK; AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY,
};
// AWS_REGION is always required for Bedrock Object.entries(requiredBedrockVars).forEach(([key, value]) => {
if (!awsRegion) { if (!value) {
errors.push("AWS_REGION is required when using AWS Bedrock."); errors.push(`${key} is required when using AWS Bedrock.`);
} }
});
// Either bearer token OR access key credentials must be provided
const hasAccessKeyCredentials = awsAccessKeyId && awsSecretAccessKey;
const hasBearerToken = awsBearerToken;
if (!hasAccessKeyCredentials && !hasBearerToken) {
errors.push(
"Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock.",
);
}
} else if (useVertex) { } else if (useVertex) {
const requiredVertexVars = { const requiredVertexVars = {
ANTHROPIC_VERTEX_PROJECT_ID: process.env.ANTHROPIC_VERTEX_PROJECT_ID, ANTHROPIC_VERTEX_PROJECT_ID: process.env.ANTHROPIC_VERTEX_PROJECT_ID,
@@ -56,16 +45,6 @@ export function validateEnvironmentVariables() {
errors.push(`${key} is required when using Google Vertex AI.`); errors.push(`${key} is required when using Google Vertex AI.`);
} }
}); });
} else if (useFoundry) {
const foundryResource = process.env.ANTHROPIC_FOUNDRY_RESOURCE;
const foundryBaseUrl = process.env.ANTHROPIC_FOUNDRY_BASE_URL;
// Either resource name or base URL is required
if (!foundryResource && !foundryBaseUrl) {
errors.push(
"Either ANTHROPIC_FOUNDRY_RESOURCE or ANTHROPIC_FOUNDRY_BASE_URL is required when using Microsoft Foundry.",
);
}
} }
if (errors.length > 0) { if (errors.length > 0) {

View File

@@ -113,7 +113,7 @@ describe("parseAndSetStructuredOutputs", () => {
await expect( await expect(
parseAndSetStructuredOutputs(TEST_EXECUTION_FILE), parseAndSetStructuredOutputs(TEST_EXECUTION_FILE),
).rejects.toThrow( ).rejects.toThrow(
"--json-schema was provided but Claude did not return structured_output", "json_schema was provided but Claude did not return structured_output",
); );
}); });
@@ -127,7 +127,7 @@ describe("parseAndSetStructuredOutputs", () => {
await expect( await expect(
parseAndSetStructuredOutputs(TEST_EXECUTION_FILE), parseAndSetStructuredOutputs(TEST_EXECUTION_FILE),
).rejects.toThrow( ).rejects.toThrow(
"--json-schema was provided but Claude did not return structured_output", "json_schema was provided but Claude did not return structured_output",
); );
}); });

View File

@@ -13,19 +13,15 @@ describe("validateEnvironmentVariables", () => {
delete process.env.ANTHROPIC_API_KEY; delete process.env.ANTHROPIC_API_KEY;
delete process.env.CLAUDE_CODE_USE_BEDROCK; delete process.env.CLAUDE_CODE_USE_BEDROCK;
delete process.env.CLAUDE_CODE_USE_VERTEX; delete process.env.CLAUDE_CODE_USE_VERTEX;
delete process.env.CLAUDE_CODE_USE_FOUNDRY;
delete process.env.AWS_REGION; delete process.env.AWS_REGION;
delete process.env.AWS_ACCESS_KEY_ID; delete process.env.AWS_ACCESS_KEY_ID;
delete process.env.AWS_SECRET_ACCESS_KEY; delete process.env.AWS_SECRET_ACCESS_KEY;
delete process.env.AWS_SESSION_TOKEN; delete process.env.AWS_SESSION_TOKEN;
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
delete process.env.ANTHROPIC_BEDROCK_BASE_URL; delete process.env.ANTHROPIC_BEDROCK_BASE_URL;
delete process.env.ANTHROPIC_VERTEX_PROJECT_ID; delete process.env.ANTHROPIC_VERTEX_PROJECT_ID;
delete process.env.CLOUD_ML_REGION; delete process.env.CLOUD_ML_REGION;
delete process.env.GOOGLE_APPLICATION_CREDENTIALS; delete process.env.GOOGLE_APPLICATION_CREDENTIALS;
delete process.env.ANTHROPIC_VERTEX_BASE_URL; delete process.env.ANTHROPIC_VERTEX_BASE_URL;
delete process.env.ANTHROPIC_FOUNDRY_RESOURCE;
delete process.env.ANTHROPIC_FOUNDRY_BASE_URL;
}); });
afterEach(() => { afterEach(() => {
@@ -96,58 +92,31 @@ describe("validateEnvironmentVariables", () => {
); );
}); });
test("should fail when only AWS_SECRET_ACCESS_KEY is provided without bearer token", () => { test("should fail when AWS_ACCESS_KEY_ID is missing", () => {
process.env.CLAUDE_CODE_USE_BEDROCK = "1"; process.env.CLAUDE_CODE_USE_BEDROCK = "1";
process.env.AWS_REGION = "us-east-1"; process.env.AWS_REGION = "us-east-1";
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key"; process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
expect(() => validateEnvironmentVariables()).toThrow( expect(() => validateEnvironmentVariables()).toThrow(
"Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock.", "AWS_ACCESS_KEY_ID is required when using AWS Bedrock.",
); );
}); });
test("should fail when only AWS_ACCESS_KEY_ID is provided without bearer token", () => { test("should fail when AWS_SECRET_ACCESS_KEY is missing", () => {
process.env.CLAUDE_CODE_USE_BEDROCK = "1"; process.env.CLAUDE_CODE_USE_BEDROCK = "1";
process.env.AWS_REGION = "us-east-1"; process.env.AWS_REGION = "us-east-1";
process.env.AWS_ACCESS_KEY_ID = "test-access-key"; process.env.AWS_ACCESS_KEY_ID = "test-access-key";
expect(() => validateEnvironmentVariables()).toThrow( expect(() => validateEnvironmentVariables()).toThrow(
"Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock.", "AWS_SECRET_ACCESS_KEY is required when using AWS Bedrock.",
); );
}); });
test("should pass when AWS_BEARER_TOKEN_BEDROCK is provided instead of access keys", () => { test("should report all missing Bedrock variables", () => {
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
process.env.AWS_REGION = "us-east-1";
process.env.AWS_BEARER_TOKEN_BEDROCK = "test-bearer-token";
expect(() => validateEnvironmentVariables()).not.toThrow();
});
test("should pass when both bearer token and access keys are provided", () => {
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
process.env.AWS_REGION = "us-east-1";
process.env.AWS_BEARER_TOKEN_BEDROCK = "test-bearer-token";
process.env.AWS_ACCESS_KEY_ID = "test-access-key";
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
expect(() => validateEnvironmentVariables()).not.toThrow();
});
test("should fail when no authentication method is provided", () => {
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
process.env.AWS_REGION = "us-east-1";
expect(() => validateEnvironmentVariables()).toThrow(
"Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock.",
);
});
test("should report missing region and authentication", () => {
process.env.CLAUDE_CODE_USE_BEDROCK = "1"; process.env.CLAUDE_CODE_USE_BEDROCK = "1";
expect(() => validateEnvironmentVariables()).toThrow( expect(() => validateEnvironmentVariables()).toThrow(
/AWS_REGION is required when using AWS Bedrock.*Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock/s, /AWS_REGION is required when using AWS Bedrock.*AWS_ACCESS_KEY_ID is required when using AWS Bedrock.*AWS_SECRET_ACCESS_KEY is required when using AWS Bedrock/s,
); );
}); });
}); });
@@ -198,56 +167,6 @@ describe("validateEnvironmentVariables", () => {
}); });
}); });
describe("Microsoft Foundry", () => {
test("should pass when ANTHROPIC_FOUNDRY_RESOURCE is provided", () => {
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "test-resource";
expect(() => validateEnvironmentVariables()).not.toThrow();
});
test("should pass when ANTHROPIC_FOUNDRY_BASE_URL is provided", () => {
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
process.env.ANTHROPIC_FOUNDRY_BASE_URL =
"https://test-resource.services.ai.azure.com";
expect(() => validateEnvironmentVariables()).not.toThrow();
});
test("should pass when both resource and base URL are provided", () => {
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "test-resource";
process.env.ANTHROPIC_FOUNDRY_BASE_URL =
"https://custom.services.ai.azure.com";
expect(() => validateEnvironmentVariables()).not.toThrow();
});
test("should construct Foundry base URL from resource name when ANTHROPIC_FOUNDRY_BASE_URL is not provided", () => {
// This test verifies our action.yml change, which constructs:
// ANTHROPIC_FOUNDRY_BASE_URL: ${{ env.ANTHROPIC_FOUNDRY_BASE_URL || (env.ANTHROPIC_FOUNDRY_RESOURCE && format('https://{0}.services.ai.azure.com', env.ANTHROPIC_FOUNDRY_RESOURCE)) }}
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "my-foundry-resource";
// ANTHROPIC_FOUNDRY_BASE_URL is intentionally not set
// The actual URL construction happens in the composite action in action.yml
// This test is a placeholder to document the behavior
expect(() => validateEnvironmentVariables()).not.toThrow();
// In the actual action, ANTHROPIC_FOUNDRY_BASE_URL would be:
// https://my-foundry-resource.services.ai.azure.com
});
test("should fail when neither ANTHROPIC_FOUNDRY_RESOURCE nor ANTHROPIC_FOUNDRY_BASE_URL is provided", () => {
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
expect(() => validateEnvironmentVariables()).toThrow(
"Either ANTHROPIC_FOUNDRY_RESOURCE or ANTHROPIC_FOUNDRY_BASE_URL is required when using Microsoft Foundry.",
);
});
});
describe("Multiple providers", () => { describe("Multiple providers", () => {
test("should fail when both Bedrock and Vertex are enabled", () => { test("should fail when both Bedrock and Vertex are enabled", () => {
process.env.CLAUDE_CODE_USE_BEDROCK = "1"; process.env.CLAUDE_CODE_USE_BEDROCK = "1";
@@ -260,51 +179,7 @@ describe("validateEnvironmentVariables", () => {
process.env.CLOUD_ML_REGION = "us-central1"; process.env.CLOUD_ML_REGION = "us-central1";
expect(() => validateEnvironmentVariables()).toThrow( expect(() => validateEnvironmentVariables()).toThrow(
"Cannot use multiple providers simultaneously. Please set only one of: CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY.", "Cannot use both Bedrock and Vertex AI simultaneously. Please set only one provider.",
);
});
test("should fail when both Bedrock and Foundry are enabled", () => {
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
// Provide all required vars to isolate the mutual exclusion error
process.env.AWS_REGION = "us-east-1";
process.env.AWS_ACCESS_KEY_ID = "test-access-key";
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "test-resource";
expect(() => validateEnvironmentVariables()).toThrow(
"Cannot use multiple providers simultaneously. Please set only one of: CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY.",
);
});
test("should fail when both Vertex and Foundry are enabled", () => {
process.env.CLAUDE_CODE_USE_VERTEX = "1";
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
// Provide all required vars to isolate the mutual exclusion error
process.env.ANTHROPIC_VERTEX_PROJECT_ID = "test-project";
process.env.CLOUD_ML_REGION = "us-central1";
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "test-resource";
expect(() => validateEnvironmentVariables()).toThrow(
"Cannot use multiple providers simultaneously. Please set only one of: CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY.",
);
});
test("should fail when all three providers are enabled", () => {
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
process.env.CLAUDE_CODE_USE_VERTEX = "1";
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
// Provide all required vars to isolate the mutual exclusion error
process.env.AWS_REGION = "us-east-1";
process.env.AWS_ACCESS_KEY_ID = "test-access-key";
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
process.env.ANTHROPIC_VERTEX_PROJECT_ID = "test-project";
process.env.CLOUD_ML_REGION = "us-central1";
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "test-resource";
expect(() => validateEnvironmentVariables()).toThrow(
"Cannot use multiple providers simultaneously. Please set only one of: CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY.",
); );
}); });
}); });
@@ -329,7 +204,10 @@ describe("validateEnvironmentVariables", () => {
" - AWS_REGION is required when using AWS Bedrock.", " - AWS_REGION is required when using AWS Bedrock.",
); );
expect(error!.message).toContain( expect(error!.message).toContain(
" - Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock.", " - AWS_ACCESS_KEY_ID is required when using AWS Bedrock.",
);
expect(error!.message).toContain(
" - AWS_SECRET_ACCESS_KEY is required when using AWS Bedrock.",
); );
}); });
}); });

View File

@@ -1,17 +1,16 @@
# Cloud Providers # Cloud Providers
You can authenticate with Claude using any of these four methods: You can authenticate with Claude using any of these three methods:
1. Direct Anthropic API (default) 1. Direct Anthropic API (default)
2. Amazon Bedrock with OIDC authentication 2. Amazon Bedrock with OIDC authentication
3. Google Vertex AI with OIDC authentication 3. Google Vertex AI with OIDC authentication
4. Microsoft Foundry with OIDC authentication
For detailed setup instructions for AWS Bedrock and Google Vertex AI, see the [official documentation](https://docs.anthropic.com/en/docs/claude-code/github-actions#using-with-aws-bedrock-%26-google-vertex-ai). For detailed setup instructions for AWS Bedrock and Google Vertex AI, see the [official documentation](https://docs.anthropic.com/en/docs/claude-code/github-actions#using-with-aws-bedrock-%26-google-vertex-ai).
**Note**: **Note**:
- Bedrock, Vertex, and Microsoft Foundry use OIDC authentication exclusively - Bedrock and Vertex use OIDC authentication exclusively
- AWS Bedrock automatically uses cross-region inference profiles for certain models - AWS Bedrock automatically uses cross-region inference profiles for certain models
- For cross-region inference profile models, you need to request and be granted access to the Claude models in all regions that the inference profile uses - For cross-region inference profile models, you need to request and be granted access to the Claude models in all regions that the inference profile uses
@@ -41,19 +40,11 @@ Use provider-specific model names based on your chosen provider:
claude_args: | claude_args: |
--model claude-4-0-sonnet@20250805 --model claude-4-0-sonnet@20250805
# ... other inputs # ... other inputs
# For Microsoft Foundry with OIDC
- uses: anthropics/claude-code-action@v1
with:
use_foundry: "true"
claude_args: |
--model claude-sonnet-4-5
# ... other inputs
``` ```
## OIDC Authentication for Cloud Providers ## OIDC Authentication for Bedrock and Vertex
AWS Bedrock, GCP Vertex AI, and Microsoft Foundry all support OIDC authentication. Both AWS Bedrock and GCP Vertex AI require OIDC authentication.
```yaml ```yaml
# For AWS Bedrock with OIDC # For AWS Bedrock with OIDC
@@ -106,36 +97,3 @@ AWS Bedrock, GCP Vertex AI, and Microsoft Foundry all support OIDC authenticatio
permissions: permissions:
id-token: write # Required for OIDC id-token: write # Required for OIDC
``` ```
```yaml
# For Microsoft Foundry with OIDC
- name: Authenticate to Azure
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: anthropics/claude-code-action@v1
with:
use_foundry: "true"
claude_args: |
--model claude-sonnet-4-5
# ... other inputs
env:
ANTHROPIC_FOUNDRY_BASE_URL: https://my-resource.services.ai.azure.com
permissions:
id-token: write # Required for OIDC
```
## Microsoft Foundry Setup
For detailed setup instructions for Microsoft Foundry, see the [official documentation](https://docs.anthropic.com/en/docs/claude-code/microsoft-foundry).

View File

@@ -61,3 +61,68 @@ For specialized use cases, you can fine-tune behavior using `claude_args`:
--system-prompt "You are a code review specialist" --system-prompt "You are a code review specialist"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
``` ```
## Network Restrictions
For enhanced security, you can restrict Claude's network access to specific domains only. This feature is particularly useful for:
- Enterprise environments with strict security policies
- Preventing access to external services
- Limiting Claude to only your internal APIs and services
When `experimental_allowed_domains` is set, Claude can only access the domains you explicitly list. You'll need to include the appropriate provider domains based on your authentication method.
### Provider-Specific Examples
#### If using Anthropic API or subscription
```yaml
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Or: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
experimental_allowed_domains: |
.anthropic.com
```
#### If using AWS Bedrock
```yaml
- uses: anthropics/claude-code-action@v1
with:
use_bedrock: "true"
experimental_allowed_domains: |
bedrock.*.amazonaws.com
bedrock-runtime.*.amazonaws.com
```
#### If using Google Vertex AI
```yaml
- uses: anthropics/claude-code-action@v1
with:
use_vertex: "true"
experimental_allowed_domains: |
*.googleapis.com
vertexai.googleapis.com
```
### Common GitHub Domains
In addition to your provider domains, you may need to include GitHub-related domains. For GitHub.com users, common domains include:
```yaml
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
experimental_allowed_domains: |
.anthropic.com # For Anthropic API
.github.com
.githubusercontent.com
ghcr.io
.blob.core.windows.net
```
For GitHub Enterprise users, replace the GitHub.com domains above with your enterprise domains (e.g., `.github.company.com`, `packages.company.com`, etc.).
To determine which domains your workflow needs, you can temporarily run without restrictions and monitor the network requests, or check your GitHub Enterprise configuration for the specific services you use.

View File

@@ -38,7 +38,7 @@ The following permissions are requested but not yet actively used. These will en
## Commit Signing ## Commit Signing
Commits made by Claude through this action are no longer automatically signed with commit signatures. To enable commit signing set `use_commit_signing: True` in the workflow(s). This ensures the authenticity and integrity of commits, providing a verifiable trail of changes made by the action. All commits made by Claude through this action are automatically signed with commit signatures. This ensures the authenticity and integrity of commits, providing a verifiable trail of changes made by the action.
## ⚠️ Authentication Protection ## ⚠️ Authentication Protection

View File

@@ -70,6 +70,7 @@ jobs:
| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` | | `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` |
| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" | | `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" |
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" | | `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` | | `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID) | No | `41898282` | | `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID) | No | `41898282` |
| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name) | No | `claude[bot]` | | `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name) | No | `claude[bot]` |
@@ -79,6 +80,7 @@ jobs:
| `path_to_bun_executable` | Optional path to a custom Bun executable. Skips automatic Bun installation. Useful for Nix, custom containers, or specialized environments | No | "" | | `path_to_bun_executable` | Optional path to a custom Bun executable. Skips automatic Bun installation. Useful for Nix, custom containers, or specialized environments | No | "" |
| `plugin_marketplaces` | Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., see example in workflow above). Marketplaces are added before plugin installation | No | "" | | `plugin_marketplaces` | Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., see example in workflow above). Marketplaces are added before plugin installation | No | "" |
| `plugins` | Newline-separated list of Claude Code plugin names to install (e.g., see example in workflow above). Plugins are installed before Claude Code execution | No | "" | | `plugins` | Newline-separated list of Claude Code plugin names to install (e.g., see example in workflow above). Plugins are installed before Claude Code execution | No | "" |
| `json_schema` | JSON schema for structured output validation. See [Structured Outputs](#structured-outputs) section below | No | "" |
### Deprecated Inputs ### Deprecated Inputs
@@ -199,8 +201,16 @@ Get validated JSON results from Claude that automatically become GitHub Action o
prompt: | prompt: |
Check the CI logs and determine if this is a flaky test. Check the CI logs and determine if this is a flaky test.
Return: is_flaky (boolean), confidence (0-1), summary (string) Return: is_flaky (boolean), confidence (0-1), summary (string)
claude_args: | json_schema: |
--json-schema '{"type":"object","properties":{"is_flaky":{"type":"boolean"},"confidence":{"type":"number"},"summary":{"type":"string"}},"required":["is_flaky"]}' {
"type": "object",
"properties": {
"is_flaky": {"type": "boolean"},
"confidence": {"type": "number"},
"summary": {"type": "string"}
},
"required": ["is_flaky"]
}
- name: Retry if flaky - name: Retry if flaky
if: fromJSON(steps.analyze.outputs.structured_output).is_flaky == true if: fromJSON(steps.analyze.outputs.structured_output).is_flaky == true
@@ -209,7 +219,7 @@ Get validated JSON results from Claude that automatically become GitHub Action o
### How It Works ### How It Works
1. **Define Schema**: Provide a JSON schema via `--json-schema` flag in `claude_args` 1. **Define Schema**: Provide a JSON schema in the `json_schema` input
2. **Claude Executes**: Claude uses tools to complete your task 2. **Claude Executes**: Claude uses tools to complete your task
3. **Validated Output**: Result is validated against your schema 3. **Validated Output**: Result is validated against your schema
4. **JSON Output**: All fields are returned in a single `structured_output` JSON string 4. **JSON Output**: All fields are returned in a single `structured_output` JSON string

View File

@@ -43,8 +43,27 @@ jobs:
- is_flaky: true if likely flaky, false if real bug - is_flaky: true if likely flaky, false if real bug
- confidence: number 0-1 indicating confidence level - confidence: number 0-1 indicating confidence level
- summary: brief one-sentence explanation - summary: brief one-sentence explanation
claude_args: | json_schema: |
--json-schema '{"type":"object","properties":{"is_flaky":{"type":"boolean","description":"Whether this appears to be a flaky test failure"},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Confidence level in the determination"},"summary":{"type":"string","description":"One-sentence explanation of the failure"}},"required":["is_flaky","confidence","summary"]}' {
"type": "object",
"properties": {
"is_flaky": {
"type": "boolean",
"description": "Whether this appears to be a flaky test failure"
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Confidence level in the determination"
},
"summary": {
"type": "string",
"description": "One-sentence explanation of the failure"
}
},
"required": ["is_flaky", "confidence", "summary"]
}
# Auto-retry only if flaky AND high confidence (>= 0.7) # Auto-retry only if flaky AND high confidence (>= 0.7)
- name: Retry flaky tests - name: Retry flaky tests

View File

@@ -0,0 +1,123 @@
#!/bin/bash
# Setup Network Restrictions with Squid Proxy
# This script sets up a Squid proxy to restrict network access to whitelisted domains only.
set -e
# Check if experimental_allowed_domains is provided
if [ -z "$EXPERIMENTAL_ALLOWED_DOMAINS" ]; then
echo "ERROR: EXPERIMENTAL_ALLOWED_DOMAINS environment variable is required"
exit 1
fi
# Check required environment variables
if [ -z "$RUNNER_TEMP" ]; then
echo "ERROR: RUNNER_TEMP environment variable is required"
exit 1
fi
if [ -z "$GITHUB_ENV" ]; then
echo "ERROR: GITHUB_ENV environment variable is required"
exit 1
fi
echo "Setting up network restrictions with Squid proxy..."
SQUID_START_TIME=$(date +%s.%N)
# Create whitelist file
echo "$EXPERIMENTAL_ALLOWED_DOMAINS" > $RUNNER_TEMP/whitelist.txt
# Ensure each domain has proper format
# If domain doesn't start with a dot and isn't an IP, add the dot for subdomain matching
mv $RUNNER_TEMP/whitelist.txt $RUNNER_TEMP/whitelist.txt.orig
while IFS= read -r domain; do
if [ -n "$domain" ]; then
# Trim whitespace
domain=$(echo "$domain" | xargs)
# If it's not empty and doesn't start with a dot, add one
if [[ "$domain" != .* ]] && [[ ! "$domain" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo ".$domain" >> $RUNNER_TEMP/whitelist.txt
else
echo "$domain" >> $RUNNER_TEMP/whitelist.txt
fi
fi
done < $RUNNER_TEMP/whitelist.txt.orig
# Create Squid config with whitelist
echo "http_port 3128" > $RUNNER_TEMP/squid.conf
echo "" >> $RUNNER_TEMP/squid.conf
echo "# Define ACLs" >> $RUNNER_TEMP/squid.conf
echo "acl whitelist dstdomain \"/etc/squid/whitelist.txt\"" >> $RUNNER_TEMP/squid.conf
echo "acl localnet src 127.0.0.1/32" >> $RUNNER_TEMP/squid.conf
echo "acl localnet src 172.17.0.0/16" >> $RUNNER_TEMP/squid.conf
echo "acl SSL_ports port 443" >> $RUNNER_TEMP/squid.conf
echo "acl Safe_ports port 80" >> $RUNNER_TEMP/squid.conf
echo "acl Safe_ports port 443" >> $RUNNER_TEMP/squid.conf
echo "acl CONNECT method CONNECT" >> $RUNNER_TEMP/squid.conf
echo "" >> $RUNNER_TEMP/squid.conf
echo "# Deny requests to certain unsafe ports" >> $RUNNER_TEMP/squid.conf
echo "http_access deny !Safe_ports" >> $RUNNER_TEMP/squid.conf
echo "" >> $RUNNER_TEMP/squid.conf
echo "# Only allow CONNECT to SSL ports" >> $RUNNER_TEMP/squid.conf
echo "http_access deny CONNECT !SSL_ports" >> $RUNNER_TEMP/squid.conf
echo "" >> $RUNNER_TEMP/squid.conf
echo "# Allow localhost" >> $RUNNER_TEMP/squid.conf
echo "http_access allow localhost" >> $RUNNER_TEMP/squid.conf
echo "" >> $RUNNER_TEMP/squid.conf
echo "# Allow localnet access to whitelisted domains" >> $RUNNER_TEMP/squid.conf
echo "http_access allow localnet whitelist" >> $RUNNER_TEMP/squid.conf
echo "" >> $RUNNER_TEMP/squid.conf
echo "# Deny everything else" >> $RUNNER_TEMP/squid.conf
echo "http_access deny all" >> $RUNNER_TEMP/squid.conf
echo "Starting Squid proxy..."
# First, remove any existing container
sudo docker rm -f squid-proxy 2>/dev/null || true
# Ensure whitelist file is not empty (Squid fails with empty files)
if [ ! -s "$RUNNER_TEMP/whitelist.txt" ]; then
echo "WARNING: Whitelist file is empty, adding a dummy entry"
echo ".example.com" >> $RUNNER_TEMP/whitelist.txt
fi
# Use sudo to prevent Claude from stopping the container
CONTAINER_ID=$(sudo docker run -d \
--name squid-proxy \
-p 127.0.0.1:3128:3128 \
-v $RUNNER_TEMP/squid.conf:/etc/squid/squid.conf:ro \
-v $RUNNER_TEMP/whitelist.txt:/etc/squid/whitelist.txt:ro \
ubuntu/squid:latest 2>&1) || {
echo "ERROR: Failed to start Squid container"
exit 1
}
# Wait for proxy to be ready (usually < 1 second)
READY=false
for i in {1..30}; do
if nc -z 127.0.0.1 3128 2>/dev/null; then
TOTAL_TIME=$(echo "scale=3; $(date +%s.%N) - $SQUID_START_TIME" | bc)
echo "Squid proxy ready in ${TOTAL_TIME}s"
READY=true
break
fi
sleep 0.1
done
if [ "$READY" != "true" ]; then
echo "ERROR: Squid proxy failed to start within 3 seconds"
echo "Container logs:"
sudo docker logs squid-proxy 2>&1 || true
echo "Container status:"
sudo docker ps -a | grep squid-proxy || true
exit 1
fi
# Set proxy environment variables
echo "http_proxy=http://127.0.0.1:3128" >> $GITHUB_ENV
echo "https_proxy=http://127.0.0.1:3128" >> $GITHUB_ENV
echo "HTTP_PROXY=http://127.0.0.1:3128" >> $GITHUB_ENV
echo "HTTPS_PROXY=http://127.0.0.1:3128" >> $GITHUB_ENV
echo "Network restrictions setup completed successfully"

View File

@@ -26,6 +26,7 @@ export function collectActionInputsPresence(): void {
max_turns: "", max_turns: "",
use_sticky_comment: "false", use_sticky_comment: "false",
use_commit_signing: "false", use_commit_signing: "false",
experimental_allowed_domains: "",
}; };
const allInputsJson = process.env.ALL_INPUTS; const allInputsJson = process.env.ALL_INPUTS;

View File

@@ -7,6 +7,7 @@ import { parseAllowedTools } from "./parse-tools";
import { configureGitAuth } from "../../github/operations/git-config"; import { configureGitAuth } from "../../github/operations/git-config";
import type { GitHubContext } from "../../github/context"; import type { GitHubContext } from "../../github/context";
import { isEntityContext } from "../../github/context"; import { isEntityContext } from "../../github/context";
import { appendJsonSchemaArg } from "../../utils/json-schema";
/** /**
* Extract GitHub context as environment variables for agent mode * Extract GitHub context as environment variables for agent mode
@@ -149,6 +150,9 @@ export const agentMode: Mode = {
claudeArgs = `--mcp-config '${escapedOurConfig}'`; claudeArgs = `--mcp-config '${escapedOurConfig}'`;
} }
// Add JSON schema if provided
claudeArgs = appendJsonSchemaArg(claudeArgs);
// Append user's claude_args (which may have more --mcp-config flags) // Append user's claude_args (which may have more --mcp-config flags)
claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim(); claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim();

View File

@@ -15,6 +15,7 @@ import { isEntityContext } from "../../github/context";
import type { PreparedContext } from "../../create-prompt/types"; import type { PreparedContext } from "../../create-prompt/types";
import type { FetchDataResult } from "../../github/data/fetcher"; import type { FetchDataResult } from "../../github/data/fetcher";
import { parseAllowedTools } from "../agent/parse-tools"; import { parseAllowedTools } from "../agent/parse-tools";
import { appendJsonSchemaArg } from "../../utils/json-schema";
/** /**
* Tag mode implementation. * Tag mode implementation.
@@ -177,6 +178,9 @@ export const tagMode: Mode = {
// Add required tools for tag mode // Add required tools for tag mode
claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`; claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`;
// Add JSON schema if provided
claudeArgs = appendJsonSchemaArg(claudeArgs);
// Append user's claude_args (which may have more --mcp-config flags) // Append user's claude_args (which may have more --mcp-config flags)
if (userClaudeArgs) { if (userClaudeArgs) {
claudeArgs += ` ${userClaudeArgs}`; claudeArgs += ` ${userClaudeArgs}`;

17
src/utils/json-schema.ts Normal file
View File

@@ -0,0 +1,17 @@
/**
* Appends JSON schema CLI argument if json_schema is provided
* Escapes schema for safe shell passing
*/
export function appendJsonSchemaArg(
claudeArgs: string,
jsonSchemaStr?: string,
): string {
const schema = jsonSchemaStr || process.env.JSON_SCHEMA || "";
if (!schema) {
return claudeArgs;
}
// CLI validates schema - just escape for safe shell passing
const escapedSchema = schema.replace(/'/g, "'\\''");
return `${claudeArgs} --json-schema '${escapedSchema}'`;
}