fix(queue): opportunistically refresh installation health on a PR-write 403 - #2410
Conversation
…te 403 The local installations.permissions snapshot is read once at the top of each sweep/webhook pass and self-heals only via a maintainer-consented permission-upgrade webhook (GitHub sends none for a downgrade) or the ~30-minute health-refresh cron. A maintainer-initiated downgrade of pull_requests write access can therefore go undetected by the readiness gate for up to 30 minutes, during which the executor keeps attempting merge/close/review/update_branch calls that GitHub correctly rejects with 403 — wasted attempts, not a security gap (GitHub's own server-side enforcement is already the real backstop). When a PR-write mutation fails with a 403, opportunistically trigger a per-installation health refresh so the DB row (and therefore every later sweep/webhook read of it, for this or any other PR on the installation) self-heals immediately instead of waiting for the next cron tick. Exports the existing githubErrorStatus helper from github/app.ts to detect the 403 without duplicating status-reading logic.
|
Warning 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨 ⏸️ Gittensory review result - manual review recommendedReview updated: 2026-07-02 01:07:13 UTC
⏸️ Suggested Action - Manual Review
Review summary Nits — 3 non-blocking
Review context
Contributor next steps
Signal definitions
🟩 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.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2410 +/- ##
=======================================
Coverage 95.90% 95.90%
=======================================
Files 224 224
Lines 25217 25219 +2
Branches 9169 9170 +1
=======================================
+ Hits 24185 24187 +2
Misses 419 419
Partials 613 613
🚀 New features to boost your workflow:
|
…ealth-refresh-on-403 # Conflicts: # src/services/agent-action-executor.ts # test/unit/agent-action-executor.test.ts
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
gittensory-ui | a3f42e7 | Commit Preview URL Branch Preview URL |
Jul 02 2026, 01:01 AM |
What
installationPermissionsis read once from theinstallationstable at the top of the action loop and threaded through the whole pass. That DB row only self-heals via a consented permission-upgrade webhook (GitHub sends none for a maintainer-initiated downgrade) or the ~30-minute health-refresh cron. A live downgrade ofpull_requestswrite access can therefore go undetected by the readiness gate for up to 30 minutes — during which the executor keeps attempting merge/close/review/update_branch calls that GitHub correctly rejects with 403.This is a wasted-attempt/staleness gap, not an authorization bypass: GitHub enforces the live scope server-side on the actual mutation and returns 403, which the existing terminal-403 handling already catches and holds correctly.
Fix
When a PR-write mutation (
request_changes/approve/merge/close/update_branch) fails with a 403, opportunistically callrefreshInstallationHealthForInstallation(already used by the 30-minute cron) so the DB row self-heals immediately instead of waiting for the next tick — every later sweep/webhook read of it, for this or any other PR on the installation, then reflects the corrected permissions. A failed refresh is swallowed (best-effort; GitHub's own enforcement is already the real backstop).Exported the existing
githubErrorStatushelper fromgithub/app.tsto detect the 403 rather than duplicating status-reading logic.Tests
refreshInstallationHealthForInstallation(env, installationId).label(usesissues:write, notpull_requests:write— not in the write-permission gate) does NOT trigger the refresh.npx tsc --noEmitclean.npx vitest run test/unit/agent-action-executor.test.ts— 33/33 pass.npm run test:coverage— 309 files / 5661 tests pass.npm audit --audit-level=moderate— 0 vulnerabilities.Advances #1936. Closes #2265.