Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions .github/workflows/assign-random-reviewer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: Assign Random Reviewer

on:
pull_request:
types: [opened, ready_for_review, reopened]

jobs:
assign-reviewer:
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`);
Comment on lines +9 to +109

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 12 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: write

No 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.

Suggested changeset 1
.github/workflows/assign-random-reviewer.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/assign-random-reviewer.yml b/.github/workflows/assign-random-reviewer.yml
--- a/.github/workflows/assign-random-reviewer.yml
+++ b/.github/workflows/assign-random-reviewer.yml
@@ -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:
EOF
@@ -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:
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Loading