{"record":{"id":"9f3d5ca566d376a0","repo":"BigPizzaV3/CodexPlusPlus","slug":"release-payload-missing-tag-name","errorCode":null,"errorMessage":"release payload missing tag_name","messagePattern":"release payload missing tag_name","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/update.rs","lineNumber":77,"sourceCode":"        .split('.')\n        .map(|part| part.parse::<u64>().map_err(Into::into))\n        .collect()\n}\n\npub fn is_newer_version(candidate: &str, current: &str) -> anyhow::Result<bool> {\n    let mut left = parse_version_tag(candidate)?;\n    let mut right = parse_version_tag(current)?;\n    let len = left.len().max(right.len());\n    left.resize(len, 0);\n    right.resize(len, 0);\n    Ok(left > right)\n}\n\npub fn release_from_github_payload(payload: &Value) -> anyhow::Result<Release> {\n    let version = payload\n        .get(\"tag_name\")\n        .and_then(Value::as_str)\n        .ok_or_else(|| anyhow::anyhow!(\"release payload missing tag_name\"))?\n        .to_string();\n    let assets = payload\n        .get(\"assets\")\n        .and_then(Value::as_array)\n        .into_iter()\n        .flatten()\n        .filter_map(|asset| {\n            Some((\n                asset.get(\"name\")?.as_str()?.to_string(),\n                asset.get(\"browser_download_url\")?.as_str()?.to_string(),\n            ))\n        })\n        .collect::<Vec<_>>();\n    let selected = select_update_asset(&assets);\n    Ok(Release {\n        version,\n        url: payload\n            .get(\"html_url\")","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/update.rs#L59-L95","documentation":"release_from_github_payload parses a GitHub Releases API JSON payload into a Release struct. The library requires the payload to contain a string 'tag_name' field, which is the sole source of the release version. If the field is absent or is not a JSON string (e.g. null, number, object), the parser cannot determine the version and throws this error instead of returning a Release with an empty version.","triggerScenarios":"Calling release_from_github_payload with: (1) a payload from a non-release endpoint (e.g. a rate-limit or error JSON body), (2) a draft release object lacking tag_name, (3) hand-written test/fixture JSON that omits tag_name, or (4) a payload where tag_name is a non-string value.","commonSituations":"Fetching the GitHub 'latest release' URL while unauthenticated and receiving a 404 JSON body without tag_name; proxy/mirror services returning a wrapped or stripped payload; CI scripts posting partial release metadata; unit-test fixtures missing the field after a schema change.","solutions":["Inspect the payload with serde_json::to_string_pretty and confirm a string 'tag_name' field exists at the top level before parsing.","Check the HTTP status before parsing — only feed release_from_github_payload bodies from a 200 response of the releases API endpoint.","Ensure the release is published (draft releases are not exposed via the public API and other endpoints may return stub bodies).","For custom/mirrored payloads, normalize them first: inject tag_name from whatever field holds the version.","Wrap the call and fall back to release_from_latest_json_payload, which accepts 'version' or 'tag_name'."],"exampleFix":"// before\nlet payload: serde_json::Value = resp.json().await?;\nlet release = release_from_github_payload(&payload)?;\n// after\nlet payload: serde_json::Value = resp.json().await?;\nif payload.get(\"tag_name\").and_then(|v| v.as_str()).is_none() {\n    anyhow::bail!(\"unexpected payload, no tag_name: {}\", payload);\n}\nlet release = release_from_github_payload(&payload)?;","handlingStrategy":"validation","validationCode":"fn has_tag_name(payload: &serde_json::Value) -> bool {\n    payload.get(\"tag_name\").and_then(|v| v.as_str()).map_or(false, |s| !s.is_empty())\n}\n// call: if !has_tag_name(&payload) { /* fetch correct endpoint or bail */ }","typeGuard":"fn as_tag_name(payload: &serde_json::Value) -> Option<&str> {\n    payload.get(\"tag_name\")?.as_str()\n}","tryCatchPattern":"match release_from_github_payload(&payload) {\n    Ok(release) => release,\n    Err(e) if e.to_string().contains(\"missing tag_name\") => {\n        // fall back to latest.json parser which also accepts 'version'\n        release_from_latest_json_payload(&payload)?\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Only feed this parser bodies from a verified-200 GitHub releases API response.","Validate the payload shape (tag_name present and a string) before parsing.","Prefer release_from_latest_json_payload for manifests, since it accepts both 'version' and 'tag_name'.","Keep test fixtures aligned with the real GitHub release schema."],"tags":["github-api","release-parsing","json-validation","update-check"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}