diff --git a/base-action/action.yml b/base-action/action.yml index a0b5bc8..fa1bd2f 100644 --- a/base-action/action.yml +++ b/base-action/action.yml @@ -73,7 +73,7 @@ inputs: default: "" json_schema: description: | - JSON schema for structured output validation. Claude must return JSON matching this schema + JSON schema for structured output validation. Claude must return JSON matching this schema or the action will fail. Outputs are automatically set for each field. Access outputs via: steps..outputs. @@ -81,8 +81,7 @@ inputs: Limitations: - Field names must start with letter or underscore (A-Z, a-z, _) - Special characters in field names are replaced with underscores - - Each output is limited to 1MB (values will be truncated) - - Objects and arrays are JSON stringified + - Each output is limited to 1MB required: false default: "" diff --git a/base-action/src/index.ts b/base-action/src/index.ts index 5efb18b..d6c7903 100644 --- a/base-action/src/index.ts +++ b/base-action/src/index.ts @@ -36,10 +36,10 @@ async function run() { claudeArgs += ` --allowedTools "${process.env.INPUT_ALLOWED_TOOLS}"`; } - // Add JSON schema if specified + // Add JSON schema if specified (no escaping - parseShellArgs handles it) if (process.env.JSON_SCHEMA) { - const escapedSchema = process.env.JSON_SCHEMA.replace(/'/g, "'\\''"); - claudeArgs += ` --json-schema '${escapedSchema}'`; + // Wrap in single quotes for parseShellArgs + claudeArgs += ` --json-schema '${process.env.JSON_SCHEMA}'`; } await runClaude(promptConfig.path, { diff --git a/base-action/src/run-claude.ts b/base-action/src/run-claude.ts index 3b2bb07..e65b5fc 100644 --- a/base-action/src/run-claude.ts +++ b/base-action/src/run-claude.ts @@ -184,13 +184,11 @@ async function parseAndSetStructuredOutputs( ); if (!result?.structured_output) { - const error = new Error( + throw new Error( `json_schema was provided but Claude did not return structured_output.\n` + `Found ${messages.length} messages. Result exists: ${!!result}\n` + `The schema may be invalid or Claude failed to call the StructuredOutput tool.`, ); - core.setFailed(error.message); - throw error; } // Set GitHub Action output for each field diff --git a/src/modes/agent/index.ts b/src/modes/agent/index.ts index 0d15bdf..d094b57 100644 --- a/src/modes/agent/index.ts +++ b/src/modes/agent/index.ts @@ -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 @@ -150,12 +151,7 @@ export const agentMode: Mode = { } // Add JSON schema if provided - const jsonSchemaStr = process.env.JSON_SCHEMA || ""; - if (jsonSchemaStr) { - // CLI validates schema - just escape for safe shell passing - const escapedSchema = jsonSchemaStr.replace(/'/g, "'\\''"); - claudeArgs += ` --json-schema '${escapedSchema}'`; - } + claudeArgs = appendJsonSchemaArg(claudeArgs); // Append user's claude_args (which may have more --mcp-config flags) claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim(); diff --git a/src/modes/tag/index.ts b/src/modes/tag/index.ts index 11a8e7b..a9bcae8 100644 --- a/src/modes/tag/index.ts +++ b/src/modes/tag/index.ts @@ -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. @@ -178,12 +179,7 @@ export const tagMode: Mode = { claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`; // Add JSON schema if provided - const jsonSchemaStr = process.env.JSON_SCHEMA || ""; - if (jsonSchemaStr) { - // CLI validates schema - just escape for safe shell passing - const escapedSchema = jsonSchemaStr.replace(/'/g, "'\\''"); - claudeArgs += ` --json-schema '${escapedSchema}'`; - } + claudeArgs = appendJsonSchemaArg(claudeArgs); // Append user's claude_args (which may have more --mcp-config flags) if (userClaudeArgs) { diff --git a/src/utils/json-schema.ts b/src/utils/json-schema.ts new file mode 100644 index 0000000..5a889c7 --- /dev/null +++ b/src/utils/json-schema.ts @@ -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}'`; +}