main
yml 788 lines 35.4 KB
Raw
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 if: github.repository_owner == 'vllm-project'
14 runs-on: ubuntu-latest
15 steps:
16 - name: Label issues based on keywords
17 id: label-step
18 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
19 with:
20 script: |
21 // Configuration: Add new labels and keywords here
22 const labelConfig = {
23 rocm: {
24 // Keyword search - matches whole words only (with word boundaries)
25 keywords: [
26 {
27 term: "composable kernel",
28 searchIn: "both"
29 },
30 {
31 term: "rccl",
32 searchIn: "body" // only search in body
33 },
34 {
35 term: "migraphx",
36 searchIn: "title" // only search in title
37 },
38 {
39 term: "hipgraph",
40 searchIn: "both"
41 },
42 {
43 term: "ROCm System Management Interface",
44 searchIn: "body"
45 },
46 ],
47 // Substring search - matches anywhere in text (partial matches)
48 substrings: [
49 {
50 term: "VLLM_ROCM_",
51 searchIn: "both"
52 },
53 {
54 term: "aiter",
55 searchIn: "title"
56 },
57 {
58 term: "rocm",
59 searchIn: "title"
60 },
61 {
62 term: "amd",
63 searchIn: "title"
64 },
65 {
66 term: "hip-",
67 searchIn: "both"
68 },
69 {
70 term: "gfx",
71 searchIn: "both"
72 },
73 {
74 term: "cdna",
75 searchIn: "both"
76 },
77 {
78 term: "rdna",
79 searchIn: "both"
80 },
81 {
82 term: "torch_hip",
83 searchIn: "body" // only in body
84 },
85 {
86 term: "_hip",
87 searchIn: "both"
88 },
89 {
90 term: "hip_",
91 searchIn: "both"
92 },
93 // ROCm tools and libraries
94 {
95 term: "hipify",
96 searchIn: "both"
97 },
98 ],
99 // Regex patterns - for complex pattern matching
100 regexPatterns: [
101 {
102 pattern: "\\bmi\\d{3}[a-z]*\\b",
103 description: "AMD GPU names (mi + 3 digits + optional letters)",
104 flags: "gi",
105 searchIn: "both" // "title", "body", or "both"
106 }
107 ],
108 },
109 cpu: {
110 // Keyword search - matches whole words only (with word boundaries)
111 keywords: [
112 {
113 term: "CPU Backend",
114 searchIn: "title"
115 },
116 {
117 term: "x86",
118 searchIn: "title"
119 },
120 {
121 term: "ARM",
122 searchIn: "title"
123 },
124 {
125 term: "Apple Silicon",
126 searchIn: "title"
127 },
128 {
129 term: "IBM Z",
130 searchIn: "title"
131 },
132 ],
133 },
134 kimi: {
135 keywords: [
136 { term: "Kimi", searchIn: "both" },
137 { term: "Moonshot", searchIn: "both" },
138 ],
139 substrings: [
140 { term: "moonshotai/", searchIn: "both" },
141 { term: "kimi", searchIn: "title" },
142 ],
143 },
144 k3: {
145 keywords: [
146 { term: "Kimi K3", searchIn: "both" },
147 { term: "K3", searchIn: "title" },
148 ],
149 substrings: [
150 { term: "moonshotai/kimi-k3", searchIn: "both" },
151 ],
152 },
153 quantization: {
154 keywords: [
155 {
156 term: "quantization",
157 searchIn: "both"
158 },
159 {
160 term: "quantized",
161 searchIn: "both"
162 },
163 ],
164 },
165 "intel-gpu": {
166 // Keyword search - matches whole words only (with word boundaries)
167 keywords: [
168 {
169 term: "B50",
170 searchIn: "both"
171 },
172 {
173 term: "B60",
174 searchIn: "both"
175 },
176 {
177 term: "B70",
178 searchIn: "both"
179 },
180 {
181 term: "intel gpu",
182 searchIn: "both"
183 },
184 {
185 term: "Arc GPU",
186 searchIn: "both"
187 },
188 {
189 term: "BMG",
190 searchIn: "both"
191 },
192 ],
193 },
194 // Model labels. Title-only by design: issue bodies carry pasted
195 // collect_env output, tracebacks and configs that name unrelated
196 // models, hardware and libraries.
197 deepseek: {
198 keywords: [{ term: "DeepSeek", searchIn: "title" }],
199 },
200 // DSv4 and DSv4.1 are distinct architectures; these keep them exclusive.
201 DSv4: {
202 regexPatterns: [{ pattern: "(?:\\bDSv4\\b|deepseek[-\\s_]?v4)(?![._]?1)", flags: "gi", searchIn: "title" }],
203 },
204 "DSv4.1": {
205 regexPatterns: [{ pattern: "(?:\\bDSv4[._]?1\\b|deepseek[-\\s_]?v4[._]?1)", flags: "gi", searchIn: "title" }],
206 },
207 llama: {
208 keywords: [{ term: "Llama", searchIn: "title" }],
209 },
210 mistral: {
211 keywords: [
212 { term: "Mistral", searchIn: "title" },
213 { term: "Ministral", searchIn: "title" },
214 { term: "Voxtral", searchIn: "title" },
215 { term: "Mixtral", searchIn: "title" },
216 { term: "Pixtral", searchIn: "title" },
217 ],
218 },
219 qwen: {
220 keywords: [{ term: "Qwen", searchIn: "title" }],
221 },
222 "gpt-oss": {
223 keywords: [{ term: "harmony", searchIn: "title" }],
224 substrings: [{ term: "gpt-oss", searchIn: "title" }],
225 },
226 glm: {
227 keywords: [
228 { term: "GLM", searchIn: "title" },
229 { term: "ChatGLM", searchIn: "title" },
230 ],
231 },
232 minimax: {
233 substrings: [{ term: "minimax", searchIn: "title" }],
234 },
235 inkling: {
236 keywords: [{ term: "Inkling", searchIn: "title" }],
237 },
238 // Hardware backends, mirroring the cpu/rocm/intel-gpu rules above.
239 nvidia: {
240 keywords: [
241 { term: "NVIDIA", searchIn: "title" },
242 { term: "CUTLASS", searchIn: "title" },
243 ],
244 },
245 tpu: {
246 keywords: [
247 { term: "TPU", searchIn: "title" },
248 { term: "XLA", searchIn: "title" },
249 ],
250 },
251 // Subsystems.
252 //
253 // Single distinctive words go in `keywords` so the word-boundary
254 // match keeps them from firing inside longer words. Multi-word
255 // phrases go in `substrings`: they are specific enough on their
256 // own, and a boundary match would miss ordinary variations --
257 // "structured outputs", "tool parsers", "prefix caching".
258 "speculative-decoding": {
259 keywords: [
260 { term: "eagle", searchIn: "title" },
261 { term: "medusa", searchIn: "title" },
262 ],
263 substrings: [
264 { term: "speculative decoding", searchIn: "title" },
265 { term: "speculative decode", searchIn: "title" },
266 { term: "draft model", searchIn: "title" },
267 ],
268 },
269 "structured-output": {
270 // Whole-word terms: backends, engine internals, and format
271 // vocabulary that is unambiguous inside vLLM.
272 keywords: [
273 { term: "xgrammar", searchIn: "title" },
274 { term: "outlines", searchIn: "title" }, // "this RFC outlines..." in bodies
275 { term: "llguidance", searchIn: "title" },
276 { term: "grammar", searchIn: "title" },
277 { term: "FSM", searchIn: "title" },
278 { term: "EBNF", searchIn: "title" },
279 { term: "bitmask", searchIn: "title" },
280 ],
281 // Substrings: separator variants of the feature name, the request
282 // fields that turn it on, and traceback frames it crashes in.
283 substrings: [
284 { term: "structured output", searchIn: "title" },
285 { term: "structured-output", searchIn: "title" },
286 { term: "structured_output", searchIn: "title" },
287 { term: "structuredoutput", searchIn: "title" }, // StructuredOutputManager
288 { term: "structured decoding", searchIn: "title" },
289 { term: "structured generation", searchIn: "title" },
290 { term: "guided decoding", searchIn: "title" },
291 { term: "guided generation", searchIn: "title" },
292 { term: "guided_decoding", searchIn: "title" },
293 { term: "constrained decoding", searchIn: "title" },
294 { term: "json schema", searchIn: "title" },
295 { term: "json_schema", searchIn: "title" },
296 { term: "json_object", searchIn: "title" },
297 { term: "response_format", searchIn: "title" },
298 { term: "structural tag", searchIn: "title" },
299 { term: "guided_json", searchIn: "both" },
300 { term: "guided_grammar", searchIn: "both" },
301 { term: "guided_choice", searchIn: "both" },
302 { term: "guided_regex", searchIn: "both" },
303 { term: "structural_tag", searchIn: "title" },
304 { term: "lm-format-enforcer", searchIn: "title" },
305 { term: "backend_xgrammar", searchIn: "body" },
306 { term: "apply_grammar_bitmask", searchIn: "body" },
307 { term: "grammar_bitmask", searchIn: "body" },
308 { term: "Failed to advance FSM", searchIn: "body" },
309 { term: "StructuredOutputsParams", searchIn: "body" },
310 ],
311 },
312 "tool-calling": {
313 keywords: [
314 { term: "tool", searchIn: "title" },
315 { term: "parser", searchIn: "title" },
316 { term: "tools", searchIn: "title" },
317 { term: "reasoning", searchIn: "title" },
318 { term: "thinking", searchIn: "title" },
319 { term: "harmony", searchIn: "title" },
320 { term: "mcp", searchIn: "title" },
321 ],
322 // Prefixes also match plural and compound forms.
323 substrings: [
324 { term: "tool call", searchIn: "title" },
325 { term: "toolcall", searchIn: "title" },
326 { term: "tool_call", searchIn: "title" },
327 { term: "tool choice", searchIn: "title" },
328 { term: "tool_choice", searchIn: "title" },
329 { term: "tool parser", searchIn: "title" },
330 { term: "tool_parser", searchIn: "title" },
331 { term: "tool-call-parser", searchIn: "title" },
332 { term: "tool use", searchIn: "title" },
333 { term: "function call", searchIn: "title" },
334 { term: "function_call", searchIn: "title" },
335 { term: "functioncall", searchIn: "title" },
336 { term: "reasoning parser", searchIn: "title" },
337 { term: "reasoning_parser", searchIn: "title" },
338 { term: "reasoning-parser", searchIn: "title" },
339 { term: "reasoning_content", searchIn: "title" },
340 { term: "reasoning_effort", searchIn: "title" },
341 { term: "enable_reasoning", searchIn: "title" },
342 { term: "enable_thinking", searchIn: "title" },
343 { term: "thinking_token_budget", searchIn: "title" },
344 { term: "<think>", searchIn: "title" },
345 { term: "</think>", searchIn: "title" },
346 { term: "chain of thought", searchIn: "title" },
347 // In bodies, match identifiers only; common CLI flags cause
348 // false positives in pasted `vllm serve` commands.
349 { term: "extract_tool_calls", searchIn: "body" },
350 { term: "DeltaToolCall", searchIn: "body" },
351 { term: "tool_parsers/", searchIn: "body" },
352 { term: "vllm/reasoning/", searchIn: "body" },
353 { term: "hermes_tool_parser", searchIn: "body" },
354 ],
355 },
356 "kv-connector": {
357 keywords: [
358 { term: "NIXL", searchIn: "title" },
359 { term: "LMCache", searchIn: "title" },
360 ],
361 substrings: [
362 { term: "KV transfer", searchIn: "title" },
363 { term: "disaggregat", searchIn: "title" },
364 ],
365 },
366 scheduler: {
367 keywords: [{ term: "scheduler", searchIn: "title" }],
368 },
369 "kv-cache-manager": {
370 substrings: [
371 { term: "kv cache manager", searchIn: "title" },
372 { term: "block manager", searchIn: "title" },
373 { term: "prefix cache", searchIn: "title" },
374 { term: "prefix caching", searchIn: "title" },
375 ],
376 },
377 "torch.compile": {
378 substrings: [{ term: "torch.compile", searchIn: "title" }],
379 },
380 "multi-modality": {
381 keywords: [
382 { term: "multimodal", searchIn: "title" },
383 { term: "multi-modal", searchIn: "title" },
384 ],
385 substrings: [
386 { term: "image input", searchIn: "title" },
387 { term: "audio input", searchIn: "title" },
388 ],
389 },
390 "ci-failure": {
391 keywords: [{ term: "flaky", searchIn: "title" }],
392 substrings: [
393 { term: "CI failure", searchIn: "title" },
394 { term: "test failure", searchIn: "title" },
395 ],
396 },
397 rl: {
398 keywords: [{ term: "RLHF", searchIn: "title" }],
399 substrings: [
400 { term: "reinforcement learning", searchIn: "title" },
401 { term: "weight update", searchIn: "title" },
402 ],
403 },
404 ray: {
405 substrings: [
406 { term: "Ray cluster", searchIn: "title" },
407 { term: "Ray executor", searchIn: "title" },
408 { term: "placement group", searchIn: "title" },
409 ],
410 },
411 "vllm-ir": {
412 substrings: [
413 { term: "vllm-ir", searchIn: "title" },
414 { term: "vllm ir", searchIn: "title" },
415 ],
416 },
417 rust: {
418 keywords: [
419 { term: "Rust", searchIn: "title" },
420 { term: "PyO3", searchIn: "title" },
421 ],
422 },
423 // Add more label configurations here as needed
424 // example: {
425 // keywords: [...],
426 // substrings: [...],
427 // regexPatterns: [...]
428 // },
429 };
430 // Helper function to create regex based on search type
431 function createSearchRegex(term, type) {
432 // Escape special regex characters in the term
433 const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
434 switch (type) {
435 case 'keyword':
436 // Word boundary search - matches whole words only
437 return new RegExp(`\\b${escapedTerm}\\b`, "gi");
438 case 'substring':
439 // Substring search - matches anywhere in the text
440 return new RegExp(escapedTerm, "gi");
441 default:
442 throw new Error(`Unknown search type: ${type}`);
443 }
444 }
445 // Helper function to find matching terms in text with line information
446 function findMatchingTermsWithLines(text, searchTerms = [], searchType = 'keyword', searchLocation = '') {
447 const matches = [];
448 const lines = text.split('\n');
449 for (const termConfig of searchTerms) {
450 let regex;
451 let term, searchIn, pattern, description, flags;
452 // Handle different input formats (string or object)
453 if (typeof termConfig === 'string') {
454 term = termConfig;
455 searchIn = 'both'; // default
456 } else {
457 term = termConfig.term;
458 searchIn = termConfig.searchIn || 'both';
459 pattern = termConfig.pattern;
460 description = termConfig.description;
461 flags = termConfig.flags;
462 }
463 // Skip if this term shouldn't be searched in the current location
464 if (searchIn !== 'both' && searchIn !== searchLocation) {
465 continue;
466 }
467 // Create appropriate regex
468 if (searchType === 'regex') {
469 regex = new RegExp(pattern, flags || "gi");
470 } else {
471 regex = createSearchRegex(term, searchType);
472 }
473 const termMatches = [];
474 // Check each line for matches
475 lines.forEach((line, lineIndex) => {
476 const lineMatches = line.match(regex);
477 if (lineMatches) {
478 lineMatches.forEach(match => {
479 termMatches.push({
480 match: match,
481 lineNumber: lineIndex + 1,
482 lineContent: line.trim(),
483 searchType: searchType,
484 searchLocation: searchLocation,
485 originalTerm: term || pattern,
486 description: description,
487 // Show context around the match in the line
488 context: line.length > 100 ?
489 line.substring(Math.max(0, line.toLowerCase().indexOf(match.toLowerCase()) - 30),
490 line.toLowerCase().indexOf(match.toLowerCase()) + match.length + 30) + '...'
491 : line.trim()
492 });
493 });
494 }
495 });
496 if (termMatches.length > 0) {
497 matches.push({
498 term: term || (description || pattern),
499 searchType: searchType,
500 searchLocation: searchLocation,
501 searchIn: searchIn,
502 pattern: pattern,
503 matches: termMatches,
504 count: termMatches.length
505 });
506 }
507 }
508 return matches;
509 }
510 // Helper function to check if label should be added
511 async function processLabel(labelName, config) {
512 const body = context.payload.issue.body || "";
513 const title = context.payload.issue.title || "";
514 core.notice(`Processing label: ${labelName}`);
515 core.notice(`Issue Title: "${title}"`);
516 core.notice(`Issue Body length: ${body.length} characters`);
517 let shouldAddLabel = false;
518 let allMatches = [];
519 let reason = '';
520 const keywords = config.keywords || [];
521 const substrings = config.substrings || [];
522 const regexPatterns = config.regexPatterns || [];
523 core.notice(`Searching with ${keywords.length} keywords, ${substrings.length} substrings, and ${regexPatterns.length} regex patterns`);
524 // Search in title
525 if (title.trim()) {
526 core.notice(`Searching in title: "${title}"`);
527 const titleKeywordMatches = findMatchingTermsWithLines(title, keywords, 'keyword', 'title');
528 const titleSubstringMatches = findMatchingTermsWithLines(title, substrings, 'substring', 'title');
529 const titleRegexMatches = findMatchingTermsWithLines(title, regexPatterns, 'regex', 'title');
530 allMatches.push(...titleKeywordMatches, ...titleSubstringMatches, ...titleRegexMatches);
531 }
532 // Search in body
533 if (body.trim()) {
534 core.notice(`Searching in body (${body.length} characters)`);
535 const bodyKeywordMatches = findMatchingTermsWithLines(body, keywords, 'keyword', 'body');
536 const bodySubstringMatches = findMatchingTermsWithLines(body, substrings, 'substring', 'body');
537 const bodyRegexMatches = findMatchingTermsWithLines(body, regexPatterns, 'regex', 'body');
538 allMatches.push(...bodyKeywordMatches, ...bodySubstringMatches, ...bodyRegexMatches);
539 }
540 if (allMatches.length > 0) {
541 core.notice(`Found ${allMatches.length} matching term(s):`);
542 for (const termMatch of allMatches) {
543 const locationText = termMatch.searchLocation === 'title' ? 'title' : 'body';
544 const searchInText = termMatch.searchIn === 'both' ? 'both' : termMatch.searchIn;
545 if (termMatch.searchType === 'regex') {
546 core.notice(` 📍 Regex: "${termMatch.term}" (pattern: ${termMatch.pattern}) found ${termMatch.count} time(s) in ${locationText} (configured to search in: ${searchInText}):`);
547 } else {
548 core.notice(` 📍 Term: "${termMatch.term}" (${termMatch.searchType} search) found ${termMatch.count} time(s) in ${locationText} (configured to search in: ${searchInText}):`);
549 }
550 // Show details for each match
551 termMatch.matches.forEach((match, index) => {
552 core.notice(` ${index + 1}. Line ${match.lineNumber} in ${match.searchLocation}: "${match.match}" [${match.searchType}]`);
553 if (match.description) {
554 core.notice(` Description: ${match.description}`);
555 }
556 core.notice(` Context: ${match.context}`);
557 if (match.lineContent !== match.context) {
558 core.notice(` Full line: ${match.lineContent}`);
559 }
560 });
561 }
562 shouldAddLabel = true;
563 const totalMatches = allMatches.reduce((sum, t) => sum + t.count, 0);
564 const titleMatches = allMatches.filter(t => t.searchLocation === 'title').reduce((sum, t) => sum + t.count, 0);
565 const bodyMatches = allMatches.filter(t => t.searchLocation === 'body').reduce((sum, t) => sum + t.count, 0);
566 const keywordMatches = allMatches.filter(t => t.searchType === 'keyword').reduce((sum, t) => sum + t.count, 0);
567 const substringMatches = allMatches.filter(t => t.searchType === 'substring').reduce((sum, t) => sum + t.count, 0);
568 const regexMatches = allMatches.filter(t => t.searchType === 'regex').reduce((sum, t) => sum + t.count, 0);
569 reason = `Found ${totalMatches} total matches (${titleMatches} in title, ${bodyMatches} in body) - ${keywordMatches} keyword matches, ${substringMatches} substring matches, ${regexMatches} regex matches`;
570 }
571 core.notice(`Final decision: ${shouldAddLabel ? 'ADD LABEL' : 'DO NOT ADD LABEL'}`);
572 core.notice(`Reason: ${reason || 'No matching terms found'}`);
573 if (shouldAddLabel) {
574 const existingLabels = context.payload.issue.labels.map(l => l.name);
575 if (!existingLabels.includes(labelName)) {
576 await github.rest.issues.addLabels({
577 owner: context.repo.owner,
578 repo: context.repo.repo,
579 issue_number: context.issue.number,
580 labels: [labelName],
581 });
582 core.notice(`Label "${labelName}" added. ${reason}`);
583 return true;
584 }
585 core.notice(`Label "${labelName}" already present.`);
586 return false;
587 }
588 core.notice(`No matching terms found for label "${labelName}".`);
589 return false;
590 }
591 // Process all configured labels
592 const labelsAddedResults = await Promise.all(
593 Object.entries(labelConfig).map(([labelName, config]) =>
594 processLabel(labelName, config).then(added => ({ labelName, added }))
595 )
596 );
597
598 const numLabelsAdded = labelsAddedResults.filter(r => r.added).length;
599 core.notice(`Processing complete. ${numLabelsAdded} label(s) added.`);
600
601 // Return which labels were added for the next step
602 const addedLabels = labelsAddedResults.filter(r => r.added).map(r => r.labelName);
603 core.setOutput('labels_added', JSON.stringify(addedLabels));
604 return addedLabels;
605
606 - name: CC users for labeled issues
607 if: steps.label-step.outputs.labels_added != '[]'
608 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
609 with:
610 script: |
611 // Configuration: Map labels to GitHub users to CC
612 // You can add multiple users per label, and multiple label configurations
613 // {users} will be replaced with @mentions
614 const ccConfig = {
615 rocm: {
616 users: ['hongxiayang', 'tjtanaa', 'vllmellm', 'giuseppegrossi'],
617 message: 'CC {users} for ROCm-related issue',
618 },
619 cohere: {
620 users: ['walterbm', 'ekagra-ranjan', 'andrewbcohere', 'jasonozuzu-cohere'],
621 message: 'CC {users} for Cohere-related issue',
622 },
623 mistral: {
624 users: ['patrickvonplaten', 'juliendenize', 'andylolu2', 'NickLucche'],
625 message: 'CC {users} for Mistral-related issue',
626 },
627 // Add more label -> user mappings here
628 // Example:
629 // cuda: {
630 // users: ['user1', 'user2'],
631 // message: 'CC {users} for CUDA-related issue',
632 // },
633 // performance: {
634 // users: ['perfexpert'],
635 // message: 'CC {users} for performance issue',
636 // },
637 };
638
639 const labelsAdded = JSON.parse('${{ steps.label-step.outputs.labels_added }}');
640 core.notice(`Labels added: ${labelsAdded.join(', ')}`);
641
642 // Get existing comments to check for already mentioned users
643 const comments = await github.rest.issues.listComments({
644 owner: context.repo.owner,
645 repo: context.repo.repo,
646 issue_number: context.issue.number,
647 });
648
649 const issueBody = context.payload.issue.body || '';
650 const allExistingText = issueBody + '\n' + comments.data.map(c => c.body).join('\n');
651
652 // Process each label that was added
653 for (const label of labelsAdded) {
654 if (ccConfig[label]) {
655 const config = ccConfig[label];
656 const usersToMention = [];
657
658 // Check which users haven't been mentioned yet
659 for (const user of config.users) {
660 const mentionPattern = new RegExp(`@${user}\\b`, 'i');
661 if (!mentionPattern.test(allExistingText)) {
662 usersToMention.push(user);
663 } else {
664 core.notice(`@${user} already mentioned for label "${label}", skipping`);
665 }
666 }
667
668 // Post comment if there are users to mention
669 if (usersToMention.length > 0) {
670 const mentions = usersToMention.map(u => `@${u}`).join(' ');
671 const message = config.message.replace('{users}', mentions);
672
673 await github.rest.issues.createComment({
674 owner: context.repo.owner,
675 repo: context.repo.repo,
676 issue_number: context.issue.number,
677 body: message
678 });
679
680 core.notice(`CC comment added for label "${label}": ${mentions}`);
681 } else {
682 core.notice(`All users for label "${label}" already mentioned, skipping comment`);
683 }
684 }
685 }
686
687 - name: Request missing ROCm info from issue author
688 if: contains(steps.label-step.outputs.labels_added, 'rocm') && contains(toJSON(github.event.issue.labels.*.name), 'bug')
689 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
690 with:
691 script: |
692 const body = (context.payload.issue.body || '').toLowerCase();
693
694 // Check for existing bot comments to avoid duplicate requests
695 const comments = await github.rest.issues.listComments({
696 owner: context.repo.owner,
697 repo: context.repo.repo,
698 issue_number: context.issue.number,
699 });
700 const botAlreadyAsked = comments.data.some(
701 c => c.user.type === 'Bot' && c.body.includes('<!-- rocm-info-request -->')
702 );
703 if (botAlreadyAsked) {
704 core.notice('ROCm info request already posted, skipping');
705 return;
706 }
707
708 // Define required information and detection patterns
709 const requiredInfo = [
710 {
711 name: 'Reproducer',
712 patterns: [
713 /reproduc/i, /minimal.?example/i, /repro\b/i, /steps to reproduce/i,
714 /code.?snippet/i, /sample.?code/i,
715 /```python[\s\S]*?```/, /```bash[\s\S]*?```/, /```sh[\s\S]*?```/,
716 ],
717 ask: 'A minimal reproducer (code snippet or script that triggers the issue)',
718 },
719 {
720 name: 'Error message',
721 patterns: [
722 /error/i, /traceback/i, /exception/i, /fault/i, /crash/i,
723 /failed/i, /abort/i, /panic/i,
724 ],
725 ask: 'The full error message or traceback',
726 },
727 {
728 name: 'Installation method',
729 patterns: [
730 /docker/i, /rocm\/pytorch/i, /dockerfile/i, /from source/i,
731 /pip install/i, /build.?from/i, /container/i, /image/i,
732 /wheel/i, /\.whl/i, /nightly/i,
733 ],
734 ask: 'How you installed vLLM (Docker image name, pip install, or build from source steps)',
735 },
736 {
737 name: 'Command',
738 patterns: [
739 /vllm serve/i, /python\s+\S+\.py/i, /```bash[\s\S]*?```/,
740 /```sh[\s\S]*?```/, /command/i, /launch/i, /run\s/i,
741 /--model/i, /--tensor-parallel/i, /--gpu-memory/i,
742 ],
743 ask: 'The command you used to launch vLLM (e.g., `vllm serve ...` or the Python script)',
744 },
745 {
746 name: 'GFX architecture',
747 patterns: [
748 /gfx\d{3,4}/i, /mi\d{3}/i, /mi\d{2}\b/i, /radeon/i,
749 /gpu.?arch/i, /rocm-smi/i, /rocminfo/i, /navi/i,
750 /instinct/i,
751 ],
752 ask: 'Your GPU model and GFX architecture (e.g., MI300X / gfx942) — run `rocminfo | grep gfx`',
753 },
754 ];
755
756 const issueBody = context.payload.issue.body || '';
757 const missing = requiredInfo.filter(info =>
758 !info.patterns.some(p => p.test(issueBody))
759 );
760
761 if (missing.length === 0) {
762 core.notice('All required ROCm info appears to be present');
763 return;
764 }
765
766 const author = context.payload.issue.user.login;
767 const checklist = requiredInfo.map(info => {
768 const found = !missing.includes(info);
769 return `- [${found ? 'x' : ' '}] ${info.ask}`;
770 }).join('\n');
771 const message = [
772 '<!-- rocm-info-request -->',
773 `Hi @${author}, thanks for reporting this ROCm issue!`,
774 '',
775 'To help us investigate, please make sure the following information is included:',
776 '',
777 checklist,
778 '',
779 'Please provide any unchecked items above. This will help us reproduce and resolve the issue faster. Thank you!',
780 ].join('\n');
781
782 await github.rest.issues.createComment({
783 owner: context.repo.owner,
784 repo: context.repo.repo,
785 issue_number: context.issue.number,
786 body: message,
787 });
788 core.notice(`Requested missing ROCm info from @${author}: ${missing.map(m => m.name).join(', ')}`);