static async Task SpecificationApprovals(Launchpad.Project project) { Console.WriteLine("Specification approvals"); Console.WriteLine("======================="); Console.WriteLine(); foreach (var specification in await project.GetValidSpecifications()) { var milestone = await specification.GetMilestone(); if (specification.Direction != Direction.Approved) { Console.WriteLine( $"- [{specification.Name} ({milestone?.Name})]({specification.Json.web_link})\n" + $" - **Status:** {specification.Lifecycle}, {specification.Priority}, {specification.Direction}, {specification.Definition}, {specification.Implementation}" ); Console.WriteLine(); } } }
static async Task BugTriage(Launchpad.Project project, IConfigurationSection config, List <Commit> commits) { Console.WriteLine("Bug triage"); Console.WriteLine("=========="); Console.WriteLine(); var bugsConfig = config.GetSection("bugs"); var scanAttachments = GetConfigPatternMatchers(bugsConfig.GetSection("scanAttachments")); var commitReferencesConfig = config.GetSection("commits").GetSection("bugReferences"); var commitReferencesSource = GetConfigPatternValueMatchers(commitReferencesConfig.GetSection("source")); var duplicateMinWords = int.Parse(bugsConfig["duplicateMinWords"] ?? "1"); var bugDuplicates = new Dictionary <string, (string Title, string Link)>(); foreach (var bugTask in await project.GetRecentBugTasks(DateTimeOffset.Parse(bugsConfig["startDate"]))) { var bug = await bugTask.GetBug(); var milestone = await bugTask.GetMilestone(); var attachments = await bug.GetAttachments(); var attachmentLogs = await Task.WhenAll(attachments .Where(attachment => attachment.Type != Launchpad.Type.Patch && scanAttachments.Any(pattern => pattern(attachment.Name))) .Select(async attachment => await attachment.GetData())); var issues = new List <string>(); var idealTitles = new List <string>(); if (bugsConfig["scanDescriptions"] == "True") { foreach (var message in await bug.GetMessages()) { var idealTitle = GetBugIdealTitle(bugsConfig.GetSection("idealTitle"), message.Description); if (idealTitle.Length > 0) { idealTitles.Add(idealTitle); } } } foreach (var attachmentLog in attachmentLogs) { var idealTitle = GetBugIdealTitle(bugsConfig.GetSection("idealTitle"), attachmentLog); if (idealTitle.Length > 0) { idealTitles.Add(idealTitle); } } var idealTagsTitle = bug.Name; if (idealTitles.Count >= 1) { if (!bug.Name.Contains(idealTitles[0])) { issues.Add($"Ideal title: {idealTitles[0]}"); idealTagsTitle = idealTitles[0]; } } var allIdealTags = new SortedSet <string>(); foreach (var idealTagConfig in bugsConfig.GetSection("idealTags").GetChildren()) { if (Regex.IsMatch(idealTagsTitle, idealTagConfig.Key)) { var knownTags = new SortedSet <string>(); foreach (var knownTag in idealTagConfig["knownTags"].Split(' ')) { knownTags.Add(knownTag); } var idealTags = GetBugIdealTags(idealTagConfig, idealTagsTitle); allIdealTags.UnionWith(idealTags); foreach (var tag in idealTags.Where(tag => !bug.Tags.Contains(tag))) { issues.Add($"Missing known tag {tag}"); } foreach (var tag in knownTags.Where(tag => bug.Tags.Contains(tag) && !idealTags.Contains(tag))) { issues.Add($"Extra known tag {tag}"); } } } var duplicates = new List <(double Match, string Link)>(); var duplicateTitleWords = idealTagsTitle.Split(" "); for (var i = duplicateTitleWords.Length; i >= duplicateMinWords; i--) { var duplicateTitle = String.Join(" ", duplicateTitleWords.Take(i)); if (bugDuplicates.ContainsKey(duplicateTitle)) { if (!duplicates.Any(d => d.Link == bugDuplicates[duplicateTitle].Link)) { duplicates.Add(( 50d * duplicateTitle.Length / idealTagsTitle.Length + 50d * duplicateTitle.Length / bugDuplicates[duplicateTitle].Title.Length, bugDuplicates[duplicateTitle].Link )); } } else { bugDuplicates[duplicateTitle] = (bug.Name, GetBugLink(bugTask, bug)); } } foreach (var duplicate in duplicates.OrderBy(d => - d.Match)) { issues.Add($"Possible duplicate {duplicate.Match:F0}% - {duplicate.Link}"); } foreach (var idealStatusConfig in bugsConfig.GetSection("idealStatus").GetChildren()) { var statusMatch = IsValuePresentMissing(idealStatusConfig.GetSection("status"), bugTask.Status.ToString()); var tagsMatch = IsValuePresentMissing(idealStatusConfig.GetSection("tags"), allIdealTags.ToArray()); if (statusMatch && tagsMatch && bugTask.Status.ToString() != idealStatusConfig.Key) { issues.Add($"Status should be {idealStatusConfig.Key}"); } } var commitMentions = commits.Where(commit => commit.Message.Contains(bugTask.Json.web_link)); foreach (var message in await bug.GetMessages()) { foreach (var referenceSource in commitReferencesSource) { var match = referenceSource(message.Description); if (match != "") { var target = commitReferencesConfig["target"].Replace("%1", match); commitMentions = commitMentions.Union(commits.Where(commit => commit.Message.Contains(target))); } } } if (commitMentions.Any()) { if (bugTask.Status < Status.InProgress) { issues.Add("Code was committed but bug is not in progress or fixed"); } var latestCommit = commitMentions.OrderBy(commit => commit.AuthorDate).Last(); if ((DateTimeOffset.Now - latestCommit.AuthorDate).TotalDays > 28 && bugTask.Status < Status.FixCommitted) { issues.Add("Code was committed exclusively more than 28 days ago but bug is not fixed"); } } else { if (bugTask.Status >= Status.FixCommitted) { issues.Add("No code was committed but bug is fixed"); } } WriteBugIssues(bugTask, bug, milestone, issues); } foreach (var bugTask in await project.GetUnreleasedBugTasks()) { var bug = await bugTask.GetBug(); var milestone = await bugTask.GetMilestone(); var issues = new List <string>(); if (bugTask.Status == Status.InProgress && !bugTask.HasAssignee) { issues.Add("No assignee set but bug is in progress"); } else if (bugTask.Status >= Status.FixCommitted && !bugTask.HasAssignee) { issues.Add("No assignee set but bug is fixed"); } if (bugTask.Status >= Status.FixCommitted && milestone == null) { issues.Add("No milestone set but bug is fixed"); } WriteBugIssues(bugTask, bug, milestone, issues); } foreach (var bugTask in await project.GetIncompleteBugTasks()) { var bug = await bugTask.GetBug(); var milestone = await bugTask.GetMilestone(); var messages = await bug.GetMessages(); var issues = new List <string>(); var incompleteMessages = messages.Where(m => m.Created >= bugTask.Incomplete).ToList(); if (incompleteMessages.Count > 0) { var lastMessage = incompleteMessages.Last(); var diff = lastMessage.Created - bugTask.Incomplete; if (diff.TotalMinutes >= 1) { var lastMessageUser = await lastMessage.GetOwner(); var lastMessageAge = DateTimeOffset.Now - lastMessage.Created; issues.Add($"{incompleteMessages.Count} messages added since incomplete status was set; last message was by {lastMessageUser.Name}, {lastMessageAge.TotalDays:N1} days ago"); } } WriteBugIssues(bugTask, bug, milestone, issues); } }
static async Task SpecificationTriage(Launchpad.Project project, IConfigurationSection config, List <Commit> commits) { Console.WriteLine("Specification triage"); Console.WriteLine("===================="); Console.WriteLine(); var commitsConfig = config.GetSection("commits"); var commitReferencesConfig = commitsConfig.GetSection("specificationReferences"); var commitReferencesSource = commitReferencesConfig.GetSection("source").GetChildren(); foreach (var specification in await project.GetSpecifications()) { var milestone = await specification.GetMilestone(); var issues = new List <string>(); if (specification.Direction == Direction.Approved && specification.Priority <= Priority.Undefined) { issues.Add("Direction is approved but priority is missing"); } if (specification.Definition == Definition.Approved && specification.Direction != Direction.Approved) { issues.Add("Definition is approved but direction is not approved"); } foreach (var link in config.GetSection("links").GetChildren()) { var hasStartDate = DateTimeOffset.TryParse(link["startDate"] ?? "", out var startDate); var startMilestone = link["startMilestone"]; var forms = link.GetSection("expectedForms").GetChildren(); if ((!hasStartDate || specification.Created >= startDate) && (milestone == null || startMilestone == null || string.Compare(milestone.Id, startMilestone) >= 0) && specification.Definition == Definition.Approved && specification.Implementation != Implementation.Informational) { if (!specification.Summary.Contains(link["baseUrl"])) { issues.Add($"Definition is approved but no {link.Key} link is found"); } else if (!forms.Any(form => specification.Summary.Contains(form.Value))) { issues.Add($"Definition is approved but no normal {link.Key} link is found"); } } } if (specification.Definition == Definition.Approved && !specification.HasApprover) { issues.Add("Definition is approved but approver is missing"); } if (specification.Definition <= Definition.Drafting && !specification.HasDrafter) { issues.Add("Definition is drafting (or later) but drafter is missing"); } if (specification.Implementation >= Implementation.Started && specification.Definition != Definition.Approved && specification.Definition <= Definition.New) { issues.Add("Implementation is started (or later) but definition is not approved"); } if (specification.Implementation >= Implementation.Started && !specification.HasAssignee) { issues.Add("Implementation is started (or later) but assignee is missing"); } if (specification.Implementation == Implementation.Implemented && !specification.HasMilestone) { issues.Add("Implementation is completed but milestone is missing"); } var commitMentions = commits.Where(commit => commit.Message.Contains(specification.Json.web_link)); if (specification.Whiteboard != null) { foreach (var referenceSource in commitReferencesSource) { var match = Regex.Match(specification.Whiteboard, referenceSource.Value, RegexOptions.IgnoreCase); while (match.Success) { var target = commitReferencesConfig["target"].Replace("%1", match.Groups[1].Value); commitMentions = commitMentions.Union(commits.Where(commit => commit.Message.Contains(target))); match = match.NextMatch(); } } } if (commitMentions.Any()) { if (milestone != null && milestone.Id != commitsConfig["currentMilestone"]) { issues.Add($"Code was committed but milestone is {milestone.Id} (expected missing/{commitsConfig["currentMilestone"]})"); } if (specification.Definition != Definition.Approved && specification.Definition <= Definition.New) { issues.Add("Code was committed but definition is not approved"); } var latestCommit = commitMentions.OrderBy(commit => commit.AuthorDate).Last(); if ((DateTimeOffset.Now - latestCommit.AuthorDate).TotalDays > 28 && specification.Implementation != Implementation.Implemented) { issues.Add("Code was committed exclusively more than 28 days ago but implementation is not complete"); } } else { if (specification.Implementation == Implementation.Implemented && milestone != null && milestone.Id == commitsConfig["currentMilestone"]) { issues.Add("No code was committed but implementation for current milestone is complete"); } } if (issues.Count > 0) { Console.WriteLine( $"- [{specification.Name} ({milestone?.Name})]({specification.Json.web_link})\n" + $" - **Status:** {specification.Lifecycle}, {specification.Priority}, {specification.Direction}, {specification.Definition}, {specification.Implementation}\n" + String.Join("\n", issues.Select(issue => $" - **Issue:** {issue}")) ); Console.WriteLine(); } } }