Skip to content

Commit 44ac25e

Browse files
[CI] [Doc]: Add GH Action for auto labeling issues with rocm tag (#20988)
Signed-off-by: vllmellm <[email protected]> Co-authored-by: Cyrus Leung <[email protected]>
1 parent 7ea22e4 commit 44ac25e

File tree

1 file changed

+305
-0
lines changed

1 file changed

+305
-0
lines changed

.github/workflows/issue_autolabel.yml

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
name: Label issues based on keywords
2+
on:
3+
issues:
4+
types: [opened, edited, reopened]
5+
permissions:
6+
issues: write # needed so the workflow can add labels
7+
contents: read
8+
concurrency:
9+
group: issue-labeler-${{ github.event.issue.number }}
10+
cancel-in-progress: true
11+
jobs:
12+
add-labels:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Label issues based on keywords
16+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
17+
with:
18+
script: |
19+
// Configuration: Add new labels and keywords here
20+
const labelConfig = {
21+
rocm: {
22+
// Keyword search - matches whole words only (with word boundaries)
23+
keywords: [
24+
{
25+
term: "composable kernel",
26+
searchIn: "both"
27+
},
28+
{
29+
term: "rccl",
30+
searchIn: "body" // only search in body
31+
},
32+
{
33+
term: "migraphx",
34+
searchIn: "title" // only search in title
35+
},
36+
{
37+
term: "hipgraph",
38+
searchIn: "both"
39+
},
40+
{
41+
term: "ROCm System Management Interface",
42+
searchIn: "body"
43+
},
44+
],
45+
46+
// Substring search - matches anywhere in text (partial matches)
47+
substrings: [
48+
{
49+
term: "VLLM_ROCM_",
50+
searchIn: "both"
51+
},
52+
{
53+
term: "rocm",
54+
searchIn: "title"
55+
},
56+
{
57+
term: "amd",
58+
searchIn: "title"
59+
},
60+
{
61+
term: "hip-",
62+
searchIn: "both"
63+
},
64+
{
65+
term: "gfx",
66+
searchIn: "both"
67+
},
68+
{
69+
term: "cdna",
70+
searchIn: "both"
71+
},
72+
{
73+
term: "rdna",
74+
searchIn: "both"
75+
},
76+
{
77+
term: "torch_hip",
78+
searchIn: "body" // only in body
79+
},
80+
{
81+
term: "_hip",
82+
searchIn: "both"
83+
},
84+
{
85+
term: "hip_",
86+
searchIn: "both"
87+
},
88+
89+
// ROCm tools and libraries
90+
{
91+
term: "hipify",
92+
searchIn: "both"
93+
},
94+
],
95+
96+
// Regex patterns - for complex pattern matching
97+
regexPatterns: [
98+
{
99+
pattern: "\\bmi\\d{3}[a-z]*\\b",
100+
description: "AMD GPU names (mi + 3 digits + optional letters)",
101+
flags: "gi",
102+
searchIn: "both" // "title", "body", or "both"
103+
}
104+
],
105+
},
106+
};
107+
108+
// Helper function to create regex based on search type
109+
function createSearchRegex(term, type) {
110+
// Escape special regex characters in the term
111+
const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
112+
113+
switch (type) {
114+
case 'keyword':
115+
// Word boundary search - matches whole words only
116+
return new RegExp(`\\b${escapedTerm}\\b`, "gi");
117+
case 'substring':
118+
// Substring search - matches anywhere in the text
119+
return new RegExp(escapedTerm, "gi");
120+
default:
121+
throw new Error(`Unknown search type: ${type}`);
122+
}
123+
}
124+
125+
// Helper function to find matching terms in text with line information
126+
function findMatchingTermsWithLines(text, searchTerms = [], searchType = 'keyword', searchLocation = '') {
127+
const matches = [];
128+
const lines = text.split('\n');
129+
130+
for (const termConfig of searchTerms) {
131+
let regex;
132+
let term, searchIn, pattern, description, flags;
133+
134+
// Handle different input formats (string or object)
135+
if (typeof termConfig === 'string') {
136+
term = termConfig;
137+
searchIn = 'both'; // default
138+
} else {
139+
term = termConfig.term;
140+
searchIn = termConfig.searchIn || 'both';
141+
pattern = termConfig.pattern;
142+
description = termConfig.description;
143+
flags = termConfig.flags;
144+
}
145+
146+
// Skip if this term shouldn't be searched in the current location
147+
if (searchIn !== 'both' && searchIn !== searchLocation) {
148+
continue;
149+
}
150+
151+
// Create appropriate regex
152+
if (searchType === 'regex') {
153+
regex = new RegExp(pattern, flags || "gi");
154+
} else {
155+
regex = createSearchRegex(term, searchType);
156+
}
157+
158+
const termMatches = [];
159+
160+
// Check each line for matches
161+
lines.forEach((line, lineIndex) => {
162+
const lineMatches = line.match(regex);
163+
if (lineMatches) {
164+
lineMatches.forEach(match => {
165+
termMatches.push({
166+
match: match,
167+
lineNumber: lineIndex + 1,
168+
lineContent: line.trim(),
169+
searchType: searchType,
170+
searchLocation: searchLocation,
171+
originalTerm: term || pattern,
172+
description: description,
173+
// Show context around the match in the line
174+
context: line.length > 100 ?
175+
line.substring(Math.max(0, line.toLowerCase().indexOf(match.toLowerCase()) - 30),
176+
line.toLowerCase().indexOf(match.toLowerCase()) + match.length + 30) + '...'
177+
: line.trim()
178+
});
179+
});
180+
}
181+
});
182+
183+
if (termMatches.length > 0) {
184+
matches.push({
185+
term: term || (description || pattern),
186+
searchType: searchType,
187+
searchLocation: searchLocation,
188+
searchIn: searchIn,
189+
pattern: pattern,
190+
matches: termMatches,
191+
count: termMatches.length
192+
});
193+
}
194+
}
195+
196+
return matches;
197+
}
198+
199+
// Helper function to check if label should be added
200+
async function processLabel(labelName, config) {
201+
const body = context.payload.issue.body || "";
202+
const title = context.payload.issue.title || "";
203+
204+
core.notice(`Processing label: ${labelName}`);
205+
core.notice(`Issue Title: "${title}"`);
206+
core.notice(`Issue Body length: ${body.length} characters`);
207+
208+
let shouldAddLabel = false;
209+
let allMatches = [];
210+
let reason = '';
211+
212+
const keywords = config.keywords || [];
213+
const substrings = config.substrings || [];
214+
const regexPatterns = config.regexPatterns || [];
215+
216+
core.notice(`Searching with ${keywords.length} keywords, ${substrings.length} substrings, and ${regexPatterns.length} regex patterns`);
217+
218+
// Search in title
219+
if (title.trim()) {
220+
core.notice(`Searching in title: "${title}"`);
221+
222+
const titleKeywordMatches = findMatchingTermsWithLines(title, keywords, 'keyword', 'title');
223+
const titleSubstringMatches = findMatchingTermsWithLines(title, substrings, 'substring', 'title');
224+
const titleRegexMatches = findMatchingTermsWithLines(title, regexPatterns, 'regex', 'title');
225+
226+
allMatches.push(...titleKeywordMatches, ...titleSubstringMatches, ...titleRegexMatches);
227+
}
228+
229+
// Search in body
230+
if (body.trim()) {
231+
core.notice(`Searching in body (${body.length} characters)`);
232+
233+
const bodyKeywordMatches = findMatchingTermsWithLines(body, keywords, 'keyword', 'body');
234+
const bodySubstringMatches = findMatchingTermsWithLines(body, substrings, 'substring', 'body');
235+
const bodyRegexMatches = findMatchingTermsWithLines(body, regexPatterns, 'regex', 'body');
236+
237+
allMatches.push(...bodyKeywordMatches, ...bodySubstringMatches, ...bodyRegexMatches);
238+
}
239+
240+
if (allMatches.length > 0) {
241+
core.notice(`Found ${allMatches.length} matching term(s):`);
242+
243+
for (const termMatch of allMatches) {
244+
const locationText = termMatch.searchLocation === 'title' ? 'title' : 'body';
245+
const searchInText = termMatch.searchIn === 'both' ? 'both' : termMatch.searchIn;
246+
247+
if (termMatch.searchType === 'regex') {
248+
core.notice(` 📍 Regex: "${termMatch.term}" (pattern: ${termMatch.pattern}) found ${termMatch.count} time(s) in ${locationText} (configured to search in: ${searchInText}):`);
249+
} else {
250+
core.notice(` 📍 Term: "${termMatch.term}" (${termMatch.searchType} search) found ${termMatch.count} time(s) in ${locationText} (configured to search in: ${searchInText}):`);
251+
}
252+
253+
// Show details for each match
254+
termMatch.matches.forEach((match, index) => {
255+
core.notice(` ${index + 1}. Line ${match.lineNumber} in ${match.searchLocation}: "${match.match}" [${match.searchType}]`);
256+
if (match.description) {
257+
core.notice(` Description: ${match.description}`);
258+
}
259+
core.notice(` Context: ${match.context}`);
260+
if (match.lineContent !== match.context) {
261+
core.notice(` Full line: ${match.lineContent}`);
262+
}
263+
});
264+
}
265+
266+
shouldAddLabel = true;
267+
const totalMatches = allMatches.reduce((sum, t) => sum + t.count, 0);
268+
const titleMatches = allMatches.filter(t => t.searchLocation === 'title').reduce((sum, t) => sum + t.count, 0);
269+
const bodyMatches = allMatches.filter(t => t.searchLocation === 'body').reduce((sum, t) => sum + t.count, 0);
270+
const keywordMatches = allMatches.filter(t => t.searchType === 'keyword').reduce((sum, t) => sum + t.count, 0);
271+
const substringMatches = allMatches.filter(t => t.searchType === 'substring').reduce((sum, t) => sum + t.count, 0);
272+
const regexMatches = allMatches.filter(t => t.searchType === 'regex').reduce((sum, t) => sum + t.count, 0);
273+
274+
reason = `Found ${totalMatches} total matches (${titleMatches} in title, ${bodyMatches} in body) - ${keywordMatches} keyword matches, ${substringMatches} substring matches, ${regexMatches} regex matches`;
275+
}
276+
277+
core.notice(`Final decision: ${shouldAddLabel ? 'ADD LABEL' : 'DO NOT ADD LABEL'}`);
278+
core.notice(`Reason: ${reason || 'No matching terms found'}`);
279+
280+
if (shouldAddLabel) {
281+
const existingLabels = context.payload.issue.labels.map(l => l.name);
282+
if (!existingLabels.includes(labelName)) {
283+
await github.rest.issues.addLabels({
284+
owner: context.repo.owner,
285+
repo: context.repo.repo,
286+
issue_number: context.issue.number,
287+
labels: [labelName],
288+
});
289+
core.notice(`Label "${labelName}" added. ${reason}`);
290+
return true;
291+
}
292+
core.notice(`Label "${labelName}" already present.`);
293+
return false;
294+
}
295+
296+
core.notice(`No matching terms found for label "${labelName}".`);
297+
return false;
298+
}
299+
300+
// Process all configured labels
301+
const processLabels = Object.entries(labelConfig)
302+
.map(([labelName, config]) => processLabel(labelName, config));
303+
const labelsAdded = await Promise.all(processLabels);
304+
const numLabelsAdded = labelsAdded.reduce((x, y) => x + y, 0);
305+
core.notice(`Processing complete. ${numLabelsAdded} label(s) added.`);

0 commit comments

Comments
 (0)