name: Comment Cop # Flags multi-line code comments added in src/ by claude-labeled PRs and # asks for them to be deleted. Groups containing a SAFETY: marker are # skipped. Runs entirely against the GitHub API (no checkout of PR code). on: pull_request_target: types: [opened, synchronize, reopened, labeled] permissions: contents: read pull-requests: write jobs: comment-cop: if: > github.repository == 'oven-sh/bun' && contains(github.event.pull_request.labels.*.name, 'claude') && (github.event.action != 'labeled' || github.event.label.name == 'claude') runs-on: ubuntu-latest timeout-minutes: 5 concurrency: group: comment-cop-${{ github.event.pull_request.number }} cancel-in-progress: true steps: - name: Scan diff for added multi-line comments uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const crypto = require('crypto'); const owner = context.repo.owner; const repo = context.repo.repo; const pull_number = context.payload.pull_request.number; const headSha = context.payload.pull_request.head.sha; const SRC_EXT = /\.(rs|c|cc|cpp|h|hpp|m|mm|ts|tsx|mts|cts|js|jsx|mjs|cjs)$/; const MIN_LINES = 2; function isCommentLine(line) { const t = line.trimStart(); if (t.startsWith('//')) return true; if (t.startsWith('/*')) return true; if (t === '*' || t === '*/' || t.startsWith('* ')) return true; return false; } function groupsFromPatch(path, patch) { const out = []; let newLine = 0; let cur = null; const flush = () => { if (cur && cur.lines.length >= MIN_LINES) { const text = cur.lines.join('\n'); if (!/SAFETY:/.test(text)) out.push({ path, start: cur.start, end: cur.end, text }); } cur = null; }; for (const raw of patch.split('\n')) { if (raw.startsWith('@@')) { flush(); const m = /\+(\d+)/.exec(raw); newLine = m ? parseInt(m[1], 10) : 1; } else if (raw.startsWith('+')) { const content = raw.slice(1); if (isCommentLine(content)) { if (cur) { cur.end = newLine; cur.lines.push(content); } else { cur = { start: newLine, end: newLine, lines: [content] }; } } else { flush(); } newLine++; } else if (raw.startsWith('-')) { flush(); } else if (raw.startsWith('\\')) { // "\ No newline at end of file" } else { // context line (leading space, or blank) flush(); newLine++; } } flush(); return out; } const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100, }); const groups = []; for (const f of files) { if (f.status === 'removed') continue; if (!f.filename.startsWith('src/')) continue; if (!SRC_EXT.test(f.filename)) continue; if (!f.patch) continue; for (const g of groupsFromPatch(f.filename, f.patch)) groups.push(g); } const keyFor = g => `${g.path}:${crypto.createHash('sha256').update(g.text).digest('hex').slice(0, 12)}`; const presentKeys = new Set(groups.map(keyFor)); // Fetch existing comment-cop review threads (for dedup + auto-resolve). const threads = []; { const q = ` query($owner: String!, $repo: String!, $pr: Int!, $after: String) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { reviewThreads(first: 100, after: $after) { pageInfo { hasNextPage endCursor } nodes { id isResolved comments(first: 1) { nodes { body } } } } } } }`; let after = null; for (;;) { const res = await github.graphql(q, { owner, repo, pr: pull_number, after }); const page = res.repository.pullRequest.reviewThreads; for (const t of page.nodes) threads.push(t); if (!page.pageInfo.hasNextPage) break; after = page.pageInfo.endCursor; } } const seenKeys = new Set(); const toResolve = []; for (const t of threads) { const body = t.comments.nodes[0]?.body || ''; const m = //.exec(body); if (!m) continue; const key = m[1]; seenKeys.add(key); if (!t.isResolved && !presentKeys.has(key)) toResolve.push(t.id); } // Auto-resolve threads whose flagged block is gone from the current diff. if (toResolve.length > 0) { const mut = ` mutation($id: ID!) { resolveReviewThread(input: { threadId: $id }) { thread { id } } }`; for (const id of toResolve) { try { await github.graphql(mut, { id }); } catch (e) { core.warning(`resolveReviewThread failed for ${id}: ${e.message}`); } } core.info(`Resolved ${toResolve.length} stale comment-cop thread(s).`); } // Post new line comments for groups not already flagged. const fresh = groups.filter(g => !seenKeys.has(keyFor(g))); if (fresh.length === 0) { core.info(`No new comment groups to flag (${groups.length} present, all already flagged).`); return; } const bodyFor = g => `\n` + `If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code\n\n`; let posted = 0; for (const g of fresh) { const params = { owner, repo, pull_number, commit_id: headSha, path: g.path, line: g.end, side: 'RIGHT', body: bodyFor(g), }; if (g.start < g.end) { params.start_line = g.start; params.start_side = 'RIGHT'; } try { await github.rest.pulls.createReviewComment(params); posted++; } catch (e) { core.warning(`createReviewComment failed for ${g.path}:${g.start}-${g.end}: ${e.message}`); } } core.info(`Posted ${posted} review comment(s).`);