Compare commits

..

21 Commits

Author SHA1 Message Date
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
GitHub Actions
08f88abe2b chore: bump Claude Code version to 2.0.42 2025-11-15 00:17:35 +00:00
GitHub Actions
14ab4250bb chore: bump Claude Code version to 2.0.37 2025-11-11 00:21:46 +00:00
GitHub Actions
c7fdd19642 chore: bump Claude Code version to 2.0.36 2025-11-07 22:08:15 +00:00
GitHub Actions
92d173475f chore: bump Claude Code version to 2.0.35 2025-11-06 21:07:07 +00:00
GitHub Actions
108e982900 chore: bump Claude Code version to 2.0.34 2025-11-05 21:11:28 +00:00
GitHub Actions
7bb53ae6ee chore: bump Claude Code version to 2.0.33 2025-11-04 23:40:50 +00:00
GitHub Actions
804b418b93 chore: bump Claude Code version to 2.0.32 2025-11-03 23:22:17 +00:00
GitHub Actions
500439cb9b chore: bump Claude Code version to 2.0.31 2025-10-31 22:00:23 +00:00
GitHub Actions
4cda0ef6d1 chore: bump Claude Code version to 2.0.30 2025-10-30 23:35:37 +00:00
Ashwin Bhat
037b85d0d2 docs: update action version from @beta to @v1 in docs (#650)
Updates documentation examples to use @v1 instead of @beta in:
- docs/setup.md: custom GitHub app example
- docs/configuration.md: additional permissions examples

Migration guide and usage comparison examples intentionally kept with @beta to show old syntax.

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-29 21:45:52 -07:00
GitHub Actions
8a1c437175 chore: bump Claude Code version to 2.0.29 2025-10-29 23:25:55 +00:00
David Dworken
56c8ae7d88 Add show_full_output option to control output verbosity (#580)
* Add show_full_output option to control output verbosity

* Update base-action/src/run-claude.ts

Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>

* Wire show_full_output through to base-action

* Document show_full_output security warnings in docs/security.md

---------

Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
2025-10-28 11:52:18 -07:00
17 changed files with 1006 additions and 37 deletions

View File

@@ -0,0 +1,349 @@
name: Test Structured Outputs
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
test-basic-types:
name: Test Basic Type Conversions
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Test with explicit values
id: test
uses: ./base-action
with:
prompt: |
Run this command: echo "test"
Then return EXACTLY these values:
- text_field: "hello"
- number_field: 42
- boolean_true: true
- 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 }}
allowed_tools: "Bash"
- name: Verify outputs
run: |
# Parse the structured_output JSON
OUTPUT='${{ steps.test.outputs.structured_output }}'
# Test string pass-through
TEXT_FIELD=$(echo "$OUTPUT" | jq -r '.text_field')
if [ "$TEXT_FIELD" != "hello" ]; then
echo "❌ String: expected 'hello', got '$TEXT_FIELD'"
exit 1
fi
# Test number → string conversion
NUMBER_FIELD=$(echo "$OUTPUT" | jq -r '.number_field')
if [ "$NUMBER_FIELD" != "42" ]; then
echo "❌ Number: expected '42', got '$NUMBER_FIELD'"
exit 1
fi
# Test boolean → "true" conversion
BOOLEAN_TRUE=$(echo "$OUTPUT" | jq -r '.boolean_true')
if [ "$BOOLEAN_TRUE" != "true" ]; then
echo "❌ Boolean true: expected 'true', got '$BOOLEAN_TRUE'"
exit 1
fi
# Test boolean → "false" conversion
BOOLEAN_FALSE=$(echo "$OUTPUT" | jq -r '.boolean_false')
if [ "$BOOLEAN_FALSE" != "false" ]; then
echo "❌ Boolean false: expected 'false', got '$BOOLEAN_FALSE'"
exit 1
fi
echo "✅ All basic type conversions correct"
test-complex-types:
name: Test Arrays and Objects
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Test complex types
id: test
uses: ./base-action
with:
prompt: |
Run: echo "ready"
Return EXACTLY:
- items: ["apple", "banana", "cherry"]
- config: {"key": "value", "count": 3}
- 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 }}
allowed_tools: "Bash"
- name: Verify JSON stringification
run: |
# Parse the structured_output JSON
OUTPUT='${{ steps.test.outputs.structured_output }}'
# Arrays should be JSON stringified
if ! echo "$OUTPUT" | jq -e '.items | length == 3' > /dev/null; then
echo "❌ Array not properly formatted"
echo "$OUTPUT" | jq '.items'
exit 1
fi
# Objects should be JSON stringified
if ! echo "$OUTPUT" | jq -e '.config.key == "value"' > /dev/null; then
echo "❌ Object not properly formatted"
echo "$OUTPUT" | jq '.config'
exit 1
fi
# Empty arrays should work
if ! echo "$OUTPUT" | jq -e '.empty_array | length == 0' > /dev/null; then
echo "❌ Empty array not properly formatted"
echo "$OUTPUT" | jq '.empty_array'
exit 1
fi
echo "✅ All complex types handled correctly"
test-edge-cases:
name: Test Edge Cases
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Test edge cases
id: test
uses: ./base-action
with:
prompt: |
Run: echo "test"
Return EXACTLY:
- zero: 0
- empty_string: ""
- negative: -5
- 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 }}
allowed_tools: "Bash"
- name: Verify edge cases
run: |
# Parse the structured_output JSON
OUTPUT='${{ steps.test.outputs.structured_output }}'
# Zero should be "0", not empty or falsy
ZERO=$(echo "$OUTPUT" | jq -r '.zero')
if [ "$ZERO" != "0" ]; then
echo "❌ Zero: expected '0', got '$ZERO'"
exit 1
fi
# Empty string should be empty (not "null" or missing)
EMPTY_STRING=$(echo "$OUTPUT" | jq -r '.empty_string')
if [ "$EMPTY_STRING" != "" ]; then
echo "❌ Empty string: expected '', got '$EMPTY_STRING'"
exit 1
fi
# Negative numbers should work
NEGATIVE=$(echo "$OUTPUT" | jq -r '.negative')
if [ "$NEGATIVE" != "-5" ]; then
echo "❌ Negative: expected '-5', got '$NEGATIVE'"
exit 1
fi
# Decimals should preserve precision
DECIMAL=$(echo "$OUTPUT" | jq -r '.decimal')
if [ "$DECIMAL" != "3.14" ]; then
echo "❌ Decimal: expected '3.14', got '$DECIMAL'"
exit 1
fi
echo "✅ All edge cases handled correctly"
test-name-sanitization:
name: Test Output Name Sanitization
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Test special characters in field names
id: test
uses: ./base-action
with:
prompt: |
Run: echo "test"
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 }}
allowed_tools: "Bash"
- name: Verify sanitized names work
run: |
# Parse the structured_output JSON
OUTPUT='${{ steps.test.outputs.structured_output }}'
# Hyphens should be preserved in the JSON
TEST_RESULT=$(echo "$OUTPUT" | jq -r '.["test-result"]')
if [ "$TEST_RESULT" != "passed" ]; then
echo "❌ Hyphenated name failed: expected 'passed', got '$TEST_RESULT'"
exit 1
fi
# Underscores should work
ITEM_COUNT=$(echo "$OUTPUT" | jq -r '.item_count')
if [ "$ITEM_COUNT" != "10" ]; then
echo "❌ Underscore name failed: expected '10', got '$ITEM_COUNT'"
exit 1
fi
echo "✅ Name sanitization works"
test-execution-file-structure:
name: Test Execution File Format
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Run with structured output
id: test
uses: ./base-action
with:
prompt: "Run: echo 'complete'. Return: {done: true}"
json_schema: |
{
"type": "object",
"properties": {
"done": {"type": "boolean"}
},
"required": ["done"]
}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: "Bash"
- name: Verify execution file contains structured_output
run: |
FILE="${{ steps.test.outputs.execution_file }}"
# Check file exists
if [ ! -f "$FILE" ]; then
echo "❌ Execution file missing"
exit 1
fi
# Check for structured_output field
if ! jq -e '.[] | select(.type == "result") | .structured_output' "$FILE" > /dev/null; then
echo "❌ No structured_output in execution file"
cat "$FILE"
exit 1
fi
# Verify the actual value
DONE=$(jq -r '.[] | select(.type == "result") | .structured_output.done' "$FILE")
if [ "$DONE" != "true" ]; then
echo "❌ Wrong value in execution file"
exit 1
fi
echo "✅ Execution file format correct"
test-summary:
name: Summary
runs-on: ubuntu-latest
needs:
- test-basic-types
- test-complex-types
- test-edge-cases
- test-name-sanitization
- test-execution-file-structure
if: always()
steps:
- name: Generate Summary
run: |
echo "# Structured Output Tests (Optimized)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Fast, deterministic tests using explicit prompts" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Test | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Basic Types | ${{ needs.test-basic-types.result == 'success' && '✅ PASS' || '❌ FAIL' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Complex Types | ${{ needs.test-complex-types.result == 'success' && '✅ PASS' || '❌ FAIL' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Edge Cases | ${{ needs.test-edge-cases.result == 'success' && '✅ PASS' || '❌ FAIL' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Name Sanitization | ${{ needs.test-name-sanitization.result == 'success' && '✅ PASS' || '❌ FAIL' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Execution File | ${{ needs.test-execution-file-structure.result == 'success' && '✅ PASS' || '❌ FAIL' }} |" >> $GITHUB_STEP_SUMMARY
# Check if all passed
ALL_PASSED=${{
needs.test-basic-types.result == 'success' &&
needs.test-complex-types.result == 'success' &&
needs.test-edge-cases.result == 'success' &&
needs.test-name-sanitization.result == 'success' &&
needs.test-execution-file-structure.result == 'success'
}}
if [ "$ALL_PASSED" = "true" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "## ✅ All Tests Passed" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## ❌ Some Tests Failed" >> $GITHUB_STEP_SUMMARY
exit 1
fi

View File

@@ -13,6 +13,7 @@ A general-purpose [Claude Code](https://claude.ai/code) action for GitHub PRs an
- 💬 **PR/Issue Integration**: Works seamlessly with GitHub comments and PR reviews
- 🛠️ **Flexible Tool Access**: Access to GitHub APIs and file operations (additional tools can be enabled via configuration)
- 📋 **Progress Tracking**: Visual progress indicators with checkboxes that dynamically update as Claude completes tasks
- 📊 **Structured Outputs**: Get validated JSON results that automatically become GitHub Action outputs for complex automations
- 🏃 **Runs on Your Infrastructure**: The action executes entirely on your own GitHub runner (Anthropic API calls go to your chosen provider)
- ⚙️ **Simplified Configuration**: Unified `prompt` and `claude_args` inputs provide clean, powerful configuration aligned with Claude Code SDK

View File

@@ -101,6 +101,10 @@ inputs:
description: "Optional path to a custom Bun executable. If provided, skips automatic Bun installation and uses this executable instead. WARNING: Using an incompatible version may cause problems if the action requires specific Bun features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment."
required: false
default: ""
show_full_output:
description: "Show full JSON output from Claude Code. WARNING: This outputs ALL Claude messages including tool execution results which may contain secrets, API keys, or other sensitive information. These logs are publicly visible in GitHub Actions. Only enable for debugging in non-sensitive environments."
required: false
default: "false"
plugins:
description: "Newline-separated list of Claude Code plugin names to install (e.g., 'code-review@claude-code-plugins\nfeature-dev@claude-code-plugins')"
required: false
@@ -109,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')"
required: false
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:
execution_file:
@@ -170,6 +178,7 @@ runs:
TRACK_PROGRESS: ${{ inputs.track_progress }}
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
CLAUDE_ARGS: ${{ inputs.claude_args }}
JSON_SCHEMA: ${{ inputs.json_schema }}
ALL_INPUTS: ${{ toJson(inputs) }}
- name: Install Base Action Dependencies
@@ -185,7 +194,7 @@ runs:
# Install Claude Code if no custom executable is provided
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
echo "Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash -s 2.0.28
curl -fsSL https://claude.ai/install.sh | bash -s 2.0.42
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
else
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
@@ -221,8 +230,10 @@ runs:
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }}
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }}
INPUT_PLUGINS: ${{ inputs.plugins }}
INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }}
JSON_SCHEMA: ${{ inputs.json_schema }}
# Model configuration
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}

View File

@@ -85,29 +85,32 @@ Add the following to your workflow file:
## Inputs
| Input | Description | Required | Default |
| ------------------------- | ------------------------------------------------------------------------------------------------- | -------- | ---------------------------- |
| `prompt` | The prompt to send to Claude Code | No\* | '' |
| `prompt_file` | Path to a file containing the prompt to send to Claude Code | No\* | '' |
| `allowed_tools` | Comma-separated list of allowed tools for Claude Code to use | No | '' |
| `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 | '' |
| `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 | '' |
| `system_prompt` | Override 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 | '' |
| `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' |
| `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' |
| `anthropic_api_key` | Anthropic API key (required for direct Anthropic API) | No | '' |
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No | '' |
| `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' |
| Input | Description | Required | Default |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------- |
| `prompt` | The prompt to send to Claude Code | No\* | '' |
| `prompt_file` | Path to a file containing the prompt to send to Claude Code | No\* | '' |
| `allowed_tools` | Comma-separated list of allowed tools for Claude Code to use | No | '' |
| `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 | '' |
| `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 | '' |
| `system_prompt` | Override 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 | '' |
| `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' |
| `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' |
| `anthropic_api_key` | Anthropic API key (required for direct Anthropic API) | No | '' |
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No | '' |
| `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.
\*\*`show_full_output` is automatically enabled when GitHub Actions debug mode is active. See [security documentation](../docs/security.md#-full-output-security-warning) for important security considerations.
## Outputs
| Output | Description |

View File

@@ -24,6 +24,10 @@ inputs:
description: "Additional arguments to pass directly to Claude CLI (e.g., '--max-turns 3 --mcp-config /path/to/config.json')"
required: false
default: ""
allowed_tools:
description: "Comma-separated list of allowed tools (e.g., 'Read,Write,Bash'). Passed as --allowedTools to Claude CLI"
required: false
default: ""
# Authentication settings
anthropic_api_key:
@@ -55,6 +59,10 @@ inputs:
description: "Optional path to a custom Bun executable. If provided, skips automatic Bun installation and uses this executable instead. WARNING: Using an incompatible version may cause problems if the action requires specific Bun features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment."
required: false
default: ""
show_full_output:
description: "Show full JSON output from Claude Code. WARNING: This outputs ALL Claude messages including tool execution results which may contain secrets, API keys, or other sensitive information. These logs are publicly visible in GitHub Actions. Only enable for debugging in non-sensitive environments."
required: false
default: "false"
plugins:
description: "Newline-separated list of Claude Code plugin names to install (e.g., 'code-review@claude-code-plugins\nfeature-dev@claude-code-plugins')"
required: false
@@ -63,6 +71,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')"
required: false
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:
conclusion:
@@ -71,6 +87,9 @@ outputs:
execution_file:
description: "Path to the JSON file containing Claude Code execution log"
value: ${{ steps.run_claude.outputs.execution_file }}
structured_output:
description: "JSON string containing all structured output fields (use fromJSON() or jq to parse)"
value: ${{ steps.run_claude.outputs.structured_output }}
runs:
using: "composite"
@@ -107,7 +126,7 @@ runs:
run: |
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
echo "Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash -s 2.0.28
curl -fsSL https://claude.ai/install.sh | bash -s 2.0.45
else
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
# Add the directory containing the custom executable to PATH
@@ -134,8 +153,11 @@ runs:
INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }}
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }}
INPUT_PLUGINS: ${{ inputs.plugins }}
INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }}
INPUT_ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
JSON_SCHEMA: ${{ inputs.json_schema }}
# Provider configuration
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}

View File

@@ -28,8 +28,22 @@ async function run() {
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, {
claudeArgs: process.env.INPUT_CLAUDE_ARGS,
claudeArgs: claudeArgs.trim(),
allowedTools: process.env.INPUT_ALLOWED_TOOLS,
disallowedTools: process.env.INPUT_DISALLOWED_TOOLS,
maxTurns: process.env.INPUT_MAX_TURNS,
@@ -41,6 +55,7 @@ async function run() {
model: process.env.ANTHROPIC_MODEL,
pathToClaudeCodeExecutable:
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT,
});
} catch (error) {
core.setFailed(`Action failed with error: ${error}`);

View File

@@ -1,7 +1,7 @@
import * as core from "@actions/core";
import { exec } from "child_process";
import { promisify } from "util";
import { unlink, writeFile, stat } from "fs/promises";
import { unlink, writeFile, stat, readFile } from "fs/promises";
import { createWriteStream } from "fs";
import { spawn } from "child_process";
import { parse as parseShellArgs } from "shell-quote";
@@ -12,6 +12,59 @@ const PIPE_PATH = `${process.env.RUNNER_TEMP}/claude_prompt_pipe`;
const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`;
const BASE_ARGS = ["--verbose", "--output-format", "stream-json"];
/**
* Sanitizes JSON output to remove sensitive information when full output is disabled
* Returns a safe summary message or null if the message should be completely suppressed
*/
function sanitizeJsonOutput(
jsonObj: any,
showFullOutput: boolean,
): string | null {
if (showFullOutput) {
// In full output mode, return the full JSON
return JSON.stringify(jsonObj, null, 2);
}
// In non-full-output mode, provide minimal safe output
const type = jsonObj.type;
const subtype = jsonObj.subtype;
// System initialization - safe to show
if (type === "system" && subtype === "init") {
return JSON.stringify(
{
type: "system",
subtype: "init",
message: "Claude Code initialized",
model: jsonObj.model || "unknown",
},
null,
2,
);
}
// Result messages - Always show the final result
if (type === "result") {
// These messages contain the final result and should always be visible
return JSON.stringify(
{
type: "result",
subtype: jsonObj.subtype,
is_error: jsonObj.is_error,
duration_ms: jsonObj.duration_ms,
num_turns: jsonObj.num_turns,
total_cost_usd: jsonObj.total_cost_usd,
permission_denials: jsonObj.permission_denials,
},
null,
2,
);
}
// For any other message types, suppress completely in non-full-output mode
return null;
}
export type ClaudeOptions = {
claudeArgs?: string;
model?: string;
@@ -24,6 +77,7 @@ export type ClaudeOptions = {
appendSystemPrompt?: string;
claudeEnv?: string;
fallbackModel?: string;
showFullOutput?: string;
};
type PreparedConfig = {
@@ -68,6 +122,48 @@ export function prepareRunConfig(
};
}
/**
* Parses structured_output from execution file and sets GitHub Action outputs
* Only runs if json_schema was explicitly provided by the user
* Exported for testing
*/
export async function parseAndSetStructuredOutputs(
executionFile: string,
): Promise<void> {
try {
const content = await readFile(executionFile, "utf-8");
const messages = JSON.parse(content) as {
type: string;
structured_output?: Record<string, unknown>;
}[];
// Search backwards - result is typically last or second-to-last message
const result = messages.findLast(
(m) => m.type === "result" && m.structured_output,
);
if (!result?.structured_output) {
throw new Error(
`json_schema was provided but Claude did not return structured_output.\n` +
`Found ${messages.length} messages. Result exists: ${!!result}\n`,
);
}
// Set the complete structured output as a single JSON string
// This works around GitHub Actions limitation that composite actions can't have dynamic outputs
const structuredOutputJson = JSON.stringify(result.structured_output);
core.setOutput("structured_output", structuredOutputJson);
core.info(
`Set structured_output with ${Object.keys(result.structured_output).length} field(s)`,
);
} catch (error) {
if (error instanceof Error) {
throw error; // Preserve original error and stack trace
}
throw new Error(`Failed to parse structured outputs: ${error}`);
}
}
export async function runClaude(promptPath: string, options: ClaudeOptions) {
const config = prepareRunConfig(promptPath, options);
@@ -138,12 +234,27 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
pipeStream.destroy();
});
// Determine if full output should be shown
// Show full output if explicitly set to "true" OR if GitHub Actions debug mode is enabled
const isDebugMode = process.env.ACTIONS_STEP_DEBUG === "true";
let showFullOutput = options.showFullOutput === "true" || isDebugMode;
if (isDebugMode && options.showFullOutput !== "false") {
console.log("Debug mode detected - showing full output");
showFullOutput = true;
} else if (!showFullOutput) {
console.log("Running Claude Code (full output hidden for security)...");
console.log(
"Rerun in debug mode or enable `show_full_output: true` in your workflow file for full output.",
);
}
// Capture output for parsing execution metrics
let output = "";
claudeProcess.stdout.on("data", (data) => {
const text = data.toString();
// Try to parse as JSON and pretty print if it's on a single line
// Try to parse as JSON and handle based on verbose setting
const lines = text.split("\n");
lines.forEach((line: string, index: number) => {
if (line.trim() === "") return;
@@ -151,17 +262,24 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
try {
// Check if this line is a JSON object
const parsed = JSON.parse(line);
const prettyJson = JSON.stringify(parsed, null, 2);
process.stdout.write(prettyJson);
if (index < lines.length - 1 || text.endsWith("\n")) {
process.stdout.write("\n");
const sanitizedOutput = sanitizeJsonOutput(parsed, showFullOutput);
if (sanitizedOutput) {
process.stdout.write(sanitizedOutput);
if (index < lines.length - 1 || text.endsWith("\n")) {
process.stdout.write("\n");
}
}
} catch (e) {
// Not a JSON object, print as is
process.stdout.write(line);
if (index < lines.length - 1 || text.endsWith("\n")) {
process.stdout.write("\n");
// Not a JSON object
if (showFullOutput) {
// In full output mode, print as is
process.stdout.write(line);
if (index < lines.length - 1 || text.endsWith("\n")) {
process.stdout.write("\n");
}
}
// In non-full-output mode, suppress non-JSON output
}
});
@@ -232,8 +350,23 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
core.warning(`Failed to process output for execution metrics: ${e}`);
}
core.setOutput("conclusion", "success");
core.setOutput("execution_file", EXECUTION_FILE);
// Parse and set structured outputs only if user provided json_schema
if (process.env.JSON_SCHEMA) {
try {
await parseAndSetStructuredOutputs(EXECUTION_FILE);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
core.setFailed(errorMessage);
core.setOutput("conclusion", "failure");
process.exit(1);
}
}
// Set conclusion to success if we reached here
core.setOutput("conclusion", "success");
} else {
core.setOutput("conclusion", "failure");

View File

@@ -78,5 +78,19 @@ describe("prepareRunConfig", () => {
"stream-json",
]);
});
test("should include json-schema flag when provided", () => {
const options: ClaudeOptions = {
claudeArgs:
'--json-schema \'{"type":"object","properties":{"result":{"type":"boolean"}}}\'',
};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.claudeArgs).toContain("--json-schema");
expect(prepared.claudeArgs).toContain(
'{"type":"object","properties":{"result":{"type":"boolean"}}}',
);
});
});
});

View File

@@ -0,0 +1,158 @@
#!/usr/bin/env bun
import { describe, test, expect, afterEach, beforeEach, spyOn } from "bun:test";
import { writeFile, unlink } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
import { parseAndSetStructuredOutputs } from "../src/run-claude";
import * as core from "@actions/core";
// Mock execution file path
const TEST_EXECUTION_FILE = join(tmpdir(), "test-execution-output.json");
// Helper to create mock execution file with structured output
async function createMockExecutionFile(
structuredOutput?: Record<string, unknown>,
includeResult: boolean = true,
): Promise<void> {
const messages: any[] = [
{ type: "system", subtype: "init" },
{ type: "turn", content: "test" },
];
if (includeResult) {
messages.push({
type: "result",
cost_usd: 0.01,
duration_ms: 1000,
structured_output: structuredOutput,
});
}
await writeFile(TEST_EXECUTION_FILE, JSON.stringify(messages));
}
// Spy on core functions
let setOutputSpy: any;
let infoSpy: any;
beforeEach(() => {
setOutputSpy = spyOn(core, "setOutput").mockImplementation(() => {});
infoSpy = spyOn(core, "info").mockImplementation(() => {});
});
describe("parseAndSetStructuredOutputs", () => {
afterEach(async () => {
setOutputSpy?.mockRestore();
infoSpy?.mockRestore();
try {
await unlink(TEST_EXECUTION_FILE);
} catch {
// Ignore if file doesn't exist
}
});
test("should set structured_output with valid data", async () => {
await createMockExecutionFile({
is_flaky: true,
confidence: 0.85,
summary: "Test looks flaky",
});
await parseAndSetStructuredOutputs(TEST_EXECUTION_FILE);
expect(setOutputSpy).toHaveBeenCalledWith(
"structured_output",
'{"is_flaky":true,"confidence":0.85,"summary":"Test looks flaky"}',
);
expect(infoSpy).toHaveBeenCalledWith(
"Set structured_output with 3 field(s)",
);
});
test("should handle arrays and nested objects", async () => {
await createMockExecutionFile({
items: ["a", "b", "c"],
config: { key: "value", nested: { deep: true } },
});
await parseAndSetStructuredOutputs(TEST_EXECUTION_FILE);
const callArgs = setOutputSpy.mock.calls[0];
expect(callArgs[0]).toBe("structured_output");
const parsed = JSON.parse(callArgs[1]);
expect(parsed).toEqual({
items: ["a", "b", "c"],
config: { key: "value", nested: { deep: true } },
});
});
test("should handle special characters in field names", async () => {
await createMockExecutionFile({
"test-result": "passed",
"item.count": 10,
"user@email": "test",
});
await parseAndSetStructuredOutputs(TEST_EXECUTION_FILE);
const callArgs = setOutputSpy.mock.calls[0];
const parsed = JSON.parse(callArgs[1]);
expect(parsed["test-result"]).toBe("passed");
expect(parsed["item.count"]).toBe(10);
expect(parsed["user@email"]).toBe("test");
});
test("should throw error when result exists but structured_output is undefined", async () => {
const messages = [
{ type: "system", subtype: "init" },
{ type: "result", cost_usd: 0.01, duration_ms: 1000 },
];
await writeFile(TEST_EXECUTION_FILE, JSON.stringify(messages));
await expect(
parseAndSetStructuredOutputs(TEST_EXECUTION_FILE),
).rejects.toThrow(
"json_schema was provided but Claude did not return structured_output",
);
});
test("should throw error when no result message exists", async () => {
const messages = [
{ type: "system", subtype: "init" },
{ type: "turn", content: "test" },
];
await writeFile(TEST_EXECUTION_FILE, JSON.stringify(messages));
await expect(
parseAndSetStructuredOutputs(TEST_EXECUTION_FILE),
).rejects.toThrow(
"json_schema was provided but Claude did not return structured_output",
);
});
test("should throw error with malformed JSON", async () => {
await writeFile(TEST_EXECUTION_FILE, "{ invalid json");
await expect(
parseAndSetStructuredOutputs(TEST_EXECUTION_FILE),
).rejects.toThrow();
});
test("should throw error when file does not exist", async () => {
await expect(
parseAndSetStructuredOutputs("/nonexistent/file.json"),
).rejects.toThrow();
});
test("should handle empty structured_output object", async () => {
await createMockExecutionFile({});
await parseAndSetStructuredOutputs(TEST_EXECUTION_FILE);
expect(setOutputSpy).toHaveBeenCalledWith("structured_output", "{}");
expect(infoSpy).toHaveBeenCalledWith(
"Set structured_output with 0 field(s)",
);
});
});

View File

@@ -130,7 +130,7 @@ To allow Claude to view workflow run results, job logs, and CI status:
2. **Configure the action with additional permissions**:
```yaml
- uses: anthropics/claude-code-action@beta
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
additional_permissions: |
@@ -162,7 +162,7 @@ jobs:
claude-ci-helper:
runs-on: ubuntu-latest
steps:
- uses: anthropics/claude-code-action@beta
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
additional_permissions: |

View File

@@ -56,3 +56,31 @@ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
anthropic_api_key: "sk-ant-api03-..." # Exposed and vulnerable!
claude_code_oauth_token: "oauth_token_..." # Exposed and vulnerable!
```
## ⚠️ Full Output Security Warning
The `show_full_output` option is **disabled by default** for security reasons. When enabled, it outputs ALL Claude Code messages including:
- Full outputs from tool executions (e.g., `ps`, `env`, file reads)
- API responses that may contain tokens or credentials
- File contents that may include secrets
- Command outputs that may expose sensitive system information
**These logs are publicly visible in GitHub Actions for public repositories!**
### Automatic Enabling in Debug Mode
Full output is **automatically enabled** when GitHub Actions debug mode is active (when `ACTIONS_STEP_DEBUG` secret is set to `true`). This helps with debugging but carries the same security risks.
### When to Enable Full Output
Only enable `show_full_output: true` or GitHub Actions debug mode when:
- Working in a private repository with controlled access
- Debugging issues in a non-production environment
- You have verified no secrets will be exposed in the output
- You understand the security implications
### Recommended Practice
For debugging, prefer using `show_full_output: false` (the default) and rely on Claude Code's sanitized output, which shows only essential information like errors and completion status without exposing sensitive data.

View File

@@ -117,7 +117,7 @@ If you prefer to configure the app manually or need custom permissions:
private-key: ${{ secrets.APP_PRIVATE_KEY }}
# Use Claude with your custom app's token
- uses: anthropics/claude-code-action@beta
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ steps.app-token.outputs.token }}

View File

@@ -80,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 | "" |
| `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 | "" |
| `json_schema` | JSON schema for structured output validation. Automatically sets GitHub Action outputs for each field. See [Structured Outputs](#structured-outputs) section below | No | "" |
### Deprecated Inputs
@@ -185,6 +186,82 @@ For a comprehensive guide on migrating from v0.x to v1.0, including step-by-step
Focus on the changed files in this PR.
```
## Structured Outputs
Get validated JSON results from Claude that automatically become GitHub Action outputs. This enables building complex automation workflows where Claude analyzes data and subsequent steps use the results.
### Basic Example
```yaml
- name: Detect flaky tests
id: analyze
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
Check the CI logs and determine if this is a flaky test.
Return: is_flaky (boolean), confidence (0-1), summary (string)
json_schema: |
{
"type": "object",
"properties": {
"is_flaky": {"type": "boolean"},
"confidence": {"type": "number"},
"summary": {"type": "string"}
},
"required": ["is_flaky"]
}
- name: Retry if flaky
if: fromJSON(steps.analyze.outputs.structured_output).is_flaky == true
run: gh workflow run CI
```
### How It Works
1. **Define Schema**: Provide a JSON schema in the `json_schema` input
2. **Claude Executes**: Claude uses tools to complete your task
3. **Validated Output**: Result is validated against your schema
4. **JSON Output**: All fields are returned in a single `structured_output` JSON string
### Accessing Structured Outputs
All structured output fields are available in the `structured_output` output as a JSON string:
**In GitHub Actions expressions:**
```yaml
if: fromJSON(steps.analyze.outputs.structured_output).is_flaky == true
run: |
CONFIDENCE=${{ fromJSON(steps.analyze.outputs.structured_output).confidence }}
```
**In bash with jq:**
```yaml
- name: Process results
run: |
OUTPUT='${{ steps.analyze.outputs.structured_output }}'
IS_FLAKY=$(echo "$OUTPUT" | jq -r '.is_flaky')
SUMMARY=$(echo "$OUTPUT" | jq -r '.summary')
```
**Note**: Due to GitHub Actions limitations, composite actions cannot expose dynamic outputs. All fields are bundled in the single `structured_output` JSON string.
### Complete Example
See `examples/test-failure-analysis.yml` for a working example that:
- Detects flaky test failures
- Uses confidence thresholds in conditionals
- Auto-retries workflows
- Comments on PRs
### Documentation
For complete details on JSON Schema syntax and Agent SDK structured outputs:
https://docs.claude.com/en/docs/agent-sdk/structured-outputs
## Ways to Tag @claude
These examples show how to interact with Claude using comments in PRs and issues. By default, Claude will be triggered anytime you mention `@claude`, but you can customize the exact trigger phrase using the `trigger_phrase` input in the workflow.

View File

@@ -0,0 +1,133 @@
name: Auto-Retry Flaky Tests
# This example demonstrates using structured outputs to detect flaky test failures
# and automatically retry them, reducing noise from intermittent failures.
#
# Use case: When CI fails, automatically determine if it's likely flaky and retry if so.
on:
workflow_run:
workflows: ["CI"]
types: [completed]
permissions:
contents: read
actions: write
jobs:
detect-flaky:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Detect flaky test failures
id: detect
uses: anthropics/claude-code-action@main
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
The CI workflow failed: ${{ github.event.workflow_run.html_url }}
Check the logs: gh run view ${{ github.event.workflow_run.id }} --log-failed
Determine if this looks like a flaky test failure by checking for:
- Timeout errors
- Race conditions
- Network errors
- "Expected X but got Y" intermittent failures
- Tests that passed in previous commits
Return:
- is_flaky: true if likely flaky, false if real bug
- confidence: number 0-1 indicating confidence level
- summary: brief one-sentence explanation
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"]
}
# Auto-retry only if flaky AND high confidence (>= 0.7)
- name: Retry flaky tests
if: |
fromJSON(steps.detect.outputs.structured_output).is_flaky == true &&
fromJSON(steps.detect.outputs.structured_output).confidence >= 0.7
env:
GH_TOKEN: ${{ github.token }}
run: |
OUTPUT='${{ steps.detect.outputs.structured_output }}'
CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence')
SUMMARY=$(echo "$OUTPUT" | jq -r '.summary')
echo "🔄 Flaky test detected (confidence: $CONFIDENCE)"
echo "Summary: $SUMMARY"
echo ""
echo "Triggering automatic retry..."
gh workflow run "${{ github.event.workflow_run.name }}" \
--ref "${{ github.event.workflow_run.head_branch }}"
# Low confidence flaky detection - skip retry
- name: Low confidence detection
if: |
fromJSON(steps.detect.outputs.structured_output).is_flaky == true &&
fromJSON(steps.detect.outputs.structured_output).confidence < 0.7
run: |
OUTPUT='${{ steps.detect.outputs.structured_output }}'
CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence')
echo "⚠️ Possible flaky test but confidence too low ($CONFIDENCE)"
echo "Not retrying automatically - manual review recommended"
# Comment on PR if this was a PR build
- name: Comment on PR
if: github.event.workflow_run.event == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
run: |
OUTPUT='${{ steps.detect.outputs.structured_output }}'
IS_FLAKY=$(echo "$OUTPUT" | jq -r '.is_flaky')
CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence')
SUMMARY=$(echo "$OUTPUT" | jq -r '.summary')
pr_number=$(gh pr list --head "${{ github.event.workflow_run.head_branch }}" --json number --jq '.[0].number')
if [ -n "$pr_number" ]; then
if [ "$IS_FLAKY" = "true" ]; then
TITLE="🔄 Flaky Test Detected"
ACTION="✅ Automatically retrying the workflow"
else
TITLE="❌ Test Failure"
ACTION="⚠️ This appears to be a real bug - manual intervention needed"
fi
gh pr comment "$pr_number" --body "$(cat <<EOF
## $TITLE
**Analysis**: $SUMMARY
**Confidence**: $CONFIDENCE
$ACTION
[View workflow run](${{ github.event.workflow_run.html_url }})
EOF
)"
fi

View File

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

View File

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