-
Notifications
You must be signed in to change notification settings - Fork 518
ci: auto pr assign by CODEOWNERS #249
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
base: main
Are you sure you want to change the base?
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,25 @@ | ||
| name: Auto Assign PR | ||
|
|
||
| on: | ||
| pull_request_target: | ||
| types: [opened, reopened, ready_for_review] | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
feihongxu0824 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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,148 @@ | ||
| name: Community PR Handler | ||
|
|
||
| on: | ||
| pull_request_target: | ||
| types: [opened, reopened, ready_for_review] | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
feihongxu0824 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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 | ||
| }); | ||
| const changedFiles = files.map(f => f.filename); | ||
|
Comment on lines
+66
to
+71
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.
Use 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); |
||
| 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)) { | ||
| matchedOwner = rule.owners[0]; | ||
| matchedPattern = pattern; | ||
| } | ||
| } else if (file === pattern || file === pattern.replace(/^\//, '')) { | ||
| matchedOwner = rule.owners[0]; | ||
| matchedPattern = pattern; | ||
| } | ||
|
Comment on lines
+102
to
+116
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 inner loop iterates all rules and continuously overwrites More critically, the parser only handles three pattern shapes:
Any future CODEOWNERS rules using standard glob patterns (e.g., |
||
| } | ||
| 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.
/.github/CODEOWNERS entry means this PR itself won't auto-assign@Cuiyus@Cuiyusis listed as the owner of/.github/, but this PR is being submitted by@feihongxu0824who is almost certainly a MEMBER/OWNER, so thecommunity-pr-handler.ymlwould skip it anyway. More importantly, for future community PRs that touch.github/files, thecommunity-pr-handler.ymlwill correctly assign@Cuiyus— but native GitHub CODEOWNERS review requests are only triggered automatically if the CODEOWNERS file is in the default branch. Until this PR is merged, any in-flight fork PRs touching.github/files won't automatically request a review from@Cuiyusvia native GitHub CODEOWNERS. This is expected behaviour, just worth being aware of.