From 732d6aaa3f5ca60f03c0fc1fbb3bf292226b1983 Mon Sep 17 00:00:00 2001 From: jorisstrakeljahn Date: Sun, 2 Aug 2026 16:33:37 +0200 Subject: [PATCH] ci: Aggregate collaborative launch result hashes --- .github/workflows/hash-aggregator.yml | 124 ++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 .github/workflows/hash-aggregator.yml diff --git a/.github/workflows/hash-aggregator.yml b/.github/workflows/hash-aggregator.yml new file mode 100644 index 0000000000..d24ae244f4 --- /dev/null +++ b/.github/workflows/hash-aggregator.yml @@ -0,0 +1,124 @@ +name: Aggregate Collaborative Hashes + +on: + issue_comment: + types: [created, edited, deleted] + issues: + types: [opened, edited] + workflow_dispatch: + inputs: + issue_number: + description: Issue number to refresh + required: true + type: string + +concurrency: + group: hash-agg-${{ github.event.issue.number || github.event.inputs.issue_number }} + cancel-in-progress: true + +jobs: + update: + if: >- + github.event_name == 'workflow_dispatch' || + startsWith(github.event.issue.title, 'Collaborative Launch') + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/github-script@v7 + with: + script: | + const MAGIC = ''; + const SKIP = ''; + const BOT = 'github-actions[bot]'; + const { owner, repo } = context.repo; + + // DrahtBot-style: ignore the bot's own comments (avoids edit loops) + if (context.eventName === 'issue_comment' && + context.payload.comment.user.login === BOT) { + return; + } + + function extractHash(line) { + if (line.trimStart().startsWith('>')) return null; + // Strip backticks/bold/italic wrappers; keyword is case-insensitive + const s = line.replace(/[`*_]+/g, '').trim(); + const m = s.match(/^hash:\s*([0-9a-fA-F]{64})\s*$/i); + return m ? m[1].toLowerCase() : null; + } + + function userLink(login) { + return `[${login}](https://github.com/${login})`; + } + + let issue_number, title, state; + if (context.eventName === 'workflow_dispatch') { + issue_number = Number(context.payload.inputs.issue_number); + const issue = (await github.rest.issues.get({ owner, repo, issue_number })).data; + title = issue.title; + state = issue.state; + } else { + issue_number = context.issue.number; + title = context.payload.issue.title; + state = context.payload.issue.state; + } + if (!title.startsWith('Collaborative Launch')) return; + if (state !== 'open') return; + + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number, per_page: 100, + }); + + // Per user: set of distinct hashes across all their comments + const userHashes = new Map(); + for (const c of comments) { + const body = c.body || ''; + if (body.includes(MAGIC) || body.includes(SKIP)) continue; + const login = c.user.login; + if (!userHashes.has(login)) userHashes.set(login, new Set()); + for (const line of body.split('\n')) { + const hash = extractHash(line); + if (hash) userHashes.get(login).add(hash); + } + } + + const byHash = new Map(); + for (const [user, hashes] of userHashes) { + for (const hash of hashes) { + if (!byHash.has(hash)) byHash.set(hash, []); + byHash.get(hash).push(user); + } + } + const rows = [...byHash.entries()] + .map(([hash, users]) => ({ hash, users: users.sort() })) + .sort((a, b) => b.users.length - a.users.length || a.hash.localeCompare(b.hash)); + + const n = [...userHashes.values()].filter(s => s.size > 0).length; + let table = '| hash | users | share of matches |\n| --- | --- | --- |\n'; + if (!rows.length) { + table += '| _none_ | | |\n'; + } else { + for (const r of rows) { + table += `| \`${r.hash}\` | ${r.users.map(userLink).join(', ')} | ${r.users.length}/${n} |\n`; + } + } + const body = [ + MAGIC, + '### Collaborative Launch Hashes', + '', + table, + '', + 'Report with one line `HASH: ` per result.', + 'If a comment should be ignored by this table, copy-paste <!--meta-tag:bot-skip--> into it.', + '', + ].join('\n'); + + // Only treat the summary as magic. Skip API write if unchanged. + const magic = comments.find(c => + c.user.login === BOT && (c.body || '').includes(MAGIC)); + if (magic) { + if (magic.body === body) return; + await github.rest.issues.updateComment({ owner, repo, comment_id: magic.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + }