{"record":{"id":"0e9f6126332f46c1","repo":"gitbutlerapp/gitbutler","slug":"could-not-restore-native-github-stack-membership-a","errorCode":null,"errorMessage":"Could not restore native GitHub stack membership after the push failed:\n{}","messagePattern":"Could not restore native GitHub stack membership after the push failed:\n(.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-forge/src/review.rs","lineNumber":2511,"sourceCode":"    preferred_forge_user: &Option<crate::ForgeUser>,\n    forge_repo_info: &crate::forge::ForgeRepoInfo,\n    stacks: &[Vec<i64>],\n    storage: &but_forge_storage::Controller,\n) -> Result<()> {\n    let crate::forge::ForgeRepoInfo { owner, repo, .. } = forge_repo_info;\n    let preferred_account = preferred_forge_user.as_ref().and_then(|user| user.github());\n    let mut errors = Vec::new();\n    for pull_requests in stacks {\n        if let Err(err) =\n            but_github::stacks::create(preferred_account, owner, repo, pull_requests, storage).await\n        {\n            errors.push(format!(\"{err:#}\"));\n        }\n    }\n    if errors.is_empty() {\n        Ok(())\n    } else {\n        anyhow::bail!(\n            \"Could not restore native GitHub stack membership after the push failed:\\n{}\",\n            errors.join(\"\\n\")\n        )\n    }\n}\n\nasync fn github_review_bodies(\n    preferred_account: Option<&but_github::GithubAccountIdentifier>,\n    owner: &str,\n    repo: &str,\n    reviews: &[ForgeReviewUpdate],\n    storage: &but_forge_storage::Controller,\n) -> Result<(Vec<Option<Option<String>>>, Vec<String>)> {\n    let mut bodies = Vec::with_capacity(reviews.len());\n    let mut errors = Vec::new();\n    for review in reviews {\n        if review.update_description {\n            bodies.push(Some(review.body.clone()));","sourceCodeStart":2493,"sourceCodeEnd":2529,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-forge/src/review.rs#L2493-L2529","documentation":"During review updates GitButler first dissolves native GitHub 'stack' membership, pushes, then re-registers stacks via `restore_native_stacks()`. Each `but_github::stacks::create()` failure is collected (formatted with `{err:#}` to flatten context chains), and if any failed the aggregate bail fires after the push already failed — so the repo is back on old refs but GitHub's stack graph may still be dissolved. The inner lines name each failing stack's PR list and cause.","triggerScenarios":"A push failure while updating reviews, followed by a rollback where re-creating GitHub stack membership fails too — expired OAuth token, rate limiting, network drop, or GitHub refusing a stack that now contains a closed PR.","commonSituations":"Token revoked or expired mid-session; GitHub API secondary rate limits during rapid push/retry cycles; one PR in the stack closed/merged while the push was in flight; flaky connectivity.","solutions":["Read the inner error lines: 401/403 points to auth (re-authenticate the GitHub account in GitButler), 403 rate-limit says to wait; other codes name the stack/PRs involved.","Re-run the push/review update once auth and network are healthy — successful subsequent updates recreate correct stack membership.","If GitHub still shows dissolved stacks, manually re-stack via the PR UI or `but` forge tooling once all member PRs are open.","Verify no member PR was closed/merged mid-operation; GitHub refuses stacks containing closed PRs (a restorable stack needs >= 2 open members)."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// After the push already failed, a stack-restore failure is best-effort:\n// log it, keep the original push error primary, and surface a follow-up action.\nmatch restore_native_stacks(&user, &info, &stacks, &storage).await {\n    Ok(()) => {}\n    Err(restore_err) => {\n        tracing::warn!(\"{restore_err:#}\");\n        notify_user(\"GitHub stack membership may be dissolved; re-run sync once re-authenticated to repair\");\n        // do NOT mask the original push failure with restore_err\n    }\n}","preventionTips":["Check GitHub auth (valid, unexpired token) before starting push/review update flows at all.","Avoid closing/merging member PRs while a stacked push is in flight; GitHub refuses stacks with closed members.","Handle secondary rate limits: back off before retrying, don't hammer stacks::create after a failed push.","After any occurrence, verify GitHub PR stacking state once connectivity/auth is confirmed and repair by re-running sync."],"tags":["github","pr-stack","push","rollback","api"],"backgroundTag":"github-api-request-failed","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}