Skip to content

Commit dcd6471

Browse files
author
Konrad Jamrozik
authored
Extract Contacts class + related refactorings to NotificationConfigurator class. (#5214)
1 parent d4c712d commit dcd6471

File tree

3 files changed

+153
-55
lines changed

3 files changed

+153
-55
lines changed

tools/identity-resolution/Services/GitHubService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public GitHubService(ILogger<GitHubService> logger)
3535
/// </summary>
3636
/// <param name="repoUrl">GitHub repository URL</param>
3737
/// <returns>Contents fo the located CODEOWNERS file</returns>
38-
public async Task<List<CodeownersEntry>> GetCodeownersFile(Uri repoUrl)
38+
public async Task<List<CodeownersEntry>> GetCodeownersFileEntries(Uri repoUrl)
3939
{
4040
List<CodeownersEntry> result;
4141
if (codeownersFileCache.TryGetValue(repoUrl.ToString(), out result))
@@ -68,7 +68,7 @@ private async Task<List<CodeownersEntry>> GetCodeownersFileImpl(Uri repoUrl)
6868
}
6969

7070
logger.LogWarning("Could not retrieve CODEOWNERS file URL = {0} ResponseCode = {1}", codeOwnersUrl, result.StatusCode);
71-
return default;
71+
return null;
7272
}
7373

7474
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Text.RegularExpressions;
6+
using System.Threading.Tasks;
7+
using Azure.Sdk.Tools.CodeOwnersParser;
8+
using Microsoft.Extensions.Logging;
9+
using Microsoft.TeamFoundation.Build.WebApi;
10+
11+
namespace Azure.Sdk.Tools.NotificationConfiguration;
12+
13+
/// <summary>
14+
/// This class represents a set of contacts obtained from CODEOWNERS file
15+
/// located in repository attached to given build definition [1].
16+
///
17+
/// The contacts are the CODEOWNERS path owners of path that matches the build definition file path.
18+
///
19+
/// To obtain the contacts, construct this class and then call GetFromBuildDefinitionRepoCodeowners(buildDefinition).
20+
///
21+
/// [1] https://learn.microsoft.com/en-us/rest/api/azure/devops/build/definitions/get?view=azure-devops-rest-7.0#builddefinition
22+
/// </summary>
23+
internal class Contacts
24+
{
25+
private readonly ILogger log;
26+
private readonly GitHubService gitHubService;
27+
28+
// Type 2 maps to a build definition YAML file in the repository.
29+
// You can confirm it by decompiling Microsoft.TeamFoundation.Build.WebApi.YamlProcess..ctor.
30+
private const int BuildDefinitionYamlProcessType = 2;
31+
32+
internal Contacts(GitHubService gitHubService, ILogger log)
33+
{
34+
this.log = log;
35+
this.gitHubService = gitHubService;
36+
}
37+
38+
/// <summary>
39+
/// See the class comment.
40+
/// </summary>
41+
public async Task<List<string>> GetFromBuildDefinitionRepoCodeowners(BuildDefinition buildDefinition)
42+
{
43+
if (buildDefinition.Process.Type != BuildDefinitionYamlProcessType)
44+
{
45+
this.log.LogDebug(
46+
"buildDefinition.Process.Type: '{buildDefinitionProcessType}' " +
47+
"for buildDefinition.Name: '{buildDefinitionName}' " +
48+
"must be '{BuildDefinitionYamlProcessType}'.",
49+
buildDefinition.Process.Type,
50+
buildDefinition.Name,
51+
BuildDefinitionYamlProcessType);
52+
return null;
53+
}
54+
YamlProcess yamlProcess = (YamlProcess)buildDefinition.Process;
55+
56+
Uri repoUrl = GetCodeownersRepoUrl(buildDefinition);
57+
if (repoUrl == null)
58+
{
59+
// assert: the reason why repoUrl is null has been already logged.
60+
return null;
61+
}
62+
63+
List<CodeownersEntry> codeownersEntries = await gitHubService.GetCodeownersFileEntries(repoUrl);
64+
if (codeownersEntries == null)
65+
{
66+
this.log.LogInformation("CODEOWNERS file in '{repoUrl}' not found. Skipping sync.", repoUrl);
67+
return null;
68+
}
69+
70+
// yamlProcess.YamlFilename is misleading here. It is actually a file path, not file name.
71+
// E.g. it is "sdk/foo_service/ci.yml".
72+
string buildDefinitionFilePath = yamlProcess.YamlFilename;
73+
74+
this.log.LogInformation(
75+
"Searching CODEOWNERS for matching path for '{buildDefinitionFilePath}'",
76+
buildDefinitionFilePath);
77+
78+
CodeownersEntry matchingCodeownersEntry = GetMatchingCodeownersEntry(yamlProcess, codeownersEntries);
79+
List<string> contacts = matchingCodeownersEntry.Owners;
80+
81+
this.log.LogInformation(
82+
"Found matching contacts (owners) in CODEOWNERS. " +
83+
"Searched path '{buildDefinitionOwnerFile}', Contacts#: {contactsCount}",
84+
buildDefinitionFilePath,
85+
contacts.Count);
86+
87+
return contacts;
88+
}
89+
90+
private Uri GetCodeownersRepoUrl(BuildDefinition buildDefinition)
91+
{
92+
Uri repoUrl = buildDefinition.Repository.Url;
93+
this.log.LogInformation("Fetching CODEOWNERS file from repoUrl: '{repoUrl}'", repoUrl);
94+
95+
if (!string.IsNullOrEmpty(repoUrl?.ToString()))
96+
{
97+
repoUrl = new Uri(Regex.Replace(repoUrl.ToString(), @"\.git$", String.Empty));
98+
}
99+
else
100+
{
101+
this.log.LogError(
102+
"No repository url returned from buildDefinition. " +
103+
"buildDefinition.Name: '{buildDefinitionName}' " +
104+
"buildDefinition.Repository.Id: {buildDefinitionRepositoryId}",
105+
buildDefinition.Name,
106+
buildDefinition.Repository.Id);
107+
}
108+
109+
return repoUrl;
110+
}
111+
112+
113+
private CodeownersEntry GetMatchingCodeownersEntry(YamlProcess process, List<CodeownersEntry> codeownersEntries)
114+
{
115+
CodeownersEntry matchingCodeownersEntry =
116+
CodeownersFile.GetMatchingCodeownersEntry(process.YamlFilename, codeownersEntries);
117+
118+
matchingCodeownersEntry.ExcludeNonUserAliases();
119+
120+
return matchingCodeownersEntry;
121+
}
122+
}

tools/notification-configuration/notification-creator/NotificationConfigurator.cs

Lines changed: 29 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
using System.Threading.Tasks;
1313
using Azure.Sdk.Tools.NotificationConfiguration.Helpers;
1414
using System;
15-
using Azure.Sdk.Tools.CodeOwnersParser;
16-
using System.Text.RegularExpressions;
1715

1816
namespace Azure.Sdk.Tools.NotificationConfiguration
1917
{
@@ -24,11 +22,10 @@ class NotificationConfigurator
2422
private readonly ILogger<NotificationConfigurator> logger;
2523

2624
private const int MaxTeamNameLength = 64;
27-
// Type 2 maps to a pipeline YAML file in the repository
28-
private const int PipelineYamlProcessType = 2;
25+
2926
// A cache on the code owners github identity to owner descriptor.
30-
private readonly Dictionary<string, string> codeOwnerCache = new Dictionary<string, string>();
31-
// A cache on the team member to member discriptor.
27+
private readonly Dictionary<string, string> contactsCache = new Dictionary<string, string>();
28+
// A cache on the team member to member descriptor.
3229
private readonly Dictionary<string, string> teamMemberCache = new Dictionary<string, string>();
3330

3431
public NotificationConfigurator(AzureDevOpsService service, GitHubService gitHubService, ILogger<NotificationConfigurator> logger)
@@ -171,72 +168,51 @@ private async Task<WebApiTeam> EnsureTeamExists(
171168

172169
if (purpose == TeamPurpose.SynchronizedNotificationTeam)
173170
{
174-
await SyncTeamWithCodeOwnerFile(pipeline, result, gitHubToAADConverter, gitHubService, persistChanges);
171+
await SyncTeamWithCodeownersFile(pipeline, result, gitHubToAADConverter, persistChanges);
175172
}
176173
return result;
177174
}
178175

179-
private async Task SyncTeamWithCodeOwnerFile(BuildDefinition pipeline, WebApiTeam team, GitHubToAADConverter gitHubToAADConverter, GitHubService gitHubService, bool persistChanges)
176+
private async Task SyncTeamWithCodeownersFile(
177+
BuildDefinition buildDefinition,
178+
WebApiTeam team,
179+
GitHubToAADConverter gitHubToAADConverter,
180+
bool persistChanges)
180181
{
181182
using (logger.BeginScope("Team Name = {0}", team.Name))
182183
{
183-
if (pipeline.Process.Type != PipelineYamlProcessType)
184-
{
185-
return;
186-
}
187-
188-
// Get contents of CODEOWNERS
189-
Uri repoUrl = pipeline.Repository.Url;
190-
logger.LogInformation("Fetching CODEOWNERS file from repo url '{repoUrl}'", repoUrl);
191-
192-
if (repoUrl != null)
193-
{
194-
repoUrl = new Uri(Regex.Replace(repoUrl.ToString(), @"\.git$", String.Empty));
195-
}
196-
else
197-
{
198-
logger.LogError("No repository url returned from pipeline. Repo id: {0}", pipeline.Repository.Id);
199-
return;
200-
}
201-
var codeOwnerEntries = await gitHubService.GetCodeownersFile(repoUrl);
202-
203-
if (codeOwnerEntries == default)
184+
List<string> contacts =
185+
await new Contacts(gitHubService, logger).GetFromBuildDefinitionRepoCodeowners(buildDefinition);
186+
if (contacts == null)
204187
{
205-
logger.LogInformation("CODEOWNERS file not found, skipping sync");
188+
// assert: the reason for why contacts is null has been already logged.
206189
return;
207190
}
208-
var process = pipeline.Process as YamlProcess;
209-
210-
logger.LogInformation("Searching CODEOWNERS for matching path for {0}", process.YamlFilename);
211-
212-
var codeOwnerEntry = CodeownersFile.GetMatchingCodeownersEntry(process.YamlFilename, codeOwnerEntries);
213-
codeOwnerEntry.ExcludeNonUserAliases();
214-
215-
logger.LogInformation("Matching Contacts Path = {0}, NumContacts = {1}", process.YamlFilename, codeOwnerEntry.Owners.Count);
216191

217192
// Get set of team members in the CODEOWNERS file
218-
var codeownersDescriptors = new List<String>();
219-
foreach (var contact in codeOwnerEntry.Owners)
193+
var contactsDescriptors = new List<string>();
194+
foreach (string contact in contacts)
220195
{
221-
if (!codeOwnerCache.ContainsKey(contact))
196+
if (!contactsCache.ContainsKey(contact))
222197
{
223198
// TODO: Better to have retry if no success on this call.
224199
var userPrincipal = gitHubToAADConverter.GetUserPrincipalNameFromGithub(contact);
225200
if (!string.IsNullOrEmpty(userPrincipal))
226201
{
227-
codeOwnerCache[contact] = await service.GetDescriptorForPrincipal(userPrincipal);
202+
contactsCache[contact] = await service.GetDescriptorForPrincipal(userPrincipal);
228203
}
229204
else
230205
{
231-
logger.LogInformation("Cannot find the user principal for github {0}", contact);
232-
codeOwnerCache[contact] = null;
206+
logger.LogInformation(
207+
"Cannot find the user principal for GitHub contact '{contact}'",
208+
contact);
209+
contactsCache[contact] = null;
233210
}
234211
}
235-
codeownersDescriptors.Add(codeOwnerCache[contact]);
212+
contactsDescriptors.Add(contactsCache[contact]);
236213
}
237214

238-
239-
var codeownersSet = new HashSet<string>(codeownersDescriptors);
215+
var contactsSet = new HashSet<string>(contactsDescriptors);
240216
// Get set of team members in the DevOps teams
241217
var teamMembers = await service.GetMembersAsync(team);
242218
var teamDescriptors = new List<String>();
@@ -250,24 +226,24 @@ private async Task SyncTeamWithCodeOwnerFile(BuildDefinition pipeline, WebApiTea
250226
teamDescriptors.Add(teamMemberCache[member.Identity.Id]);
251227
}
252228
var teamSet = new HashSet<string>(teamDescriptors);
253-
var contactsToRemove = teamSet.Except(codeownersSet);
254-
var contactsToAdd = codeownersSet.Except(teamSet);
229+
var contactsToRemove = teamSet.Except(contactsSet);
230+
var contactsToAdd = contactsSet.Except(teamSet);
255231

256-
foreach (var descriptor in contactsToRemove)
232+
foreach (string descriptor in contactsToRemove)
257233
{
258234
if (persistChanges && descriptor != null)
259235
{
260-
var teamDescriptor = await service.GetDescriptorAsync(team.Id);
236+
string teamDescriptor = await service.GetDescriptorAsync(team.Id);
261237
logger.LogInformation("Delete Contact TeamDescriptor = {0}, ContactDescriptor = {1}", teamDescriptor, descriptor);
262238
await service.RemoveMember(teamDescriptor, descriptor);
263239
}
264240
}
265241

266-
foreach (var descriptor in contactsToAdd)
242+
foreach (string descriptor in contactsToAdd)
267243
{
268244
if (persistChanges && descriptor != null)
269245
{
270-
var teamDescriptor = await service.GetDescriptorAsync(team.Id);
246+
string teamDescriptor = await service.GetDescriptorAsync(team.Id);
271247
logger.LogInformation("Add Contact TeamDescriptor = {0}, ContactDescriptor = {1}", teamDescriptor, descriptor);
272248
await service.AddToTeamAsync(teamDescriptor, descriptor);
273249
}

0 commit comments

Comments
 (0)