Skip to content

fix(actuation): classify a 401 merge failure as terminal, not transient - #2408

Merged
JSONbored merged 1 commit into
mainfrom
claude/merge-401-terminal-classify
Jul 1, 2026
Merged

fix(actuation): classify a 401 merge failure as terminal, not transient#2408
JSONbored merged 1 commit into
mainfrom
claude/merge-401-terminal-classify

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

What

classifyMergeFailure decides whether a failed merge attempt is worth retrying (up to MERGE_RETRY_CAP sweep cycles) or should be held for a human. A 401 (Bad credentials) from the merge mutation had no explicit branch, so it fell through to the generic terminal: false case.

Fix

withInstallationTokenRetry (src/github/app.ts) already evicts the cached installation token and retries once on a 401 inside the merge call itself. So a 401 reaching classifyMergeFailure means that retry already failed — a persistently unauthorized installation (the App was suspended, or its private key was rotated), not a one-off stale-token race that a re-attempt would fix.

Previously this burned the full MERGE_RETRY_CAP of 5 sweep-cycle retries against the same known-bad credential before finally holding — wasted calls against an installation that cannot succeed until a human re-consents or restores the key.

Added an explicit 401 branch returning terminal: true with a distinct "installation token rejected" reason, positioned before the 403 branch. Updated the top-of-file doc comment to document the new case alongside the existing 403/405/409/conflict cases.

Tests

  • New test: a 401 is classified terminal: true with a reason matching "installation token rejected" / "suspended or key rotated", distinct from the existing 403/409 assertions.
  • npx tsc --noEmit clean.
  • npx vitest run test/unit/merge-failure.test.ts — 6/6 pass.
  • Diff-range coverage check: src/services/merge-failure.ts fully covered, zero gaps.
  • Regression sweep: npx vitest run test/unit/agent-action-executor.test.ts — 29/29 pass.
  • Full unsharded npm run test:coverage — 309 files / 5658 tests pass.
  • npm audit --audit-level=moderate — 0 vulnerabilities.

Advances #1936. Closes #2264.

withInstallationTokenRetry already evicts-and-retries once on a 401 inside
the merge call itself, so a 401 reaching classifyMergeFailure means that
retry also failed — a persistently unauthorized installation (App
suspended or key rotated), not a one-off stale-token race. Previously this
fell through to the generic terminal:false branch, burning the full
MERGE_RETRY_CAP of 5 sweep-cycle retries against the same known-bad
credential before finally holding.

Add an explicit 401 branch returning terminal:true with a distinct
"installation token rejected" reason, positioned before the 403 branch.
@dosubot dosubot Bot added the size:XS label Jul 1, 2026
@loopover-orb

loopover-orb Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Warning

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏸️ Gittensory review result - manual review recommended

Review updated: 2026-07-01 20:05:15 UTC

2 files · 1 AI reviewer · no blockers · readiness 93/100 · CI green · clean

⏸️ Suggested Action - Manual Review

  • Touches a guarded path — held for manual review

Review summary
This change makes `classifyMergeFailure` treat HTTP 401 as a terminal merge failure before the existing 403/405/409 handling, which matches the stated contract that the merge path has already retried a rejected installation token once. The added unit test drives the real classifier with an Octokit-style `.status` error and checks for the new distinct reason text. The visible diff is narrow and preserves the existing transient 405 handling and generic transient fallback.

Nits — 6 non-blocking
  • nit: `src/services/merge-failure.ts:22` leaves `TERMINAL_MERGE_STATUSES` excluding 401 while the file-level comment now lists 401 as terminal, so a short note that 401 is handled separately for a more specific reason would reduce future drift.
  • nit: `test/unit/merge-failure.test.ts:26` verifies the new reason wording but not that the persisted audit reason includes the HTTP status, even though `src/services/merge-failure.ts:48` intentionally includes `(401)`.
  • Add an expectation in `test/unit/merge-failure.test.ts:26` for `/401/` so the status stays present in the human-facing reason.
  • Add a brief inline comment near `src/services/merge-failure.ts:22` or the 401 branch explaining why 401 is not part of `TERMINAL_MERGE_STATUSES`.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.
  • Touches a guarded path — held for manual review — A maintainer must review and merge this change.
Signal Result Evidence
Code review ✅ No blockers 1 reviewer
Linked issue ✅ Linked #2264
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (size label size:XS; 1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 67 registered-repo PR(s), 57 merged, 589 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 67 PR(s), 589 issue(s).
Gate result ⚠️ Not blocking Advisory; not blocking this PR.
Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository registration is not available in the local Gittensory cache.
  • Public profile languages: not available
  • Official Gittensor activity: 67 PR(s), 589 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Triage stale or unlinked PRs.
  • No action.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.

  • Re-run Gittensory review

@loopover-orb loopover-orb Bot added gittensor gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. labels Jul 1, 2026
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.75%. Comparing base (ac00991) to head (2face98).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2408   +/-   ##
=======================================
  Coverage   95.75%   95.75%           
=======================================
  Files         223      223           
  Lines       24812    24813    +1     
  Branches     9004     9005    +1     
=======================================
+ Hits        23758    23759    +1     
  Misses        432      432           
  Partials      622      622           
Files with missing lines Coverage Δ
src/services/merge-failure.ts 93.75% <100.00%> (+0.41%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JSONbored JSONbored self-assigned this Jul 1, 2026
@JSONbored
JSONbored merged commit 28ae561 into main Jul 1, 2026
12 checks passed
@JSONbored
JSONbored deleted the claude/merge-401-terminal-classify branch July 1, 2026 22:13
@github-project-automation github-project-automation Bot moved this from Todo to Done in gittensory - v1 roadmap Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

fix(github): a 401 on merge is misclassified as transient, burning retries before failing fast

1 participant