{"record":{"id":"ea6e4f13e977efba","repo":"Hmbown/CodeWhale","slug":"github-release-tag-returned-invalid-json-err","errorCode":null,"errorMessage":"GitHub Release ${tag} returned invalid JSON: ${error.message}","messagePattern":"GitHub Release (.+?) returned invalid JSON: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/release/ensure-release-assets-absent.js","lineNumber":49,"sourceCode":"    output = exec(ghBin, [\"api\", endpoint], {\n      encoding: \"utf8\",\n      maxBuffer: 10 * 1024 * 1024,\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n    });\n  } catch (error) {\n    if (isNotFoundError(error)) {\n      return null;\n    }\n    const detail = String(error && error.stderr ? error.stderr : \"\").trim();\n    throw new Error(\n      `Could not inspect GitHub Release ${tag}${detail ? `: ${detail}` : \"\"}`,\n    );\n  }\n\n  try {\n    return JSON.parse(output);\n  } catch (error) {\n    throw new Error(`GitHub Release ${tag} returned invalid JSON: ${error.message}`);\n  }\n}\n\nfunction assertReleaseAssetsAbsent(release, tag) {\n  if (release === null) {\n    return;\n  }\n  if (!release || !Array.isArray(release.assets)) {\n    throw new Error(`GitHub Release ${tag} did not provide an asset inventory`);\n  }\n  if (release.assets.length === 0) {\n    return;\n  }\n\n  const names = release.assets.map((asset) => asset && asset.name).filter(Boolean);\n  const inventory = names.length > 0 ? names.join(\", \") : `${release.assets.length} unnamed asset(s)`;\n  throw new Error(\n    `Refusing to replace existing assets for ${tag}: ${inventory}. ` +","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/scripts/release/ensure-release-assets-absent.js#L31-L67","documentation":"The gh api subprocess exited 0 but its stdout could not be JSON.parsed. The script expects the raw REST release object on stdout, so any pollution there — a gh warning, an active pager, a GH_BIN wrapper that reformats output — breaks parsing. The underlying SyntaxError message is appended to name where parsing failed.","triggerScenarios":"GH_PAGER or gh config writing to stdout in CI; GH_BIN pointing at a wrapper (jq filter, tee, formatter); gh emitting a notice line before the JSON; a proxy injecting a banner into piped output.","commonSituations":"Custom GH_BIN shims inside the workflow; environment-wide pager settings; global gh config changed output formatting; test harnesses stubbing gh with echo.","solutions":["Reproduce manually: gh api repos/OWNER/REPO/releases/tags/vX.Y.Z | head -c 200 to see what stdout actually contains","Unset GH_PAGER for the step (env -u GH_PAGER ...) and disable gh output formatting","If GH_BIN is a wrapper, keep stdout pristine — send any diagnostics to stderr only"],"exampleFix":"# before: GH_PAGER=less pollutes stdout -> invalid JSON\n\n# after\nenv -u GH_PAGER GH_BIN=gh node scripts/release/ensure-release-assets-absent.js acme/codewhale v1.2.3","handlingStrategy":"validation","validationCode":"const { execFileSync } = require(\"node:child_process\");\nfunction ghStdoutIsCleanJson() {\n  const probe = execFileSync(process.env.GH_BIN || \"gh\", [\"api\", \"rate_limit\"], { encoding: \"utf8\" });\n  try {\n    JSON.parse(probe);\n    return true;\n  } catch {\n    return false;\n  }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Unset GH_PAGER for CI steps that shell out to gh","Keep GH_BIN pointing at the plain gh binary; wrappers must send diagnostics to stderr only","Add the rate_limit JSON probe as a workflow preflight so stdout pollution fails early with a clear cause"],"tags":["github","cli","json","release","environment"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}