From aa530a99ba13abc06c44959dd1b95b7c57ce326c Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 13 Aug 2026 14:36:41 +0000 Subject: [PATCH] fix(tcfeed): one unreadable repository must not end the batch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A run of sixteen stopped after three and reported nothing about the thirteen it never reached. haproxy/haproxy has pull requests turned off — it takes patches on a mailing list — so GitHub answers 404 for its pulls endpoint while the repository itself reads normally, and nothing earlier caught it. The "asked once" lookup was the only gh call in prTarget without a guard, so it threw, went past the loop's try around openPr, and ended the run. Two changes, because the second is the one that matters: - the pulls lookup treats 404 as a repository that does not accept pull requests, which is a skip reason and a true one. - the loop wraps prTarget as well as openPr. Every known failure now returns a reason rather than throwing, but prTarget makes a dozen API calls against repositories nobody here controls, and the failure mode when one of them surprises it is losing the rest of the batch silently. One repository is allowed to be unreadable. A run is not allowed to end because of it. Verified: haproxy alone skips with the reason, and haproxy followed by another repository no longer stops before reaching it. Co-Authored-By: Claude Opus 5 (1M context) --- bin/tcfeed.ts | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/bin/tcfeed.ts b/bin/tcfeed.ts index f8834d32..6607b961 100755 --- a/bin/tcfeed.ts +++ b/bin/tcfeed.ts @@ -751,9 +751,22 @@ async function prTarget( // Asked once. The pull request says "closing it is the right answer and I // will not send another", and this is the line that keeps that true — state // is `all`, so a closed request counts. Nothing about "no" expires. - const asked = JSON.parse( - await gh(['api', `repos/${repo}/pulls?state=all&head=${me}:${PR_BRANCH}&per_page=1`]) - ) as { html_url: string; state: string }[]; + // + // A 404 here is not an error to propagate. GitHub returns one for the pulls + // endpoint of a repository that has pull requests turned off — haproxy does, + // because it takes patches on a mailing list — and the repository itself + // answers normally, so nothing earlier catches it. Unguarded, that one repo + // threw out of prTarget, past the loop's try around openPr, and ended the + // whole run: a batch of sixteen stopped after three and said nothing about + // the thirteen it never reached. + const said = await gh([ + 'api', + `repos/${repo}/pulls?state=all&head=${me}:${PR_BRANCH}&per_page=1`, + ]).catch((error: unknown) => (/404|Not Found/i.test(why(error)) ? '404' : '')); + if (said === '404') return 'does not accept pull requests'; + if (!said) return 'could not read its pull requests'; + + const asked = JSON.parse(said) as { html_url: string; state: string }[]; if (asked.length > 0) return `already asked — ${asked[0].state}, ${asked[0].html_url}`; // The same promise, on the other channel. Now that the question goes as an @@ -1448,7 +1461,19 @@ async function prCommand(argv: string[], cache: string): Promise { let opened = 0; const landed: { repo: string; pr: string }[] = []; for (const repo of repos) { - const target = await prTarget(repo, me); + // Belt as well as braces. Every known way prTarget can fail now returns a + // reason rather than throwing, but it makes a dozen API calls against + // repositories nobody here controls, and the failure mode when one of them + // surprises it is losing the rest of the batch silently. One repository is + // allowed to be unreadable; a run is not allowed to end because of it. + let target: Awaited>; + try { + target = await prTarget(repo, me); + } catch (error) { + console.log(`· ${repo} — skipped: could not read it (${why(error)})`); + continue; + } + if (typeof target === 'string') { console.log(`· ${repo} — skipped: ${target}`); continue;