From a3a2ac9387456d461d4b00c8b999a061f77558a0 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 13 Aug 2026 13:50:59 +0000 Subject: [PATCH] feat(tcfeed): refresh open requests to the current pack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pack is read live from sh1pt precisely so a corrected workflow reaches repositories, and that was only ever true of requests not yet sent. Thirty-three went out carrying `npm install -g "…@latest"`, the pack was fixed an hour later, and all twenty-four still open kept showing the defective version to anyone who looked at them. Three reviewers found it independently, which is as clear a signal as this gets. SonarCloud failed a quality gate on it (githubactions:S8543). CodeRabbit scored a request Moderate for "an unpinned scanner with access to a write-scoped job". Haven's maintainer declined on exactly that ground — "whoever can publish that package can run code in this repository's CI from that point on, forever, without a further PR" — while noting the project hash-pins thirteen tarballs in its ffmpeg stack alone. That is a maintainer applying their own published bar to us and finding us under it. `check --refresh` rewrites the two files on every open request to whatever the pack currently renders, and says "already current" when there is nothing to do, so it can be run after any pack change without churning commits. Written against the contents API rather than a clone: the open requests include seastar and lightning, and cloning a kernel-sized repository to rewrite two files under .github is minutes of transfer for a diff that fits on a screen. It also cannot carry anything else along by accident, which on somebody else's review is the more important half. Co-Authored-By: Claude Opus 5 (1M context) --- bin/tcfeed.ts | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/bin/tcfeed.ts b/bin/tcfeed.ts index 82affd2..90517b7 100755 --- a/bin/tcfeed.ts +++ b/bin/tcfeed.ts @@ -1718,12 +1718,99 @@ async function pushFix(repo: string, me: string, remedy: Remedy): Promise { + const open = JSON.parse( + await gh(['api', `repos/${repo}/pulls?state=open&head=${me}:${PR_BRANCH}&per_page=1`]) + ) as { number: number }[]; + if (open.length === 0) return 'no open request'; + + const pack = packDir(); + const inputs = packInputs(spec); + const want = [ + { + path: OUR_WORKFLOW, + content: render(fs.readFileSync(path.join(pack, 'workflow.yml'), 'utf8'), inputs), + }, + { + path: '.github/scripts/threatcrush-to-sarif.py', + content: fs.readFileSync(path.join(pack, 'threatcrush-to-sarif.py'), 'utf8'), + }, + ]; + + const fork = `${me}/${repo.split('/')[1]}`; + const changed: string[] = []; + + for (const file of want) { + const said = await gh([ + 'api', + `repos/${fork}/contents/${file.path}?ref=${PR_BRANCH}`, + '--jq', + '{sha: .sha, content: .content}', + ]).catch(() => ''); + if (!said) return `cannot read ${file.path} on ${fork}`; + + const have = JSON.parse(said) as { sha: string; content: string }; + // The API wraps base64 at 60 columns; decoding without stripping the + // newlines yields a string that never equals the file and rewrites both + // files on every run. + const current = Buffer.from(have.content.replace(/\n/g, ''), 'base64').toString('utf8'); + if (current === file.content) continue; + + await gh([ + 'api', + '-X', + 'PUT', + `repos/${fork}/contents/${file.path}`, + '-f', + `message=ci: update the ThreatCrush scan workflow to the reviewed pack`, + '-f', + `content=${Buffer.from(file.content, 'utf8').toString('base64')}`, + '-f', + `sha=${have.sha}`, + '-f', + `branch=${PR_BRANCH}`, + ]); + changed.push(path.basename(file.path)); + } + + return changed.length === 0 ? 'already current' : `updated ${changed.join(', ')}`; +} + /** * The subcommand. With no arguments it reads every repository this has an open * request on, which is the list it is entitled to act on and no wider. */ async function checkCommand(argv: string[]): Promise { const fix = argv.includes('--fix'); + const refresh = argv.includes('--refresh'); const named = argv.filter((arg) => !arg.startsWith('-')); for (const tool of ['git', 'gh']) { @@ -1759,7 +1846,21 @@ async function checkCommand(argv: string[]): Promise { return 0; } + // Resolved once, and only when it is going to be used: --refresh is the only + // path that renders the pack, and `check` should keep working on a machine + // that has no sh1pt checkout beside it. + const spec = refresh ? await resolveSpec() : ''; + for (const repo of repos) { + if (refresh) { + try { + console.log(`· ${repo} — ${await refreshOne(repo, me, spec)}`); + } catch (error) { + console.log(`· ${repo} — could not refresh: ${why(error)}`); + } + continue; + } + try { await checkOne(repo, me, fix); } catch (error) { @@ -1821,6 +1922,7 @@ async function main(): Promise { console.log(' --no-issue skip the question, open the request alone'); console.log(' --no-follow skip waiting on their checks afterwards'); console.log(' tcfeed check [owner/name ...] [--fix] how are the open requests doing'); + console.log(' --refresh bring open requests up to the current pack'); console.log(' tcfeed rss [list|add|remove] [url ...] feeds to read alongside reddit'); return 0; }