Conversation
|
Note Gemini is unable to generate a summary for this pull request due to the file types involved not being currently supported. |
| runs-on: ubuntu-latest | ||
| # Skip draft PRs | ||
| if: github.event.pull_request.draft == false | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const fs = require('fs'); | ||
|
|
||
| // Parse CODEOWNERS into [{pattern, owners}] entries | ||
| function parseCODEOWNERS(content) { | ||
| return content | ||
| .split('\n') | ||
| .map(line => line.trim()) | ||
| .filter(line => line && !line.startsWith('#')) | ||
| .map(line => { | ||
| const parts = line.split(/\s+/); | ||
| return { | ||
| pattern: parts[0], | ||
| owners: parts.slice(1).map(o => o.replace(/^@/, '').toLowerCase()) | ||
| }; | ||
| }); | ||
| } | ||
|
|
||
| // Check if a file path matches a CODEOWNERS pattern | ||
| function matches(filePath, pattern) { | ||
| // Normalise — strip leading slash from pattern | ||
| const p = pattern.replace(/^\//, ''); | ||
| if (p === '*') return true; | ||
| if (p.endsWith('/')) return filePath.startsWith(p); | ||
| if (p.includes('*')) { | ||
| const re = new RegExp('^' + p.replace(/\*/g, '.*') + '$'); | ||
| return re.test(filePath); | ||
| } | ||
| return filePath === p || filePath.startsWith(p + '/'); | ||
| } | ||
|
|
||
| // Read CODEOWNERS (try both locations) | ||
| let codeownersContent = ''; | ||
| for (const path of ['.github/CODEOWNERS', 'CODEOWNERS']) { | ||
| if (fs.existsSync(path)) { | ||
| codeownersContent = fs.readFileSync(path, 'utf8'); | ||
| console.log(`Found CODEOWNERS at ${path}`); | ||
| break; | ||
| } | ||
| } | ||
| if (!codeownersContent) { | ||
| console.log('No CODEOWNERS file found — skipping'); | ||
| return; | ||
| } | ||
|
|
||
| const rules = parseCODEOWNERS(codeownersContent); | ||
| console.log(`Parsed ${rules.length} CODEOWNERS rules`); | ||
|
|
||
| // Get all files changed in this PR | ||
| const { data: files } = await github.rest.pulls.listFiles({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| pull_number: context.payload.pull_request.number, | ||
| per_page: 100 | ||
| }); | ||
|
|
||
| console.log(`PR has ${files.length} changed files`); | ||
|
|
||
| // For each changed file, find the last matching CODEOWNERS rule | ||
| // (GitHub uses last-match-wins semantics) | ||
| const ownerSet = new Set(); | ||
| for (const file of files) { | ||
| for (let i = rules.length - 1; i >= 0; i--) { | ||
| if (matches(file.filename, rules[i].pattern)) { | ||
| rules[i].owners.forEach(o => ownerSet.add(o)); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Remove the PR author from candidates | ||
| const author = context.payload.pull_request.user.login.toLowerCase(); | ||
| const candidates = [...ownerSet].filter(o => o !== author); | ||
|
|
||
| console.log(`Candidates after excluding author (${author}): ${candidates.join(', ') || 'none'}`); | ||
|
|
||
| if (candidates.length === 0) { | ||
| console.log('No eligible reviewers found after excluding PR author'); | ||
| return; | ||
| } | ||
|
|
||
| // Pick one at random | ||
| const chosen = candidates[Math.floor(Math.random() * candidates.length)]; | ||
| console.log(`Randomly chose: ${chosen}`); | ||
|
|
||
| await github.rest.pulls.requestReviewers({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| pull_number: context.payload.pull_request.number, | ||
| reviewers: [chosen] | ||
| }); | ||
|
|
||
| console.log(`Successfully assigned ${chosen} as reviewer`); |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 11 days ago
To fix this, add an explicit permissions: block that grants only the minimal scopes required. This job needs to (1) read pull request details and files, and (2) request reviewers. That implies at least contents: read (to safely read repository data if required) and pull-requests: write to assign reviewers. Declare permissions: at the job level (assign-reviewer) so it applies only to this job and does not unintentionally affect other workflows.
Concretely, in .github/workflows/assign-random-reviewer.yml, under jobs: assign-reviewer: and at the same indentation level as runs-on, insert:
permissions:
contents: read
pull-requests: writeNo imports or additional methods are needed because this is a YAML configuration change only; existing functionality (assigning a random reviewer) will continue working, but with a GITHUB_TOKEN limited to the specific capabilities it uses.
| @@ -7,6 +7,9 @@ | ||
| jobs: | ||
| assign-reviewer: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
| # Skip draft PRs | ||
| if: github.event.pull_request.draft == false | ||
| steps: |
Automatically assigns one random reviewer from CODEOWNERS when a PR is opened.