public void ParseCodeOwners() { const string Content = @" # This is a comment. # Each line is a file pattern followed by one or more owners. # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, # @global-owner1 and @global-owner2 will be requested for # review when someone opens a pull request. * @global-owner1 @global-owner2 # Order is important; the last matching pattern takes the most # precedence. When someone opens a pull request that only # modifies JS files, only @js-owner and not the global # owner(s) will be requested for a review. *.js @js-owner # You can also use email addresses if you prefer. They'll be # used to look up users just like we do for commit author # emails. *.go [email protected] # In this example, @doctocat owns any files in the build/logs # directory at the root of the repository and any of its # subdirectories. /build/logs/ @doctocat # The `docs/*` pattern will match files like # `docs/getting-started.md` but not further nested files like # `docs/build-app/troubleshooting.md`. docs/* [email protected] # In this example, @octocat owns any file in an apps directory # anywhere in your repository. apps/ @octocat # In this example, @doctocat owns any file in the `/docs` # directory in the root of your repository. /docs/ @doctocat "; var actual = CodeOwnersParser.Parse(Content).ToArray(); var expected = new CodeOwnersEntry[] { CodeOwnersEntry.FromUsername("*", "global-owner1"), CodeOwnersEntry.FromUsername("*", "global-owner2"), CodeOwnersEntry.FromUsername("*.js", "js-owner"), CodeOwnersEntry.FromEmailAddress("*.go", "*****@*****.**"), CodeOwnersEntry.FromUsername("/build/logs/", "doctocat"), CodeOwnersEntry.FromEmailAddress("docs/*", "*****@*****.**"), CodeOwnersEntry.FromUsername("apps/", "octocat"), CodeOwnersEntry.FromUsername("/docs/", "doctocat"), }; Assert.Equal(expected, actual); }
public void ParseLineEndingWithSpaces() { var actual = CodeOwnersParser.Parse("* @user1 @user2 ").ToArray(); var expected = new CodeOwnersEntry[] { CodeOwnersEntry.FromUsername("*", "user1"), CodeOwnersEntry.FromUsername("*", "user2"), }; Assert.Equal(expected, actual); }
/// <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 (Kusto access)</param> /// <param name="aadAppSecretVar">AAD App Secret environment variable name (Kusto access)</param> /// <param name="aadTenantVar">AAD Tenant environment variable name (Kusto access)</param> /// <param name="kustoUrlVar">Kusto URL environment variable name</param> /// <param name="kustoDatabaseVar">Kusto DB environment variable name</param> /// <param name="kustoTableVar">Kusto Table environment variable name</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 kustoUrlVar, string kustoDatabaseVar, string kustoTableVar, 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 githubNameResolver = new GitHubNameResolver( Environment.GetEnvironmentVariable(aadAppIdVar), Environment.GetEnvironmentVariable(aadAppSecretVar), Environment.GetEnvironmentVariable(aadTenantVar), Environment.GetEnvironmentVariable(kustoUrlVar), Environment.GetEnvironmentVariable(kustoDatabaseVar), Environment.GetEnvironmentVariable(kustoTableVar), loggerFactory.CreateLogger <GitHubNameResolver>() ); var logger = loggerFactory.CreateLogger <Program>(); var pipelineGroupTasks = (await devOpsService.GetAllTeamsAsync(project)) .Where(team => YamlHelper.Deserialize <TeamMetadata>(team.Description, swallowExceptions: true)?.Purpose == TeamPurpose.SynchronizedNotificationTeam ).Select(async team => { var pipelineId = YamlHelper.Deserialize <TeamMetadata>(team.Description).PipelineId; return(new { Pipeline = await devOpsService.GetPipelineAsync(project, pipelineId), 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 codeownersContent = await gitHubService.GetCodeownersFile(managementUrl); if (codeownersContent == default) { logger.LogInformation("CODEOWNERS file not found, skipping sync"); continue; } var process = group.Pipeline.Process as YamlProcess; // Find matching contacts for the YAML file's path var parser = new CodeOwnersParser(codeownersContent); var matchPath = PathWithoutFilename(process.YamlFilename); var searchPath = $"/{matchPath}/"; logger.LogInformation("Searching CODEOWNERS for matching path Path = {0}", searchPath); var contacts = parser.GetContactsForPath(searchPath); logger.LogInformation("Matching Contacts Path = {0}, NumContacts = {1}", searchPath, contacts.Count); // Get set of team members in the CODEOWNERS file var contactResolutionTasks = contacts .Where(contact => contact.StartsWith("@")) .Select(contact => githubNameResolver.GetInternalUserPrincipal(contact.Substring(1))); var codeownerPrincipals = await Task.WhenAll(contactResolutionTasks); 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); } } } } } }
public void ParseEmptyCodeOwners() { var actual = CodeOwnersParser.Parse("").ToArray(); Assert.Empty(actual); }
public void ParseTwice() { const string Content = "* @user1 @user2 "; Assert.Equal(CodeOwnersParser.Parse(Content).ToArray(), CodeOwnersParser.Parse(Content).ToArray()); }