From e5aed29231779abee3c5f866f7496074fe3fb5c7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Jul 2025 10:39:30 +1200 Subject: [PATCH] [CI] Only mention codeowners once (#9727) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../workflows/codeowner-review-request.yml | 95 +++++++++++++++---- .github/workflows/issue-codeowner-notify.yml | 45 ++++++++- 2 files changed, 117 insertions(+), 23 deletions(-) diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index ddf5698211..9a0b43a51d 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -178,6 +178,51 @@ jobs: reviewedUsers.add(review.user.login); }); + // Check for previous comments from this workflow to avoid duplicate pings + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number: pr_number + }); + + const previouslyPingedUsers = new Set(); + const previouslyPingedTeams = new Set(); + + // Look for comments from github-actions bot that contain codeowner pings + const workflowComments = comments.filter(comment => + comment.user.type === 'Bot' && + comment.user.login === 'github-actions[bot]' && + comment.body.includes("I've automatically requested reviews from codeowners") + ); + + // 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 => { + const username = mention.slice(1); // remove @ + previouslyPingedUsers.add(username); + }); + + // Match @org/team patterns + const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/([a-zA-Z0-9_.-]+)/g) || []; + teamMentions.forEach(mention => { + const teamName = mention.split('/')[1]; + previouslyPingedTeams.add(teamName); + }); + } + + console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams`); + + // Remove users who have already been pinged in previous workflow comments + previouslyPingedUsers.forEach(user => { + matchedOwners.delete(user); + }); + + previouslyPingedTeams.forEach(team => { + matchedTeams.delete(team); + }); + // Remove only users who have already submitted reviews (not just requested reviewers) reviewedUsers.forEach(reviewer => { matchedOwners.delete(reviewer); @@ -192,7 +237,7 @@ jobs: const teamsList = Array.from(matchedTeams); if (reviewersList.length === 0 && teamsList.length === 0) { - console.log('No eligible reviewers found (all may already be requested or reviewed)'); + console.log('No eligible reviewers found (all may already be requested, reviewed, or previously pinged)'); return; } @@ -227,31 +272,41 @@ jobs: console.log('All codeowners are already requested reviewers or have reviewed'); } - // Add a comment to the PR mentioning what happened (include all matched codeowners) - const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true); + // Only add a comment if there are new codeowners to mention (not previously pinged) + if (reviewersList.length > 0 || teamsList.length > 0) { + const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true); - await github.rest.issues.createComment({ - owner, - repo, - issue_number: pr_number, - body: commentBody - }); + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pr_number, + body: commentBody + }); + console.log(`Added comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`); + } else { + console.log('No new codeowners to mention in comment (all previously pinged)'); + } } catch (error) { if (error.status === 422) { console.log('Some reviewers may already be requested or unavailable:', error.message); - // Try to add a comment even if review request failed - const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false); + // Only try to add a comment if there are new codeowners to mention + if (reviewersList.length > 0 || teamsList.length > 0) { + const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false); - try { - await github.rest.issues.createComment({ - owner, - repo, - issue_number: pr_number, - body: commentBody - }); - } catch (commentError) { - console.log('Failed to add comment:', commentError.message); + try { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pr_number, + body: commentBody + }); + console.log(`Added fallback comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`); + } catch (commentError) { + console.log('Failed to add comment:', commentError.message); + } + } else { + console.log('No new codeowners to mention in fallback comment'); } } else { throw error; diff --git a/.github/workflows/issue-codeowner-notify.yml b/.github/workflows/issue-codeowner-notify.yml index 3ff9c58510..27976a7952 100644 --- a/.github/workflows/issue-codeowner-notify.yml +++ b/.github/workflows/issue-codeowner-notify.yml @@ -92,10 +92,49 @@ jobs: mention !== `@${issueAuthor}` ); - const allMentions = [...filteredUserOwners, ...teamOwners]; + // Check for previous comments from this workflow to avoid duplicate pings + const { data: comments } = await 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.user.login === 'github-actions[bot]' && + comment.body.includes(`component: ${componentName}`) && + comment.body.includes("you've been identified as a codeowner") + ); + + // 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 codeowners to notify (issue author is the only codeowner)'); + console.log('No new codeowners to notify (all previously pinged or issue author is the only codeowner)'); return; } @@ -111,7 +150,7 @@ jobs: body: commentBody }); - console.log(`Successfully notified codeowners: ${mentionString}`); + console.log(`Successfully notified new codeowners: ${mentionString}`); } catch (error) { console.log('Failed to process codeowner notifications:', error.message);