Skip to content

Make Refresh all bring the board to GitHub's state - #510

Open
jarstelfox wants to merge 4 commits into
masterfrom
refresh-all-matches-github
Open

jarstelfox wants to merge 4 commits into
masterfrom
refresh-all-matches-github

Conversation

@jarstelfox

Copy link
Copy Markdown
Member

🤖

Connects to #502. Slack thread on the Sep 25 to 30 gap

From Sep 25 to Sep 30, Pulldasher's database missed every update GitHub sent. By Oct 2 the board was missing 76 PRs and still showed 34 closed ones as open. Refresh all couldn't fix that. It only re-reads PRs the board already shows as open, and doing all of them costs more GitHub calls than Pulldasher gets in an hour. Now one press checks the whole board against GitHub in about 30 seconds, then re-reads only the PRs that differ.

Changes

  • Refresh all: runs on the server now, one press at a time, instead of one re-read per open PR from the browser. Every board shows its progress.
  • Re-reads: 8 GitHub calls instead of 10 to 21, because CI comes from one call, not one per workflow run.

Numbers

One press Before After
Board matches GitHub ~5,400 REST calls, hours 29 GraphQL + 16 REST calls, ~40 s
After the Sep 25 to 30 gap (at least 110 PRs wrong) ~5,400, and the 76 missing stay missing 29 GraphQL + 880 REST calls, 7 to 13 min
How these were measured

All read-only with a personal token on Oct 2, so the bot's quota was never touched. GraphQL calls spend a separate hourly budget from the REST calls webhooks use.

Re-reads, old code then new, back to back on the same PRs:

Repo (PRs sampled) Calls per PR ms per PR
ifixit (15) 20.9 to 8.0 1,134 to 557
ops (3) 17.7 to 8.0 1,120 to 596
fixbot (3) 19.3 to 8.0 1,480 to 538
ifixit-schooner-fw (3) 17.0 to 8.0 1,451 to 513
server-templates (3) 14.0 to 8.0 1,048 to 514
Product-Development (3) 10.3 to 8.0 792 to 518
ifixit-cli (3) 13.0 to 8.0 1,374 to 528

All 33 PRs got the same checks, labels, comments and reviews both ways. If GitHub refuses check runs, as it does for a fine-grained token (1accc56c), re-reads fall back to the old calls: 20.1 per iFixit/ifixit PR.

Before, a full press: GitHub listed 251 open PRs in 23 repos. At the rates above, and 15.2 for the 16 repos not sampled, that's 4,515 calls. The board held 303 open at the Oct 2 deploy, against GitHub's 251 that evening, so a press cost about 5,400: more than the token's 5,000 an hour, which webhooks share.

After, the check: listing the 25 repos (23 with open PRs, 2 with only recent closes) took 29 GraphQL calls and 33 s, for 251 open and 650 closed PRs. At 23:16Z, 2 open PRs had a check running. On the 15 iFixit/ifixit PRs, GraphQL's comment and review counts and labels matched what a re-read stores, so a healthy board doesn't trip them.

After, every open PR differing: 251 re-reads at 8 calls is 2,008 REST calls, plus the 29 GraphQL.

After, end to end: a real press with a board of 6 Product-Development and ifixit-cli PRs, the DB stubbed out, re-read none of those 6 and all 42 that were missing (16 open, 26 closed in 14 days). It made 2 GraphQL and 336 REST calls in 53 s.

Merges: a trial merge into projectdasher, its timer taken, passes all 168 backend and 736 frontend tests. git merge-tree with #503's branch finds no conflicts.

Pacing: the pacer spreads re-reads so the REST quota left lasts until it resets. With 4,000 calls left and 45 minutes to the reset, the gap press takes about 13 minutes; with 4,500 left and 30 minutes, about 7.

Decisions for you

  • Repos: config.repos plus every repo with a PR on the board, as you picked.
  • Closed cutoff: 14 days, the board's own window for closed PRs, instead of 7.
  • Left out: CI re-runs that finish while webhooks are down, one GraphQL call for several repos, a timeout.
Why these
  • Repos: a repo whose PRs were all missed stays invisible. Re-read pulls updated in the last 20 minutes #503's org-wide search would find it.
  • Closed cutoff: a PR closed 10 days ago shows on the board, so a press should catch it missing. In iFixit/ifixit that window holds 339 closed PRs, 4 pages.
  • CI re-runs: a check that goes from failed to passed while webhooks are down stays red. The row's refresh button fixes it. Catching it on every press costs a call per open PR, or GraphQL check runs a fine-grained token may not read.
  • One call for several repos: Octokit spaces GraphQL calls a second apart, so the 29 calls set most of the 33 s check. Fewer calls would mean fewer gaps.
  • Timeout: a GitHub call that never answers keeps the button disabled until a restart. No refresh in Pulldasher has a timeout today.

QA

  • Run npm test on Node 24 and confirm 86 pass.
  • Run npm --prefix frontend-v2 run dev:dummy and press Refresh all in Settings > Advanced.
  • Confirm it reads "checking GitHub…", then "refreshing 1 of 4…", then "refreshed 3 · 1 failed".
  • After a deploy to dev, press Refresh all and find listed pulls differ from the board in the container log.
  • Open a second board during that press and confirm it shows the same progress.
  • Search the log for Check runs refused. It means the token is fine-grained, so re-reads cost about 20 calls, not 8.

Note

The hourly repair comes over from projectdasher unchanged (51a635f8). Merging into projectdasher conflicts only in its timer in app.js: keep projectdasher's side. #503 merges cleanly.

🤖 Generated with Claude Code

jarstelfox and others added 4 commits October 2, 2026 15:50
A PR whose close webhook gets lost stays open on the board until the
next restart, because #501 repairs it only at startup. Every backlog
number counts those PRs: on 2026-09-29 the board listed 314 open PRs,
and 67 of them were already merged or closed on GitHub.

Now the server lists each tracked repo's open pulls once an hour and
refreshes only the ones the DB has wrong: open on GitHub but not open
in the DB (a lost `opened` or `reopened`), and open in the DB but
missing from the listing (a lost `closed`, repaired by #501's own
refreshStaleOpenPulls). The listing costs one API call per 30 open
pulls in each repo, and an hour with nothing wrong refreshes nothing.

This is option 1 from #501's list, which went with startup only. The
startup refresh still refetches every open pull; this doesn't.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A full refresh of an iFixit/ifixit pull makes about 21 GitHub calls,
and most of them list jobs: one call per workflow run on the head
commit. On 15 open pulls that was 176 of 314 calls. Refresh all runs
a full refresh for every open pull, so this is most of what a press
costs.

checks.listForRef lists every check run on the commit in one call.
1accc56 moved off it in 2023 because fine-grained tokens can't call
it. GitHub's list of the endpoints they can call still leaves it out:
https://docs.github.com/en/rest/authentication/endpoints-available-for-fine-grained-personal-access-tokens
Nobody could say which kind of token the bot has, so the first 403
saying "not accessible" switches the process back to runs and jobs.

Two smaller savings: the pull already has its labels, so the
issues.get call is gone, and every list asks for 100 items a page.

Measured with a personal token on the same pulls, old code then new:

  repo (pulls sampled)       calls per pull    ms per pull
  ifixit (15)                20.9 to 8.0       1134 to 557
  ops (3)                    17.7 to 8.0       1120 to 596
  fixbot (3)                 19.3 to 8.0       1480 to 538
  ifixit-schooner-fw (3)     17.0 to 8.0       1451 to 513
  server-templates (3)       14.0 to 8.0       1048 to 514
  Product-Development (3)    10.3 to 8.0        792 to 518
  ifixit-cli (3)             13.0 to 8.0       1374 to 528

With check runs refused, iFixit/ifixit takes 20.1 calls. On all 33
pulls the board gets the same checks, labels, comments and reviews.
Three pulls' CI moved between the two runs; listed at one moment,
check runs and jobs agreed on all 22, 24 and 47 of their checks.

Note: check runs include checks from apps other than Actions, which
the jobs path never read. None of the 33 pulls has one, and the
check_run webhook already saves them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From Sep 25 to Sep 30, prod's container wrote into the dev database,
most likely because a deploy started it with the dev env file. Prod's
own database never saw those webhooks. By Oct 2, 76 PRs opened in the
window were missing from the board and 34 that closed then still read
open: https://ifixit.slack.com/archives/C6YD8UBS7/p1790970090546029

Refresh all couldn't fix either. The browser sent one refresh per open
PR it already showed, so it never found a missing one, and each was a
full refresh. For the 303 open PRs the board held on Oct 2 that's about
5,400 GitHub calls at the rates in the previous commit, more than the
token's 5,000 an hour. A press took hours, and the refreshes webhooks
start waited behind it in the same queue.

Now a press is one job on the server, one at a time:

1. List every open PR, and every PR closed within the board's 14 days.
2. Compare with the board, and refetch only the PRs that differ:
   - missing from the board, or open on it but not on GitHub
   - a different head, draft state or labels
   - more comments or reviews on GitHub than on the board
   - an updated_at newer than anything the board knows
   - a CR or QA stamp standing from before the head commit
   - a check the board still shows running
3. Refetch them one at a time behind a pacer that spaces them by the
   quota left. Webhook refreshes keep their own queue, so they never
   wait behind a press. Every board hears how far it got.

It lists the configured repos plus every repo with a PR on the board.
Webhooks come in for the whole org, so the board holds PRs from repos
the config doesn't list: 53 of the 76 missing were in ops.

Why counts and not only updated_at: comment and review webhooks never
write the pull's row, though GitHub's updated_at moves with them. 14 of
101 open iFixit/ifixit PRs have a comment or review as their newest
activity. A check against the row alone would refetch those on every
press, so the newest comment and review count as known too. And a
timestamp can't see a missed comment once a newer one came in, which
is how stamps went missing in #502. The counts can.

The listing is GraphQL. One call returns 100 PRs with their counts,
and its points come from a separate hourly budget from the REST calls
webhooks spend. Octokit runs GraphQL calls one at a time, a second
apart, in the same lane as a claim's review request. So a press lists
one repo at a time, with one query for both lists. The pacer now skips
GraphQL's rate-limit headers, which it would have read as REST's.

Measured with a personal token on Oct 2, about 23:15Z, listing all 25
repos took 29 GraphQL calls and 33 seconds, for 251 open and 650
closed PRs. 2 open PRs had a check running then. So a press on a board
that matches GitHub costs 29 GraphQL calls and 2 refetches. A real
press with a 6-PR board and the DB stubbed out refetched none of the
6 and all 42 that were missing, at 8 REST calls each.

Note: a check re-run that finishes while webhooks are down, failed to
passed, isn't caught; the row's refresh button or the next push fixes
it. Catching it would mean listing every open PR's checks, a REST call
each, or GraphQL check runs a fine-grained token may not read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit moved Refresh all to the server. The button now
sends one refreshAll event instead of a refresh per open PR. The board
takes the press's progress from the server, where it used to count the
pullChange events that came back. Every open board shows the same
press, and the button waits while one runs.

The status line says what the press is doing: "checking GitHub…",
then "refreshing 3 of 12…". Then it says what the press did, like
"refreshed 11 · 1 failed", or "up to date" when nothing differed. It stays 6 seconds,
then clears. The header shows the same words, from one function.

The dummy board plays a press of 4 PRs with one failing, so every
state shows without GitHub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant