mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			164 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			YAML
		
	
	
	
	
	
			
		
		
	
	
			164 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			YAML
		
	
	
	
	
	
| # 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 = '<!-- issue-codeowner-notify-bot -->';
 | |
| 
 | |
|             // 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);
 | |
|             }
 |