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
90 changes: 90 additions & 0 deletions .github/workflows/codeowner-approval-status.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: Code-owner Approval Status
on:
pull_request_review:
types: [submitted]
Comment on lines +3 to +4
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Dismissed reviews won't re-trigger this check.

If an approving review is dismissed, the commit status will remain success until another event (like a new push) re-triggers the workflow. Add dismissed to the pull_request_review types to handle this case.

Proposed fix
   pull_request_review:
-    types: [submitted]
+    types: [submitted, dismissed]
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pull_request_review:
types: [submitted]
pull_request_review:
types: [submitted, dismissed]
πŸ€– Prompt for AI Agents
In @.github/workflows/codeowner-approval-status.yml around lines 3 - 4, The
workflow's pull_request_review trigger only listens for "submitted" events, so
dismissed reviews won't re-run the check; update the pull_request_review.types
array to include "dismissed" in addition to "submitted" (i.e., modify the
pull_request_review -> types setting) so the workflow is retriggered when a
review is dismissed.

pull_request:
types: [opened, synchronize]
Comment on lines +2 to +6
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Workflow cannot set statuses on fork PRs.

pull_request events from forks receive a read-only GITHUB_TOKEN, so createCommitStatus will fail for external contributions. Since this workflow already checks out the base SHA and doesn't execute fork code, switching to pull_request_target would grant the needed write token safely:

Proposed fix
 on:
   pull_request_review:
     types: [submitted]
-  pull_request:
+  pull_request_target:
     types: [opened, synchronize]

Note: pull_request_target runs in the base-branch context, which is already the intent here (Line 18 checks out the base SHA). The script only calls GitHub APIs and reads base-branch files β€” no fork-supplied code is executed β€” so this is safe.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
on:
pull_request_review:
types: [submitted]
pull_request:
types: [opened, synchronize]
on:
pull_request_review:
types: [submitted]
pull_request_target:
types: [opened, synchronize]
πŸ€– Prompt for AI Agents
In @.github/workflows/codeowner-approval-status.yml around lines 2 - 6, The
workflow uses the pull_request event which provides a read-only GITHUB_TOKEN for
forked PRs, causing createCommitStatus to fail on external contributions; switch
the trigger from pull_request to pull_request_target so the job runs in the
base-branch context with a writable token, keep the existing checkout of the
base SHA (the step that checks out the base ref), and ensure no steps execute
untrusted code from the forkβ€”only call GitHub API functions like
createCommitStatus and read base-branch files so the token remains safe to use.


permissions:
pull-requests: read
statuses: write
Comment on lines +8 to +10
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

Missing contents: read permission β€” actions/checkout will fail.

When the permissions key is explicitly specified, any unmentioned permission defaults to none. The checkout step needs contents: read to clone the repo and read .github/CODEOWNERS.

Proposed fix
 permissions:
+  contents: read
   pull-requests: read
   statuses: write
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
permissions:
pull-requests: read
statuses: write
permissions:
contents: read
pull-requests: read
statuses: write
πŸ€– Prompt for AI Agents
In @.github/workflows/codeowner-approval-status.yml around lines 8 - 10, The
workflow's explicit permissions block omits contents, causing actions/checkout
to fail; update the permissions mapping by adding "contents: read" alongside the
existing "pull-requests: read" and "statuses: write" entries so the
actions/checkout step can clone the repo and read .github/CODEOWNERS (adjust the
top-level permissions block where "pull-requests" and "statuses" are defined).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do i need to clone repo? codeowners = fs.readFileSync('.github/CODEOWNERS', 'utf8') seems to be working in an experimental repo

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# View the complete workflow file to see all steps
cat .github/workflows/codeowner-approval-status.yml

Length of output: 3695


@aleozlx You're right that fs.readFileSync works to read the fileβ€”but only after the actions/checkout@v4 step successfully clones the repository at line 16. The issue is that the checkout action itself requires contents: read permission to clone the repo in the first place.

When you explicitly define a permissions: block in a workflow, GitHub Actions sets all unmentioned permissions to none by default. So your current config gives:

  • contents: none ← blocks actions/checkout
  • pull-requests: read βœ…
  • statuses: write βœ…

If your experimental repo worked without contents: read, it likely either:

  1. Didn't have an explicit permissions: block (so default permissions applied), or
  2. Hasn't actually run with this exact configuration yet

You can verify this by checking the Actions logsβ€”without contents: read, the checkout step will fail with a permission error like:

Error: Resource not accessible by integration

TL;DR: contents: read is needed for line 16's actions/checkout@v4 to succeed, which makes the file available for fs.readFileSync at line 38.


jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.sha }}
- run: npm install minimatch@9
- uses: actions/github-script@v7
with:
script: |
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner, repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
});
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner: context.repo.owner, repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
});

// Parse CODEOWNERS (reuse your existing file)
const fs = require('fs');
let codeowners = [];
try {
codeowners = fs.readFileSync('.github/CODEOWNERS', 'utf8')
.split('\n').filter(l => l.trim() && !l.startsWith('#'))
.map(l => { const [pattern, ...owners] = l.split(/\s+/); return { pattern, owners }; });
Comment on lines +36 to +38
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Lines with leading whitespace will be misparsed.

l.split(/\s+/) on a line like *.py@owner`` produces ['', '*.py', '@owner'], so `pattern` becomes `''` (silently dropping the rule). Trim each line before splitting:

Proposed fix
             codeowners = fs.readFileSync('.github/CODEOWNERS', 'utf8')
-                .split('\n').filter(l => l.trim() && !l.startsWith('#'))
-                .map(l => { const [pattern, ...owners] = l.split(/\s+/); return { pattern, owners }; });
+                .split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'))
+                .map(l => { const [pattern, ...owners] = l.split(/\s+/); return { pattern, owners }; });
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
codeowners = fs.readFileSync('.github/CODEOWNERS', 'utf8')
.split('\n').filter(l => l.trim() && !l.startsWith('#'))
.map(l => { const [pattern, ...owners] = l.split(/\s+/); return { pattern, owners }; });
codeowners = fs.readFileSync('.github/CODEOWNERS', 'utf8')
.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'))
.map(l => { const [pattern, ...owners] = l.split(/\s+/); return { pattern, owners }; });
πŸ€– Prompt for AI Agents
In @.github/workflows/codeowner-approval-status.yml around lines 36 - 38, The
parsing drops patterns when lines have leading whitespace because you split on
the raw line; change the pipeline to trim each line first and use that trimmed
string for both filtering and splitting (e.g. replace usages of l in the .filter
and .map callbacks with a trimmed variable), so the .filter uses t &&
!t.startsWith('#') and the .map splits t (const [pattern, ...owners] =
t.split(/\s+/)) before returning { pattern, owners } β€” update the callbacks
around .split('\n'), .filter(...), and .map(...) accordingly.

} catch (e) {
// Do nothing, but let the same logic handle the error later.
}
Comment on lines +35 to +41
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Silent catch produces a misleading failure message when CODEOWNERS is missing.

If the file doesn't exist or can't be read, allOwners stays empty and the status shows "no qualified approver. update CODEOWNERS!" β€” which is misleading (the file may exist but just wasn't found at the checked-out ref). Consider logging the error or setting a distinct description.

Also, !l.startsWith('#') doesn't handle indented comments (e.g., # comment). Using l.trim() for the comment check would be safer:

Proposed fix
             try {
               codeowners = fs.readFileSync('.github/CODEOWNERS', 'utf8')
-                .split('\n').filter(l => l.trim() && !l.startsWith('#'))
+                .split('\n').filter(l => l.trim() && !l.trim().startsWith('#'))
                 .map(l => { const [pattern, ...owners] = l.split(/\s+/); return { pattern, owners }; });
             } catch (e) {
-              // Do nothing, but let the same logic handle the error later.
+              core.warning(`Could not read .github/CODEOWNERS: ${e.message}`);
             }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
codeowners = fs.readFileSync('.github/CODEOWNERS', 'utf8')
.split('\n').filter(l => l.trim() && !l.startsWith('#'))
.map(l => { const [pattern, ...owners] = l.split(/\s+/); return { pattern, owners }; });
} catch (e) {
// Do nothing, but let the same logic handle the error later.
}
try {
codeowners = fs.readFileSync('.github/CODEOWNERS', 'utf8')
.split('\n').filter(l => l.trim() && !l.trim().startsWith('#'))
.map(l => { const [pattern, ...owners] = l.split(/\s+/); return { pattern, owners }; });
} catch (e) {
core.warning(`Could not read .github/CODEOWNERS: ${e.message}`);
}
πŸ€– Prompt for AI Agents
In @.github/workflows/codeowner-approval-status.yml around lines 35 - 41, The
catch currently swallows FS errors when reading '.github/CODEOWNERS', causing
misleading "update CODEOWNERS!" messages and also the comment filter uses
startsWith on the untrimmed line; update the .readFileSync block to (1) change
the filter to use trimmed lines: .filter(l => l.trim() &&
!l.trim().startsWith('#')).map(...) so indented comments are ignored, and (2) in
the catch, record/log the error (e.g., set a boolean like codeownersReadError or
call the existing logger) instead of silently swallowing it so downstream logic
that inspects codeowners/allOwners can produce a distinct, accurate status
message when the file couldn't be read versus when CODEOWNERS truly has no
owners.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intentional for simplicity. The maintainer will check the file if it doesn't exist etc and discover the problem by debugging

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aleozlx, understood β€” I'll defer to your judgment on keeping the error handling simple for this workflow. If the file is missing or unreadable, the resulting empty owner set will naturally trigger a failure status, signaling that something needs attention.


const { minimatch } = require('minimatch');
const allOwners = new Set();
for (const f of files) {
// Each file's code owner is determined by the last matched rule
// This a native CODEOWNERS file behavior
let lastMatch = null;
for (const rule of codeowners) {
if (minimatch(f.filename, rule.pattern)) lastMatch = rule;
}
if (lastMatch) lastMatch.owners.forEach(o => allOwners.add(o.replace('@', '')));
}
Comment on lines +45 to +53
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

minimatch glob semantics differ from CODEOWNERS pattern semantics.

CODEOWNERS patterns follow .gitignore-style rules: a pattern without a / (e.g., *.py) matches anywhere in the path tree (foo/bar.py). By default, minimatch('foo/bar.py', '*.py') returns false because it doesn't match across directory separators. You likely need { matchBase: true } (or convert patterns yourself) to replicate the expected behavior.

Proposed fix
-              if (minimatch(f.filename, rule.pattern)) lastMatch = rule;
+              if (minimatch(f.filename, rule.pattern, { matchBase: true })) lastMatch = rule;
πŸ€– Prompt for AI Agents
In @.github/workflows/codeowner-approval-status.yml around lines 45 - 53, The
CODEOWNERS matching loop uses minimatch(f.filename, rule.pattern) which does not
emulate CODEOWNERS/.gitignore semantics for patterns without slashes; update the
match call to pass the option { matchBase: true } (or normalize rule.patterns to
'**/pattern' before matching) so patterns like '*.py' match files in
subdirectories; modify the inner loop where minimatch is invoked (referencing
variables minimatch, f.filename, rule.pattern, lastMatch) to include the option
and keep the rest of the lastMatch/allOwners logic unchanged.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apply the change

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aleozlx, I'll apply the suggested fix to ensure CODEOWNERS patterns match correctly.

Task: Add matchBase option to minimatch call

  • Update the minimatch call in .github/workflows/codeowner-approval-status.yml to include { matchBase: true } option
  • This ensures patterns like *.py match files in subdirectories, consistent with CODEOWNERS/.gitignore semantics

πŸ’‘ For code changes across multiple files, use @coderabbitai plan in an issue comment


// Note allOwners can be an empty set and is accepted by the following logic.
// If empty, "failure" will be issued for this check, blocking the merge.

const prAuthor = context.payload.pull_request.user.login;
const hasQualifiedApprover = !(allOwners.size === 0 || (allOwners.size === 1 && allOwners.has(prAuthor)));

const latestReviews = new Map();
for (const r of reviews) {
latestReviews.set(r.user.login, r);
}

const approved = hasQualifiedApprover && [...latestReviews.values()].some(r =>
r.state === 'APPROVED' && allOwners.has(r.user.login)
);

const owners_array = Array.from(allOwners);
const display = hasQualifiedApprover
? (owners_array.length <= 3
? owners_array.map(o => `@${o}`).join(', ')
: `@${owners_array.slice(0, 3).join(', @')} +${owners_array.length - 3} more`)
: "❌ no qualified approver. update CODEOWNERS!";

const approver = approved
? [...latestReviews.values()].find(r => r.state === 'APPROVED' && allOwners.has(r.user.login)).user.login
: null;

await github.rest.repos.createCommitStatus({
owner: context.repo.owner, repo: context.repo.repo,
sha: context.payload.pull_request.head.sha,
state: approved ? 'success' : 'failure',
context: 'codeowner-approval',
description: approved
? `Approved by ${approver}`
: `❌ need approval: ${display}`,
});
Comment on lines +81 to +89
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

GitHub commit-status description has a 140-character limit.

If there are many owners with long usernames, description may be silently truncated by GitHub. Also, when hasQualifiedApprover is false, the description becomes "❌ need approval: ❌ no qualified approver…" β€” the doubled emoji is a minor visual glitch.

Consider truncating explicitly and keeping the display path simpler:

Proposed fix
+            const descriptionRaw = approved
+              ? `Approved by ${approver}`
+              : hasQualifiedApprover
+                ? `Need approval from: ${owners_array.length <= 3 ? owners_array.map(o => `@${o}`).join(', ') : `@${owners_array.slice(0, 3).join(', @')} +${owners_array.length - 3} more`}`
+                : 'No qualified approver found β€” update CODEOWNERS';
+            const description = descriptionRaw.slice(0, 140);
+
             await github.rest.repos.createCommitStatus({
               owner: context.repo.owner, repo: context.repo.repo,
               sha: context.payload.pull_request.head.sha,
               state: approved ? 'success' : 'failure',
               context: 'codeowner-approval',
-              description: approved
-                ? `Approved by ${approver}`
-                : `❌ need approval: ${display}`,
+              description,
             });
πŸ€– Prompt for AI Agents
In @.github/workflows/codeowner-approval-status.yml around lines 81 - 89, The
commit status description can be silently truncated by GitHub and may show a
double emoji when display already starts with one; before calling
github.rest.repos.createCommitStatus (the createCommitStatus call that sets
description using approved, approver, and display), build a
sanitizedDescription: trim/normalize display to a short form, strip any leading
emoji or leading "❌" when approved is false to avoid doubling, then truncate the
final description to 140 chars (append ellipsis if trimmed). Use that
sanitizedDescription in the description field instead of constructing it inline.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i already truncate this to top-3 in the code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aleozlx You're right that limiting to top-3 owners significantly reduces the risk!

Two minor edge cases remain:

  1. 140-char limit: Three long usernames could still exceed it (GitHub usernames can be up to 39 chars). For example: "❌ need approval: @very-long-username-here, @another-long-one, @third-long-name +2 more" could approach or exceed 140 characters.
  2. Double emoji: When there are no qualified approvers, display is set to "❌ no qualified approver...", so the final description becomes "❌ need approval: ❌ no qualified approver...".

These are quite rare edge cases, so if you're comfortable with the current approach, it's reasonable to keep it as is. If you'd like to be extra defensive, adding an explicit .slice(0, 140) and tweaking the display logic to avoid the double emoji would eliminate these edge cases entirely.


🧠 Learnings used
Learnt from: yongwww
Repo: flashinfer-ai/flashinfer PR: 2500
File: .github/workflows/pr-test-runner.yml:48-50
Timestamp: 2026-02-06T06:23:23.483Z
Learning: In GitHub Actions workflows with check runs, check runs are per-SHA. When a new commit is pushed to a PR, old check runs associated with previous commit SHAs are automatically hidden from the PR status panel and only the latest commit's check runs are displayed. Therefore, orphaned in-progress check runs from cancelled workflows do not clutter the PR status or affect the workflow.


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Fix trailing whitespace β€” CI is failing.

The pipeline reports a trailing-whitespace pre-commit hook failure on this line. Remove the trailing whitespace to pass CI.

πŸ€– Prompt for AI Agents
In @.github/workflows/codeowner-approval-status.yml at line 90, Remove the
trailing whitespace from the reported line in the workflow file so the
pre-commit `trailing-whitespace` hook passes; open
.github/workflows/codeowner-approval-status.yml, locate the
blank/whitespace-only characters at the end of the failing line and delete them
(ensure the line ends with no spaces or tabs), then save and commit the change.

Loading