mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 23:14:13 +08:00
Compare commits
4 Commits
inigo/add-
...
v1.0.18
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6902c227aa | ||
|
|
e45f28fae7 | ||
|
|
8c4e1e7eb1 | ||
|
|
906bd89c74 |
72
.github/workflows/test-structured-output.yml
vendored
72
.github/workflows/test-structured-output.yml
vendored
@@ -30,19 +30,10 @@ jobs:
|
||||
- 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"
|
||||
claude_args: |
|
||||
--allowedTools Bash
|
||||
--json-schema '{"type":"object","properties":{"text_field":{"type":"string"},"number_field":{"type":"number"},"boolean_true":{"type":"boolean"},"boolean_false":{"type":"boolean"}},"required":["text_field","number_field","boolean_true","boolean_false"]}'
|
||||
|
||||
- name: Verify outputs
|
||||
run: |
|
||||
@@ -97,21 +88,10 @@ jobs:
|
||||
- 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"
|
||||
claude_args: |
|
||||
--allowedTools Bash
|
||||
--json-schema '{"type":"object","properties":{"items":{"type":"array","items":{"type":"string"}},"config":{"type":"object"},"empty_array":{"type":"array"}},"required":["items","config","empty_array"]}'
|
||||
|
||||
- name: Verify JSON stringification
|
||||
run: |
|
||||
@@ -160,19 +140,10 @@ jobs:
|
||||
- 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"
|
||||
claude_args: |
|
||||
--allowedTools Bash
|
||||
--json-schema '{"type":"object","properties":{"zero":{"type":"number"},"empty_string":{"type":"string"},"negative":{"type":"number"},"decimal":{"type":"number"}},"required":["zero","empty_string","negative","decimal"]}'
|
||||
|
||||
- name: Verify edge cases
|
||||
run: |
|
||||
@@ -223,17 +194,10 @@ jobs:
|
||||
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"
|
||||
claude_args: |
|
||||
--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
|
||||
run: |
|
||||
@@ -268,16 +232,10 @@ jobs:
|
||||
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"
|
||||
claude_args: |
|
||||
--allowedTools Bash
|
||||
--json-schema '{"type":"object","properties":{"done":{"type":"boolean"}},"required":["done"]}'
|
||||
|
||||
- name: Verify execution file contains structured_output
|
||||
run: |
|
||||
|
||||
11
action.yml
11
action.yml
@@ -113,10 +113,6 @@ 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:
|
||||
@@ -128,6 +124,9 @@ outputs:
|
||||
github_token:
|
||||
description: "The GitHub token used by the action (Claude App token if available)"
|
||||
value: ${{ steps.prepare.outputs.github_token }}
|
||||
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"
|
||||
value: ${{ steps.claude-code.outputs.structured_output }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -178,7 +177,6 @@ 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
|
||||
@@ -194,7 +192,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.42
|
||||
curl -fsSL https://claude.ai/install.sh | bash -s 2.0.45
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
else
|
||||
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
||||
@@ -233,7 +231,6 @@ runs:
|
||||
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 }}
|
||||
|
||||
@@ -24,10 +24,6 @@ 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:
|
||||
@@ -71,14 +67,6 @@ 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:
|
||||
@@ -88,7 +76,7 @@ outputs:
|
||||
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)"
|
||||
description: "JSON string containing all structured output fields when --json-schema is provided in claude_args (use fromJSON() or jq to parse)"
|
||||
value: ${{ steps.run_claude.outputs.structured_output }}
|
||||
|
||||
runs:
|
||||
@@ -156,8 +144,6 @@ runs:
|
||||
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 }}
|
||||
|
||||
@@ -28,22 +28,8 @@ 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: claudeArgs.trim(),
|
||||
claudeArgs: process.env.INPUT_CLAUDE_ARGS,
|
||||
allowedTools: process.env.INPUT_ALLOWED_TOOLS,
|
||||
disallowedTools: process.env.INPUT_DISALLOWED_TOOLS,
|
||||
maxTurns: process.env.INPUT_MAX_TURNS,
|
||||
|
||||
@@ -124,7 +124,7 @@ 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
|
||||
* Only runs if --json-schema was explicitly provided in claude_args
|
||||
* Exported for testing
|
||||
*/
|
||||
export async function parseAndSetStructuredOutputs(
|
||||
@@ -144,7 +144,7 @@ export async function parseAndSetStructuredOutputs(
|
||||
|
||||
if (!result?.structured_output) {
|
||||
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`,
|
||||
);
|
||||
}
|
||||
@@ -167,6 +167,9 @@ export async function parseAndSetStructuredOutputs(
|
||||
export async function runClaude(promptPath: string, options: ClaudeOptions) {
|
||||
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
|
||||
try {
|
||||
await unlink(PIPE_PATH);
|
||||
@@ -352,8 +355,8 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
|
||||
|
||||
core.setOutput("execution_file", EXECUTION_FILE);
|
||||
|
||||
// Parse and set structured outputs only if user provided json_schema
|
||||
if (process.env.JSON_SCHEMA) {
|
||||
// Parse and set structured outputs only if user provided --json-schema in claude_args
|
||||
if (hasJsonSchema) {
|
||||
try {
|
||||
await parseAndSetStructuredOutputs(EXECUTION_FILE);
|
||||
} catch (error) {
|
||||
|
||||
@@ -113,7 +113,7 @@ describe("parseAndSetStructuredOutputs", () => {
|
||||
await expect(
|
||||
parseAndSetStructuredOutputs(TEST_EXECUTION_FILE),
|
||||
).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(
|
||||
parseAndSetStructuredOutputs(TEST_EXECUTION_FILE),
|
||||
).rejects.toThrow(
|
||||
"json_schema was provided but Claude did not return structured_output",
|
||||
"--json-schema was provided but Claude did not return structured_output",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -80,7 +80,6 @@ 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
|
||||
|
||||
@@ -201,16 +200,8 @@ Get validated JSON results from Claude that automatically become GitHub Action o
|
||||
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"]
|
||||
}
|
||||
claude_args: |
|
||||
--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
|
||||
@@ -219,7 +210,7 @@ Get validated JSON results from Claude that automatically become GitHub Action o
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Define Schema**: Provide a JSON schema in the `json_schema` input
|
||||
1. **Define Schema**: Provide a JSON schema via `--json-schema` flag in `claude_args`
|
||||
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
|
||||
|
||||
@@ -43,27 +43,8 @@ jobs:
|
||||
- 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"]
|
||||
}
|
||||
claude_args: |
|
||||
--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
|
||||
|
||||
@@ -7,7 +7,6 @@ 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
|
||||
@@ -150,9 +149,6 @@ 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();
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ 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.
|
||||
@@ -178,9 +177,6 @@ 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}`;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* 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}'`;
|
||||
}
|
||||
Reference in New Issue
Block a user