# This workflow automatically notifies codeowners when an issue is labeled with component labels. # It reads the CODEOWNERS file to find the maintainers for the labeled components # and posts a comment mentioning them to ensure they're aware of the issue. name: Notify Issue Codeowners on: issues: types: [labeled] permissions: issues: write contents: read jobs: notify-codeowners: name: Run if: ${{ startsWith(github.event.label.name, format('component{0} ', ':')) }} runs-on: ubuntu-latest steps: - name: Notify codeowners for component issues uses: actions/github-script@v7.0.1 with: script: | const owner = context.repo.owner; const repo = context.repo.repo; const issue_number = context.payload.issue.number; const labelName = context.payload.label.name; console.log(`Processing issue #${issue_number} with label: ${labelName}`); // Hidden marker to identify bot comments from this workflow const BOT_COMMENT_MARKER = ''; // Extract component name from label const componentName = labelName.replace('component: ', ''); console.log(`Component: ${componentName}`); try { // Fetch CODEOWNERS file from root const { data: codeownersFile } = await github.rest.repos.getContent({ owner, repo, path: 'CODEOWNERS' }); const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); // Parse CODEOWNERS file to extract component mappings const codeownersLines = codeownersContent.split('\n') .map(line => line.trim()) .filter(line => line && !line.startsWith('#')); let componentOwners = null; for (const line of codeownersLines) { const parts = line.split(/\s+/); if (parts.length < 2) continue; const pattern = parts[0]; const owners = parts.slice(1); // Look for component patterns: esphome/components/{component}/* const componentMatch = pattern.match(/^esphome\/components\/([^\/]+)\/\*$/); if (componentMatch && componentMatch[1] === componentName) { componentOwners = owners; break; } } if (!componentOwners) { console.log(`No codeowners found for component: ${componentName}`); return; } console.log(`Found codeowners for '${componentName}': ${componentOwners.join(', ')}`); // Separate users and teams const userOwners = []; const teamOwners = []; for (const owner of componentOwners) { const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner; if (cleanOwner.includes('/')) { // Team mention (org/team-name) teamOwners.push(`@${cleanOwner}`); } else { // Individual user userOwners.push(`@${cleanOwner}`); } } // Remove issue author from mentions to avoid self-notification const issueAuthor = context.payload.issue.user.login; const filteredUserOwners = userOwners.filter(mention => mention !== `@${issueAuthor}` ); // Check for previous comments from this workflow to avoid duplicate pings const comments = await github.paginate( github.rest.issues.listComments, { owner, repo, issue_number: issue_number } ); const previouslyPingedUsers = new Set(); const previouslyPingedTeams = new Set(); // Look for comments from github-actions bot that contain codeowner pings for this component const workflowComments = comments.filter(comment => comment.user.type === 'Bot' && comment.body.includes(BOT_COMMENT_MARKER) && comment.body.includes(`component: ${componentName}`) ); // Extract previously mentioned users and teams from workflow comments for (const comment of workflowComments) { // Match @username patterns (not team mentions) const userMentions = comment.body.match(/@([a-zA-Z0-9_.-]+)(?![/])/g) || []; userMentions.forEach(mention => { previouslyPingedUsers.add(mention); // Keep @ prefix for easy comparison }); // Match @org/team patterns const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+/g) || []; teamMentions.forEach(mention => { previouslyPingedTeams.add(mention); }); } console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams for component ${componentName}`); // Remove previously pinged users and teams const newUserOwners = filteredUserOwners.filter(mention => !previouslyPingedUsers.has(mention)); const newTeamOwners = teamOwners.filter(mention => !previouslyPingedTeams.has(mention)); const allMentions = [...newUserOwners, ...newTeamOwners]; if (allMentions.length === 0) { console.log('No new codeowners to notify (all previously pinged or issue author is the only codeowner)'); return; } // Create comment body const mentionString = allMentions.join(', '); const commentBody = `${BOT_COMMENT_MARKER}\nšŸ‘‹ Hey ${mentionString}!\n\nThis issue has been labeled with \`component: ${componentName}\` and you've been identified as a codeowner of this component. Please take a look when you have a chance!\n\nThanks for maintaining this component! šŸ™`; // Post comment await github.rest.issues.createComment({ owner, repo, issue_number: issue_number, body: commentBody }); console.log(`Successfully notified new codeowners: ${mentionString}`); } catch (error) { console.log('Failed to process codeowner notifications:', error.message); console.error(error); }