private async Task EnsureLabelsAsync() { GitHubConnectionOptions options = _githubOptions.Value; IGitHubClient client = await _gitHubApplicationClientFactory.CreateGitHubClientAsync(options.Organization, options.Repository); await GitHubModifications.TryCreateAsync( () => client.Issue.Labels.Create( options.Organization, options.Repository, new NewLabel(_githubOptions.Value.RcaLabel, "009999")), _logger ); }
/// <summary> /// Creates/updates the github issue. /// </summary> /// <param name="updateHistoryError">Error info for which github issue has to be created</param> /// <param name="issueRepo">Repository where the github issue is created</param> /// <param name="shouldReplaceDescription">Func that carries info the description has to be replaced </param> /// <param name="description">Description for the issue body / comment body</param> /// <returns></returns> private async Task CreateOrUpdateGithubIssueAsync( RepositoryBranchUpdateHistoryEntry updateHistoryError, string issueRepo, Func <string, string, bool> shouldReplaceDescription, string description) { _logger.LogInformation($"Error Message : '{updateHistoryError.ErrorMessage}' in repository : '{updateHistoryError.Repository}'"); IReliableDictionary <(string repository, string branch), int> gitHubIssueEvaluator = await _stateManager.GetOrAddAsync <IReliableDictionary <(string repository, string branch), int> >("gitHubIssueEvaluator"); var parseRepoUri = ParseRepoUri(issueRepo); IGitHubClient client = await _authenticateGitHubApplicationClient.CreateGitHubClientAsync(parseRepoUri.owner, parseRepoUri.repo); Octokit.Repository repo = await client.Repository.Get( parseRepoUri.owner, parseRepoUri.repo); var issueNumber = new ConditionalValue <int>(); using (ITransaction tx = _stateManager.CreateTransaction()) { issueNumber = await gitHubIssueEvaluator.TryGetValueAsync( tx, (updateHistoryError.Repository, updateHistoryError.Branch)); await tx.CommitAsync(); } if (issueNumber.HasValue) { Issue issue = await client.Issue.Get(repo.Id, issueNumber.Value); // check if the issue is open only then update it else create a new issue and update the dictionary. if (issue.State.Equals("Open")) { _logger.LogInformation($@"Updating a gitHub issue number : '{issueNumber}' for the error : '{updateHistoryError.ErrorMessage}' for the repository : '{updateHistoryError.Repository}'"); await UpdateIssueAsync( client, updateHistoryError, shouldReplaceDescription, description, issue, repo.Id); return; } } // Create a new issue for the error if the issue is already closed or the issue does not exists. _logger.LogInformation($@"Creating a new gitHub issue for dependency Update Error, for the error message : '{updateHistoryError.ErrorMessage} for the repository : '{updateHistoryError.Repository}'"); await CreateIssueAsync( client, updateHistoryError, gitHubIssueEvaluator, description, repo.Id, issueRepo); }
private async Task ProcessBuildNotificationsAsync(Build build) { const string fullBranchPrefix = "refs/heads/"; foreach (var monitor in _options.Value.Monitor.Builds) { if (!string.Equals(build.Project.Name, monitor.Project, StringComparison.OrdinalIgnoreCase)) { continue; } if (!string.Equals(monitor.DefinitionPath, $"{build.Definition.Path}\\{build.Definition.Name}", StringComparison.OrdinalIgnoreCase)) { continue; } if (monitor.Branches.All(mb => !string.Equals($"{fullBranchPrefix}{mb}", build.SourceBranch, StringComparison.OrdinalIgnoreCase))) { continue; } string prettyBranch = build.SourceBranch; if (prettyBranch.StartsWith(fullBranchPrefix)) { prettyBranch = prettyBranch.Substring(fullBranchPrefix.Length); } _logger.LogInformation( "Build '{buildNumber}' in project '{projectName}' with definition '{definitionPath}' and branch '{branch}' matches monitoring criteria, sending notification", build.BuildNumber, build.Project.Name, build.Definition.Path, build.SourceBranch); _logger.LogInformation("Fetching timeline messages..."); string timelineMessage = await BuildTimelineMessage(build); _logger.LogInformation("Fetching changes messages..."); string changesMessage = await BuildChangesMessage(build); BuildMonitorOptions.IssuesOptions repo = _options.Value.Issues; IGitHubClient github = await _gitHubApplicationClientFactory.CreateGitHubClientAsync(repo.Owner, repo.Name); DateTimeOffset?finishTime = DateTimeOffset.TryParse(build.FinishTime, out var parsedFinishTime) ?parsedFinishTime: (DateTimeOffset?)null; DateTimeOffset?startTime = DateTimeOffset.TryParse(build.StartTime, out var parsedStartTime) ? parsedStartTime:(DateTimeOffset?)null; string timeString = ""; string durationString = ""; if (finishTime.HasValue) { timeString = finishTime.Value.ToString("R"); if (startTime.HasValue) { durationString = ((int)(finishTime.Value - startTime.Value).TotalMinutes) + " minutes"; } } string icon = build.Result == "failed" ? ":x:" : ":warning:"; string body = @$ "Build [#{build.BuildNumber}]({build.Links.Web.Href}) {build.Result} ## {icon} : {build.Project.Name} / {build.Definition.Name} {build.Result} ### Summary **Finished** - {timeString} **Duration** - {durationString} **Requested for** - {build.RequestedFor.DisplayName} **Reason** - {build.Reason} ### Details {timelineMessage} ### Changes {changesMessage}
/// <summary> /// Creates/updates the github issue. /// </summary> /// <param name="updateHistoryError">Error info for which github issue has to be created</param> /// <param name="issueRepo">Repository where the github issue is created</param> /// <param name="shouldReplaceDescription">Func that carries info the description has to be replaced </param> /// <param name="description">Description for the issue body / comment body</param> /// <returns></returns> private async Task CreateOrUpdateGithubIssueAsync( UpdateHistoryEntry updateHistoryError, string issueRepo, Func <string, string, bool> shouldReplaceDescription, string description) { var parsedRepoUri = ParseRepoUri(issueRepo); IGitHubClient client = await _authenticateGitHubApplicationClient.CreateGitHubClientAsync(parsedRepoUri.owner, parsedRepoUri.repo); Repository repo = await client.Repository.Get( parsedRepoUri.owner, parsedRepoUri.repo); var issueNumber = new ConditionalValue <int>(); Issue issue = null; switch (updateHistoryError) { case RepositoryBranchUpdateHistoryEntry repoBranchUpdateHistoryError: { _logger.LogInformation($"Error Message : '{repoBranchUpdateHistoryError.ErrorMessage}' in repository : '{repoBranchUpdateHistoryError.Repository}'"); IReliableDictionary <(string repository, string branch), int> gitHubIssueEvaluator = await _stateManager.GetOrAddAsync <IReliableDictionary <(string repository, string branch), int> >("gitHubIssueEvaluator"); using (ITransaction tx = _stateManager.CreateTransaction()) { issueNumber = await gitHubIssueEvaluator.TryGetValueAsync( tx, (repoBranchUpdateHistoryError.Repository, repoBranchUpdateHistoryError.Branch)); await tx.CommitAsync(); } if (issueNumber.HasValue) { _logger.LogInformation($"Found a matching issue for {repoBranchUpdateHistoryError.Repository}:{repoBranchUpdateHistoryError.Branch}. Issue number: {issueNumber}"); issue = await client.Issue.Get(repo.Id, issueNumber.Value); // check if the issue is open only then update it else create a new issue and update the dictionary. if (issue.State.Equals("Open")) { // Found an existing issue, fall through to update. break; } _logger.LogInformation($"Matching issue {issueNumber} for {repoBranchUpdateHistoryError.Repository}:{repoBranchUpdateHistoryError.Branch} was closed. Creating a new issue."); } // Create a new issue for the error if the issue is already closed or the issue does not exist. _logger.LogInformation($@"Creating a new gitHub issue for dependency Update Error, for the error message : '{repoBranchUpdateHistoryError.ErrorMessage} for the repository : '{repoBranchUpdateHistoryError.Repository}'"); await CreateDependencyUpdateErrorIssueAsync( client, repoBranchUpdateHistoryError, gitHubIssueEvaluator, description, repo.Id, issueRepo); break; } case SubscriptionUpdateHistoryEntry subscriptionUpdateHistoryError: { _logger.LogInformation($"Error Message : '{subscriptionUpdateHistoryError.ErrorMessage}' in subscription : '{subscriptionUpdateHistoryError.SubscriptionId}'"); IReliableDictionary <Guid, int> gitHubIssueEvaluator = await _stateManager.GetOrAddAsync <IReliableDictionary <Guid, int> >("gitHubSubscriptionIssueEvaluator"); using (ITransaction tx = _stateManager.CreateTransaction()) { issueNumber = await gitHubIssueEvaluator.TryGetValueAsync( tx, subscriptionUpdateHistoryError.SubscriptionId); await tx.CommitAsync(); } if (issueNumber.HasValue) { _logger.LogInformation($"Found a matching issue for subscription {subscriptionUpdateHistoryError.SubscriptionId}. Issue number: {issueNumber}"); issue = await client.Issue.Get(repo.Id, issueNumber.Value); // check if the issue is open only then update it else create a new issue and update the dictionary. if (issue.State.Equals("Open")) { // Found an existing issue, fall through to update. break; } _logger.LogInformation($"Matching issue {issueNumber} for subscription {subscriptionUpdateHistoryError.SubscriptionId} was closed. Creating a new issue."); } // Create a new issue for the error if the issue is already closed or the issue does not exist. _logger.LogInformation($@"Creating a new gitHub issue for Subscription Update Error, for the error message : '{subscriptionUpdateHistoryError.ErrorMessage} for subscription : '{subscriptionUpdateHistoryError.SubscriptionId}'"); await CreateSubscriptionUpdateErrorIssueAsync( client, subscriptionUpdateHistoryError, gitHubIssueEvaluator, description, repo.Id, issueRepo); break; } default: throw new InvalidOperationException($"Unknown update history entry type: {updateHistoryError.GetType()}"); } // Updating an existing issue; can use same codepath. if (issueNumber.HasValue) { // check if the issue is open only then update it. Otherwise, we already created the new issue, so do nothing. if (issue.State.Equals("Open")) { _logger.LogInformation($@"Updating a gitHub issue number : '{issueNumber}' for the error : '{updateHistoryError.ErrorMessage}' for {GetPrintableDescription(updateHistoryError)}"); await UpdateIssueAsync(client, updateHistoryError, shouldReplaceDescription, description, issue, repo.Id); return; } } }
private async Task ProcessBuildNotificationsAsync(Build build) { const string fullBranchPrefix = "refs/heads/"; foreach (var monitor in _options.Value.Monitor.Builds) { if (!string.Equals(build.Project.Name, monitor.Project, StringComparison.OrdinalIgnoreCase)) { continue; } if (!string.Equals(monitor.DefinitionPath, $"{build.Definition.Path}\\{build.Definition.Name}", StringComparison.OrdinalIgnoreCase)) { continue; } if (monitor.Branches.All(mb => !string.Equals($"{fullBranchPrefix}{mb}", build.SourceBranch, StringComparison.OrdinalIgnoreCase))) { continue; } if (monitor.Tags != null && monitor.Tags.Any() && !(monitor.Tags.Intersect(build.Tags).Any())) { // We should only skip processing if tags were specified in the monitor, and none of those tags were found in the build continue; } string prettyBranch = build.SourceBranch; if (prettyBranch.StartsWith(fullBranchPrefix)) { prettyBranch = prettyBranch.Substring(fullBranchPrefix.Length); } string prettyTags = (monitor.Tags != null && monitor.Tags.Any()) ? $"{string.Join(", ", build.Tags)}" : ""; _logger.LogInformation( "Build '{buildNumber}' in project '{projectName}' with definition '{definitionPath}', tags '{prettyTags}', and branch '{branch}' matches monitoring criteria, sending notification", build.BuildNumber, build.Project.Name, build.Definition.Path, prettyTags, build.SourceBranch); _logger.LogInformation("Fetching timeline messages..."); string timelineMessage = await BuildTimelineMessage(build); _logger.LogInformation("Fetching changes messages..."); string changesMessage = await BuildChangesMessage(build); BuildMonitorOptions.IssuesOptions repo = _options.Value.Issues.SingleOrDefault(i => string.Equals(monitor.IssuesId, i.Id, StringComparison.OrdinalIgnoreCase)); if (repo != null) { IGitHubClient github = await _gitHubApplicationClientFactory.CreateGitHubClientAsync(repo.Owner, repo.Name); DateTimeOffset?finishTime = DateTimeOffset.TryParse(build.FinishTime, out var parsedFinishTime) ?parsedFinishTime: (DateTimeOffset?)null; DateTimeOffset?startTime = DateTimeOffset.TryParse(build.StartTime, out var parsedStartTime) ? parsedStartTime:(DateTimeOffset?)null; string timeString = ""; string durationString = ""; if (finishTime.HasValue) { timeString = finishTime.Value.ToString("R"); if (startTime.HasValue) { durationString = ((int)(finishTime.Value - startTime.Value).TotalMinutes) + " minutes"; } } string icon = build.Result == "failed" ? ":x:" : ":warning:"; string body = @$ "Build [#{build.BuildNumber}]({build.Links.Web.Href}) {build.Result} ## {icon} : {build.Project.Name} / {build.Definition.Name} {build.Result} ### Summary **Finished** - {timeString} **Duration** - {durationString} **Requested for** - {build.RequestedFor.DisplayName} **Reason** - {build.Reason} ### Details {timelineMessage} ### Changes {changesMessage} "; string issueTitlePrefix = $"Build failed: {build.Definition.Name}/{prettyBranch} {prettyTags}"; if (repo.UpdateExisting) { // There is no way to get the username of our bot directly from the GithubApp with the C# api. // Issue opened in Octokit: https://github.com/octokit/octokit.net/issues/2335 // We do, however, have access to the HtmlUrl, which ends with the name of the bot. // Additionally, when the bot opens issues, the username used ends with [bot], which isn't strictly // part of the name anywhere else. So, to get the correct creator name, get the HtmlUrl, grab // the bot's name from it, and append [bot] to that string. var githubAppClient = _gitHubApplicationClientFactory.CreateGitHubAppClient(); string creator = (await githubAppClient.GitHubApps.GetCurrent()).HtmlUrl.Split("/").Last(); RepositoryIssueRequest issueRequest = new RepositoryIssueRequest { Creator = $"{creator}[bot]", State = ItemStateFilter.Open, SortProperty = IssueSort.Created, SortDirection = SortDirection.Descending }; foreach (string label in repo.Labels.OrEmpty()) { issueRequest.Labels.Add(label); } foreach (string label in monitor.Labels.OrEmpty()) { issueRequest.Labels.Add(label); } List <Issue> matchingIssues = (await github.Issue.GetAllForRepository(repo.Owner, repo.Name, issueRequest)).ToList(); Issue matchingIssue = matchingIssues.FirstOrDefault(i => i.Title.StartsWith(issueTitlePrefix)); if (matchingIssue != null) { _logger.LogInformation("Found matching issue {issueNumber} in {owner}/{repo}. Will attempt to add a new comment.", matchingIssue.Number, repo.Owner, repo.Name); // Add a new comment to the issue with the body IssueComment newComment = await github.Issue.Comment.Create(repo.Owner, repo.Name, matchingIssue.Number, body); _logger.LogInformation("Logged comment in {owner}/{repo}#{issueNumber} for build failure", repo.Owner, repo.Name, matchingIssue.Number); return; } else { _logger.LogInformation("Matching issues for {issueTitlePrefix} not found. Creating a new issue.", issueTitlePrefix); } } // Create new issue if repo.UpdateExisting is false or there were no matching issues var newIssue = new NewIssue($"{issueTitlePrefix} #{build.BuildNumber}") { Body = body, }; if (!string.IsNullOrEmpty(monitor.Assignee)) { newIssue.Assignees.Add(monitor.Assignee); } foreach (string label in repo.Labels.OrEmpty()) { newIssue.Labels.Add(label); } foreach (string label in monitor.Labels.OrEmpty()) { newIssue.Labels.Add(label); } Issue issue = await github.Issue.Create(repo.Owner, repo.Name, newIssue); _logger.LogInformation("Logged issue {owner}/{repo}#{issueNumber} for build failure", repo.Owner, repo.Name, issue.Number); }