fix: inline --json-schema file paths so structured output works on CLI 2.1.160 (closes #3)
The docs recommend the file-path form (`--json-schema /path/to/schema.json`) as primary, but the default-pinned Claude CLI 2.1.160 only accepts an inline JSON string. Passing a path makes the CLI exit 0 with empty output, so the action's fail-loud branch trips on every run. Resolve the value before spawning the CLI: a file-path schema is read in-process and passed inline (validated + compacted), while an already-inline schema is left untouched. Because the value is handed to spawn() as an argv element (never through a shell), a schema with `$ref`/`$defs` round-trips intact. Unreadable files and invalid JSON now fail with a clear error instead of the CLI's silent empty output. Docs (README, example workflow, action.yml) are corrected to explain that the file-path form is auto-inlined and works on the default CLI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import * as core from "@actions/core";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { unlink, writeFile, stat } from "fs/promises";
|
||||
import { createWriteStream } from "fs";
|
||||
import { createWriteStream, readFileSync } from "fs";
|
||||
import { spawn } from "child_process";
|
||||
import { parse as parseShellArgs } from "shell-quote";
|
||||
|
||||
@@ -66,6 +66,84 @@ export function parseClaudeArgs(claudeArgs?: string): string[] {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a single `--json-schema` value. The Claude CLI's `--json-schema`
|
||||
* flag accepts an *inline* JSON string. The documented (and recommended) form
|
||||
* passes a file *path* so a schema containing `$ref`/`$defs` survives shell
|
||||
* tokenization without `$` expansion — but CLIs that only accept inline schemas
|
||||
* (including the default-pinned 2.1.160) silently ignore a path and emit no
|
||||
* structured output. To make the file-path form work everywhere, we read the
|
||||
* file in-process and return its contents as compact inline JSON; because this
|
||||
* value is handed to the CLI as an argv element (never through a shell), the
|
||||
* `$` characters are safe. Values that are already inline JSON (the trimmed
|
||||
* value starts with `{` or `[`) are passed through untouched.
|
||||
*/
|
||||
function resolveJsonSchemaValue(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
||||
return value; // Already an inline JSON schema — leave exactly as supplied.
|
||||
}
|
||||
|
||||
let contents: string;
|
||||
try {
|
||||
contents = readFileSync(value, "utf8");
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to read --json-schema file "${value}": ${
|
||||
e instanceof Error ? e.message : String(e)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(contents));
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`--json-schema file "${value}" is not valid JSON: ${
|
||||
e instanceof Error ? e.message : String(e)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk tokenized CLI args and inline any file-path value supplied to
|
||||
* `--json-schema` (both the `--json-schema <path>` and `--json-schema=<path>`
|
||||
* forms). See {@link resolveJsonSchemaValue} for why. Non-schema args and
|
||||
* already-inline schemas are returned unchanged.
|
||||
*/
|
||||
export function inlineJsonSchemaArgs(args: string[]): string[] {
|
||||
const result: string[] = [];
|
||||
const PREFIX = "--json-schema=";
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--json-schema") {
|
||||
result.push(arg);
|
||||
const value = args[i + 1];
|
||||
if (value !== undefined) {
|
||||
result.push(resolveJsonSchemaValue(value));
|
||||
i++; // Consume the value token we just inlined.
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith(PREFIX)) {
|
||||
const value = arg.slice(PREFIX.length);
|
||||
result.push(`${PREFIX}${resolveJsonSchemaValue(value)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push(arg);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate the schema-validated result emitted by the Claude CLI. With
|
||||
* `--output-format stream-json` the CLI prints one JSON object per line and the
|
||||
@@ -177,8 +255,10 @@ export function prepareRunConfig(
|
||||
|
||||
// Forward any additional raw CLI arguments verbatim, after the args this
|
||||
// action manages. Empty/unset claude_args leaves the arg list byte-identical
|
||||
// to before this feature, preserving behavior for existing callers.
|
||||
const extraArgs = parseClaudeArgs(options.claudeArgs);
|
||||
// to before this feature, preserving behavior for existing callers. A
|
||||
// file-path value passed to `--json-schema` is inlined here so it works on
|
||||
// CLIs that only accept inline schemas (e.g. the default-pinned 2.1.160).
|
||||
const extraArgs = inlineJsonSchemaArgs(parseClaudeArgs(options.claudeArgs));
|
||||
claudeArgs.push(...extraArgs);
|
||||
|
||||
const hasJsonSchema = extraArgs.some(
|
||||
|
||||
Reference in New Issue
Block a user