{"record":{"id":"2ef3cb27c7456c21","repo":"can1357/oh-my-pi","slug":"github-release-expectedtag-has-no-asset-list","errorCode":null,"errorMessage":"GitHub release ${expectedTag} has no asset list","messagePattern":"GitHub release (.+?) has no asset list","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/cli/update-cli.ts","lineNumber":209,"sourceCode":"\trelease: unknown,\n\texpectedTag: string,\n\tbinaryName: string,\n\toptions: { allowPrerelease?: boolean } = {},\n): ReleaseBinaryAsset {\n\tif (!isRecord(release)) {\n\t\tthrow new Error(\"Invalid GitHub release metadata\");\n\t}\n\tif (release.tag_name !== expectedTag) {\n\t\tthrow new Error(`GitHub release tag mismatch: expected ${expectedTag}`);\n\t}\n\tif (release.draft !== false) {\n\t\tthrow new Error(`GitHub release ${expectedTag} is a draft, not a published release`);\n\t}\n\tif (release.prerelease !== false && !options.allowPrerelease) {\n\t\tthrow new Error(`GitHub release ${expectedTag} is a prerelease; only canary updates install prerelease assets`);\n\t}\n\tif (!Array.isArray(release.assets)) {\n\t\tthrow new Error(`GitHub release ${expectedTag} has no asset list`);\n\t}\n\n\tconst matches = release.assets.filter(asset => isRecord(asset) && asset.name === binaryName);\n\tif (matches.length !== 1) {\n\t\tthrow new Error(`GitHub release ${expectedTag} has ${matches.length} assets named ${binaryName}`);\n\t}\n\n\tconst asset = matches[0];\n\tif (!isRecord(asset) || asset.state !== \"uploaded\") {\n\t\tthrow new Error(`GitHub release asset ${binaryName} is not fully uploaded`);\n\t}\n\tif (typeof asset.size !== \"number\" || !Number.isSafeInteger(asset.size) || asset.size <= 0) {\n\t\tthrow new Error(`GitHub release asset ${binaryName} has an invalid size`);\n\t}\n\tif (typeof asset.digest !== \"string\") {\n\t\tthrow new Error(`GitHub release asset ${binaryName} has no digest`);\n\t}\n\tconst digest = /^sha256:([0-9a-f]{64})$/i.exec(asset.digest)?.[1];","sourceCodeStart":191,"sourceCodeEnd":227,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/cli/update-cli.ts#L191-L227","documentation":"resolveReleaseBinaryAsset validates raw GitHub API release metadata before selecting the update binary. The fetched release object did not include an `assets` array, so no binary could be located. This guards against malformed or unexpected API responses (the code treats release metadata as untrusted `unknown`).","triggerScenarios":"getReleaseBinaryAsset calls GET /repos/<REPO>/releases/tags/<tag> and passes the parsed JSON to resolveReleaseBinaryAsset; the error throws when `release.assets` is not an array (missing, null, or a non-array shape) while tag/draft/prerelease checks already passed.","commonSituations":"A GitHub API schema change or API-version drift (X-GitHub-Api-Version), a mocked/stubbed fetch in tests returning a partial release object, a corporate proxy or mirror rewriting/stripping the response body, or a hand-built release object missing assets.","solutions":["Re-run the update to refetch release metadata; transient truncation is rare but possible.","Verify the GitHub API response manually: curl -H 'X-GitHub-Api-Version: 2022-11-28' https://api.github.com/repos/<owner>/<repo>/releases/tags/<tag> and confirm an assets array exists.","Check whether a proxy/interceptor or custom fetchImpl is altering the response.","If GitHub changed its API shape, update the validation in packages/coding-agent/src/cli/update-cli.ts."],"exampleFix":"// before (test stub missing assets)\nfetchImpl = async () => new Response(JSON.stringify({ tag_name: \"v1.2.3\", draft: false, prerelease: false }), { headers: { \"content-type\": \"application/json\" } });\n// after\nfetchImpl = async () => new Response(JSON.stringify({ tag_name: \"v1.2.3\", draft: false, prerelease: false, assets: [{ name: binaryName, state: \"uploaded\", size: 1024, digest: \"sha256:\" + \"a\".repeat(64), browser_download_url: expectedUrl }] }), { headers: { \"content-type\": \"application/json\" } });","handlingStrategy":"type-guard","validationCode":"const releaseRes = await fetch(`https://api.github.com/repos/${REPO}/releases/tags/${tag}`, { headers: { \"X-GitHub-Api-Version\": \"2022-11-28\" } });\nconst release = await releaseRes.json();\nif (!releaseRes.ok || !Array.isArray(release?.assets)) throw new Error(\"Release metadata malformed; aborting update\");","typeGuard":"function hasAssets(release: unknown): release is { assets: Record<string, unknown>[] } {\n  return typeof release === \"object\" && release !== null && Array.isArray((release as { assets?: unknown }).assets);\n}","tryCatchPattern":"try {\n  await update();\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"has no asset list\")) {\n    console.error(\"GitHub returned malformed release metadata; retry or check api.github.com directly.\");\n  } else throw err;\n}","preventionTips":["Always pass the X-GitHub-Api-Version header when calling the GitHub API to pin the response schema.","Validate parsed JSON shape before consuming it; treat API responses as untrusted.","In tests, build release stubs with a full assets array."],"tags":["network","github-api","update","response-shape"],"backgroundTag":"malformed-api-response","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}