internal DevIssuesClient(GitHubClient gitHubClient) { GitHubClient = gitHubClient; IssueMap[GetKey("dotnet", "runtime", 702)] = new GitHubIssueKey("jaredpar", "devops-util", 5); IssueMap[GetKey("dotnet", "runtime", 34472)] = new GitHubIssueKey("jaredpar", "devops-util", 8); }
public void TryCreateTests(string uri, string organization, string repository, int number) { Assert.True(GitHubIssueKey.TryCreateFromUri(uri, out var key)); Assert.Equal(organization, key.Organization); Assert.Equal(repository, key.Repository); Assert.Equal(number, key.Number); }
/// <summary> /// Get the report for builds that are associated with the given GitHub Issue Key /// </summary> public async Task <string> GetAssociatedIssueReportAsync(GitHubIssueKey issueKey) { var query = TriageContextUtil .GetModelGitHubIssuesQuery(issueKey) .Select(x => new { x.ModelBuild.AzureOrganization, x.ModelBuild.AzureProject, x.ModelBuild.BuildNumber, x.ModelBuild.QueueTime, x.ModelBuild.PullRequestNumber, x.ModelBuild.GitHubOrganization, x.ModelBuild.GitHubRepository, x.ModelBuild.GitHubTargetBranch, }); var builds = await query.ToListAsync().ConfigureAwait(false); var results = builds .Select(x => (new BuildInfo( x.AzureOrganization, x.AzureProject, x.BuildNumber, new GitHubBuildInfo(x.GitHubOrganization, x.GitHubRepository, x.PullRequestNumber, x.GitHubTargetBranch)), x.QueueTime)); var report = ReportBuilder.BuildManual(results); return(WrapInStartEndMarkers(report)); }
private async Task <bool> UpdateGitHubIssueReport(GitHubIssueKey issueKey, string reportBody) { try { var issueClient = GitHubClient.Issue; var issue = await issueClient.Get(issueKey.Organization, issueKey.Repository, issueKey.Number); if (TryUpdateIssueText(reportBody, issue.Body, out var newIssueBody)) { var issueUpdate = issue.ToUpdate(); issueUpdate.Body = newIssueBody; await issueClient.Update(issueKey.Organization, issueKey.Repository, issueKey.Number, issueUpdate); return(true); } else { Logger.LogInformation("Cannot find the replacement section in the issue"); } } catch (Exception ex) { Logger.LogError($"Error updating issue {issueKey}: {ex.Message}"); } return(false); }
public async Task AssociatedIssueReport() { var def = AddBuildDefinition("dnceng|public|roslyn|42"); var issueKey = new GitHubIssueKey("dotnet", "test", 13); for (int i = 0; i < 5; i++) { AddGitHubIssue(issueKey, AddBuild($"|||2020-10-0{i + 1}", def)); await Context.SaveChangesAsync(); } var expected = @" <!-- runfo report start --> |Build|Kind|Start Time| |---|---|---| [0](https://dev.azure.com/dnceng/public/_build/results?buildId=0)|Rolling|2020-01-10| [1](https://dev.azure.com/dnceng/public/_build/results?buildId=1)|Rolling|2020-02-10| [2](https://dev.azure.com/dnceng/public/_build/results?buildId=2)|Rolling|2020-03-10| [3](https://dev.azure.com/dnceng/public/_build/results?buildId=3)|Rolling|2020-04-10| [4](https://dev.azure.com/dnceng/public/_build/results?buildId=4)|Rolling|2020-05-10| <!-- runfo report end --> "; var report = await TrackingGitHubUtil.GetAssociatedIssueReportAsync(issueKey); Assert.Equal(expected.TrimNewlines(), report.TrimNewlines()); }
public bool TryCreateTimelineQuery(IssueKind kind, GitHubIssueKey issueKey, string text) { if (TryGetTimelineQuery(issueKey, out var timelineQuery)) { return(false); } timelineQuery = new ModelTimelineQuery() { GitHubOrganization = issueKey.Organization, GitHubRepository = issueKey.Repository, IssueNumber = issueKey.Number, SearchText = text }; try { Context.ModelTimelineQueries.Add(timelineQuery); Context.SaveChanges(); return(true); } catch (Exception ex) { Console.WriteLine(ex.Message); return(false); } }
public bool TryGetTimelineQuery(GitHubIssueKey issueKey, [NotNullWhen(true)] out ModelTimelineQuery timelineQuery) { timelineQuery = Context.ModelTimelineQueries .Where(x => x.GitHubOrganization == issueKey.Organization && x.GitHubRepository == issueKey.Repository && x.IssueNumber == issueKey.Number) .FirstOrDefault(); return(timelineQuery is object); }
public async Task <IActionResult> OnPost(string gitHubIssueUri, string formAction) { if (!GitHubIssueKey.TryCreateFromUri(gitHubIssueUri, out var issueKey)) { return(await OnError("Not a valid GitHub Issue Url")); } var buildKey = GetBuildKey(Number !.Value); if (formAction == "addIssue") { try { var modelBuild = await TriageContextUtil.GetModelBuildAsync(buildKey); await TriageContextUtil.EnsureGitHubIssueAsync(modelBuild, issueKey, saveChanges : true); } catch (DbUpdateException ex) { if (!ex.IsUniqueKeyViolation()) { throw; } } } else { var query = TriageContextUtil .GetModelBuildQuery(buildKey) .SelectMany(x => x.ModelGitHubIssues) .Where(x => x.Organization == issueKey.Organization && x.Repository == issueKey.Repository && x.Number == issueKey.Number); var modelGitHubIssue = await query.FirstOrDefaultAsync(); if (modelGitHubIssue is object) { TriageContextUtil.Context.ModelGitHubIssues.Remove(modelGitHubIssue); await TriageContextUtil.Context.SaveChangesAsync(); } } var util = new TrackingGitHubUtil(GitHubClientFactory, TriageContextUtil.Context, SiteLinkUtil.Published, Logger); await util.UpdateAssociatedGitHubIssueAsync(issueKey); return(await OnGet()); async Task <IActionResult> OnError(string message) { GitHubIssueAddErrorMessage = message; return(await OnGet()); } }
/// <summary> /// Ensure the issue has the appropriate start / end markers in the body so it can be updated later /// by automation /// </summary> public async Task EnsureGitHubIssueHasMarkers(GitHubIssueKey issueKey) { IGitHubClient?gitHubClient = await TryCreateForIssue(issueKey).ConfigureAwait(false); if (gitHubClient is null) { return; } await EnsureGitHubIssueHasMarkers(gitHubClient, issueKey).ConfigureAwait(false); }
public ModelGitHubIssue AddGitHubIssue(GitHubIssueKey issueKey, ModelBuild build) { var issue = new ModelGitHubIssue() { Organization = issueKey.Organization, Repository = issueKey.Repository, Number = issueKey.Number, ModelBuild = build, }; Context.ModelGitHubIssues.Add(issue); return(issue); }
public async Task UpdateStatusIssue() { var gitHubClient = await GitHubClientFactory.CreateForAppAsync("dotnet", "runtime").ConfigureAwait(false); var text = await GetStatusIssueTextAsync(gitHubClient).ConfigureAwait(false); var issueKey = new GitHubIssueKey("dotnet", "runtime", 702); var issueClient = gitHubClient.Issue; var issue = await issueClient.Get(issueKey.Organization, issueKey.Repository, issueKey.Number).ConfigureAwait(false); var updateIssue = issue.ToUpdate(); updateIssue.Body = text; await gitHubClient.Issue.Update(issueKey.Organization, issueKey.Repository, issueKey.Number, updateIssue).ConfigureAwait(false); }
public async Task <IGitHubClient?> TryCreateForIssue(GitHubIssueKey issueKey) { try { var gitHubClient = await GitHubClientFactory.CreateForAppAsync( issueKey.Organization, issueKey.Repository).ConfigureAwait(false); return(gitHubClient); } catch (Exception ex) { Logger.LogError($"Cannot create GitHubClient for {issueKey.Organization} {issueKey.Repository}: {ex.Message}"); return(null); } }
/// <summary> /// Update the GitHub issue with the associated builds /// </summary> public async Task <bool> UpdateAssociatedGitHubIssueAsync(GitHubIssueKey issueKey) { IGitHubClient?gitHubClient = await TryCreateForIssue(issueKey).ConfigureAwait(false); if (gitHubClient is null) { return(false); } await EnsureGitHubIssueHasMarkers(gitHubClient, issueKey).ConfigureAwait(false); var report = await GetAssociatedIssueReportAsync(issueKey).ConfigureAwait(false); var succeeded = await UpdateGitHubIssueReport(gitHubClient, issueKey, report); Logger.LogInformation($"Updated {issueKey.IssueUri}"); return(true); }
/// <summary> /// Ensure the issue has the appropriate start / end markers in the body so it can be updated later /// by automation /// </summary> public static async Task EnsureGitHubIssueHasMarkers(IGitHubClient gitHubClient, GitHubIssueKey issueKey) { var issue = await gitHubClient.Issue.Get(issueKey.Organization, issueKey.Repository, issueKey.Number).ConfigureAwait(false); if (HasMarkers()) { return; } var builder = new StringBuilder(); builder.AppendLine(issue.Body); builder.AppendLine(MarkdownReportStart); builder.AppendLine(MarkdownReportEnd); var issueUpdate = issue.ToUpdate(); issueUpdate.Body = builder.ToString(); await gitHubClient.Issue.Update(issueKey.Organization, issueKey.Repository, issueKey.Number, issueUpdate).ConfigureAwait(false); bool HasMarkers() { using var reader = new StringReader(issue.Body); bool foundStart = false; while (reader.ReadLine() is string line) { if (MarkdownReportStartRegex.IsMatch(line)) { foundStart = true; } if (MarkdownReportEndRegex.IsMatch(line) && foundStart) { return(true); } } return(false); } }
private async Task <bool> UpdateTrackingGitHubIssueAsync(ModelTrackingIssue modelTrackingIssue, GitHubIssueKey issueKey) { IGitHubClient?gitHubClient = await TryCreateForIssue(issueKey).ConfigureAwait(false); if (gitHubClient is null) { return(false); } var report = await GetTrackingIssueReport(modelTrackingIssue).ConfigureAwait(false); var succeeded = await UpdateGitHubIssueReport(gitHubClient, issueKey, report).ConfigureAwait(false); Logger.LogInformation($"Updated {issueKey.IssueUri}"); return(succeeded); }
public async Task <ModelGitHubIssue> EnsureGitHubIssueAsync(ModelBuild modelBuild, GitHubIssueKey issueKey, bool saveChanges) { var query = GetModelBuildQuery(modelBuild.GetBuildKey()) .SelectMany(x => x.ModelGitHubIssues) .Where(x => x.Number == issueKey.Number && x.Organization == issueKey.Organization && x.Repository == issueKey.Repository); var modelGitHubIssue = await query.SingleOrDefaultAsync().ConfigureAwait(false); if (modelGitHubIssue is object) { return(modelGitHubIssue); } modelGitHubIssue = new ModelGitHubIssue() { Organization = issueKey.Organization, Repository = issueKey.Repository, Number = issueKey.Number, ModelBuild = modelBuild, }; Context.ModelGitHubIssues.Add(modelGitHubIssue); if (saveChanges) { await Context.SaveChangesAsync().ConfigureAwait(false); } return(modelGitHubIssue); }
public IQueryable <ModelGitHubIssue> GetModelGitHubIssuesQuery(GitHubIssueKey issueKey) => Context .ModelGitHubIssues .Where(x => x.Number == issueKey.Number && x.Organization == issueKey.Organization && x.Repository == issueKey.Repository);
public async Task <IActionResult> OnPost() { if (TrackingKind == TrackingKind.Unknown) { ErrorMessage = "Invalid Tracking Kind"; return(Page()); } if (string.IsNullOrEmpty(IssueTitle)) { ErrorMessage = "Need an issue title"; return(Page()); } if (IssueTitle.Length >= ModelTrackingIssue.IssueTitleLengthLimit) { ErrorMessage = $"Please limit issue title to {ModelTrackingIssue.IssueTitleLengthLimit} characters"; return(Page()); } if (string.IsNullOrEmpty(SearchText)) { ErrorMessage = "Must provide search text"; return(Page()); } ModelBuildDefinition?modelBuildDefinition = null; if (!string.IsNullOrEmpty(Definition)) { modelBuildDefinition = await TriageContextUtil.FindModelBuildDefinitionAsync(Definition); if (modelBuildDefinition is null) { ErrorMessage = $"Cannot find build definition with name or ID: {Definition}"; return(Page()); } } switch (TrackingKind) { #pragma warning disable 618 case TrackingKind.HelixConsole: case TrackingKind.HelixRunClient: ErrorMessage = $"'{TrackingKind}' is deprecated. Please use {TrackingKind.HelixLogs}"; return(Page()); case TrackingKind.HelixLogs: { if (TryParseQueryString <SearchHelixLogsRequest>(out var request)) { if (request.HelixLogKinds.Count == 0) { ErrorMessage = "Need to specify at least one log kind to search"; return(Page()); } } else { return(Page()); } } break; case TrackingKind.Test: if (!TryParseQueryString <SearchTestsRequest>(out _)) { return(Page()); } break; case TrackingKind.Timeline: if (!TryParseQueryString <SearchTimelinesRequest>(out _)) { return(Page()); } break; } GitHubIssueKey?issueKey = null; if (!string.IsNullOrEmpty(GitHubIssueUri)) { if (GitHubIssueKey.TryCreateFromUri(GitHubIssueUri, out var key)) { issueKey = key; GitHubOrganization = key.Organization; GitHubRepository = key.Repository; } else { ErrorMessage = $"Invalid GitHub issue link: {GitHubIssueUri}"; return(Page()); } } else if (string.IsNullOrEmpty(GitHubRepository) || string.IsNullOrEmpty(GitHubOrganization)) { ErrorMessage = "Must provide GitHub organization and repository"; return(Page()); } IGitHubClient?gitHubClient = null; try { gitHubClient = await GitHubClientFactory.CreateForAppAsync(GitHubOrganization, GitHubRepository); } catch (Exception ex) { ErrorMessage = $"Cannot create GitHub client for that repository: {ex.Message}"; return(Page()); } var modelTrackingIssue = await CreateTrackingIssue(gitHubClient); return(RedirectToPage( "./Issue", new { id = modelTrackingIssue.Id })); async Task <ModelTrackingIssue> CreateTrackingIssue(IGitHubClient gitHubClient) { var issueKey = await GetOrCreateGitHubIssueAsync(gitHubClient); var modelTrackingIssue = new ModelTrackingIssue() { IsActive = true, IssueTitle = IssueTitle, TrackingKind = TrackingKind, SearchQuery = SearchText, ModelBuildDefinition = modelBuildDefinition, GitHubOrganization = issueKey.Organization, GitHubRepository = issueKey.Repository, GitHubIssueNumber = issueKey.Number, }; TriageContext.ModelTrackingIssues.Add(modelTrackingIssue); await TriageContext.SaveChangesAsync(); await InitialTriageAsync(modelTrackingIssue); return(modelTrackingIssue); } async Task InitialTriageAsync(ModelTrackingIssue modelTrackingIssue) { if (modelBuildDefinition is object) { // The initial triage is only done for tracking issues that have definitions // associated with. Lacking a definition we end up querying all builds and that // can produce a *lot* of data. Hard to find a good formula that is performant // there hence we limit to only builds with definitions. var request = new SearchBuildsRequest() { Definition = modelBuildDefinition?.DefinitionId.ToString() ?? null, Queued = new DateRequestValue(7, RelationalKind.GreaterThan), Result = new BuildResultRequestValue(BuildResult.Succeeded, EqualsKind.NotEquals), }; await FunctionQueueUtil.QueueTriageBuildAttempts(TriageContextUtil, modelTrackingIssue, request); // Issues are bulk updated on a 15 minute cycle. This is a new issue though so want to make sure that // the user sees progress soon. Schedule two manual updates in the near future on this so the issue // gets rolling then it will fall into the 15 minute bulk cycle. await FunctionQueueUtil.QueueUpdateIssueAsync(modelTrackingIssue, TimeSpan.FromSeconds(30)); await FunctionQueueUtil.QueueUpdateIssueAsync(modelTrackingIssue, TimeSpan.FromMinutes(2)); } else { await FunctionQueueUtil.QueueUpdateIssueAsync(modelTrackingIssue, TimeSpan.FromMinutes(0)); } } async Task <GitHubIssueKey> GetOrCreateGitHubIssueAsync(IGitHubClient gitHubClient) { if (issueKey is { } key) { await TrackingGitHubUtil.EnsureGitHubIssueHasMarkers(gitHubClient, key); return(key); } var newIssue = new NewIssue(IssueTitle) { Body = TrackingGitHubUtil.WrapInStartEndMarkers("Runfo Creating Tracking Issue (data being generated)") }; var issue = await gitHubClient.Issue.Create(GitHubOrganization, GitHubRepository, newIssue); return(issue.GetIssueKey()); } bool TryParseQueryString <T>(out T value) where T : ISearchRequest, new() { value = new T(); try { value.ParseQueryString(SearchText ?? ""); return(true); } catch (Exception ex) { ErrorMessage = ex.ToString(); return(false); } } }
public async Task UpdateStatusIssue() { var header = new StringBuilder(); var body = new StringBuilder(); var footer = new StringBuilder(); header.AppendLine("## Overview"); header.AppendLine("Please use this queries to discover issues"); await BuildOne("Blocking CI", "blocking-clean-ci", DotNetUtil.GetBuildDefinitionKeyFromFriendlyName("runtime")); await BuildOne("Blocking Official Build", "blocking-official-build", DotNetUtil.GetBuildDefinitionKeyFromFriendlyName("runtime-official")); await BuildOne("Blocking CI Optional", "blocking-clean-ci-optional", DotNetUtil.GetBuildDefinitionKeyFromFriendlyName("runtime")); await BuildOne("Blocking Outerloop", "blocking-outerloop", null); // Blank line to move past the table header.AppendLine(""); BuildFooter(); await UpdateIssue(); void BuildFooter() { footer.AppendLine(@"## Goals 1. A minimum 95% passing rate for the `runtime` pipeline ## Resources 1. [runtime pipeline analytics](https://dnceng.visualstudio.com/public/_build?definitionId=686&view=ms.vss-pipelineanalytics-web.new-build-definition-pipeline-analytics-view-cardmetrics)"); } async Task BuildOne(string title, string label, BuildDefinitionKey?definitionKey) { header.AppendLine($"- [{title}](https://github.com/dotnet/runtime/issues?q=is%3Aopen+is%3Aissue+label%3A{label})"); body.AppendLine($"## {title}"); body.AppendLine("|Status|Issue|Build Count|"); body.AppendLine("|---|---|---|"); var query = (await DoSearch(label)) .Select(x => { var issueKey = x.GetIssueKey(); var count = definitionKey.HasValue ? GetImpactedBuildsCount(issueKey, definitionKey.Value) : null; return(x, Count: count); }) .OrderByDescending(x => x.Count); foreach (var(issue, count) in query) { var emoji = issue.Labels.Any(x => x.Name == "intermittent") ? ":warning:" : ":fire:"; var titleLimit = 75; var issueText = issue.Title.Length >= titleLimit ? issue.Title.Substring(0, titleLimit - 5) + " ..." : issue.Title; var issueEntry = $"[{issueText}]({issue.HtmlUrl})"; var countStr = count.HasValue ? count.ToString() : "N/A"; body.AppendLine($"|{emoji}|{issueEntry}|{countStr}|"); } } async Task <List <Octokit.Issue> > DoSearch(string label) { var request = new SearchIssuesRequest() { Labels = new [] { label }, State = ItemState.Open, Type = IssueTypeQualifier.Issue, Repos = { { "dotnet", "runtime" } }, }; var result = await GitHubClient.Search.SearchIssues(request); return(result.Items.ToList()); } async Task UpdateIssue() { var issueKey = new GitHubIssueKey("dotnet", "runtime", 702); var issueClient = GitHubClient.Issue; var issue = await issueClient.Get(issueKey.Organization, issueKey.Repository, issueKey.Number); var updateIssue = issue.ToUpdate(); updateIssue.Body = header.ToString() + body.ToString() + footer.ToString(); await GitHubClient.Issue.Update(issueKey.Organization, issueKey.Repository, issueKey.Number, updateIssue); } int?GetImpactedBuildsCount(GitHubIssueKey issueKey, BuildDefinitionKey definitionKey) { if (!TriageContextUtil.TryGetTimelineQuery(issueKey, out var timelineQuery)) { return(null); } // TODO: need to be able to filter to the repo the build ran against var count = Context.ModelTimelineItems .Include(x => x.ModelBuild) .ThenInclude(x => x.ModelBuildDefinition) .Where(x => x.ModelTimelineQueryId == timelineQuery.Id && x.ModelBuild.ModelBuildDefinition.AzureOrganization == definitionKey.Organization && x.ModelBuild.ModelBuildDefinition.AzureProject == definitionKey.Project && x.ModelBuild.ModelBuildDefinition.DefinitionId == definitionKey.Id) .Count(); return(count);; } }