Skip to content

ci: add assign random reviewer workflow#312

Closed
tty47 wants to merge 1 commit intomainfrom
ci/assign-random-reviewer
Closed

ci: add assign random reviewer workflow#312
tty47 wants to merge 1 commit intomainfrom
ci/assign-random-reviewer

Conversation

@tty47
Copy link

@tty47 tty47 commented Feb 24, 2026

Automatically assigns one random reviewer from CODEOWNERS when a PR is opened.

  • Triggers on PR open, ready_for_review, and reopen events
  • Skips draft PRs
  • Parses CODEOWNERS to find eligible reviewers for changed files
  • Excludes PR author from candidates
  • Picks one reviewer at random

@gemini-code-assist
Copy link

Note

Gemini is unable to generate a summary for this pull request due to the file types involved not being currently supported.

@tty47 tty47 requested a review from cmwaters February 24, 2026 15:34
Comment on lines +9 to +109
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

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 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: 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
@tty47 tty47 closed this Feb 24, 2026
@tty47 tty47 deleted the ci/assign-random-reviewer branch February 24, 2026 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant