name: Label issues based on keywords on: issues: types: [opened, edited, reopened] permissions: issues: write # needed so the workflow can add labels contents: read concurrency: group: issue-labeler-${{ github.event.issue.number }} cancel-in-progress: true jobs: add-labels: runs-on: ubuntu-latest steps: - name: Label issues based on keywords uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | // Configuration: Add new labels and keywords here const labelConfig = { rocm: { // Keyword search - matches whole words only (with word boundaries) keywords: [ { term: "composable kernel", searchIn: "both" }, { term: "rccl", searchIn: "body" // only search in body }, { term: "migraphx", searchIn: "title" // only search in title }, { term: "hipgraph", searchIn: "both" }, { term: "ROCm System Management Interface", searchIn: "body" }, ], // Substring search - matches anywhere in text (partial matches) substrings: [ { term: "VLLM_ROCM_", searchIn: "both" }, { term: "aiter", searchIn: "title" }, { term: "rocm", searchIn: "title" }, { term: "amd", searchIn: "title" }, { term: "hip-", searchIn: "both" }, { term: "gfx", searchIn: "both" }, { term: "cdna", searchIn: "both" }, { term: "rdna", searchIn: "both" }, { term: "torch_hip", searchIn: "body" // only in body }, { term: "_hip", searchIn: "both" }, { term: "hip_", searchIn: "both" }, // ROCm tools and libraries { term: "hipify", searchIn: "both" }, ], // Regex patterns - for complex pattern matching regexPatterns: [ { pattern: "\\bmi\\d{3}[a-z]*\\b", description: "AMD GPU names (mi + 3 digits + optional letters)", flags: "gi", searchIn: "both" // "title", "body", or "both" } ], }, }; // Helper function to create regex based on search type function createSearchRegex(term, type) { // Escape special regex characters in the term const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); switch (type) { case 'keyword': // Word boundary search - matches whole words only return new RegExp(`\\b${escapedTerm}\\b`, "gi"); case 'substring': // Substring search - matches anywhere in the text return new RegExp(escapedTerm, "gi"); default: throw new Error(`Unknown search type: ${type}`); } } // Helper function to find matching terms in text with line information function findMatchingTermsWithLines(text, searchTerms = [], searchType = 'keyword', searchLocation = '') { const matches = []; const lines = text.split('\n'); for (const termConfig of searchTerms) { let regex; let term, searchIn, pattern, description, flags; // Handle different input formats (string or object) if (typeof termConfig === 'string') { term = termConfig; searchIn = 'both'; // default } else { term = termConfig.term; searchIn = termConfig.searchIn || 'both'; pattern = termConfig.pattern; description = termConfig.description; flags = termConfig.flags; } // Skip if this term shouldn't be searched in the current location if (searchIn !== 'both' && searchIn !== searchLocation) { continue; } // Create appropriate regex if (searchType === 'regex') { regex = new RegExp(pattern, flags || "gi"); } else { regex = createSearchRegex(term, searchType); } const termMatches = []; // Check each line for matches lines.forEach((line, lineIndex) => { const lineMatches = line.match(regex); if (lineMatches) { lineMatches.forEach(match => { termMatches.push({ match: match, lineNumber: lineIndex + 1, lineContent: line.trim(), searchType: searchType, searchLocation: searchLocation, originalTerm: term || pattern, description: description, // Show context around the match in the line context: line.length > 100 ? line.substring(Math.max(0, line.toLowerCase().indexOf(match.toLowerCase()) - 30), line.toLowerCase().indexOf(match.toLowerCase()) + match.length + 30) + '...' : line.trim() }); }); } }); if (termMatches.length > 0) { matches.push({ term: term || (description || pattern), searchType: searchType, searchLocation: searchLocation, searchIn: searchIn, pattern: pattern, matches: termMatches, count: termMatches.length }); } } return matches; } // Helper function to check if label should be added async function processLabel(labelName, config) { const body = context.payload.issue.body || ""; const title = context.payload.issue.title || ""; core.notice(`Processing label: ${labelName}`); core.notice(`Issue Title: "${title}"`); core.notice(`Issue Body length: ${body.length} characters`); let shouldAddLabel = false; let allMatches = []; let reason = ''; const keywords = config.keywords || []; const substrings = config.substrings || []; const regexPatterns = config.regexPatterns || []; core.notice(`Searching with ${keywords.length} keywords, ${substrings.length} substrings, and ${regexPatterns.length} regex patterns`); // Search in title if (title.trim()) { core.notice(`Searching in title: "${title}"`); const titleKeywordMatches = findMatchingTermsWithLines(title, keywords, 'keyword', 'title'); const titleSubstringMatches = findMatchingTermsWithLines(title, substrings, 'substring', 'title'); const titleRegexMatches = findMatchingTermsWithLines(title, regexPatterns, 'regex', 'title'); allMatches.push(...titleKeywordMatches, ...titleSubstringMatches, ...titleRegexMatches); } // Search in body if (body.trim()) { core.notice(`Searching in body (${body.length} characters)`); const bodyKeywordMatches = findMatchingTermsWithLines(body, keywords, 'keyword', 'body'); const bodySubstringMatches = findMatchingTermsWithLines(body, substrings, 'substring', 'body'); const bodyRegexMatches = findMatchingTermsWithLines(body, regexPatterns, 'regex', 'body'); allMatches.push(...bodyKeywordMatches, ...bodySubstringMatches, ...bodyRegexMatches); } if (allMatches.length > 0) { core.notice(`Found ${allMatches.length} matching term(s):`); for (const termMatch of allMatches) { const locationText = termMatch.searchLocation === 'title' ? 'title' : 'body'; const searchInText = termMatch.searchIn === 'both' ? 'both' : termMatch.searchIn; if (termMatch.searchType === 'regex') { core.notice(` 📍 Regex: "${termMatch.term}" (pattern: ${termMatch.pattern}) found ${termMatch.count} time(s) in ${locationText} (configured to search in: ${searchInText}):`); } else { core.notice(` 📍 Term: "${termMatch.term}" (${termMatch.searchType} search) found ${termMatch.count} time(s) in ${locationText} (configured to search in: ${searchInText}):`); } // Show details for each match termMatch.matches.forEach((match, index) => { core.notice(` ${index + 1}. Line ${match.lineNumber} in ${match.searchLocation}: "${match.match}" [${match.searchType}]`); if (match.description) { core.notice(` Description: ${match.description}`); } core.notice(` Context: ${match.context}`); if (match.lineContent !== match.context) { core.notice(` Full line: ${match.lineContent}`); } }); } shouldAddLabel = true; const totalMatches = allMatches.reduce((sum, t) => sum + t.count, 0); const titleMatches = allMatches.filter(t => t.searchLocation === 'title').reduce((sum, t) => sum + t.count, 0); const bodyMatches = allMatches.filter(t => t.searchLocation === 'body').reduce((sum, t) => sum + t.count, 0); const keywordMatches = allMatches.filter(t => t.searchType === 'keyword').reduce((sum, t) => sum + t.count, 0); const substringMatches = allMatches.filter(t => t.searchType === 'substring').reduce((sum, t) => sum + t.count, 0); const regexMatches = allMatches.filter(t => t.searchType === 'regex').reduce((sum, t) => sum + t.count, 0); reason = `Found ${totalMatches} total matches (${titleMatches} in title, ${bodyMatches} in body) - ${keywordMatches} keyword matches, ${substringMatches} substring matches, ${regexMatches} regex matches`; } core.notice(`Final decision: ${shouldAddLabel ? 'ADD LABEL' : 'DO NOT ADD LABEL'}`); core.notice(`Reason: ${reason || 'No matching terms found'}`); if (shouldAddLabel) { const existingLabels = context.payload.issue.labels.map(l => l.name); if (!existingLabels.includes(labelName)) { await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, labels: [labelName], }); core.notice(`Label "${labelName}" added. ${reason}`); return true; } core.notice(`Label "${labelName}" already present.`); return false; } core.notice(`No matching terms found for label "${labelName}".`); return false; } // Process all configured labels const processLabels = Object.entries(labelConfig) .map(([labelName, config]) => processLabel(labelName, config)); const labelsAdded = await Promise.all(processLabels); const numLabelsAdded = labelsAdded.reduce((x, y) => x + y, 0); core.notice(`Processing complete. ${numLabelsAdded} label(s) added.`);