Skip to content

Commit e9e86e3

Browse files
authored
Add code owner processing to PipelineWitness (#3742)
1 parent 86cf18b commit e9e86e3

File tree

5 files changed

+213
-7
lines changed

5 files changed

+213
-7
lines changed

tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/BlobUploadProcessor.cs

Lines changed: 125 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
namespace Azure.Sdk.Tools.PipelineWitness
1+
using System.Diagnostics;
2+
3+
namespace Azure.Sdk.Tools.PipelineWitness
24
{
35
using System;
46
using System.Collections.Generic;
57
using System.Collections.Immutable;
68
using System.IO;
9+
using System.IO.Compression;
710
using System.Linq;
811
using System.Net;
912
using System.Text;
@@ -32,6 +35,7 @@ public class BlobUploadProcessor
3235
private const string BuildTimelineRecordsContainerName = "buildtimelinerecords";
3336
private const string BuildDefinitionsContainerName = "builddefinitions";
3437
private const string BuildFailuresContainerName = "buildfailures";
38+
private const string PipelineOwnersContainerName = "pipelineowners";
3539
private const string TestRunsContainerName = "testruns";
3640

3741
private const string TimeFormat = @"yyyy-MM-dd\THH:mm:ss.fffffff\Z";
@@ -52,6 +56,7 @@ public class BlobUploadProcessor
5256
private readonly BlobContainerClient testRunsContainerClient;
5357
private readonly BlobContainerClient buildDefinitionsContainerClient;
5458
private readonly BlobContainerClient buildFailuresContainerClient;
59+
private readonly BlobContainerClient pipelineOwnersContainerClient;
5560
private readonly IOptions<PipelineWitnessSettings> options;
5661
private readonly Dictionary<string, int?> cachedDefinitionRevisions = new();
5762
private readonly IFailureAnalyzer failureAnalyzer;
@@ -82,6 +87,7 @@ public BlobUploadProcessor(
8287
this.buildFailuresContainerClient = blobServiceClient.GetBlobContainerClient(BuildFailuresContainerName);
8388
this.testRunsContainerClient = blobServiceClient.GetBlobContainerClient(TestRunsContainerName);
8489
this.buildDefinitionsContainerClient = blobServiceClient.GetBlobContainerClient(BuildDefinitionsContainerName);
90+
this.pipelineOwnersContainerClient = blobServiceClient.GetBlobContainerClient(PipelineOwnersContainerName);
8591
this.failureAnalyzer = failureAnalyzer;
8692
}
8793

@@ -176,6 +182,119 @@ public async Task UploadBuildBlobsAsync(string account, Guid projectId, int buil
176182
{
177183
await UploadLogLinesBlobAsync(account, build, log);
178184
}
185+
186+
if (build.Definition.Id == options.Value.PipelineOwnersDefinitionId)
187+
{
188+
await UploadPipelineOwnersBlobAsync(account, build, timeline);
189+
}
190+
}
191+
192+
private async Task UploadPipelineOwnersBlobAsync(string account, Build build, Timeline timeline)
193+
{
194+
try
195+
{
196+
var blobPath = $"{build.Project.Name}/{build.FinishTime:yyyy/MM/dd}/{build.Id}-{timeline.ChangeId}.jsonl";
197+
var blobClient = this.pipelineOwnersContainerClient.GetBlobClient(blobPath);
198+
199+
if (await blobClient.ExistsAsync())
200+
{
201+
this.logger.LogInformation("Skipping existing build failure blob for build {BuildId}", build.Id);
202+
return;
203+
}
204+
205+
var owners = await GetOwnersFromBuildArtifactAsync(build);
206+
207+
if (owners == null)
208+
{
209+
// no need to log anything here. GetOwnersFromBuildArtifactAsync logs a warning before returning null;
210+
return;
211+
}
212+
213+
this.logger.LogInformation("Creating owners blob for build {DefinitionId} change {ChangeId}", build.Id, timeline.ChangeId);
214+
215+
var stringBuilder = new StringBuilder();
216+
217+
foreach (var owner in owners)
218+
{
219+
var contentLine = JsonConvert.SerializeObject(new
220+
{
221+
OrganizationName = account,
222+
BuildDefinitionId = owner.Key,
223+
Owners = owner.Value,
224+
Timestamp = new DateTimeOffset(build.FinishTime.Value).ToUniversalTime(),
225+
EtlIngestDate = DateTimeOffset.UtcNow
226+
}, jsonSettings);
227+
228+
stringBuilder.AppendLine(contentLine);
229+
}
230+
231+
await blobClient.UploadAsync(new BinaryData(stringBuilder.ToString()));
232+
}
233+
catch (RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.Conflict)
234+
{
235+
this.logger.LogInformation("Ignoring exception from existing owners blob for build {BuildId}", build.Id);
236+
}
237+
catch (Exception ex)
238+
{
239+
this.logger.LogError(ex, "Error processing owners artifact from build {BuildId}", build.Id);
240+
throw;
241+
}
242+
}
243+
244+
private async Task<Dictionary<int, string[]>> GetOwnersFromBuildArtifactAsync(Build build)
245+
{
246+
var artifactName = this.options.Value.PipelineOwnersArtifactName;
247+
var filePath = this.options.Value.PipelineOwnersFilePath;
248+
249+
try
250+
{
251+
await using var artifactStream = await this.buildClient.GetArtifactContentZipAsync(build.Project.Id, build.Id, artifactName);
252+
using var zip = new ZipArchive(artifactStream);
253+
254+
var fileEntry = zip.GetEntry(filePath);
255+
256+
if (fileEntry == null)
257+
{
258+
this.logger.LogWarning("Artifact {ArtifactName} in build {BuildId} didn't contain the expected file {FilePath}", artifactName, build.Id, filePath);
259+
return null;
260+
}
261+
262+
await using var contentStream = fileEntry.Open();
263+
using var contentReader = new StreamReader(contentStream);
264+
var content = await contentReader.ReadToEndAsync();
265+
266+
if (string.IsNullOrEmpty(content))
267+
{
268+
this.logger.LogWarning("The file {filePath} in artifact {ArtifactName} in build {BuildId} contained no content", filePath, artifactName, build.Id);
269+
return null;
270+
}
271+
272+
var ownersDictionary = JsonConvert.DeserializeObject<Dictionary<int, string[]>>(content);
273+
274+
if (ownersDictionary == null)
275+
{
276+
this.logger.LogWarning("The file {filePath} in artifact {ArtifactName} in build {BuildId} contained a null json object", filePath, artifactName, build.Id);
277+
}
278+
279+
return ownersDictionary;
280+
}
281+
catch (ArtifactNotFoundException ex)
282+
{
283+
this.logger.LogWarning(ex, "Build {BuildId} did not contain the expected artifact {ArtifactName}", build.Id, artifactName);
284+
}
285+
catch (InvalidDataException ex)
286+
{
287+
this.logger.LogWarning(ex, "Unable to read ZIP contents from artifact {ArtifactName} in build {BuildId}", artifactName, build.Id);
288+
289+
// rethrow the exception so the queue message will be retried.
290+
throw;
291+
}
292+
catch (JsonSerializationException ex)
293+
{
294+
this.logger.LogWarning(ex, "Problem deserializing JSON from artifact {ArtifactName} in build {BuildId}", artifactName, build.Id);
295+
}
296+
297+
return null;
179298
}
180299

181300
private async Task UploadBuildFailureBlobAsync(string account, Build build, Timeline timeline)
@@ -217,7 +336,8 @@ private async Task UploadBuildFailureBlobAsync(string account, Build build, Time
217336
RecordId = failure.Record.Id,
218337
BuildTimelineId = timeline.Id,
219338
ErrorClassification = failure.Classification,
220-
}, jsonSettings);
339+
EtlIngestDate = DateTimeOffset.UtcNow
340+
}, jsonSettings);
221341
stringBuilder.AppendLine(contentLine);
222342
}
223343

@@ -241,16 +361,16 @@ public async Task UploadBuildDefinitionBlobsAsync(string account, string project
241361
foreach (var definition in definitions)
242362
{
243363
var cacheKey = $"{definition.Project.Id}:{definition.Id}";
244-
364+
245365
if (!this.cachedDefinitionRevisions.TryGetValue(cacheKey, out var cachedRevision) || cachedRevision != definition.Revision)
246-
{
366+
{
247367
await UploadBuildDefinitionBlobAsync(account, definition);
248368
}
249369

250370
this.cachedDefinitionRevisions[cacheKey] = definition.Revision;
251371
}
252372
}
253-
373+
254374
private async Task UploadBuildDefinitionBlobAsync(string account, BuildDefinition definition)
255375
{
256376
var blobPath = $"{definition.Project.Name}/{definition.Id}-{definition.Revision}.jsonl";

tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/PipelineWitnessSettings.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,20 @@ public class PipelineWitnessSettings
6565
/// Gets or sets the number of concurrent build complete queue workers to register
6666
/// </summary>
6767
public int BuildCompleteWorkerCount { get; set; } = 1;
68+
69+
/// <summary>
70+
/// Gets or sets the artifact name used by the pipeline owners extraction build
71+
/// </summary>
72+
public string PipelineOwnersArtifactName { get; set; }
73+
74+
/// <summary>
75+
/// Gets or sets the file name used by the pipeline owners extraction build
76+
/// </summary>
77+
public string PipelineOwnersFilePath { get; set; }
78+
79+
/// <summary>
80+
/// Gets or sets the definition id of the pipeline owners extraction build
81+
/// </summary>
82+
public int PipelineOwnersDefinitionId { get; set; }
6883
}
6984
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System;
2+
using System.IO;
3+
using System.Net.Http;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
using Microsoft.TeamFoundation.Build.WebApi;
8+
using Microsoft.VisualStudio.Services.Common;
9+
10+
namespace Azure.Sdk.Tools.PipelineWitness.Services;
11+
12+
public class EnhancedBuildHttpClient : BuildHttpClient
13+
14+
{
15+
public EnhancedBuildHttpClient(Uri baseUrl, VssCredentials credentials)
16+
: base(baseUrl, credentials)
17+
{}
18+
19+
public EnhancedBuildHttpClient(Uri baseUrl, VssCredentials credentials, VssHttpRequestSettings settings)
20+
: base(baseUrl, credentials, settings)
21+
{}
22+
23+
public EnhancedBuildHttpClient(Uri baseUrl, VssCredentials credentials, params DelegatingHandler[] handlers)
24+
: base(baseUrl, credentials, handlers)
25+
{}
26+
27+
public EnhancedBuildHttpClient(Uri baseUrl, VssCredentials credentials, VssHttpRequestSettings settings, params DelegatingHandler[] handlers)
28+
: base(baseUrl, credentials, settings, handlers)
29+
{}
30+
31+
public EnhancedBuildHttpClient(Uri baseUrl, HttpMessageHandler pipeline, bool disposeHandler)
32+
: base(baseUrl, pipeline, disposeHandler)
33+
{}
34+
35+
public override async Task<Stream> GetArtifactContentZipAsync(
36+
Guid project,
37+
int buildId,
38+
string artifactName,
39+
object userState = null,
40+
CancellationToken cancellationToken = default)
41+
{
42+
var artifact = await base.GetArtifactAsync(project, buildId, artifactName, userState, cancellationToken);
43+
return await GetArtifactContentZipAsync(artifact, cancellationToken);
44+
}
45+
46+
public override async Task<Stream> GetArtifactContentZipAsync(
47+
string project,
48+
int buildId,
49+
string artifactName,
50+
object userState = null,
51+
CancellationToken cancellationToken = default)
52+
{
53+
var artifact = await base.GetArtifactAsync(project, buildId, artifactName, userState, cancellationToken);
54+
return await GetArtifactContentZipAsync(artifact, cancellationToken);
55+
}
56+
57+
private async Task<Stream> GetArtifactContentZipAsync(BuildArtifact artifact, CancellationToken cancellationToken)
58+
{
59+
var downloadUrl = artifact?.Resource?.DownloadUrl;
60+
if (string.IsNullOrWhiteSpace(downloadUrl))
61+
{
62+
throw new InvalidArtifactDataException("Artifact contained no download url");
63+
}
64+
65+
var responseStream = await Client.GetStreamAsync(downloadUrl, cancellationToken);
66+
return responseStream;
67+
}
68+
}

tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Startup.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public static void Configure(WebApplicationBuilder builder)
4949
});
5050

5151
builder.Services.AddSingleton(provider => provider.GetRequiredService<VssConnection>().GetClient<ProjectHttpClient>());
52-
builder.Services.AddSingleton(provider => provider.GetRequiredService<VssConnection>().GetClient<BuildHttpClient>());
52+
builder.Services.AddSingleton<BuildHttpClient>(provider => provider.GetRequiredService<VssConnection>().GetClient<EnhancedBuildHttpClient>());
5353
builder.Services.AddSingleton(provider => provider.GetRequiredService<VssConnection>().GetClient<TestResultsHttpClient>());
5454

5555
builder.Services.AddLogging();

tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/appsettings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
"MaxDequeueCount": 5,
2121
"Account": "azure-sdk",
2222
"Projects": [ "internal", "playground", "public" ],
23-
"BuildDefinitionLoopPeriod": "00:05:00"
23+
"BuildDefinitionLoopPeriod": "00:05:00",
24+
"PipelineOwnersArtifactName": "pipelineOwners",
25+
"PipelineOwnersFilePath": "pipelineOwners/pipelineOwners.json",
26+
"PipelineOwnersDefinitionId": 5112
2427
}
2528
}

0 commit comments

Comments
 (0)