affaan-m/ECC · error · Error

gh ${args.join(' ')} returned invalid JSON: ${error.message}

Error message

gh ${args.join(' ')} returned invalid JSON: ${error.message}

What it means

Thrown by runGhJson() in scripts/lib/github-discussions.js when the gh command exited zero but its stdout was not valid JSON. The wrapper expects gh api --jq or graphql output to be parseable; anything else (text, HTML, empty) triggers this error.

Source

Thrown at scripts/lib/github-discussions.js:51

  return result.stdout || '';
}

function runGhJson(args, options = {}) {
  const shimPath = process.env.ECC_GH_SHIM;
  const command = shimPath ? process.execPath : 'gh';
  const commandArgs = shimPath ? [shimPath, ...args] : args;
  const env = { ...process.env };

  if (!options.useEnvGithubToken) {
    delete env.GITHUB_TOKEN;
  }

  const stdout = runCommand(command, commandArgs, { env });
  try {
    return JSON.parse(stdout || 'null');
  } catch (error) {
    throw new Error(`gh ${args.join(' ')} returned invalid JSON: ${error.message}`);
  }
}

function discussionNeedsMaintainerTouch(discussion) {
  if (MAINTAINER_ASSOCIATIONS.has(discussion.authorAssociation)) {
    return false;
  }

  if (
    discussion.answer
    && MAINTAINER_ASSOCIATIONS.has(discussion.answer.authorAssociation)
  ) {
    return false;
  }

  const comments = discussion.comments && Array.isArray(discussion.comments.nodes)
    ? discussion.comments.nodes
    : [];

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Reproduce manually: gh api graphql -f query=... --jq '.' and inspect the raw stdout.
  2. If gh prints warnings on stdout, upgrade gh or redirect stdout filtering; ensure logs go to stderr only.
  3. If using ECC_GH_SHIM, ensure it does console.log(JSON.stringify(payload)) and writes any diagnostics to stderr.
  4. Upgrade gh CLI to a recent stable release.

Example fix

// before
return JSON.parse(stdout || 'null');

// after (defensive trim of leading non-JSON)
const trimmed = String(stdout || '').trim();
if (!trimmed || trimmed[0] !== '{' && trimmed[0] !== '[' && trimmed !== 'null') {
  throw new Error(`gh ${args.join(' ')} returned non-JSON output: ${trimmed.slice(0, 200)}`);
}
return JSON.parse(trimmed);
Defensive patterns

Strategy: validation

Validate before calling

function looksJson(s) {
  const t = String(s || '').trim();
  return t === 'null' || t.startsWith('{') || t.startsWith('[');
}
const stdout = runCommand(cmd, args, opts);
if (!looksJson(stdout)) {
  throw new Error(`Expected JSON from ${cmd}; got: ${stdout.slice(0, 200)}`);
}

Try / catch

try {
  return JSON.parse(stdout || 'null');
} catch (err) {
  if (/Unexpected token|JSON/.test(err.message)) {
    throw new Error(`gh ${args.join(' ')} returned non-JSON: ${stdout.slice(0, 200)}`);
  }
  throw err;
}

Prevention

When it happens

Trigger: runGhJson calls runCommand successfully (status 0), then JSON.parse(stdout || 'null') throws. Happens when gh prints a human-readable message, a warning interleaved with JSON, an empty string with garbage, or when ECC_GH_SHIM emits non-JSON stdout.

Common situations: gh CLI version prints a deprecation notice on stdout before the JSON; shim script forgets to JSON.stringify its result; wrong gh subcommand returned plain text; paging footer or 'Processing...' line leaked into stdout; gh wrote logs to stdout instead of stderr.

Understand the failure class

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/6d18975b20edcfcf. Report an issue: GitHub.