Skip to content

Commit e2a8716

Browse files
authored
Point CI Reliability report tool to NuGet.Client CI pipeline (#116)
1 parent ff2f33a commit e2a8716

3 files changed

Lines changed: 113 additions & 41 deletions

File tree

GithubIssueTagger/Reports/CiReliability/CiReliabilityReport.cs

Lines changed: 96 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,38 @@ public async Task RunAsync(string sprintName, string outFile)
2626
{
2727
using FileStream fileStream = OpenOutputFile(outFile);
2828

29-
ReportData data = await GetDataAsync(sprintName);
29+
PipelineData buildPipelineData = new PipelineData()
30+
{
31+
PipelineName = "NuGet.Client-PrivateDev",
32+
BranchFilterQueryString = "101196%2C101196%2C101196%2C101196%2C101196",
33+
DatabaseName = "AzureDevOps",
34+
OrganizationName = "devdiv",
35+
ProjectId = "0bdbc590-a062-4c3f-b0f6-9383f67865ee",
36+
ProjectName = "DevDiv",
37+
DefinitionId = "8118",
38+
SourceBranch = "refs/heads/dev",
39+
Reason = "schedule",
40+
};
41+
42+
ReportData buildReportData = await GetDataAsync(sprintName, queryName: "dev branch builds", buildPipelineData);
43+
44+
PipelineData funcUnitTestPipelineData = new PipelineData()
45+
{
46+
PipelineName = "NuGet.Client CI",
47+
DatabaseName = "AzureDevOps",
48+
BranchFilterQueryString = "86197%2C86197%2C86197",
49+
OrganizationName = "dnceng-public",
50+
ProjectId = "cbb18261-c48f-4abb-8651-8cdcb5474649",
51+
ProjectName = "public",
52+
DefinitionId = "289",
53+
SourceBranch = "refs/heads/dev",
54+
Reason = "individualCI",
55+
};
3056

31-
Output(data, fileStream);
57+
ReportData funcUnitTestReportData = await GetDataAsync(sprintName, queryName: "dev branch Functional & Unit tests", funcUnitTestPipelineData);
58+
59+
var data = new Tuple<PipelineData, ReportData>[] { new(buildPipelineData, buildReportData), new(funcUnitTestPipelineData, funcUnitTestReportData) };
60+
Output(data, fileStream, reportSprintName: sprintName);
3261
}
3362

3463
private FileStream OpenOutputFile(string outFile)
@@ -52,7 +81,7 @@ private FileStream OpenOutputFile(string outFile)
5281
return fileStream;
5382
}
5483

55-
private async Task<ReportData> GetDataAsync(string sprintName)
84+
private async Task<ReportData> GetDataAsync(string sprintName, string queryName, PipelineData pipelineData)
5685
{
5786
TextWriter? log;
5887
if (Console.IsOutputRedirected)
@@ -71,11 +100,11 @@ private async Task<ReportData> GetDataAsync(string sprintName)
71100
string failedBuildsQuery = $@"let start = startofday(datetime(""{startOfSprint.ToString("yyyy-MM-dd")}""));
72101
let end = endofday(datetime(""{endOfSprint.ToString("yyyy-MM-dd")}""));
73102
let nugetBuilds = Build
74-
| where OrganizationName == 'devdiv' and ProjectId == '0bdbc590-a062-4c3f-b0f6-9383f67865ee' and DefinitionId == 8118 and FinishTime between (start..end) and SourceBranch == 'refs/heads/dev' and Reason == 'schedule';
103+
| where OrganizationName == '{pipelineData.OrganizationName}' and ProjectId == '{pipelineData.ProjectId}' and DefinitionId == {pipelineData.DefinitionId} and FinishTime between (start..end) and SourceBranch == '{pipelineData.SourceBranch}' and Reason == '{pipelineData.Reason}';
75104
let sprintBuilds = nugetBuilds
76105
| project BuildId;
77106
let previousAttempts = BuildTimelineRecord
78-
| where OrganizationName == 'devdiv' and ProjectId == '0bdbc590-a062-4c3f-b0f6-9383f67865ee' and BuildId in (sprintBuilds)
107+
| where OrganizationName == '{pipelineData.OrganizationName}' and ProjectId == '{pipelineData.ProjectId}' and BuildId in (sprintBuilds)
79108
| summarize PreviousAttempts=countif(PreviousAttempts !in ('', '[]')) by BuildId
80109
| where PreviousAttempts > 0
81110
| project BuildId;
@@ -86,10 +115,10 @@ private async Task<ReportData> GetDataAsync(string sprintName)
86115
string buildCountQuery = $@"let start = startofday(datetime(""{startOfSprint.ToString("yyyy-MM-dd")}""));
87116
let end = endofday(datetime(""{endOfSprint.ToString("yyyy-MM-dd")}""));
88117
Build
89-
| where OrganizationName == 'devdiv' and ProjectId == '0bdbc590-a062-4c3f-b0f6-9383f67865ee' and DefinitionId == 8118 and FinishTime between (start..end) and SourceBranch == 'refs/heads/dev'
118+
| where OrganizationName == '{pipelineData.OrganizationName}' and ProjectId == '{pipelineData.ProjectId}' and DefinitionId == {pipelineData.DefinitionId} and FinishTime between (start..end) and SourceBranch == '{pipelineData.SourceBranch}'
90119
| summarize count()";
91120

92-
var connectionBuilder = new KustoConnectionStringBuilder("https://1es.kusto.windows.net/", "AzureDevOps")
121+
var connectionBuilder = new KustoConnectionStringBuilder("https://1es.kusto.windows.net/", pipelineData.DatabaseName)
93122
{
94123
FederatedSecurity = true
95124
};
@@ -102,15 +131,18 @@ private async Task<ReportData> GetDataAsync(string sprintName)
102131

103132
using (var client = KustoClientFactory.CreateCslQueryProvider(connectionBuilder))
104133
{
134+
log?.WriteLine("Query arguments: pipelineName =" + pipelineData.PipelineName + " | " + "organizationName = " + pipelineData.OrganizationName + " | " + "projectId=" + pipelineData.ProjectId + " | " + "definitionId ="
135+
+ pipelineData.DefinitionId + " | " + "sourceBranch=" + pipelineData.SourceBranch + " | " + "reason=" + pipelineData.Reason);
105136
log?.WriteLine($"Querying builds from {startOfSprint:yyyy-MM-dd} to {endOfSprint:yyyy-MM-dd}");
106-
var (failedBuilds, trackingIssues) = await GetFailedBuilds(client, crp, failedBuildsQuery, log);
137+
var (failedBuilds, trackingIssues) = await GetFailedBuilds(client, crp, failedBuildsQuery, pipelineData, log);
107138

108139
log?.WriteLine("Querying total builds in sprint");
109-
int totalBuilds = await GetBuildCount(client, crp, buildCountQuery);
140+
int totalBuilds = await GetBuildCount(client, crp, buildCountQuery, pipelineData);
110141

111142
data = new ReportData()
112143
{
113144
SprintName = sprintName,
145+
QueryName = queryName,
114146
KustoQuery = failedBuildsQuery,
115147
FailedBuilds = failedBuilds,
116148
TrackingIssues = trackingIssues,
@@ -121,9 +153,9 @@ private async Task<ReportData> GetDataAsync(string sprintName)
121153
return data;
122154
}
123155

124-
private async Task<int> GetBuildCount(ICslQueryProvider client, ClientRequestProperties crp, string query)
156+
private async Task<int> GetBuildCount(ICslQueryProvider client, ClientRequestProperties crp, string query, PipelineData pipelineData)
125157
{
126-
using var result = await client.ExecuteQueryAsync("AzureDevOps", query, crp);
158+
using var result = await client.ExecuteQueryAsync(pipelineData.DatabaseName, query, crp);
127159

128160
if (!result.Read())
129161
{
@@ -141,12 +173,13 @@ private async Task<int> GetBuildCount(ICslQueryProvider client, ClientRequestPro
141173
ICslQueryProvider client,
142174
ClientRequestProperties crp,
143175
string query,
176+
PipelineData pipelineData,
144177
TextWriter? log)
145178
{
146179
List<ReportData.FailedBuild> failedBuilds = new();
147180
Dictionary<string, string> trackingIssues = new();
148181

149-
var result = await client.ExecuteQueryAsync("AzureDevOps", query, crp);
182+
var result = await client.ExecuteQueryAsync(pipelineData.DatabaseName, query, crp);
150183

151184
int buildIdColumn = result.GetOrdinal("BuildId");
152185
int buildNumberColumn = result.GetOrdinal("BuildNumber");
@@ -168,7 +201,7 @@ private async Task<int> GetBuildCount(ICslQueryProvider client, ClientRequestPro
168201
{
169202
log?.WriteLine($"Checking failed build {i + 1}/{failedBuilds.Count}");
170203

171-
var (details, tracking) = await GetFailedBuildDetails(failedBuilds[i].Id, client, crp);
204+
var (details, tracking) = await GetFailedBuildDetails(failedBuilds[i].Id, client, crp, pipelineData);
172205

173206
foreach (var kvp in tracking)
174207
{
@@ -190,16 +223,17 @@ private async Task<int> GetBuildCount(ICslQueryProvider client, ClientRequestPro
190223
private async Task<(IReadOnlyList<ReportData.FailureDetail> details, IReadOnlyDictionary<string, string> tracking)> GetFailedBuildDetails(
191224
long buildId,
192225
ICslQueryProvider client,
193-
ClientRequestProperties crp)
226+
ClientRequestProperties crp,
227+
PipelineData pipelineData)
194228
{
195229
List<ReportData.FailureDetail> details = new();
196230
Dictionary<string, string> trackingIssues = new();
197231

198232
List<Dictionary<string, object>> rows = new();
199233

200-
var query = @"BuildTimelineRecord
201-
| where OrganizationName == 'devdiv' and ProjectId == '0bdbc590-a062-4c3f-b0f6-9383f67865ee' and BuildId == " + buildId;
202-
using (var result = await client.ExecuteQueryAsync("AzureDevOps", query, crp))
234+
var query = $@"BuildTimelineRecord
235+
| where OrganizationName == '{pipelineData.OrganizationName}' and ProjectId == '{pipelineData.ProjectId}' and BuildId == {buildId}";
236+
using (var result = await client.ExecuteQueryAsync(pipelineData.DatabaseName, query, crp))
203237
{
204238
while (result.Read())
205239
{
@@ -293,26 +327,57 @@ bool IsFailedTask(Dictionary<string, object> timelineEntry, string parentId)
293327
}
294328
}
295329

296-
private void Output(ReportData data, FileStream outputFileStream)
330+
private void Output(Tuple<PipelineData, ReportData>[] pipelineReportDataList, FileStream outputFileStream, string reportSprintName)
297331
{
298-
if (data == null) { throw new ArgumentNullException(nameof(data)); }
299332
if (outputFileStream is null || !outputFileStream.CanRead || !outputFileStream.CanWrite) { throw new ArgumentException(paramName: nameof(outputFileStream), message: "Cannot read and write to output file"); }
300-
if (data.FailedBuilds == null) { throw new ArgumentException(paramName: nameof(data.FailedBuilds), message: "data.FailedBuilds must not be null"); }
333+
using var sw = new StreamWriter(outputFileStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
301334

302-
float reliability = (data.TotalBuilds - data.FailedBuilds.Count) * 100.0f / data.TotalBuilds;
303-
int failedBuildsOnlyBecauseOfApex = data.FailedBuilds.Where(b => b.Details?.Count == 1 && b.Details[0].Job == "Apex Test Execution").Count();
304-
float reliabilityIgnoringApex = (data.TotalBuilds - data.FailedBuilds.Count + failedBuildsOnlyBecauseOfApex) * 100.0f / data.TotalBuilds;
335+
sw.WriteLine($"# NuGet Client CI Reliability " + reportSprintName);
305336

306-
using var sw = new StreamWriter(outputFileStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
307-
sw.WriteLine("# NuGet.Client CI Reliability " + data.SprintName);
337+
foreach (var pipelineReportData in pipelineReportDataList)
338+
{
339+
var pipelineData = pipelineReportData.Item1;
340+
var data = pipelineReportData.Item2;
341+
if (data == null) { throw new ArgumentNullException(nameof(data)); }
342+
if (data.FailedBuilds == null) { throw new ArgumentException(paramName: nameof(data.FailedBuilds), message: "data.FailedBuilds must not be null"); }
343+
344+
float reliability = (data.TotalBuilds - data.FailedBuilds.Count) * 100.0f / data.TotalBuilds;
345+
int failedBuildsOnlyBecauseOfApex = data.FailedBuilds.Where(b => b.Details?.Count == 1 && b.Details[0].Job == "Apex Test Execution").Count();
346+
float reliabilityIgnoringApex = (data.TotalBuilds - data.FailedBuilds.Count + failedBuildsOnlyBecauseOfApex) * 100.0f / data.TotalBuilds;
347+
348+
OutputPipelineData(pipelineData, data, reliability, reliabilityIgnoringApex, sw);
349+
350+
sw.WriteLine();
351+
sw.WriteLine();
352+
sw.WriteLine("### Tracking");
353+
if (data.TrackingIssues.Count == 0)
354+
{
355+
sw.WriteLine("No tracking issues");
356+
}
357+
else
358+
{
359+
foreach (var kvp in data.TrackingIssues)
360+
{
361+
sw.WriteLine();
362+
sw.WriteLine($"- {kvp.Key}");
363+
sw.WriteLine();
364+
sw.WriteLine(kvp.Value);
365+
}
366+
}
367+
}
368+
}
369+
370+
private static void OutputPipelineData(PipelineData pipelineData, ReportData data, float reliability, float reliabilityIgnoringApex, StreamWriter sw)
371+
{
372+
sw.WriteLine($"## {pipelineData.PipelineName}");
308373
sw.WriteLine();
309-
sw.WriteLine("[NuGet.Client-PR dev branch builds](https://dev.azure.com/devdiv/DevDiv/_build?definitionId=8118&branchFilter=101196%2C101196%2C101196%2C101196%2C101196)");
374+
sw.WriteLine($"[{pipelineData.PipelineName} {data.QueryName}](https://dev.azure.com/{pipelineData.OrganizationName}/{pipelineData.ProjectName}/_build?definitionId={pipelineData.DefinitionId}&branchFilter={pipelineData.BranchFilterQueryString})");
310375
sw.WriteLine();
311376
sw.WriteLine("|Total Builds|Failed Builds|Reliability|Reliability Ignoring Apex|");
312377
sw.WriteLine("|:--:|:--:|:--:|:--:|");
313378
sw.WriteLine($"|{data.TotalBuilds}|{data.FailedBuilds.Count}|{reliability:f1}%|{reliabilityIgnoringApex:f1}%|");
314379
sw.WriteLine();
315-
sw.WriteLine("## Failed Builds");
380+
sw.WriteLine("### Failed Builds");
316381
sw.WriteLine();
317382
sw.WriteLine("**Note:**: Includes builds that succeeded on retry, so first attempt failed");
318383
sw.WriteLine();
@@ -339,11 +404,11 @@ private void Output(ReportData data, FileStream outputFileStream)
339404
{
340405
if (build.Details.Count > 1)
341406
{
342-
sw.WriteLine($" <td rowspan=\"{build.Details.Count}\"><a href=\"https://dev.azure.com/devdiv/DevDiv/_build/results?buildId={build.Id}\">{build.Number}</a></td>");
407+
sw.WriteLine($" <td rowspan=\"{build.Details.Count}\"><a href=\"https://dev.azure.com/{pipelineData.OrganizationName}/{pipelineData.ProjectName}/_build/results?buildId={build.Id}\">{build.Number}</a></td>");
343408
}
344409
else
345410
{
346-
sw.WriteLine($" <td><a href=\"https://dev.azure.com/devdiv/DevDiv/_build/results?buildId={build.Id}\">{build.Number}</a></td>");
411+
sw.WriteLine($" <td><a href=\"https://dev.azure.com/{pipelineData.OrganizationName}/{pipelineData.ProjectName}/_build/results?buildId={build.Id}\">{build.Number}</a></td>");
347412
}
348413
}
349414
sw.WriteLine($" <td>{build.Details[i].Job}</td>");
@@ -353,16 +418,6 @@ private void Output(ReportData data, FileStream outputFileStream)
353418
}
354419
}
355420
sw.WriteLine("</table>");
356-
sw.WriteLine();
357-
sw.WriteLine("### Tracking");
358-
359-
foreach (var kvp in data.TrackingIssues)
360-
{
361-
sw.WriteLine();
362-
sw.WriteLine($"- {kvp.Key}");
363-
sw.WriteLine();
364-
sw.WriteLine(kvp.Value);
365-
}
366421
}
367422

368423
private class CiReliabilityCommandFactory : ICommandFactory
@@ -389,7 +444,7 @@ public Command CreateCommand(Type type, GitHubPatBinder patBinder)
389444
return command;
390445
}
391446

392-
public async Task RunAsync(string sprint, string outfile)
447+
public async Task RunAsync(string sprint, string outFile)
393448
{
394449
var serviceProvider = new ServiceCollection()
395450
.AddGithubIssueTagger()
@@ -399,7 +454,7 @@ public async Task RunAsync(string sprint, string outfile)
399454
using (scopeFactory.CreateScope())
400455
{
401456
var report = serviceProvider.GetRequiredService<CiReliabilityReport>();
402-
await report.RunAsync(sprint, outfile);
457+
await report.RunAsync(sprint, outFile);
403458
}
404459
}
405460
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace GithubIssueTagger.Reports.CiReliability
2+
{
3+
internal struct PipelineData
4+
{
5+
public string? PipelineName { get; init; }
6+
public string? BranchFilterQueryString { get; init; }
7+
public string? DatabaseName { get; init; }
8+
public string? OrganizationName { get; init; }
9+
public string? ProjectId { get; init; }
10+
public string? ProjectName { get; init; }
11+
public string? DefinitionId { get; init; }
12+
public string? SourceBranch { get; init; }
13+
public string? Reason { get; init; }
14+
}
15+
}

GithubIssueTagger/Reports/CiReliability/ReportData.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ internal class ReportData
66
{
77
public string? SprintName { get; init; }
88

9+
public string? QueryName { get; init; }
10+
911
public string? KustoQuery { get; init; }
1012

1113
public required IReadOnlyList<FailedBuild> FailedBuilds { get; init; }

0 commit comments

Comments
 (0)