-
Notifications
You must be signed in to change notification settings - Fork 2.3k
192 lines (170 loc) Β· 7.43 KB
/
pr-welcome-comment.yml
File metadata and controls
192 lines (170 loc) Β· 7.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# Post a welcome comment on new PRs telling the author who owns
# the docs areas they're changing (derived from .github/OWNERS).
#
# NOTE: This uses `pull_request_target` so it has write access to
# comment on PRs from forks. The script only reads OWNERS from
# the base branch and lists changed files β no untrusted code is executed.
name: PR Welcome Comment
on:
pull_request_target:
types: [opened, ready_for_review]
jobs:
welcome-comment:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
# Skip draft PRs β the comment will be posted when marked ready
if: ${{ !github.event.pull_request.draft }}
steps:
- name: Post welcome comment with owners
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
const author = pr.user.login;
// --- Auto-assign reviewers for bot-generated PRs ---
const botPatterns = [
'[LangGraph Server Changelog Bot]',
'[Self-Hosted Changelog Bot]'
];
if (botPatterns.some(p => pr.title.includes(p))) {
const reviewers = ['lnhsingh', 'katmayb'];
await github.rest.pulls.requestReviewers({
owner, repo,
pull_number: pr.number,
reviewers
});
console.log(`Auto-assigned reviewers ${reviewers.join(', ')} on bot PR #${pr.number}`);
return;
}
// --- Fetch OWNERS from the base branch ---
let ownersContent;
try {
const { data } = await github.rest.repos.getContent({
owner, repo,
path: '.github/OWNERS',
ref: pr.base.ref
});
ownersContent = Buffer.from(data.content, 'base64').toString();
} catch (error) {
console.log('Could not read .github/OWNERS:', error.message);
return;
}
// --- Parse OWNERS rules (CODEOWNERS syntax) ---
const rules = [];
for (const line of ownersContent.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const parts = trimmed.split(/\s+/);
const pattern = parts[0];
const owners = parts.slice(1).filter(o => o.startsWith('@'));
if (owners.length > 0) rules.push({ pattern, owners });
}
// Last matching rule wins (same as GitHub CODEOWNERS behavior)
function findOwners(filepath) {
let matched = null;
for (const rule of rules) {
let pat = rule.pattern.startsWith('/') ? rule.pattern.slice(1) : rule.pattern;
let matches = false;
if (pat === '*') {
matches = true;
} else if (pat.endsWith('/')) {
matches = filepath.startsWith(pat);
} else if (pat.includes('*')) {
const re = new RegExp(
'^' + pat.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')
);
matches = re.test(filepath);
} else {
matches = filepath === pat;
}
if (matches) matched = rule;
}
return matched ? matched.owners : [];
}
// --- Map file paths to human-readable product names ---
function getProductName(filepath) {
const map = [
['src/langsmith/fleet/', 'LangSmith Fleet'],
['src/langsmith/', 'LangSmith'],
['src/oss/deepagents/', 'Deep Agents'],
['src/oss/langgraph/', 'LangGraph'],
['src/oss/langchain/', 'LangChain'],
['src/oss/python/integrations/', 'Python integrations'],
['src/oss/javascript/integrations/', 'JavaScript integrations'],
['src/oss/', 'open source'],
];
for (const [prefix, name] of map) {
if (filepath.startsWith(prefix)) return name;
}
return null;
}
// --- Get changed files ---
const files = await github.paginate(github.rest.pulls.listFiles, {
owner, repo, pull_number: pr.number, per_page: 100
});
// --- Group by product, merging owners across files ---
const productOwners = new Map();
let authorIsOwner = false;
for (const file of files) {
const owners = findOwners(file.filename);
const ownerNames = owners.map(o => o.replace(/^@/, ''));
if (ownerNames.some(o => o.toLowerCase() === author.toLowerCase())) {
authorIsOwner = true;
}
const product = getProductName(file.filename);
if (!product || owners.length === 0) continue;
// Skip areas where the author is already an owner
if (ownerNames.some(o => o.toLowerCase() === author.toLowerCase())) continue;
if (!productOwners.has(product)) {
productOwners.set(product, new Set());
}
for (const o of ownerNames) {
productOwners.get(product).add(o);
}
}
if (productOwners.size === 0) {
// Skip fallback if author owns any of the changed files
if (authorIsOwner || author.toLowerCase() === 'lnhsingh') return;
productOwners.set('General changes', new Set(['lnhsingh']));
}
// --- Auto-assign reviewers for bot authors ---
const botAuthors = ['open-swe'];
if (botAuthors.includes(author.toLowerCase())) {
const reviewers = [...new Set(
[...productOwners.values()].flatMap(s => [...s])
)];
if (reviewers.length > 0) {
await github.rest.pulls.requestReviewers({
owner, repo,
pull_number: pr.number,
reviewers
});
console.log(`Auto-assigned reviewers ${reviewers.join(', ')} on bot PR #${pr.number}`);
}
return;
}
// --- Format the comment ---
function formatOwners(ownerSet) {
const mentions = [...ownerSet].map(o => `\`@${o}\``);
if (mentions.length === 1) return mentions[0];
if (mentions.length === 2) return `${mentions[0]} or ${mentions[1]}`;
return mentions.slice(0, -1).join(', ') + `, or ${mentions[mentions.length - 1]}`;
}
const areas = [...productOwners.entries()];
const lines = areas.map(([product, owners]) =>
`- ${formatOwners(owners)} (${product})`
);
const body = [
`Thanks for opening a docs PR, @${author}! When it's ready for review, please add the relevant reviewers:\n`,
lines.join('\n')
].join('\n');
await github.rest.issues.createComment({
owner, repo,
issue_number: pr.number,
body
});
console.log(`Posted welcome comment on PR #${pr.number}`);