Make Refresh all bring the board to GitHub's state - #510
Open
jarstelfox wants to merge 4 commits into
Open
jarstelfox wants to merge 4 commits into
jarstelfox wants to merge 4 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖
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
Numbers
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:
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-treewith #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
Why these
QA
npm teston Node 24 and confirm 86 pass.npm --prefix frontend-v2 run dev:dummyand press Refresh all in Settings > Advanced.listed pulls differ from the boardin the container log.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
projectdasherconflicts only in its timer inapp.js: keep projectdasher's side. #503 merges cleanly.🤖 Generated with Claude Code