-
Notifications
You must be signed in to change notification settings - Fork 76
/
Copy pathAddBuildToChannelOperation.cs
502 lines (430 loc) · 22.5 KB
/
AddBuildToChannelOperation.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.DotNet.Darc.Helpers;
using Microsoft.DotNet.Darc.Options;
using Microsoft.DotNet.DarcLib;
using Microsoft.DotNet.DarcLib.Helpers;
using Microsoft.DotNet.Maestro.Client;
using Microsoft.DotNet.Maestro.Client.Models;
using Microsoft.DotNet.Services.Utility;
using Microsoft.Extensions.Logging;
namespace Microsoft.DotNet.Darc.Operations;
internal class AddBuildToChannelOperation : Operation
{
private static readonly IReadOnlyDictionary<string, (string project, int pipelineId)> BuildPromotionPipelinesForAccount =
new Dictionary<string, (string project, int pipelineId)>(StringComparer.OrdinalIgnoreCase)
{
{ "dnceng", ("internal", 750) },
{ "devdiv", ("devdiv", 12603) }
};
// These channels are unsupported because the Arcade main branch
// (the branch that has build promotion infra) doesn't have YAML
// implementation for them. There is usually not a high demand for
// promoting builds to these channels.
private static readonly IReadOnlyDictionary<int, string> UnsupportedChannels = new Dictionary<int, string>
{
{ 3, ".NET Core 3 Dev" },
{ 19, ".NET Core 3 Release" },
{ 129, ".NET Core 3.1 Release" },
{ 184, ".NET Core 3.0 Internal Servicing" },
{ 344, ".NET 3 Eng" },
{ 390, ".NET 3 Eng - Validation" },
{ 531, ".NET Core 3.1 Blazor Features" },
{ 550, ".NET Core 3.1 Internal Servicing" },
{ 555, ".NET Core SDK 3.0.1xx Internal" },
{ 556, ".NET Core SDK 3.0.1xx" },
{ 557, ".NET Core SDK 3.1.2xx Internal" },
{ 558, ".NET Core SDK 3.1.2xx" },
{ 559, ".NET Core SDK 3.1.1xx Internal" },
{ 560, ".NET Core SDK 3.1.1xx" }
};
private readonly AddBuildToChannelCommandLineOptions _options;
private readonly ILogger<AddBuildToChannelOperation> _logger;
private readonly IAzureDevOpsClient _azdoClient;
private readonly IBarApiClient _barClient;
public AddBuildToChannelOperation(
AddBuildToChannelCommandLineOptions options,
IBarApiClient barClient,
IAzureDevOpsClient azdoClient,
ILogger<AddBuildToChannelOperation> logger)
{
_options = options;
_barClient = barClient;
_logger = logger;
_azdoClient = azdoClient;
}
/// <summary>
/// Assigns a build to a channel.
/// </summary>
/// <returns>Process exit code.</returns>
public override async Task<int> ExecuteAsync()
{
try
{
Build build = await _barClient.GetBuildAsync(_options.Id);
if (build == null)
{
Console.WriteLine($"Could not find a build with id '{_options.Id}'.");
return Constants.ErrorCode;
}
if (string.IsNullOrEmpty(_options.Channel) && !_options.AddToDefaultChannels)
{
Console.WriteLine("You need to use --channel or --default-channels to inform the channel(s) that the build should be promoted to.");
return Constants.ErrorCode;
}
if (_options.PublishingInfraVersion < 2 || _options.PublishingInfraVersion > 3)
{
Console.WriteLine($"Publishing version '{_options.PublishingInfraVersion}' is not configured. The following versions are available: 2, 3");
return Constants.ErrorCode;
}
if (_options.PublishingInfraVersion > 2 && _options.DoSDLValidation)
{
Console.WriteLine($"Publishing version '{_options.PublishingInfraVersion}' does not support running SDL when adding a build to a channel");
return Constants.ErrorCode;
}
List<Channel> targetChannels = [];
if (!string.IsNullOrEmpty(_options.Channel))
{
Channel targetChannel = await UxHelpers.ResolveSingleChannel(_barClient, _options.Channel);
if (targetChannel == null)
{
return Constants.ErrorCode;
}
targetChannels.Add(targetChannel);
}
if (_options.AddToDefaultChannels)
{
IEnumerable<DefaultChannel> defaultChannels = await _barClient.GetDefaultChannelsAsync(
build.GetRepository(),
build.GetBranch());
targetChannels.AddRange(
defaultChannels.
Where(dc => dc.Enabled).
Select(dc => dc.Channel).
DistinctBy(c => c.Id));
}
IEnumerable<Channel> currentChannels = build.Channels.Where(ch => targetChannels.Any(tc => tc.Id == ch.Id));
if (currentChannels.Any())
{
Console.WriteLine($"The build '{build.Id}' is already on these target channel(s):");
foreach (var channel in currentChannels)
{
Console.WriteLine($"\t{channel.Name}");
targetChannels.RemoveAll(tch => tch.Id == channel.Id);
}
}
if (!targetChannels.Any())
{
Console.WriteLine($"Build '{build.Id}' is already on all target channel(s).");
return Constants.SuccessCode;
}
if (targetChannels.Any(ch => UnsupportedChannels.ContainsKey(ch.Id)))
{
Console.WriteLine($"Currently Darc doesn't support build promotion to the following channels:");
foreach (var channel in UnsupportedChannels)
{
Console.WriteLine($"\t ({channel.Key}) {channel.Value}");
}
Console.WriteLine("Please contact @dnceng to see other options.");
return Constants.ErrorCode;
}
// Queues a build of the Build Promotion pipeline that will takes care of making sure
// that the build assets are published to the right location and also promoting the build
// to the requested channel
int promoteBuildQueuedStatus = await PromoteBuildAsync(build, targetChannels, _barClient)
.ConfigureAwait(false);
if (promoteBuildQueuedStatus != Constants.SuccessCode)
{
return Constants.ErrorCode;
}
// Get the latest build information to verify the channels
build = await _barClient.GetBuildAsync(build.Id);
Console.WriteLine($"Assigning build '{build.Id}' to the following channel(s):");
foreach (var channel in targetChannels)
{
Console.WriteLine($"\t{channel.Name}");
}
Console.WriteLine();
Console.Write(UxHelpers.GetTextBuildDescription(build));
// Be helpful. Let the user know what will happen.
string buildRepo = build.GetRepository();
List<Subscription> applicableSubscriptions = [];
foreach (var targetChannel in targetChannels)
{
IEnumerable<Subscription> appSubscriptions = await _barClient.GetSubscriptionsAsync(
sourceRepo: buildRepo,
channelId: targetChannel.Id);
applicableSubscriptions.AddRange(appSubscriptions);
}
PrintSubscriptionInfo(applicableSubscriptions);
return Constants.SuccessCode;
}
catch (AuthenticationException e)
{
Console.WriteLine(e.Message);
return Constants.ErrorCode;
}
catch (Exception e)
{
_logger.LogError(e, $"Error: Failed to assign build '{_options.Id}' to channel '{_options.Channel}'.");
return Constants.ErrorCode;
}
}
private async Task<int> PromoteBuildAsync(Build build, List<Channel> targetChannels, IBarApiClient barClient)
{
if (_options.SkipAssetsPublishing)
{
foreach (var targetChannel in targetChannels)
{
await barClient.AssignBuildToChannelAsync(build.Id, targetChannel.Id);
Console.WriteLine($"Build {build.Id} was assigned to channel '{targetChannel.Name}' bypassing the promotion pipeline.");
}
return Constants.SuccessCode;
}
var (arcadeSDKSourceBranch, arcadeSDKSourceSHA) = await GetSourceBranchInfoAsync(build).ConfigureAwait(false);
// This condition can happen when for some reason we failed to determine the source branch/sha
// of the build that produced the used Arcade SDK or when the user specify an invalid combination
// of source-sha/branch parameters.
if (arcadeSDKSourceBranch == null && arcadeSDKSourceSHA == null)
{
return Constants.ErrorCode;
}
var targetAzdoBuildStatus = await ValidateAzDOBuildAsync(_azdoClient, build.AzureDevOpsAccount, build.AzureDevOpsProject, build.AzureDevOpsBuildId.Value)
.ConfigureAwait(false);
if (!targetAzdoBuildStatus)
{
return Constants.ErrorCode;
}
if (!BuildPromotionPipelinesForAccount.TryGetValue(
build.AzureDevOpsAccount,
out (string project, int pipelineId) promotionPipelineInformation))
{
Console.WriteLine($"Promoting builds from AzureDevOps account {build.AzureDevOpsAccount} is not supported by this command.");
return Constants.ErrorCode;
}
// Construct the templateParameters and queue time variables.
// Publishing v2 uses variables and v3 uses parameters, so just use the same values for both.
var promotionPipelineVariables = new Dictionary<string, string>
{
{ "BarBuildId", build.Id.ToString() },
{ "PublishingInfraVersion", _options.PublishingInfraVersion.ToString() },
{ "PromoteToChannelIds", string.Join("-", targetChannels.Select(tch => tch.Id)) },
{ "EnableSigningValidation", _options.DoSigningValidation.ToString() },
{ "SigningValidationAdditionalParameters", _options.SigningValidationAdditionalParameters },
{ "EnableNugetValidation", _options.DoNuGetValidation.ToString() },
{ "EnableSourceLinkValidation", _options.DoSourcelinkValidation.ToString() },
{ "PublishInstallersAndChecksums", true.ToString() },
{ "SymbolPublishingAdditionalParameters", _options.SymbolPublishingAdditionalParameters },
{ "ArtifactsPublishingAdditionalParameters", _options.ArtifactPublishingAdditionalParameters },
{ "AllowPublicPublishingFromInternal", _options.AllowPublicPublishingFromInternal.ToString() }
};
if (_options.DoSDLValidation)
{
promotionPipelineVariables.Add("EnableSDLValidation", _options.DoSDLValidation.ToString());
promotionPipelineVariables.Add("SDLValidationCustomParams", _options.SDLValidationParams);
promotionPipelineVariables.Add("SDLValidationContinueOnError", _options.SDLValidationContinueOnError);
}
// Pass the same values to the variables and pipeline parameters so this works with the
// v2 and v3 versions of the promotion pipeline.
int azdoBuildId = await _azdoClient.StartNewBuildAsync(build.AzureDevOpsAccount,
promotionPipelineInformation.project,
promotionPipelineInformation.pipelineId,
arcadeSDKSourceBranch,
arcadeSDKSourceSHA,
promotionPipelineVariables,
promotionPipelineVariables
).ConfigureAwait(false);
string promotionBuildUrl = $"https://dev.azure.com/{build.AzureDevOpsAccount}/{promotionPipelineInformation.project}/_build/results?buildId={azdoBuildId}";
Console.WriteLine($"Build {build.Id} will be assigned to target channel(s) once this build finishes publishing assets: {promotionBuildUrl}");
if (_options.NoWait)
{
Console.WriteLine("Returning before asset publishing and channel assignment finishes. The operation continues asynchronously in AzDO.");
return Constants.SuccessCode;
}
try
{
var waitIntervalInSeconds = TimeSpan.FromSeconds(60);
AzureDevOpsBuild promotionBuild;
do
{
Console.WriteLine($"Waiting '{waitIntervalInSeconds.TotalSeconds}' seconds for promotion build to complete.");
await Task.Delay(waitIntervalInSeconds);
promotionBuild = await _azdoClient.GetBuildAsync(
build.AzureDevOpsAccount,
promotionPipelineInformation.project,
azdoBuildId);
} while (!promotionBuild.Status.Equals("completed", StringComparison.OrdinalIgnoreCase));
}
catch (Exception e)
{
Console.WriteLine($"Darc couldn't check status of the promotion build. {e.Message}");
return Constants.ErrorCode;
}
build = await barClient.GetBuildAsync(build.Id);
if (targetChannels.All(ch => build.Channels.Any(c => c.Id == ch.Id)))
{
Console.WriteLine($"Build '{build.Id}' was successfully added to the target channel(s).");
return Constants.SuccessCode;
}
else
{
Console.WriteLine("The promotion build finished but the build isn't associated with at least one of the target channels. This is an error scenario.");
Console.WriteLine($"Details are available in the following build: {promotionBuildUrl} For any questions, contact @dnceng");
return Constants.ErrorCode;
}
}
private async Task<bool> ValidateAzDOBuildAsync(IAzureDevOpsClient azdoClient, string azureDevOpsAccount, string azureDevOpsProject, int azureDevOpsBuildId)
{
try
{
var artifacts = await azdoClient.GetBuildArtifactsAsync(azureDevOpsAccount, azureDevOpsProject, azureDevOpsBuildId, maxRetries: 5);
// The build manifest is always necessary
if (!artifacts.Any(f => f.Name.Equals("AssetManifests")))
{
Console.Write("The build that you want to add to a new channel doesn't have a Build Manifest. That's required for publishing. Aborting.");
return false;
}
if ((_options.DoSigningValidation || _options.DoNuGetValidation || _options.DoSourcelinkValidation)
&& !artifacts.Any(f => f.Name.Equals("PackageArtifacts")))
{
Console.Write("The build that you want to add to a new channel doesn't have a list of package assets in the PackageArtifacts container. That's required when running signing or NuGet validation. Aborting.");
return false;
}
if (_options.DoSourcelinkValidation && !artifacts.Any(f => f.Name.Equals("BlobArtifacts")))
{
Console.Write("The build that you want to add to a new channel doesn't have a list of blob assets in the BlobArtifacts container. That's required when running SourceLink validation. Aborting.");
return false;
}
return true;
}
catch (HttpRequestException e) when (e.Message.Contains(((int)HttpStatusCode.NotFound).ToString()))
{
Console.Write("The build that you want to add to a new channel isn't available in AzDO anymore. Aborting.");
return false;
}
catch (HttpRequestException e) when (e.Message.Contains(((int)HttpStatusCode.Unauthorized).ToString()))
{
Console.WriteLine("Got permission denied response while trying to retrieve target build from Azure DevOps. Aborting.");
Console.Write("Please make sure that your Azure DevOps PAT has the build read and execute scopes set.");
return false;
}
}
/// <summary>
/// By default the source branch/SHA for the Build Promotion pipeline will be the branch/SHA
/// that produced the Arcade.SDK used by the build being promoted. The user can override that
/// by specifying both, branch & SHA, on the command line.
/// </summary>
/// <param name="build">Build for which the Arcade SDK dependency build will be inferred.</param>
private async Task<(string sourceBranch, string sourceVersion)> GetSourceBranchInfoAsync(Build build)
{
bool hasSourceBranch = !string.IsNullOrEmpty(_options.SourceBranch);
bool hasSourceSHA = !string.IsNullOrEmpty(_options.SourceSHA);
if (hasSourceBranch)
{
_options.SourceBranch = GitHelpers.NormalizeBranchName(_options.SourceBranch);
if (_options.SourceBranch.EndsWith("release/3.x", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine($"Warning: Arcade branch {_options.SourceBranch} doesn't support build promotion. Please try specifiying --source-branch 'main'.");
Console.WriteLine("Switching source branch to Arcade main.");
return ("main", null);
}
}
if (hasSourceBranch && hasSourceSHA)
{
return (_options.SourceBranch, _options.SourceSHA);
}
else if (hasSourceSHA && !hasSourceBranch)
{
Console.WriteLine("The `source-sha` parameter needs to be specified together with `source-branch`.");
return (null, null);
}
else if (hasSourceBranch)
{
return (_options.SourceBranch, null);
}
string sourceBuildRepo = string.IsNullOrEmpty(build.GitHubRepository) ?
build.AzureDevOpsRepository :
build.GitHubRepository;
IRemote repoRemote = RemoteFactory.GetRemote(_options, sourceBuildRepo, _logger);
IEnumerable<DependencyDetail> sourceBuildDependencies = await repoRemote.GetDependenciesAsync(sourceBuildRepo, build.Commit)
.ConfigureAwait(false);
DependencyDetail sourceBuildArcadeSDKDependency = sourceBuildDependencies.GetArcadeUpdate();
if (sourceBuildArcadeSDKDependency == null)
{
Console.WriteLine("The target build doesn't have a dependency on Microsoft.DotNet.Arcade.Sdk.");
return (null, null);
}
IEnumerable<Asset> listArcadeSDKAssets = await _barClient.GetAssetsAsync(sourceBuildArcadeSDKDependency.Name, sourceBuildArcadeSDKDependency.Version)
.ConfigureAwait(false);
Asset sourceBuildArcadeSDKDepAsset = listArcadeSDKAssets.FirstOrDefault();
if (sourceBuildArcadeSDKDepAsset == null)
{
Console.WriteLine($"Could not fetch information about Microsoft.DotNet.Arcade.Sdk asset version {sourceBuildArcadeSDKDependency.Version}.");
return (null, null);
}
Build sourceBuildArcadeSDKDepBuild = await _barClient.GetBuildAsync(sourceBuildArcadeSDKDepAsset.BuildId);
if (sourceBuildArcadeSDKDepBuild == null)
{
Console.Write($"Could not find information (in BAR) about the build that produced Microsoft.DotNet.Arcade.Sdk version {sourceBuildArcadeSDKDependency.Version}.");
return (null, null);
}
if (sourceBuildArcadeSDKDepBuild.GitHubBranch.EndsWith("release/3.x", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Warning: To promote a build that uses a 3.x version of Arcade SDK you need to inform the --source-branch 'main' parameter.");
Console.WriteLine("Switching source branch to Arcade main.");
return ("main", null);
}
var oldestSupportedArcadeSDKDate = new DateTimeOffset(2020, 01, 28, 0, 0, 0, new TimeSpan(0, 0, 0));
if (DateTimeOffset.Compare(sourceBuildArcadeSDKDepBuild.DateProduced, oldestSupportedArcadeSDKDate) < 0)
{
Console.WriteLine($"The target build uses an SDK released in {sourceBuildArcadeSDKDepBuild.DateProduced}");
Console.WriteLine($"The target build needs to use an Arcade SDK 5.x.x version released after {oldestSupportedArcadeSDKDate} otherwise " +
$"you must inform the `source-branch` / `source-sha` parameters to point to a specific Arcade build.");
Console.Write($"You can also pass the `skip-assets-publishing` parameter if all you want is to " +
$"assign the build to a channel. Note, though, that this will not publish the build assets.");
return (null, null);
}
return (sourceBuildArcadeSDKDepBuild.GitHubBranch, sourceBuildArcadeSDKDepBuild.Commit);
}
private static void PrintSubscriptionInfo(List<Subscription> applicableSubscriptions)
{
IEnumerable<Subscription> subscriptionsThatWillFlowImmediately = applicableSubscriptions.Where(s => s.Enabled &&
s.Policy.UpdateFrequency == UpdateFrequency.EveryBuild);
IEnumerable<Subscription> subscriptionsThatWillFlowTomorrowOrNotAtAll = applicableSubscriptions.Where(s => s.Enabled &&
s.Policy.UpdateFrequency != UpdateFrequency.EveryBuild);
IEnumerable<Subscription> disabledSubscriptions = applicableSubscriptions.Where(s => !s.Enabled);
// Print out info
if (subscriptionsThatWillFlowImmediately.Any())
{
Console.WriteLine("The following repos/branches will apply this build immediately:");
foreach (var sub in subscriptionsThatWillFlowImmediately)
{
Console.WriteLine($" {sub.TargetRepository} @ {sub.TargetBranch}");
}
}
if (subscriptionsThatWillFlowTomorrowOrNotAtAll.Any())
{
Console.WriteLine("The following repos/branches will apply this change at a later time, or not by default.");
Console.WriteLine("To flow immediately, run the specified command");
foreach (var sub in subscriptionsThatWillFlowTomorrowOrNotAtAll)
{
Console.WriteLine($" {sub.TargetRepository} @ {sub.TargetBranch} (update freq: {sub.Policy.UpdateFrequency})");
Console.WriteLine($" darc trigger-subscriptions --id {sub.Id}");
}
}
if (disabledSubscriptions.Any())
{
Console.WriteLine("The following repos/branches will not get this change because their subscriptions are disabled.");
foreach (var sub in disabledSubscriptions)
{
Console.WriteLine($" {sub.TargetRepository} @ {sub.TargetBranch}");
}
}
}
}