-
Notifications
You must be signed in to change notification settings - Fork 518
ci: auto pr assign by CODEOWNERS #248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # CODEOWNERS - Auto-assign reviewers based on file paths | ||
| # Documentation: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners | ||
|
|
||
| # Default: feihongxu0824 reviews everything not covered below | ||
| * @feihongxu0824 | ||
|
|
||
| # .github directory | ||
| /.github/ @Cuiyus | ||
|
|
||
| # cmake, examples, scripts | ||
| /cmake/ @egolearner | ||
| /examples/ @egolearner | ||
| /scripts/ @egolearner | ||
|
|
||
| # src/db, tests | ||
| /src/db/ @zhourrr | ||
| /tests/ @zhourrr | ||
|
|
||
| # src/core, src/turbo | ||
| /src/core/ @richyreachy | ||
| /src/turbo/ @richyreachy | ||
|
|
||
| # src/ailego | ||
| /src/ailego/ @iaojnh | ||
|
|
||
| # tools | ||
| /tools/ @JalinWang | ||
|
|
||
| # src/binding | ||
| /src/binding/ @Cuiyus | ||
|
|
||
| # src/include, thirdparty, .gitmodules | ||
| /src/include/ @chinaux | ||
| /thirdparty/ @chinaux | ||
| .gitmodules @chinaux |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Auto-assign configuration | ||
| # Documentation: https://github.com/kentaro-m/auto-assign-action | ||
| # | ||
| # NOTE: Reviewers are assigned via CODEOWNERS file based on file paths. | ||
| # This config only handles assignee assignment. | ||
|
|
||
| # Automatically add PR author as assignee | ||
| addAssignees: author | ||
|
|
||
| # Number of assignees to add (0 = add all from the list) | ||
| numberOfAssignees: 0 | ||
|
|
||
| # Reviewers are handled by CODEOWNERS, not here | ||
| addReviewers: false | ||
|
|
||
| # Skip draft PRs | ||
| skipDraft: true | ||
|
|
||
| # Skip keywords in PR title (won't assign if title contains these) | ||
| skipKeywords: | ||
| - wip | ||
| - WIP | ||
| - draft | ||
| - DRAFT |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| name: Auto Assign PR | ||
|
|
||
| on: | ||
| pull_request_target: | ||
| types: [opened, reopened, ready_for_review] | ||
|
|
||
| permissions: | ||
| pull-requests: write | ||
|
|
||
| jobs: | ||
| auto-assign: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout for config | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| ref: ${{ github.event.pull_request.base.ref }} | ||
| sparse-checkout: .github/auto-assign-config.yml | ||
| sparse-checkout-cone-mode: false | ||
|
|
||
| - name: Auto Assign PR Author | ||
| uses: kentaro-m/auto-assign-action@v2.0.0 | ||
| with: | ||
| configuration-path: '.github/auto-assign-config.yml' |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,147 @@ | ||||||||||||
| name: Community PR Handler | ||||||||||||
|
|
||||||||||||
| on: | ||||||||||||
| pull_request_target: | ||||||||||||
| types: [opened, reopened, ready_for_review] | ||||||||||||
|
|
||||||||||||
| permissions: | ||||||||||||
| pull-requests: write | ||||||||||||
|
Comment on lines
+7
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The workflow calls
Suggested change
|
||||||||||||
|
|
||||||||||||
| jobs: | ||||||||||||
| handle-community-pr: | ||||||||||||
| runs-on: ubuntu-latest | ||||||||||||
| if: github.event.pull_request.draft == false | ||||||||||||
| steps: | ||||||||||||
| - name: Check if community contributor | ||||||||||||
| id: check | ||||||||||||
| uses: actions/github-script@v7 | ||||||||||||
| with: | ||||||||||||
| script: | | ||||||||||||
| const pr = context.payload.pull_request; | ||||||||||||
| const prAuthor = pr.user.login; | ||||||||||||
| const association = pr.author_association; | ||||||||||||
|
|
||||||||||||
| console.log('========== PR Info =========='); | ||||||||||||
| console.log(`PR Number: #${pr.number}`); | ||||||||||||
| console.log(`PR Title: ${pr.title}`); | ||||||||||||
| console.log(`PR Author: ${prAuthor}`); | ||||||||||||
| console.log(`Author Association: ${association}`); | ||||||||||||
| console.log(`Base Branch: ${pr.base.ref}`); | ||||||||||||
| console.log(`Head Branch: ${pr.head.ref}`); | ||||||||||||
| console.log(`Head Repo: ${pr.head.repo?.full_name || 'same repo'}`); | ||||||||||||
|
|
||||||||||||
| // OWNER, MEMBER, COLLABORATOR are maintainers; others are community contributors | ||||||||||||
| const maintainerRoles = ['OWNER', 'MEMBER', 'COLLABORATOR']; | ||||||||||||
| const isCommunity = !maintainerRoles.includes(association); | ||||||||||||
|
|
||||||||||||
| console.log('========== Decision =========='); | ||||||||||||
| console.log(`Maintainer Roles: ${maintainerRoles.join(', ')}`); | ||||||||||||
| console.log(`Is Community Contributor: ${isCommunity}`); | ||||||||||||
|
|
||||||||||||
| if (!isCommunity) { | ||||||||||||
| console.log('Skipping: PR author is a maintainer, no action needed.'); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| core.setOutput('is_community', isCommunity); | ||||||||||||
|
|
||||||||||||
| - name: Checkout base branch for CODEOWNERS | ||||||||||||
| if: steps.check.outputs.is_community == 'true' | ||||||||||||
| uses: actions/checkout@v4 | ||||||||||||
| with: | ||||||||||||
| ref: ${{ github.event.pull_request.base.ref }} | ||||||||||||
| sparse-checkout: .github/CODEOWNERS | ||||||||||||
| sparse-checkout-cone-mode: false | ||||||||||||
|
|
||||||||||||
| - name: Assign reviewer as assignee | ||||||||||||
| if: steps.check.outputs.is_community == 'true' | ||||||||||||
| uses: actions/github-script@v7 | ||||||||||||
| with: | ||||||||||||
| script: | | ||||||||||||
| const fs = require('fs'); | ||||||||||||
| const prNumber = context.payload.pull_request.number; | ||||||||||||
|
|
||||||||||||
| console.log('========== Fetching Changed Files =========='); | ||||||||||||
| // Get changed files | ||||||||||||
| const { data: files } = await github.rest.pulls.listFiles({ | ||||||||||||
| owner: context.repo.owner, | ||||||||||||
| repo: context.repo.repo, | ||||||||||||
| pull_number: prNumber | ||||||||||||
| }); | ||||||||||||
|
Comment on lines
+65
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
per_page: 100
});
const changedFiles = files.map(f => f.filename); |
||||||||||||
| const changedFiles = files.map(f => f.filename); | ||||||||||||
| console.log(`Total changed files: ${changedFiles.length}`); | ||||||||||||
| changedFiles.forEach((f, i) => console.log(` [${i + 1}] ${f}`)); | ||||||||||||
|
|
||||||||||||
| console.log('========== Parsing CODEOWNERS =========='); | ||||||||||||
| // Parse CODEOWNERS | ||||||||||||
| let codeowners = []; | ||||||||||||
| try { | ||||||||||||
| const content = fs.readFileSync('.github/CODEOWNERS', 'utf8'); | ||||||||||||
| const lines = content.split('\n').filter(l => l.trim() && !l.trim().startsWith('#')); | ||||||||||||
| console.log(`Found ${lines.length} rules in CODEOWNERS`); | ||||||||||||
| for (const line of lines) { | ||||||||||||
| const parts = line.trim().split(/\s+/); | ||||||||||||
| if (parts.length >= 2) { | ||||||||||||
| const pattern = parts[0]; | ||||||||||||
| const owners = parts.slice(1).map(o => o.replace('@', '')); | ||||||||||||
| codeowners.push({ pattern, owners }); | ||||||||||||
| console.log(` Rule: "${pattern}" -> [${owners.join(', ')}]`); | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } catch (e) { | ||||||||||||
| console.log('ERROR: Could not read CODEOWNERS:', e.message); | ||||||||||||
| return; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| console.log('========== Matching Files to Owners =========='); | ||||||||||||
| // Find matching owners for changed files | ||||||||||||
| const matchedOwners = new Set(); | ||||||||||||
| for (const file of changedFiles) { | ||||||||||||
| let matchedOwner = null; | ||||||||||||
| let matchedPattern = null; | ||||||||||||
| for (const rule of codeowners) { | ||||||||||||
| const pattern = rule.pattern; | ||||||||||||
| if (pattern === '*') { | ||||||||||||
| matchedOwner = rule.owners[0]; | ||||||||||||
| matchedPattern = pattern; | ||||||||||||
| } else if (pattern.endsWith('/')) { | ||||||||||||
| const dir = pattern.replace(/^\//, '').replace(/\/$/, ''); | ||||||||||||
| if (file.startsWith(dir + '/') || file.startsWith(dir)) { | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The condition Only the
Suggested change
|
||||||||||||
| matchedOwner = rule.owners[0]; | ||||||||||||
| matchedPattern = pattern; | ||||||||||||
| } | ||||||||||||
| } else if (file === pattern || file === pattern.replace(/^\//, '')) { | ||||||||||||
| matchedOwner = rule.owners[0]; | ||||||||||||
| matchedPattern = pattern; | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+99
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The script always uses Consider using // Instead of: matchedOwner = rule.owners[0];
rule.owners.forEach(o => matchedOwners.add(o)); |
||||||||||||
| } | ||||||||||||
| if (matchedOwner) { | ||||||||||||
| matchedOwners.add(matchedOwner); | ||||||||||||
| console.log(` "${file}" -> matched "${matchedPattern}" -> @${matchedOwner}`); | ||||||||||||
| } else { | ||||||||||||
| console.log(` "${file}" -> NO MATCH`); | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| console.log('========== Setting Assignees =========='); | ||||||||||||
| // Set assignees | ||||||||||||
| const assignees = Array.from(matchedOwners); | ||||||||||||
| console.log(`Assignees to set: [${assignees.join(', ')}]`); | ||||||||||||
|
|
||||||||||||
| if (assignees.length > 0) { | ||||||||||||
| try { | ||||||||||||
| await github.rest.issues.addAssignees({ | ||||||||||||
| owner: context.repo.owner, | ||||||||||||
| repo: context.repo.repo, | ||||||||||||
| issue_number: prNumber, | ||||||||||||
| assignees: assignees | ||||||||||||
| }); | ||||||||||||
| console.log('SUCCESS: Assignees set successfully!'); | ||||||||||||
| } catch (e) { | ||||||||||||
| console.log('ERROR: Failed to set assignees:', e.message); | ||||||||||||
| console.log('Error details:', JSON.stringify(e, null, 2)); | ||||||||||||
| } | ||||||||||||
| } else { | ||||||||||||
| console.log('WARNING: No assignees to set - no matching owners found.'); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| console.log('========== Done =========='); | ||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pull_request_target+ checkout requires cautionThe workflow uses the
pull_request_targettrigger, which runs with write permissions against the base repository. Although theactions/checkout@v4step correctly checks out the base branch (not the PR head), thepulls.listFilesandissues.addAssigneescalls operate on data from the PR author's fork. If a malicious contributor crafts a file with a specially crafted filename (e.g., containing shell metacharacters), the logging statements (console.log) in the script could be misused. This is low-risk here since noevalor shell execution is performed on filenames, but it's worth being aware of thepull_request_targetattack surface and ensuring no future changes execute arbitrary content from the PR.