Repository navigation
test: cover weekly issue update helpers #280
Workflow file for this run
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
| name: PR Review Ping | ||
|
Check warning on line 1 in .github/workflows/pr-review-ping.yml
|
||
| # Comments on every new PR, tagging only the CODEOWNERS whose owned paths the | ||
| # PR actually touches - the same routing CODEOWNERS itself does, surfaced as | ||
| # a notification for when GitHub's own auto-request doesn't fire (e.g. a | ||
| # code owner hasn't accepted their collaborator invite yet). This is a | ||
| # notification only: it does not request review, assign anyone, or gate | ||
| # merging - see .github/CODEOWNERS and branch protection for that. | ||
| # | ||
| # Reviewers are computed from .github/CODEOWNERS at runtime instead of a | ||
| # hardcoded list, so they can't drift out of sync with it the way issue #273 | ||
| # found they had; and each owner is only pinged for PRs touching a path they | ||
| # actually own (last-matching CODEOWNERS pattern wins per file, mirroring | ||
| # GitHub's own CODEOWNERS matching), not every PR in the repo. | ||
| on: | ||
| pull_request_target: | ||
| types: [opened] | ||
| permissions: | ||
| pull-requests: write | ||
| jobs: | ||
| ping: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
| steps: | ||
| - name: Checkout base ref | ||
| uses: actions/checkout@v7 | ||
| - name: Mention owners of the touched paths | ||
| uses: actions/github-script@v9 | ||
| with: | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| script: | | ||
| const fs = require('fs'); | ||
| function patternMatches(pattern, filePath) { | ||
| if (pattern.startsWith('**/')) { | ||
| return filePath.split('/').pop() === pattern.slice(3); | ||
| } | ||
| const anchored = pattern.replace(/^\//, ''); | ||
| if (pattern.endsWith('/')) { | ||
| const prefix = anchored.endsWith('/') ? anchored : anchored + '/'; | ||
| return filePath.startsWith(prefix); | ||
| } | ||
| return filePath === anchored; | ||
| } | ||
| // GitHub's CODEOWNERS syntax escapes a literal space in a path | ||
| // with '\ ' (e.g. /AI\ Fair\ Recruitment/). A naive whitespace | ||
| // split breaks such a pattern into multiple tokens instead of | ||
| // one, silently losing its owner(s) - reassemble it the same | ||
| // way tests/test_codeowners.py's _split_codeowners_line() does. | ||
| function splitCodeownersLine(line) { | ||
| const tokens = line.split(/\s+/); | ||
| let pattern = tokens[0]; | ||
| let i = 1; | ||
| while (pattern.endsWith('\\') && i < tokens.length) { | ||
| pattern = pattern.slice(0, -1) + ' ' + tokens[i]; | ||
| i += 1; | ||
| } | ||
| return { pattern, owners: tokens.slice(i) }; | ||
| } | ||
| const entries = fs.readFileSync('.github/CODEOWNERS', 'utf8') | ||
| .split('\n') | ||
| .map((line) => line.trim()) | ||
| .filter((line) => line && !line.startsWith('#')) | ||
| .map((line) => { | ||
| const { pattern, owners } = splitCodeownersLine(line); | ||
| return { pattern, owners: owners.map((o) => o.replace(/^@/, '')) }; | ||
| }); | ||
| function ownersFor(filePath) { | ||
| // Last matching pattern wins - mirrors real CODEOWNERS semantics. | ||
| let owners = []; | ||
| for (const entry of entries) { | ||
| if (patternMatches(entry.pattern, filePath)) owners = entry.owners; | ||
| } | ||
| return owners; | ||
| } | ||
| const files = await github.paginate(github.rest.pulls.listFiles, { | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| pull_number: context.payload.pull_request.number, | ||
| per_page: 100, | ||
| }); | ||
| const author = context.payload.pull_request.user.login; | ||
| const toMention = new Set(); | ||
| for (const file of files) { | ||
| for (const login of ownersFor(file.filename)) { | ||
| if (login !== author) toMention.add(login); | ||
| } | ||
| } | ||
| if (toMention.size === 0) return; | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.payload.pull_request.number, | ||
| body: [...toMention].map((login) => `@${login}`).join(' ') + ' - new PR touching a path you own, please review.' | ||
| }); | ||