ReviewOS

stacks/buddy

Dependency Dashboard duplicates: a transient API failure creates a new dashboard instead of failing the run

#1451
Closed glennmichael123 opened this 11 days ago ·
11 days ago

Summary

createOrUpdateDashboard() has two paths where a transient API failure produces a permanent duplicate Dependency Dashboard instead of a failed run. Both are on main as of 4d3d670.

We hit this on stacksjs/system-cleaner, which accumulated three buddy-bot dashboards (stacksjs/system-cleaner#1 opened 2026-03-30, then stacksjs/system-cleaner#7 and stacksjs/system-cleaner#8 on 2026-04-27, 1h53m apart) alongside one from Renovate. I've since closed the extras and kept one, but the cause is upstream, so it will recur.

It's rare — two misses in about six months, and the surviving dashboard has been updated in place ever since — so this reads as a latent correctness bug rather than anything urgent.

I notice the comment at src/buddy.ts:3645-3657 documents a different duplicate-dashboard cause you've already fixed (labels removed by a maintainer made a dashboard unfindable, hence the body marker). These are two more causes in that same class.

Path 1 — the lookup fails open

src/buddy.ts:3684-3690:

      this.logger.info('No existing dashboard issue found')
      return null
    }
    catch (error) {
      this.logger.warn(`Failed to search for existing dashboard: ${error}`)
      return null
    }

"Searched, found nothing" and "the search threw" return the same value. The caller at :3358 branches solely on truthiness, so it cannot tell them apart.

The only network call inside that try is getIssues(), which rethrows on any failure — src/git/github-provider.ts:2105-2108:

    catch (error) {
      this.logger.error('❌ Failed to get issues:', error)
      throw error
    }

So any 5xx, timeout, socket error or secondary rate-limit on GET /repos/{owner}/{repo}/issues becomes null → "no dashboard exists" → createIssue(). The run still exits 0.

The race-check at :3376 calls the same function, so it fails open in exactly the same way and provides no protection here.

Path 2 — a failed update falls through to create, and the race-check guard is defeated

src/buddy.ts:3360-3388:

if (existingIssue) {
  issue = await this.updateDashboardIssue(gitProvider, existingIssue.number, issueOptions)
}

if (!issue) {
  const raceCheckIssue = await this.findExistingDashboard(gitProvider, dashboardConfig.issueNumber)

  // Skip the race-check result if it is the issue we just found to be missing
  if (raceCheckIssue && raceCheckIssue.number !== existingIssue?.number) { ... }

  if (!issue) {
    issue = await gitProvider.createIssue(issueOptions)   // duplicate
  }
}

updateDashboardIssue() returns Promise<Issue | null> and returns null from its own catch (:3597). So when the dashboard is found but updating it fails transiently:

  1. existingIssue = the real dashboard (say issue N), updateDashboardIssue(N) throws → null
  2. !issue → enter the create branch
  3. race-check finds N again — but the guard is raceCheckIssue.number !== existingIssue?.number, i.e. N !== N → false, so the one issue that would have prevented the create is explicitly skipped
  4. createIssue() → duplicate

The guard's comment says "the issue we just found to be missing", but existingIssue is populated precisely when it was not missing. This path turns a failed update into a duplicate every time.

Suggested fix

The general principle: a failed run is strictly better than a duplicate dashboard.

  1. Make the lookup fail closed. Have findExistingDashboard distinguish found / absent / errored — a discriminated result, or just let the exception propagate — and have createOrUpdateDashboard abort on errored rather than falling through to createIssue.
  2. Don't create when an update failed. Only enter the create branch when existingIssue was genuinely absent. If it was found and the update threw, fail the run and let the next scheduled one retry.
  3. Optionally, reconcile: after creating, close any older marker-bearing dashboards, so a duplicate that does slip through self-heals instead of persisting.

Happy to send a PR if the approach looks right to you.

Environment

  • buddy-bot 0.10.5 installed when observed; verified present on main at 4d3d670 (latest release v0.11.1)
  • Affected repo: stacksjs/system-cleaner

Sign in to comment on this issue.