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