public override string GetPayload()
        {
            List <CodeOwnerEntry> entries = CodeOwnersFile.ParseFile(_codeownersFile);

            foreach (CodeOwnerEntry entry in entries)
            {
                // If we have labels for the specific codeowners entry, add that to the triage list
                if (entry.ServiceLabels.Any())
                {
                    // Remove the '@' from the owners handle
                    IEnumerable <string> mentionees = entry.Owners.Select(x => x.Replace("@", "").Trim());

                    //add the service
                    AddService(entry.ServiceLabels, mentionees);
                }
            }

            Colorizer.WriteLine("Found [Yellow!{0}] service routes.", RouteCount);
            foreach (TriageConfig triage in _triageConfig)
            {
                Colorizer.WriteLine("Labels:[Yellow!{0}], Owners:[Yellow!{1}]", string.Join(',', triage.Labels), string.Join(',', triage.Mentionee));
            }

            return(s_template
                   .Replace("###repo###", GetTaskId())
                   .Replace("###labelsAndMentions###", string.Join(",", _triageConfig)));
        }
Exemple #2
0
        /// <summary>
        /// Retrieves codeowners information for specific section of the repo
        /// </summary>
        /// <param name="codeOwnerFilePath">The path of CODEOWNERS file in repo</param>
        /// <param name="targetDirectory">The directory whose information is to be retrieved</param>
        /// <param name="filterOutNonUserAliases">The option to filter out code owner team alias.</param>
        /// <returns>Exit code</returns>

        public static int Main(
            string codeOwnerFilePath,
            string targetDirectory,
            bool filterOutNonUserAliases = false
            )
        {
            var target = targetDirectory.ToLower().Trim();

            try {
                var codeOwnerEntry = CodeOwnersFile.ParseAndFindOwnersForClosestMatch(codeOwnerFilePath, target);
                if (filterOutNonUserAliases)
                {
                    codeOwnerEntry.FilterOutNonUserAliases();
                }
                var codeOwnerJson = JsonSerializer.Serialize <CodeOwnerEntry>(codeOwnerEntry, new JsonSerializerOptions {
                    WriteIndented = true
                });
                Console.WriteLine(codeOwnerJson);
                return(0);
            }
            catch (Exception e) {
                Console.Error.WriteLine(e.Message);
                return(1);
            }
        }
Exemple #3
0
        public void ValidateOwnersLines(TestAndExpected entry)
        {
            // create the content
            string content = $"{entry.LabelsLine}\r\n{entry.PathAndOwners}";

            CodeOwnerEntry coe = CodeOwnersFile.ParseContent(content).First();

            Assert.AreEqual(entry.ExpectedLabels, coe.PRLabels);
            Assert.AreEqual(entry.ExpectedOwners, coe.Owners);
            Assert.AreEqual(entry.ExpectedPath, coe.PathExpression);
        }
Exemple #4
0
        public void ParseInvalidEntry()
        {
            // no path and owners
            var entry = new TestAndExpected("", "# PRLabel: %label1 %label2", string.Empty, new string[] { }, new string[] { "label1", "label2" });

            // create the content
            string content = $"{entry.LabelsLine}\r\n{entry.PathAndOwners}";

            CodeOwnerEntry coe = CodeOwnersFile.ParseContent(content).FirstOrDefault();

            Assert.IsNull(coe);
        }
Exemple #5
0
        /// <summary>
        /// Looks for CODEOWNERS in the main branch of the given repo URL
        /// </summary>
        /// <param name="repoUrl"></param>
        /// <returns></returns>
        private async Task <List <CodeOwnerEntry> > GetCodeownersFileImpl(Uri repoUrl)
        {
            // Gets the repo path from the URL
            var relevantPathParts = repoUrl.Segments.Skip(1).Take(2);
            var repoPath          = string.Join("", relevantPathParts);

            var codeOwnersUrl = $"https://raw.githubusercontent.com/{repoPath}/main/.github/CODEOWNERS";
            var result        = await httpClient.GetAsync(codeOwnersUrl);

            if (result.IsSuccessStatusCode)
            {
                logger.LogInformation("Retrieved CODEOWNERS file URL = {0}", codeOwnersUrl);
                return(CodeOwnersFile.ParseContent(await result.Content.ReadAsStringAsync()));
            }

            logger.LogWarning("Could not retrieve CODEOWNERS file URL = {0} ResponseCode = {1}", codeOwnersUrl, result.StatusCode);
            return(default);
Exemple #6
0
        internal override void ReadConfigurationFromFile(string configurationFile)
        {
            List <CodeOwnerEntry> entries = CodeOwnersFile.ParseFile(configurationFile);

            foreach (CodeOwnerEntry entry in entries)
            {
                // If we have labels for the specific codeowners entry, add that to the triage list
                if (entry.ServiceLabels.Any())
                {
                    // Remove the '@' from the owners handle
                    IEnumerable <string> mentionees = entry.Owners.Select(x => x.Replace("@", "").Trim());

                    //add the service
                    AddRoute(entry.ServiceLabels, mentionees);
                }
            }
        }
Exemple #7
0
        internal override void ReadConfigurationFromFile(string configurationFile)
        {
            List <CodeOwnerEntry> entries = CodeOwnersFile.ParseFile(configurationFile);

            // Filter our the list of entries that we want to create.
            for (int i = 0; i < entries.Count; i++)
            {
                // Entries with wildcards are not yet supported
                if (entries[i].ContainsWildcard)
                {
                    // log a warning there

                    if (entries[i].PRLabels.Any())
                    {
                        Colorizer.WriteLine("[Yellow!Warning]: The path '[Cyan!{0}]' contains a wildcard and a label '[Magenta!{1}]' which is not supported!", entries[i].PathExpression, string.Join(',', entries[i].PRLabels));
                    }

                    continue; //TODO: regex expressions are not yet supported
                }

                if (entries[i].PathExpression.IndexOf(CodeOwnerEntry.MissingFolder, StringComparison.OrdinalIgnoreCase) != -1)
                {
                    Colorizer.WriteLine("[Yellow!Warning]: The path '[Cyan!{0}]' is marked with the non-existing path marker.", entries[i].PathExpression);

                    continue;
                }

                // Entries with more than one label are not yet supported.
                if (entries[i].PRLabels.Count > 1)
                {
                    Colorizer.WriteLine("[Yellow!Warning]: Multiple labels for the same path '[Cyan!{0}]' are not yet supported", entries[i].PathExpression);
                    continue;
                }

                if (entries[i].PRLabels.Count == 0)
                {
                    Colorizer.WriteLine("[Yellow!Warning]: The path '[Cyan!{0}]' does not contain a label.", entries[i].PathExpression, string.Join(',', entries[i].PRLabels));
                    continue;
                }

                AddEntry(entries[i].PathExpression, entries[i].PRLabels.First());
            }
        }
Exemple #8
0
        public void ValidateCodeOwnersContent()
        {
            string content = @"#Comment


# ServiceLabel: %F1 %Service Attention
# PRLabel: %F1
/folder1/                                @user1

# ServiceLabel: %F2 %Service Attention
# PRLabel: %F2
/folder2/                                @user2

# ServiceLabel: %Service Attention %F3
/folder3/                                   @user3 @user1

# PRLabel: %F4 %Service Attention
/folder4/                                   @user4

/folder5/                                   @user5

# ServiceLabel: %MyService
#/<NotInRepo>/           @user6


# ServiceLabel: %MyService
# CommentedLine           @user7


/folder6            @user7


# ServiceLabel: %MyService
/folder8           @user6  #This has comment at the end
";

            List <CodeOwnerEntry> entries = CodeOwnersFile.ParseContent(content);

            Assert.AreEqual(8, entries.Count);


            Assert.AreEqual("F1", entries[0].PRLabels[0]);
            Assert.AreEqual("F1", entries[0].ServiceLabels[0]);
            Assert.AreEqual("Service Attention", entries[0].ServiceLabels[1]);

            Assert.AreEqual("F2", entries[1].PRLabels[0]);
            Assert.AreEqual("F2", entries[1].ServiceLabels[0]);
            Assert.AreEqual("Service Attention", entries[1].ServiceLabels[1]);
            Assert.AreEqual("/folder2/", entries[1].PathExpression);

            Assert.AreEqual("Service Attention", entries[2].ServiceLabels[0]);
            Assert.AreEqual(0, entries[2].PRLabels.Count);
            Assert.AreEqual("F3", entries[2].ServiceLabels[1]);

            Assert.AreEqual("F4", entries[3].PRLabels[0]);
            Assert.AreEqual(0, entries[3].ServiceLabels.Count);
            Assert.AreEqual("Service Attention", entries[3].PRLabels[1]);

            Assert.AreEqual(0, entries[4].ServiceLabels.Count);
            Assert.AreEqual(0, entries[4].PRLabels.Count);
            Assert.AreEqual("/folder5/", entries[4].PathExpression);

            Assert.AreEqual(1, entries[5].ServiceLabels.Count);
            Assert.AreEqual(0, entries[5].PRLabels.Count);
            Assert.AreEqual("#/<NotInRepo>/", entries[5].PathExpression);

            Assert.AreEqual(1, entries[6].ServiceLabels.Count);
            Assert.AreEqual(0, entries[6].PRLabels.Count);
            Assert.AreEqual("/folder6", entries[6].PathExpression);

            Assert.AreEqual(1, entries[7].ServiceLabels.Count);
            Assert.AreEqual(0, entries[7].PRLabels.Count);
            Assert.AreEqual("/folder8", entries[7].PathExpression);
            Assert.AreEqual("user6", entries[7].Owners[0]);
        }
Exemple #9
0
        /// <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);
                    }
                }
            }
        }
        public override string GetPayload()
        {
            List <CodeOwnerEntry> entries = CodeOwnersFile.ParseFile(_codeownersFile);

            StringBuilder configPayload = new StringBuilder();

            List <CodeOwnerEntry> entriesToCreate = new List <CodeOwnerEntry>();

            // Filter our the list of entries that we want to create.
            for (int i = 0; i < entries.Count; i++)
            {
                // Entries with wildcards are not yet supported
                if (entries[i].ContainsWildcard)
                {
                    // log a warning there

                    if (entries[i].PRLabels.Any())
                    {
                        Colorizer.WriteLine("[Yellow!Warning]: The path '[Cyan!{0}]' contains a wildcard and a label '[Magenta!{1}]' which is not supported!", entries[i].PathExpression, string.Join(',', entries[i].PRLabels));
                    }

                    continue; //TODO: regex expressions are not yet supported
                }

                if (entries[i].PathExpression.IndexOf(CodeOwnerEntry.MissingFolder, StringComparison.OrdinalIgnoreCase) != -1)
                {
                    Colorizer.WriteLine("[Yellow!Warning]: The path '[Cyan!{0}]' is marked with the non-existing path marker.", entries[i].PathExpression);

                    continue;
                }

                // Entries with more than one label are not yet supported.
                if (entries[i].PRLabels.Count > 1)
                {
                    Colorizer.WriteLine("[Yellow!Warning]: Multiple labels for the same path '[Cyan!{0}]' are not yet supported", entries[i].PathExpression);
                    continue;
                }

                if (entries[i].PRLabels.Count == 0)
                {
                    Colorizer.WriteLine("[Yellow!Warning]: The path '[Cyan!{0}]' does not contain a label.", entries[i].PathExpression, string.Join(',', entries[i].PRLabels));
                    continue;
                }

                entriesToCreate.Add(entries[i]);
            }

            Colorizer.WriteLine("Found the following rules:");

            // Create the payload.
            foreach (var entry in entriesToCreate)
            {
                // get the payload
                string entryPayload = ToConfigString(entry);

                Colorizer.WriteLine("[Cyan!{0}] => [Magenta!{1}]", entry.PathExpression, entry.PRLabels.FirstOrDefault());

                configPayload.Append(ToConfigString(entry));
                configPayload.Append(',');
            }


            // remove the trailing ','
            configPayload.Remove(configPayload.Length - 1, 1);

            // Log the set of paths we are creating.


            // create the payload from the template
            return(s_template
                   .Replace("###taskId###", GetTaskId())
                   .Replace("###labelConfig###", configPayload.ToString()));
        }