{"record":{"id":"e1006d4c45d80e7d","repo":"astrid-runtime/astrid","slug":"release-has-no-tag-name","errorCode":null,"errorMessage":"release has no tag_name","messagePattern":"release has no tag_name","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/update_channel.rs","lineNumber":109,"sourceCode":"async fn fetch_release_by_tag(\n    client: &reqwest::Client,\n    owner: &str,\n    repo: &str,\n    tag: &str,\n) -> anyhow::Result<serde_json::Value> {\n    let encoded_tag: String = url::form_urlencoded::byte_serialize(tag.as_bytes()).collect();\n    let url = format!(\n        \"{}/repos/{owner}/{repo}/releases/tags/{encoded_tag}\",\n        api_base()\n    );\n    let body =\n        download_bounded(client, &url, MAX_RELEASE_METADATA_BYTES, \"release metadata\").await?;\n    let json: serde_json::Value =\n        serde_json::from_slice(&body).context(\"failed to parse release metadata\")?;\n    let actual_tag = json\n        .get(\"tag_name\")\n        .and_then(|value| value.as_str())\n        .ok_or_else(|| anyhow::anyhow!(\"release has no tag_name\"))?;\n    ensure!(\n        actual_tag == tag,\n        \"release endpoint returned tag '{actual_tag}', expected '{tag}'\"\n    );\n    Ok(json)\n}\n\npub(super) async fn resolve_signed_channel(\n    client: &reqwest::Client,\n    owner: &str,\n    repo: &str,\n    channel: UpdateChannel,\n    target: &str,\n) -> anyhow::Result<ResolvedChannelRelease> {\n    // One process owns channel acceptance through the final atomic pointer\n    // commit. Without this lock, concurrent generations could interleave and\n    // leave the lower pointer as the accepted rollback floor.\n    let _lock = acquire_channel_lock(channel)?;","sourceCodeStart":91,"sourceCodeEnd":127,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/update_channel.rs#L91-L127","documentation":"fetch_release_by_tag downloads the release metadata JSON within a size bound and then reads the tag_name field. If the JSON is valid but has no string tag_name field, the resolved actual tag cannot be compared against the requested tag, so this error is thrown. It indicates the endpoint returned something other than the expected GitHub-style release object.","triggerScenarios":"resolve_signed_channel calling fetch_release_by_tag against an endpoint whose response JSON lacks a string tag_name (e.g. an API error object, rate-limit JSON body, HTML error page that happens to parse, or a changed API schema).","commonSituations":"GitHub API rate limiting returning a message-only JSON body; pointing the update channel at a proxy/mirror with a different response shape; GitHub API schema changes or wrong endpoint URL configured.","solutions":["Check GitHub API rate limits (gh api rate_limit or curl the endpoint) and authenticate or wait before retrying","curl the release URL and inspect the JSON for the tag_name field","Fix the update-channel URL to point at the correct GitHub releases API endpoint","If a proxy is in use, ensure it forwards the upstream release JSON unmodified"],"exampleFix":"// before\nurl = \"https://api.example.com/repos/org/repo/releases/tags/v1.2.3\" // wrong shape\n// after\nurl = \"https://api.github.com/repos/org/repo/releases/tags/v1.2.3\"","handlingStrategy":"validation","validationCode":"let body: serde_json::Value = serde_json::from_slice(&meta)?;\nif body.get(\"tag_name\").and_then(|v| v.as_str()).is_none() {\n    eprintln!(\"response is not a release object (rate limited or wrong endpoint?): {body}\");\n    return;\n}","typeGuard":"fn has_string_field(v: &serde_json::Value, key: &str) -> bool {\n    v.get(key).and_then(|x| x.as_str()).is_some()\n}\nlet is_release = |v: &serde_json::Value| has_string_field(v, \"tag_name\");","tryCatchPattern":"match fetch_release_by_tag(&client, &tag).await {\n    Ok(release) => apply(release),\n    Err(e) if e.to_string().contains(\"tag_name\") => {\n        eprintln!(\"release endpoint returned an unexpected body — check rate limits and channel URL\");\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Monitor GitHub API rate limits and use authenticated tokens for channel resolution","Point update channels only at the official releases API endpoint","Log the raw response body when metadata parsing fails","Watch for GitHub API schema changes affecting release payloads"],"tags":["network","api","json","update-channel"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}