Skip to content

Commit f0ea3d9

Browse files
Merge branch 'main' into contextablemark/fix/issue631
2 parents 140cdfe + 3c66dcc commit f0ea3d9

File tree

2 files changed

+196
-1
lines changed

2 files changed

+196
-1
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
name: Auto-approve community PRs
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
7+
permissions:
8+
pull-requests: write
9+
contents: read
10+
11+
jobs:
12+
auto-approve:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0
19+
20+
- name: Fetch PR branch
21+
run: |
22+
git fetch origin ${{ github.event.pull_request.head.ref }}:${{ github.event.pull_request.head.ref }}
23+
24+
- name: Set up Node.js
25+
uses: actions/setup-node@v4
26+
with:
27+
node-version: "22"
28+
29+
- name: Auto-approve based on CODEOWNERS
30+
env:
31+
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
32+
PR_NUMBER: ${{ github.event.pull_request.number }}
33+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34+
BASE_REF: ${{ github.event.pull_request.base.ref }}
35+
HEAD_REF: ${{ github.event.pull_request.head.ref }}
36+
run: |
37+
node << 'EOF'
38+
const { execSync } = require('child_process');
39+
const fs = require('fs');
40+
const path = require('path');
41+
42+
const prAuthor = process.env.PR_AUTHOR;
43+
const prNumber = process.env.PR_NUMBER;
44+
45+
// Get changed files
46+
const changedFiles = execSync(
47+
`git diff --name-only origin/${process.env.BASE_REF}...origin/${process.env.HEAD_REF}`,
48+
{ encoding: 'utf-8' }
49+
)
50+
.trim()
51+
.split('\n')
52+
.filter(f => f.trim());
53+
54+
console.log(`Changed files (${changedFiles.length}):`);
55+
changedFiles.forEach(f => console.log(` - ${f}`));
56+
57+
// Parse CODEOWNERS file
58+
const codeownersPath = '.github/CODEOWNERS';
59+
const codeownersContent = fs.readFileSync(codeownersPath, 'utf-8');
60+
const lines = codeownersContent.split('\n');
61+
62+
// Map of path patterns to owners (excluding root * rule)
63+
const codeownersRules = [];
64+
65+
for (const line of lines) {
66+
const trimmed = line.trim();
67+
// Skip empty lines and comments
68+
if (!trimmed || trimmed.startsWith('#')) {
69+
continue;
70+
}
71+
72+
// Skip root * line
73+
if (trimmed.startsWith('* ')) {
74+
console.log('Skipping root * rule');
75+
continue;
76+
}
77+
78+
// Parse pattern and owners
79+
const parts = trimmed.split(/\s+/);
80+
if (parts.length < 2) {
81+
continue;
82+
}
83+
84+
const pattern = parts[0];
85+
const owners = parts.slice(1).map(o => o.replace('@', ''));
86+
87+
codeownersRules.push({ pattern, owners });
88+
}
89+
90+
console.log('\nCODEOWNERS rules (excluding root):');
91+
codeownersRules.forEach(rule => {
92+
console.log(` ${rule.pattern} -> ${rule.owners.join(', ')}`);
93+
});
94+
95+
// Function to check if a file matches a CODEOWNERS pattern
96+
// CODEOWNERS patterns match:
97+
// - Exact file/directory path
98+
// - pattern/ matches everything in that directory
99+
// - pattern/** matches everything recursively in that directory
100+
function matchesPattern(file, pattern) {
101+
// Normalize paths (handle both / and \ separators)
102+
const normalizePath = (p) => p.replace(/\\/g, '/');
103+
const normalizedFile = normalizePath(file);
104+
const normalizedPattern = normalizePath(pattern);
105+
106+
// Exact match
107+
if (normalizedFile === normalizedPattern) {
108+
return true;
109+
}
110+
111+
// Pattern ends with /**: matches recursively in directory
112+
if (normalizedPattern.endsWith('/**')) {
113+
const dirPrefix = normalizedPattern.slice(0, -3);
114+
return normalizedFile.startsWith(dirPrefix + '/');
115+
}
116+
117+
// Pattern ends with /: matches everything in directory
118+
if (normalizedPattern.endsWith('/')) {
119+
const dirPrefix = normalizedPattern.slice(0, -1);
120+
return normalizedFile.startsWith(dirPrefix + '/');
121+
}
122+
123+
// Pattern is a directory prefix (matches subdirectories)
124+
if (normalizedFile.startsWith(normalizedPattern + '/')) {
125+
return true;
126+
}
127+
128+
return false;
129+
}
130+
131+
// Check each changed file
132+
// CODEOWNERS rules are evaluated top-to-bottom, first match wins
133+
const unapprovedFiles = [];
134+
135+
for (const file of changedFiles) {
136+
let matched = false;
137+
let owned = false;
138+
139+
// Find the first matching rule (CODEOWNERS uses first match semantics)
140+
for (const rule of codeownersRules) {
141+
if (matchesPattern(file, rule.pattern)) {
142+
matched = true;
143+
// First match wins in CODEOWNERS, so check ownership here
144+
owned = rule.owners.includes(prAuthor);
145+
break; // Stop at first match
146+
}
147+
}
148+
149+
// File must be matched by a non-root CODEOWNERS rule AND author must own it
150+
if (!matched || !owned) {
151+
unapprovedFiles.push(file);
152+
}
153+
}
154+
155+
// Decision
156+
if (unapprovedFiles.length === 0) {
157+
console.log(`\n✅ All changed files are owned by ${prAuthor} according to CODEOWNERS`);
158+
159+
// Check if already approved by this workflow
160+
try {
161+
const reviews = JSON.parse(
162+
execSync(`gh pr view ${prNumber} --json reviews`, { encoding: 'utf-8' })
163+
);
164+
165+
// Check if there's already an approval from GitHub Actions bot
166+
// (look for approval with the auto-approve message)
167+
const hasAutoApproval = reviews.reviews.some(
168+
review => review.state === 'APPROVED' &&
169+
review.body &&
170+
review.body.includes('Auto-approved: PR author has CODEOWNERS access')
171+
);
172+
173+
if (hasAutoApproval) {
174+
console.log('PR already auto-approved by this workflow');
175+
} else {
176+
// Approve the PR using GitHub Actions bot account
177+
execSync(
178+
`gh pr review ${prNumber} --approve --body "Auto-approved: PR author ${prAuthor} has CODEOWNERS access to all changed files (excluding root rule)"`,
179+
{ stdio: 'inherit' }
180+
);
181+
console.log(`PR approved automatically for ${prAuthor}`);
182+
}
183+
} catch (error) {
184+
console.error('Error checking/approving PR:', error.message);
185+
// Don't fail the workflow if approval fails (might already be approved, etc.)
186+
console.log('Continuing despite approval error...');
187+
}
188+
} else {
189+
console.log(`\n❌ Not auto-approved: Some files are not owned by ${prAuthor}`);
190+
console.log('Unauthorized files:');
191+
unapprovedFiles.forEach(f => console.log(` - ${f}`));
192+
}
193+
EOF

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ Built for simplicity and flexibility, it enables seamless integration between AI
1616

1717
<a href="https://discord.gg/Jd3FzfdJa8" target="_blank"> Join our Discord → </a> &nbsp;&nbsp;&nbsp; <a href="https://ag-ui.com/" target="_blank"> Read the Docs → </a> &nbsp;&nbsp;&nbsp; <a href="https://dojo.ag-ui.com/" target="_blank"> Go to the AG-UI Dojo → </a> &nbsp;&nbsp;&nbsp; <a href="https://x.com/CopilotKit" target="_blank"> Follow us → </a>
1818

19-
<img width="4096" height="1752" alt="Your application-AG-UI protocol" src="https://github.com/user-attachments/assets/dc58c64c-3257-490a-b827-e163475f4166" />
19+
<img width="4096" height="1752" alt="Your application-AG-UI protocol" src="https://github.com/user-attachments/assets/0ecc3a63-7947-442f-9a6e-be887d0bf245" />
20+
21+
2022

2123
## 🚀 Getting Started
2224
Create a new AG-UI application in seconds:

0 commit comments

Comments
 (0)