{"record":{"id":"e21858392d198a52","repo":"gitbutlerapp/gitbutler","slug":"github-did-not-change-the-review-thread-resolution","errorCode":null,"errorMessage":"GitHub did not change the review thread resolution","messagePattern":"GitHub did not change the review thread resolution","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-github/src/client.rs","lineNumber":1229,"sourceCode":"        }\n\n        #[derive(Deserialize)]\n        #[serde(rename_all = \"camelCase\")]\n        struct Thread {\n            is_resolved: bool,\n        }\n\n        let query = if resolved {\n            \"mutation($threadId: ID!) { result: resolveReviewThread(input: {threadId: $threadId}) { thread { isResolved } } }\"\n        } else {\n            \"mutation($threadId: ID!) { result: unresolveReviewThread(input: {threadId: $threadId}) { thread { isResolved } } }\"\n        };\n        let data: QueryData = self.graphql_query(query, &Variables { thread_id }).await?;\n        let thread = data\n            .result\n            .and_then(|payload| payload.thread)\n            .context(\"GitHub returned no review thread after changing its resolution\")?;\n        anyhow::ensure!(\n            thread.is_resolved == resolved,\n            \"GitHub did not change the review thread resolution\"\n        );\n        Ok(())\n    }\n\n    /// Reply into an existing review thread, returning the comment it made.\n    pub async fn add_review_thread_reply(\n        &self,\n        thread_id: &str,\n        body: &str,\n    ) -> Result<PullRequestReviewThreadComment> {\n        #[derive(Serialize)]\n        struct Variables<'a> {\n            #[serde(rename = \"threadId\")]\n            thread_id: &'a str,\n            body: &'a str,\n        }","sourceCodeStart":1211,"sourceCodeEnd":1247,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-github/src/client.rs#L1211-L1247","documentation":"After resolving a review thread via GraphQL, the returned thread's `isResolved` flag is compared with the requested `resolved` value; a mismatch means GitHub accepted the request but the thread state does not reflect it. This detects silent mutation failures — e.g. GitHub resolving/unresolving a different thread or ignoring the change — and fails loudly instead of reporting success.","triggerScenarios":"Calling thread resolve/unresolve when GitHub's post-mutation query returns a thread whose `isResolved` disagrees with the requested state (race with a concurrent change, wrong thread id resolving a cached/stale node, GitHub-side inconsistency).","commonSituations":"Concurrent reviewers resolving the same thread simultaneously; operating on a thread id that was deleted or merged mid-operation; GitHub API eventual-consistency lag right after a mutation.","solutions":["Retry the resolve/unresolve operation after a short delay; eventual consistency often resolves it.","Verify the thread id is current (re-fetch the review's threads) and that it was not merged away.","Check for concurrent actors (other users/bots) changing the thread and coordinate.","If reproducible, inspect the GraphQL response payload and report the inconsistency to GitHub."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Re-fetch the thread and check current state before mutating\nlet thread = client.get_review_thread(thread_id).await?;\nif thread.is_resolved == desired_state { return Ok(()); } // already in target state","typeGuard":null,"tryCatchPattern":"for attempt in 0..3 {\n    match client.resolve_thread(thread_id, resolved).await {\n        Ok(()) => break,\n        Err(e) if e.to_string().contains(\"did not change the review thread resolution\")\n            && attempt < 2 => tokio::time::sleep(Duration::from_millis(500)).await,\n        Err(e) => return Err(e),\n    }\n}","preventionTips":["Check thread state before mutating and treat already-satisfied states as success","Avoid concurrent resolvers on the same thread","Re-fetch thread ids after a PR merge; old threads may be stale"],"tags":["github","graphql","review-thread","state-mismatch"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}