mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 22:44:13 +08:00
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>
This commit is contained in:
84
.github/workflows/test-structured-output.yml
vendored
84
.github/workflows/test-structured-output.yml
vendored
@@ -46,27 +46,34 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify outputs
|
- name: Verify outputs
|
||||||
run: |
|
run: |
|
||||||
|
# Parse the structured_output JSON
|
||||||
|
OUTPUT='${{ steps.test.outputs.structured_output }}'
|
||||||
|
|
||||||
# Test string pass-through
|
# Test string pass-through
|
||||||
if [ "${{ steps.test.outputs.text_field }}" != "hello" ]; then
|
TEXT_FIELD=$(echo "$OUTPUT" | jq -r '.text_field')
|
||||||
echo "❌ String: expected 'hello', got '${{ steps.test.outputs.text_field }}'"
|
if [ "$TEXT_FIELD" != "hello" ]; then
|
||||||
|
echo "❌ String: expected 'hello', got '$TEXT_FIELD'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test number → string conversion
|
# Test number → string conversion
|
||||||
if [ "${{ steps.test.outputs.number_field }}" != "42" ]; then
|
NUMBER_FIELD=$(echo "$OUTPUT" | jq -r '.number_field')
|
||||||
echo "❌ Number: expected '42', got '${{ steps.test.outputs.number_field }}'"
|
if [ "$NUMBER_FIELD" != "42" ]; then
|
||||||
|
echo "❌ Number: expected '42', got '$NUMBER_FIELD'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test boolean → "true" conversion
|
# Test boolean → "true" conversion
|
||||||
if [ "${{ steps.test.outputs.boolean_true }}" != "true" ]; then
|
BOOLEAN_TRUE=$(echo "$OUTPUT" | jq -r '.boolean_true')
|
||||||
echo "❌ Boolean true: expected 'true', got '${{ steps.test.outputs.boolean_true }}'"
|
if [ "$BOOLEAN_TRUE" != "true" ]; then
|
||||||
|
echo "❌ Boolean true: expected 'true', got '$BOOLEAN_TRUE'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test boolean → "false" conversion
|
# Test boolean → "false" conversion
|
||||||
if [ "${{ steps.test.outputs.boolean_false }}" != "false" ]; then
|
BOOLEAN_FALSE=$(echo "$OUTPUT" | jq -r '.boolean_false')
|
||||||
echo "❌ Boolean false: expected 'false', got '${{ steps.test.outputs.boolean_false }}'"
|
if [ "$BOOLEAN_FALSE" != "false" ]; then
|
||||||
|
echo "❌ Boolean false: expected 'false', got '$BOOLEAN_FALSE'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -108,28 +115,31 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify JSON stringification
|
- name: Verify JSON stringification
|
||||||
run: |
|
run: |
|
||||||
|
# Parse the structured_output JSON
|
||||||
|
OUTPUT='${{ steps.test.outputs.structured_output }}'
|
||||||
|
|
||||||
# Arrays should be JSON stringified
|
# Arrays should be JSON stringified
|
||||||
ITEMS='${{ steps.test.outputs.items }}'
|
if ! echo "$OUTPUT" | jq -e '.items | length == 3' > /dev/null; then
|
||||||
if ! echo "$ITEMS" | jq -e '. | length == 3' > /dev/null; then
|
echo "❌ Array not properly formatted"
|
||||||
echo "❌ Array not properly stringified: $ITEMS"
|
echo "$OUTPUT" | jq '.items'
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Objects should be JSON stringified
|
# Objects should be JSON stringified
|
||||||
CONFIG='${{ steps.test.outputs.config }}'
|
if ! echo "$OUTPUT" | jq -e '.config.key == "value"' > /dev/null; then
|
||||||
if ! echo "$CONFIG" | jq -e '.key == "value"' > /dev/null; then
|
echo "❌ Object not properly formatted"
|
||||||
echo "❌ Object not properly stringified: $CONFIG"
|
echo "$OUTPUT" | jq '.config'
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Empty arrays should work
|
# Empty arrays should work
|
||||||
EMPTY='${{ steps.test.outputs.empty_array }}'
|
if ! echo "$OUTPUT" | jq -e '.empty_array | length == 0' > /dev/null; then
|
||||||
if ! echo "$EMPTY" | jq -e '. | length == 0' > /dev/null; then
|
echo "❌ Empty array not properly formatted"
|
||||||
echo "❌ Empty array not properly stringified: $EMPTY"
|
echo "$OUTPUT" | jq '.empty_array'
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✅ All complex types JSON stringified correctly"
|
echo "✅ All complex types handled correctly"
|
||||||
|
|
||||||
test-edge-cases:
|
test-edge-cases:
|
||||||
name: Test Edge Cases
|
name: Test Edge Cases
|
||||||
@@ -166,27 +176,34 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify edge cases
|
- name: Verify edge cases
|
||||||
run: |
|
run: |
|
||||||
|
# Parse the structured_output JSON
|
||||||
|
OUTPUT='${{ steps.test.outputs.structured_output }}'
|
||||||
|
|
||||||
# Zero should be "0", not empty or falsy
|
# Zero should be "0", not empty or falsy
|
||||||
if [ "${{ steps.test.outputs.zero }}" != "0" ]; then
|
ZERO=$(echo "$OUTPUT" | jq -r '.zero')
|
||||||
echo "❌ Zero: expected '0', got '${{ steps.test.outputs.zero }}'"
|
if [ "$ZERO" != "0" ]; then
|
||||||
|
echo "❌ Zero: expected '0', got '$ZERO'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Empty string should be empty (not "null" or missing)
|
# Empty string should be empty (not "null" or missing)
|
||||||
if [ "${{ steps.test.outputs.empty_string }}" != "" ]; then
|
EMPTY_STRING=$(echo "$OUTPUT" | jq -r '.empty_string')
|
||||||
echo "❌ Empty string: expected '', got '${{ steps.test.outputs.empty_string }}'"
|
if [ "$EMPTY_STRING" != "" ]; then
|
||||||
|
echo "❌ Empty string: expected '', got '$EMPTY_STRING'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Negative numbers should work
|
# Negative numbers should work
|
||||||
if [ "${{ steps.test.outputs.negative }}" != "-5" ]; then
|
NEGATIVE=$(echo "$OUTPUT" | jq -r '.negative')
|
||||||
echo "❌ Negative: expected '-5', got '${{ steps.test.outputs.negative }}'"
|
if [ "$NEGATIVE" != "-5" ]; then
|
||||||
|
echo "❌ Negative: expected '-5', got '$NEGATIVE'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Decimals should preserve precision
|
# Decimals should preserve precision
|
||||||
if [ "${{ steps.test.outputs.decimal }}" != "3.14" ]; then
|
DECIMAL=$(echo "$OUTPUT" | jq -r '.decimal')
|
||||||
echo "❌ Decimal: expected '3.14', got '${{ steps.test.outputs.decimal }}'"
|
if [ "$DECIMAL" != "3.14" ]; then
|
||||||
|
echo "❌ Decimal: expected '3.14', got '$DECIMAL'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -220,15 +237,20 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify sanitized names work
|
- name: Verify sanitized names work
|
||||||
run: |
|
run: |
|
||||||
# Hyphens should be preserved (GitHub Actions allows them)
|
# Parse the structured_output JSON
|
||||||
if [ "${{ steps.test.outputs.test-result }}" != "passed" ]; then
|
OUTPUT='${{ steps.test.outputs.structured_output }}'
|
||||||
echo "❌ Hyphenated name failed"
|
|
||||||
|
# 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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Underscores should work
|
# Underscores should work
|
||||||
if [ "${{ steps.test.outputs.item_count }}" != "10" ]; then
|
ITEM_COUNT=$(echo "$OUTPUT" | jq -r '.item_count')
|
||||||
echo "❌ Underscore name failed"
|
if [ "$ITEM_COUNT" != "10" ]; then
|
||||||
|
echo "❌ Underscore name failed: expected '10', got '$ITEM_COUNT'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ outputs:
|
|||||||
execution_file:
|
execution_file:
|
||||||
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:
|
||||||
|
description: "JSON string containing all structured output fields (use fromJSON() or jq to parse)"
|
||||||
|
value: ${{ steps.run_claude.outputs.structured_output }}
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
|
|||||||
@@ -139,7 +139,11 @@ export function sanitizeOutputName(name: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reserved output names that cannot be used by structured outputs
|
// Reserved output names that cannot be used by structured outputs
|
||||||
const RESERVED_OUTPUTS = ["conclusion", "execution_file"] as const;
|
const RESERVED_OUTPUTS = [
|
||||||
|
"conclusion",
|
||||||
|
"execution_file",
|
||||||
|
"structured_output",
|
||||||
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts values to string format for GitHub Actions outputs
|
* Converts values to string format for GitHub Actions outputs
|
||||||
@@ -191,9 +195,17 @@ async function parseAndSetStructuredOutputs(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set GitHub Action output for each field
|
// 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)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Also set individual field outputs for direct usage (when not using composite action)
|
||||||
const entries = Object.entries(result.structured_output);
|
const entries = Object.entries(result.structured_output);
|
||||||
core.info(`Setting ${entries.length} structured output(s)`);
|
core.info(`Setting ${entries.length} individual structured output(s)`);
|
||||||
|
|
||||||
for (const [key, value] of entries) {
|
for (const [key, value] of entries) {
|
||||||
// Validate key before sanitization
|
// Validate key before sanitization
|
||||||
|
|||||||
Reference in New Issue
Block a user