diff --git a/tools/notification-configuration/notification-creator/NotificationConfigurator.cs b/tools/notification-configuration/notification-creator/NotificationConfigurator.cs
index 97ac9333908..525ec738c7f 100644
--- a/tools/notification-configuration/notification-creator/NotificationConfigurator.cs
+++ b/tools/notification-configuration/notification-creator/NotificationConfigurator.cs
@@ -12,6 +12,7 @@
using System.Threading.Tasks;
using Azure.Sdk.Tools.NotificationConfiguration.Helpers;
using System;
+using System.IO;
using Azure.Sdk.Tools.CodeOwnersParser;
using System.Text.RegularExpressions;
@@ -26,9 +27,25 @@ class NotificationConfigurator
private const int MaxTeamNameLength = 64;
// Type 2 maps to a pipeline YAML file in the repository
private const int PipelineYamlProcessType = 2;
+
+ ///
+ /// Name of the file for given build definition for which we search its owners in CODEOWNERS file.
+ /// This file is a sibling of the build definition .yml file, like ci.yml or tests.yml.
+ ///
+ /// We do look up owners of this synthetic file instead of the build definition .yml file itself
+ /// to be able to separate build definition reviewers from recipients of build failure
+ /// notifications of builds originating from given build definition.
+ ///
+ /// Note the implicit assumption here there is no file with such path in the repository.
+ ///
+ /// For more information, please see:
+ /// https://github.com/Azure/azure-sdk-tools/issues/5181
+ ///
+ private const string PipelineOwnerSyntheticFileName = "__PipelineOwner__";
+
// A cache on the code owners github identity to owner descriptor.
private readonly Dictionary codeOwnerCache = new Dictionary();
- // A cache on the team member to member discriptor.
+ // A cache on the team member to member descriptor.
private readonly Dictionary teamMemberCache = new Dictionary();
public NotificationConfigurator(AzureDevOpsService service, GitHubService gitHubService, ILogger logger)
@@ -171,12 +188,17 @@ private async Task EnsureTeamExists(
if (purpose == TeamPurpose.SynchronizedNotificationTeam)
{
- await SyncTeamWithCodeOwnerFile(pipeline, result, gitHubToAADConverter, gitHubService, persistChanges);
+ await SyncTeamWithCodeownersFile(pipeline, result, gitHubToAADConverter, gitHubService, persistChanges);
}
return result;
}
- private async Task SyncTeamWithCodeOwnerFile(BuildDefinition pipeline, WebApiTeam team, GitHubToAADConverter gitHubToAADConverter, GitHubService gitHubService, bool persistChanges)
+ private async Task SyncTeamWithCodeownersFile(
+ BuildDefinition pipeline,
+ WebApiTeam team,
+ GitHubToAADConverter gitHubToAADConverter,
+ GitHubService gitHubService,
+ bool persistChanges)
{
using (logger.BeginScope("Team Name = {0}", team.Name))
{
@@ -198,25 +220,20 @@ private async Task SyncTeamWithCodeOwnerFile(BuildDefinition pipeline, WebApiTea
logger.LogError("No repository url returned from pipeline. Repo id: {0}", pipeline.Repository.Id);
return;
}
- var codeOwnerEntries = await gitHubService.GetCodeownersFile(repoUrl);
+ List codeownersEntries = await gitHubService.GetCodeownersFile(repoUrl);
- if (codeOwnerEntries == default)
+ if (codeownersEntries == default)
{
logger.LogInformation("CODEOWNERS file not found, skipping sync");
return;
}
- var process = pipeline.Process as YamlProcess;
-
- logger.LogInformation("Searching CODEOWNERS for matching path for {0}", process.YamlFilename);
-
- var codeOwnerEntry = CodeownersFile.GetMatchingCodeownersEntry(process.YamlFilename, codeOwnerEntries);
- codeOwnerEntry.ExcludeNonUserAliases();
+ YamlProcess process = pipeline.Process as YamlProcess;
- logger.LogInformation("Matching Contacts Path = {0}, NumContacts = {1}", process.YamlFilename, codeOwnerEntry.Owners.Count);
+ CodeownersEntry codeownersEntry = GetMatchingCodeownersEntry(process, codeownersEntries);
// Get set of team members in the CODEOWNERS file
var codeownersDescriptors = new List();
- foreach (var contact in codeOwnerEntry.Owners)
+ foreach (string contact in codeownersEntry.Owners)
{
if (!codeOwnerCache.ContainsKey(contact))
{
@@ -275,6 +292,39 @@ private async Task SyncTeamWithCodeOwnerFile(BuildDefinition pipeline, WebApiTea
}
}
+ private CodeownersEntry GetMatchingCodeownersEntry(YamlProcess process, List codeownersEntries)
+ {
+ // See comment on PipelineOwnerSyntheticFileName to understand why the pipelineOwnerFile
+ // is not simply process.YamlFilename.
+ string pipelineOwnerFile = GetSiblingFilePath(
+ targetPath: process.YamlFilename,
+ siblingFileName: PipelineOwnerSyntheticFileName);
+
+ this.logger.LogInformation(
+ "Searching CODEOWNERS for matching path for {pipelineOwnerFile}, " +
+ "originating from {processYamlFilename}",
+ pipelineOwnerFile,
+ process.YamlFilename);
+
+ CodeownersEntry codeownersEntry =
+ CodeownersFile.GetMatchingCodeownersEntry(pipelineOwnerFile, codeownersEntries);
+
+ codeownersEntry.ExcludeNonUserAliases();
+
+ this.logger.LogInformation(
+ "Matching Contacts Path = {pipelineOwnerFile}, Contacts = {ownersCount}",
+ pipelineOwnerFile,
+ codeownersEntry.Owners.Count);
+ return codeownersEntry;
+ }
+
+ private string GetSiblingFilePath(string targetPath, string siblingFileName)
+ {
+ var fileNameToReplace = Path.GetFileName(targetPath);
+ var siblingPath = targetPath.Replace(fileNameToReplace, siblingFileName);
+ return siblingPath;
+ }
+
private async Task> GetPipelinesAsync(string projectName, string projectPath, PipelineSelectionStrategy strategy)
{
var definitions = await service.GetPipelinesAsync(projectName, projectPath);