/// <summary> /// Synchronizes CODEOWNERS contacts to appropriate DevOps groups /// </summary> /// <param name="organization">Azure DevOps organization name</param> /// <param name="project">Azure DevOps project name</param> /// <param name="devOpsTokenVar">Personal Access Token environment variable name</param> /// <param name="aadAppIdVar">AAD App ID environment variable name (OpensourceAPI access)</param> /// <param name="aadAppSecretVar">AAD App Secret environment variable name (OpensourceAPI access)</param> /// <param name="aadTenantVar">AAD Tenant environment variable name (OpensourceAPI access)</param> /// <param name="pathPrefix">Azure DevOps path prefix (e.g. "\net")</param> /// <param name="dryRun">Do not persist changes</param> /// <returns></returns> public static async Task Main( string organization, string project, string devOpsTokenVar, string aadAppIdVar, string aadAppSecretVar, string aadTenantVar, string pathPrefix = "", bool dryRun = false ) { #pragma warning disable CS0618 // Type or member is obsolete using (var loggerFactory = LoggerFactory.Create(builder => { builder.AddConsole(config => { config.IncludeScopes = true; }); })) #pragma warning restore CS0618 // Type or member is obsolete { var devOpsService = AzureDevOpsService.CreateAzureDevOpsService( Environment.GetEnvironmentVariable(devOpsTokenVar), $"https://dev.azure.com/{organization}/", loggerFactory.CreateLogger <AzureDevOpsService>() ); var gitHubServiceLogger = loggerFactory.CreateLogger <GitHubService>(); var gitHubService = new GitHubService(gitHubServiceLogger); var credential = new ClientSecretCredential( Environment.GetEnvironmentVariable(aadTenantVar), Environment.GetEnvironmentVariable(aadAppIdVar), Environment.GetEnvironmentVariable(aadAppSecretVar)); var githubToAadResolver = new GitHubToAADConverter( credential, loggerFactory.CreateLogger <GitHubToAADConverter>() ); var logger = loggerFactory.CreateLogger <Program>(); var pipelines = (await devOpsService.GetPipelinesAsync(project)).ToDictionary(p => p.Id); var pipelineGroupTasks = (await devOpsService.GetAllTeamsAsync(project)) .Where(team => YamlHelper.Deserialize <TeamMetadata>(team.Description, swallowExceptions: true)?.Purpose == TeamPurpose.SynchronizedNotificationTeam ).Select(async team => { BuildDefinition pipeline; var pipelineId = YamlHelper.Deserialize <TeamMetadata>(team.Description).PipelineId; if (!pipelines.ContainsKey(pipelineId)) { pipeline = await devOpsService.GetPipelineAsync(project, pipelineId); } else { pipeline = pipelines[pipelineId]; } if (pipeline == default) { logger.LogWarning($"Could not find pipeline with id {pipelineId} referenced by team {team.Name}."); } return(new { Pipeline = pipeline, Team = team }); }); var pipelineGroups = await Task.WhenAll(pipelineGroupTasks); var filteredGroups = pipelineGroups.Where(group => group.Pipeline != default && group.Pipeline.Path.StartsWith(pathPrefix)); foreach (var group in filteredGroups) { using (logger.BeginScope("Team Name = {0}", group.Team.Name)) { if (group.Pipeline.Process.Type != PipelineYamlProcessType) { continue; } // Get contents of CODEOWNERS logger.LogInformation("Fetching CODEOWNERS file"); var managementUrl = new Uri(group.Pipeline.Repository.Properties["manageUrl"]); var codeOwnerEntries = await gitHubService.GetCodeownersFile(managementUrl); if (codeOwnerEntries == default) { logger.LogInformation("CODEOWNERS file not found, skipping sync"); continue; } var process = group.Pipeline.Process as YamlProcess; logger.LogInformation("Searching CODEOWNERS for matching path for {0}", process.YamlFilename); var codeOwnerEntry = CodeOwnersFile.FindOwnersForClosestMatch(codeOwnerEntries, process.YamlFilename); codeOwnerEntry.FilterOutNonUserAliases(); logger.LogInformation("Matching Contacts Path = {0}, NumContacts = {1}", process.YamlFilename, codeOwnerEntry.Owners.Count); // Get set of team members in the CODEOWNERS file var codeownerPrincipals = codeOwnerEntry.Owners .Select(contact => githubToAadResolver.GetUserPrincipalNameFromGithub(contact)); var codeownersDescriptorsTasks = codeownerPrincipals .Where(userPrincipal => !string.IsNullOrEmpty(userPrincipal)) .Select(userPrincipal => devOpsService.GetDescriptorForPrincipal(userPrincipal)); var codeownersDescriptors = await Task.WhenAll(codeownersDescriptorsTasks); var codeownersSet = new HashSet <string>(codeownersDescriptors); // Get set of existing team members var teamMembers = await devOpsService.GetMembersAsync(group.Team); var teamContactTasks = teamMembers .Select(async member => await devOpsService.GetUserFromId(new Guid(member.Identity.Id))); var teamContacts = await Task.WhenAll(teamContactTasks); var teamDescriptors = teamContacts.Select(contact => contact.SubjectDescriptor.ToString()); var teamSet = new HashSet <string>(teamDescriptors); // Synchronize contacts var contactsToRemove = teamSet.Except(codeownersSet); var contactsToAdd = codeownersSet.Except(teamSet); var teamDescriptor = await devOpsService.GetDescriptorAsync(group.Team.Id); foreach (var descriptor in contactsToRemove) { logger.LogInformation("Delete Contact TeamDescriptor = {0}, ContactDescriptor = {1}", teamDescriptor, descriptor); if (!dryRun) { await devOpsService.RemoveMember(teamDescriptor, descriptor); } } foreach (var descriptor in contactsToAdd) { logger.LogInformation("Add Contact TeamDescriptor = {0}, ContactDescriptor = {1}", teamDescriptor, descriptor); if (!dryRun) { await devOpsService.AddToTeamAsync(teamDescriptor, descriptor); } } } } } }
private async Task SyncTeamWithCodeOwnerFile(BuildDefinition pipeline, WebApiTeam team, GitHubToAADConverter gitHubToAADConverter, GitHubService gitHubService, bool persistChanges) { using (logger.BeginScope("Team Name = {0}", team.Name)) { if (pipeline.Process.Type != PipelineYamlProcessType) { return; } // Get contents of CODEOWNERS logger.LogInformation("Fetching CODEOWNERS file"); Uri repoUrl = pipeline.Repository.Url; if (repoUrl != null) { repoUrl = new Uri(Regex.Replace(repoUrl.ToString(), @"\.git$", String.Empty)); } else { logger.LogError("No repository url returned from pipeline. Repo id: {0}", pipeline.Repository.Id); return; } var codeOwnerEntries = await gitHubService.GetCodeownersFile(repoUrl); if (codeOwnerEntries == 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.FindOwnersForClosestMatch(codeOwnerEntries, process.YamlFilename); codeOwnerEntry.FilterOutNonUserAliases(); logger.LogInformation("Matching Contacts Path = {0}, NumContacts = {1}", process.YamlFilename, codeOwnerEntry.Owners.Count); // Get set of team members in the CODEOWNERS file var codeownersDescriptors = new List <String>(); foreach (var contact in codeOwnerEntry.Owners) { if (!codeOwnerCache.ContainsKey(contact)) { // TODO: Better to have retry if no success on this call. var userPrincipal = gitHubToAADConverter.GetUserPrincipalNameFromGithub(contact); if (!string.IsNullOrEmpty(userPrincipal)) { codeOwnerCache[contact] = await service.GetDescriptorForPrincipal(userPrincipal); } else { logger.LogInformation("Cannot find the user principal for github {0}", contact); codeOwnerCache[contact] = null; } } codeownersDescriptors.Add(codeOwnerCache[contact]); } var codeownersSet = new HashSet <string>(codeownersDescriptors); // Get set of team members in the DevOps teams var teamMembers = await service.GetMembersAsync(team); var teamDescriptors = new List <String>(); foreach (var member in teamMembers) { if (!teamMemberCache.ContainsKey(member.Identity.Id)) { var teamMemberDescriptor = (await service.GetUserFromId(new Guid(member.Identity.Id))).SubjectDescriptor.ToString(); teamMemberCache[member.Identity.Id] = teamMemberDescriptor; } teamDescriptors.Add(teamMemberCache[member.Identity.Id]); } var teamSet = new HashSet <string>(teamDescriptors); var contactsToRemove = teamSet.Except(codeownersSet); var contactsToAdd = codeownersSet.Except(teamSet); foreach (var descriptor in contactsToRemove) { if (persistChanges && descriptor != null) { var teamDescriptor = await service.GetDescriptorAsync(team.Id); logger.LogInformation("Delete Contact TeamDescriptor = {0}, ContactDescriptor = {1}", teamDescriptor, descriptor); await service.RemoveMember(teamDescriptor, descriptor); } } foreach (var descriptor in contactsToAdd) { if (persistChanges && descriptor != null) { var teamDescriptor = await service.GetDescriptorAsync(team.Id); logger.LogInformation("Add Contact TeamDescriptor = {0}, ContactDescriptor = {1}", teamDescriptor, descriptor); await service.AddToTeamAsync(teamDescriptor, descriptor); } } } }