Skip to content

Commit 82ac610

Browse files
authored
[Rollout] Production Rollout 2025-10-13 (#5367)
2 parents c60155e + cb600e3 commit 82ac610

File tree

8 files changed

+174
-40
lines changed

8 files changed

+174
-40
lines changed

docs/Darc.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2745,7 +2745,10 @@ Local dependencies updated from channel '.NET 5 Dev'.
27452745
Update an existing subscription. Opens an editor so that some properties of a
27462746
subscription may be altered. Because of the way that Maestro++ tracks pull
27472747
requests, the *target* parameters of a subscription (target repository and
2748-
target branch) may not be edited.
2748+
target branch) may not be edited. Additionally, the subscription ID and Source
2749+
Enabled flag cannot be modified. If any of these immutable fields are changed,
2750+
the update will fail with an error listing all the fields that cannot be
2751+
modified.
27492752

27502753
**Sample**:
27512754
```

src/Microsoft.DotNet.Darc/Darc/Operations/UpdateSubscriptionOperation.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,34 @@ public override async Task<int> ExecuteAsync()
251251
sourceDirectory = updateSubscriptionPopUp.SourceDirectory;
252252
targetDirectory = updateSubscriptionPopUp.TargetDirectory;
253253
excludedAssets = [..updateSubscriptionPopUp.ExcludedAssets];
254+
255+
// Validate that immutable fields have not been changed
256+
var immutableFieldErrors = new List<string>();
257+
258+
if (updateSubscriptionPopUp.TargetRepository != subscription.TargetRepository)
259+
{
260+
immutableFieldErrors.Add($"Target Repository URL (cannot be changed from '{subscription.TargetRepository}')");
261+
}
262+
263+
if (updateSubscriptionPopUp.TargetBranch != subscription.TargetBranch)
264+
{
265+
immutableFieldErrors.Add($"Target Branch (cannot be changed from '{subscription.TargetBranch}')");
266+
}
267+
268+
if (updateSubscriptionPopUp.SourceEnabled != subscription.SourceEnabled)
269+
{
270+
immutableFieldErrors.Add($"Source Enabled (cannot be changed from '{subscription.SourceEnabled}')");
271+
}
272+
273+
if (immutableFieldErrors.Any())
274+
{
275+
_logger.LogError("The following immutable fields cannot be modified:");
276+
foreach (var error in immutableFieldErrors)
277+
{
278+
_logger.LogError($" - {error}");
279+
}
280+
return Constants.ErrorCode;
281+
}
254282
}
255283

256284

@@ -357,6 +385,7 @@ private bool UpdatingViaCommandLine()
357385
|| _options.SourceEnabled != null
358386
|| _options.SourceDirectory != null
359387
|| _options.ExcludedAssets != null
388+
|| _options.TargetDirectory != null
360389
|| UpdatingMergePoliciesViaCommandLine();
361390

362391
private bool UpdatingMergePoliciesViaCommandLine()

src/Microsoft.DotNet.Darc/DarcLib/VirtualMonoRepo/VmrForwardFlower.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Text;
8+
using System.Text.RegularExpressions;
79
using System.Threading;
810
using System.Threading.Tasks;
911
using LibGit2Sharp;
12+
using Maestro.Common;
1013
using Microsoft.DotNet.DarcLib.Helpers;
1114
using Microsoft.DotNet.DarcLib.Models.VirtualMonoRepo;
1215
using Microsoft.DotNet.ProductConstructionService.Client.Models;
@@ -59,8 +62,17 @@ public class VmrForwardFlower : VmrCodeFlower, IVmrForwardFlower
5962
private readonly IForwardFlowConflictResolver _conflictResolver;
6063
private readonly IWorkBranchFactory _workBranchFactory;
6164
private readonly IProcessManager _processManager;
65+
private readonly ICommentCollector _commentCollector;
6266
private readonly ILogger<VmrCodeFlower> _logger;
6367

68+
// Regex to extract PR number from a github merge commit message
69+
// e.g.: "Update dependencies from source-repo (#12345)" extracts "12345"
70+
private readonly static Regex GitHubPullRequestNumberExtractionRegex = new Regex(".+\\(#(\\d+)\\)$");
71+
72+
// Regex to extract PR number from an AzDO merge commit message
73+
// e.g.: "Merged PR 12345: Update dependencies from source-repo" extracts "12345"
74+
private readonly static Regex AzDoPullRequestNumberExtractionRegex = new Regex("^Merged PR (\\d+):");
75+
6476
public VmrForwardFlower(
6577
IVmrInfo vmrInfo,
6678
ISourceManifest sourceManifest,
@@ -91,6 +103,7 @@ public VmrForwardFlower(
91103
_conflictResolver = conflictResolver;
92104
_workBranchFactory = workBranchFactory;
93105
_processManager = processManager;
106+
_commentCollector = commentCollector;
94107
_logger = logger;
95108
}
96109

@@ -151,6 +164,11 @@ public async Task<CodeFlowResult> FlowForwardAsync(
151164
hasChanges &= await _codeflowChangeAnalyzer.ForwardFlowHasMeaningfulChangesAsync(mapping.Name, headBranch, targetBranch);
152165
}
153166

167+
if (hasChanges)
168+
{
169+
await CommentIncludedPRs(sourceRepo, lastFlows.LastForwardFlow.RepoSha, build.Commit, mapping.DefaultRemote, cancellationToken);
170+
}
171+
154172
return new CodeFlowResult(
155173
hasChanges,
156174
conflictedFiles ?? [],
@@ -366,6 +384,44 @@ .. DependencyFileManager.CodeflowDependencyFiles.Select(VmrPatchHandler.GetExclu
366384
return hadChanges;
367385
}
368386

387+
private async Task CommentIncludedPRs(
388+
ILocalGitRepo sourceRepo,
389+
string lastCommit,
390+
string currentCommit,
391+
string repoUri,
392+
CancellationToken cancellationToken)
393+
{
394+
var gitRepoType = GitRepoUrlUtils.ParseTypeFromUri(repoUri);
395+
// codeflow tests set the defaultRemote to a local path, we have to skip those
396+
if (gitRepoType == GitRepoType.Local)
397+
{
398+
return;
399+
}
400+
401+
var result = await sourceRepo.ExecuteGitCommand(["log", "--pretty=%s", $"{lastCommit}..{currentCommit}"], cancellationToken);
402+
result.ThrowIfFailed($"Failed to get the list of commits between {lastCommit} and {currentCommit} in {sourceRepo.Path}");
403+
404+
(Regex regex, string prLinkFormat) = gitRepoType switch
405+
{
406+
GitRepoType.GitHub => (GitHubPullRequestNumberExtractionRegex, $"- {repoUri}/pull/{{0}}"),
407+
GitRepoType.AzureDevOps => (AzDoPullRequestNumberExtractionRegex, $"- {repoUri}/pullrequest/{{0}}"),
408+
_ => throw new NotSupportedException($"Repository type for URI '{repoUri}' is not supported for PR link extraction.")
409+
};
410+
var commitMessages = result.GetOutputLines();
411+
StringBuilder str = new("PRs from original repository included in this codeflow update:");
412+
foreach (var message in commitMessages)
413+
{
414+
var match = regex.Match(message);
415+
if (match.Success && match.Groups.Count > 1)
416+
{
417+
str.AppendLine();
418+
str.AppendFormat(prLinkFormat, match.Groups[1].Value);
419+
}
420+
}
421+
422+
_commentCollector.AddComment(str.ToString(), CommentType.Information);
423+
}
424+
369425
protected override async Task<Codeflow?> DetectCrossingFlow(
370426
Codeflow lastFlow,
371427
Backflow? lastBackFlow,

test/Microsoft.DotNet.DarcLib.Codeflow.Tests/ForwardFlowTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,3 @@ public async Task NonLinearForwardflowIsDetectedTest()
248248
await act.Should().ThrowAsync<NonLinearCodeflowException>();
249249
}
250250
}
251-

test/ProductConstructionService.ScenarioTests/CodeFlowScenarioTestBase.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace ProductConstructionService.ScenarioTests;
1414

1515
internal class CodeFlowScenarioTestBase : ScenarioTestBase
1616
{
17-
protected async Task CheckForwardFlowGitHubPullRequest(
17+
protected async Task<PullRequest> CheckForwardFlowGitHubPullRequest(
1818
(string Repo, string Commit)[] repoUpdates,
1919
string targetRepoName,
2020
string targetBranch,
@@ -62,6 +62,8 @@ protected async Task CheckForwardFlowGitHubPullRequest(
6262
manifestRecord.CommitSha.Should().Be(update.Commit);
6363
}
6464
}
65+
66+
return pullRequest;
6567
}
6668

6769
protected async Task CheckBackwardFlowGitHubPullRequest(
@@ -222,11 +224,10 @@ public static async Task CheckIfPullRequestCommentExists(
222224
string[] stringsExpectedInComment)
223225
{
224226
IReadOnlyList<IssueComment> comments = await GitHubApi.Issue.Comment.GetAllForIssue(TestParameters.GitHubTestOrg, targetRepo, pullRequest.Number);
225-
var conflictComment = comments.First(comment => comment.Body.Contains("conflict"));
226227

227228
foreach (var s in stringsExpectedInComment)
228229
{
229-
conflictComment.Body.Should().Contain(s);
230+
comments.Any(c => c.Body.Contains(s)).Should().BeTrue();
230231
}
231232
}
232233
}

test/ProductConstructionService.ScenarioTests/ScenarioTests/ScenarioTests_CodeFlow.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public async Task Vmr_ForwardFlowTest()
4242
var branchName = GetTestBranchName();
4343
var productRepo = GetGitHubRepoUrl(TestRepository.TestRepo1Name);
4444
var targetBranchName = GetTestBranchName();
45+
var testPrNumber = 23;
4546

4647
await using AsyncDisposableValue<string> testChannel = await CreateTestChannelAsync(channelName);
4748

@@ -69,7 +70,7 @@ await CreateTargetBranchAndExecuteTest(targetBranchName, vmrDirectory.Directory,
6970
await File.WriteAllTextAsync(newFilePath, TestFilesContent[TestFile1Name]);
7071

7172
await GitAddAllAsync();
72-
await GitCommitAsync("Add new file");
73+
await GitCommitAsync($"Change that would have been made in a pr (#{testPrNumber})");
7374

7475
// Push it to github
7576
await using (await PushGitBranchAsync("origin", branchName))
@@ -92,12 +93,17 @@ await CreateTargetBranchAndExecuteTest(targetBranchName, vmrDirectory.Directory,
9293
await TriggerSubscriptionAsync(subscriptionId.Value);
9394

9495
TestContext.WriteLine("Verifying subscription PR");
95-
await CheckForwardFlowGitHubPullRequest(
96+
var pr = await CheckForwardFlowGitHubPullRequest(
9697
[(TestRepository.TestRepo1Name, repoSha)],
9798
TestRepository.VmrTestRepoName,
9899
targetBranchName,
99100
[$"src/{TestRepository.TestRepo1Name}/{TestFile1Name}"],
100101
TestFilePatches);
102+
103+
await CheckIfPullRequestCommentExists(
104+
TestRepository.VmrTestRepoName,
105+
pr,
106+
[$"https://github.com/maestro-auth-test/maestro-test1/pull/{testPrNumber}"]);
101107
}
102108
}
103109
}

tools/ProductConstructionService.ReproTool/Operations/FlowCommitOperation.cs

Lines changed: 67 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -57,34 +57,16 @@ internal override async Task RunAsync()
5757
Channel? channel = channels.FirstOrDefault(c => c.Name == _options.Channel)
5858
?? await _localPcsApi.Channels.CreateChannelAsync("test", _options.Channel);
5959

60-
var (repoName, owner) = GitRepoUrlUtils.GetRepoNameAndOwner(_options.SourceRepository);
61-
var (sourceRepoName, sourceOwner) = (repoName, owner);
60+
var (sourceRepo, sourceOwner) = GitRepoUrlUtils.GetRepoNameAndOwner(_options.SourceRepository);
61+
var (targetRepo, targetOwner) = GitRepoUrlUtils.GetRepoNameAndOwner(_options.TargetRepository);
6262

63-
bool? isBackflow = null;
64-
try
65-
{
66-
(repoName, owner) = GitRepoUrlUtils.GetRepoNameAndOwner(_options.TargetRepository);
67-
await _ghClient.Repository.Content.GetAllContents(owner, repoName, SourceMappingsPath);
68-
isBackflow = false;
69-
}
70-
catch { }
71-
72-
if (!isBackflow.HasValue)
73-
{
74-
try
75-
{
76-
(repoName, owner) = GitRepoUrlUtils.GetRepoNameAndOwner(_options.SourceRepository);
77-
await _ghClient.Repository.Content.GetAllContents(owner, repoName, SourceMappingsPath);
78-
isBackflow = true;
79-
}
80-
catch { }
81-
}
63+
bool isForwardFlow = await IsForwardFlow(sourceOwner, sourceRepo, targetOwner, targetRepo);
8264

8365
var subscriptions = await _localPcsApi.Subscriptions.ListSubscriptionsAsync(
8466
channelId: channel.Id,
8567
sourceRepository: _options.SourceRepository,
8668
targetRepository: _options.TargetRepository,
87-
sourceEnabled: isBackflow.HasValue);
69+
sourceEnabled: true);
8870

8971
Subscription subscription = subscriptions.FirstOrDefault(s => s.TargetBranch == _options.TargetBranch)
9072
?? await _localPcsApi.Subscriptions.CreateAsync(
@@ -99,17 +81,33 @@ internal override async Task RunAsync()
9981
},
10082
null)
10183
{
102-
SourceEnabled = isBackflow.HasValue,
103-
SourceDirectory = isBackflow == true ? repoName : null,
104-
TargetDirectory = isBackflow == false ? sourceRepoName : null,
84+
SourceEnabled = true,
85+
SourceDirectory = isForwardFlow ? null : targetRepo,
86+
TargetDirectory = isForwardFlow ? sourceRepo : null,
10587
});
10688

107-
var commit = (await _ghClient.Repository.Branch.Get(sourceOwner, sourceRepoName, _options.SourceBranch)).Commit;
89+
string sourceCommit;
90+
91+
if (string.IsNullOrEmpty(_options.SourceCommit) && string.IsNullOrEmpty(_options.SourceBranch))
92+
{
93+
throw new ArgumentException("Please provide a source-branch or source-commit value.");
94+
}
95+
96+
if (string.IsNullOrEmpty(_options.SourceCommit))
97+
{
98+
sourceCommit = (await _ghClient.Repository.Branch.Get(sourceOwner, sourceRepo, _options.SourceBranch))
99+
.Commit.Sha;
100+
}
101+
else
102+
{
103+
sourceCommit = _options.SourceCommit;
104+
}
108105

109106
_logger.LogInformation("Creating build for {repo}@{branch} (commit {commit})",
110107
_options.SourceRepository,
111108
_options.SourceBranch,
112-
Microsoft.DotNet.DarcLib.Commit.GetShortSha(commit.Sha));
109+
Microsoft.DotNet.DarcLib.Commit.GetShortSha(sourceCommit));
110+
113111
List<AssetData> assets;
114112
if (_options.RealBuildId > 0)
115113
{
@@ -126,13 +124,16 @@ internal override async Task RunAsync()
126124
];
127125
}
128126

127+
_logger.LogInformation("Source commit is {}", sourceCommit);
128+
_logger.LogInformation("Subscription is forward-flow: {}", isForwardFlow);
129+
129130
var build = await _localPcsApi.Builds.CreateAsync(new BuildData(
130-
commit.Sha,
131+
sourceCommit,
131132
"dnceng",
132133
"internal",
133134
$"{DateTime.UtcNow:yyyyMMdd}.{new Random().Next(1, 75)}",
134-
$"https://dev.azure.com/dnceng/internal/_git/{owner}-{repoName}",
135-
_options.SourceBranch,
135+
$"https://dev.azure.com/dnceng/internal/_git/{sourceOwner}-{sourceRepo}",
136+
_options.SourceBranch ?? sourceCommit,
136137
released: false,
137138
stable: false)
138139
{
@@ -149,4 +150,40 @@ internal override async Task RunAsync()
149150

150151
_logger.LogInformation("Subscription triggered. Wait for a PR in {url}", $"{_options.TargetRepository}/pulls");
151152
}
153+
154+
private async Task<bool> IsVmr(string repoName, string repoOwner)
155+
{
156+
try
157+
{
158+
await _ghClient.Repository.Content.GetAllContents(repoOwner, repoName, SourceManifestPath);
159+
return true;
160+
}
161+
catch (Octokit.ApiException e)
162+
{
163+
if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
164+
{
165+
return false;
166+
}
167+
throw;
168+
}
169+
}
170+
171+
private async Task<bool> IsForwardFlow(
172+
string sourceOwner,
173+
string sourceRepo,
174+
string targetOwner,
175+
string targetRepo)
176+
{
177+
if (await IsVmr(targetRepo, targetOwner))
178+
{
179+
return true;
180+
}
181+
182+
if (await IsVmr(sourceRepo, sourceOwner))
183+
{
184+
return false;
185+
}
186+
187+
throw new InvalidOperationException("Neither the source nor the target repository appears to be a VMR.");
188+
}
152189
}

tools/ProductConstructionService.ReproTool/Options/FlowCommitOptions.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ internal class FlowCommitOptions : Options
1616
[Option("source-repo", HelpText = "Repo (full URI) to flow the commit from (must be under maestro-auth-test/)", Required = true)]
1717
public required string SourceRepository { get; init; }
1818

19-
[Option("source-branch", HelpText = "Branch whose commit will be flown", Required = true)]
20-
public required string SourceBranch { get; init; }
19+
[Option("source-branch", HelpText = "Either --source-branch or --source-commit is required. Branch whose commit will be flown. Ignored if source-commit is provided.", Required = false)]
20+
public string? SourceBranch { get; init; }
21+
22+
[Option("source-commit", HelpText = "Either --source-branch or --source-commit is required. Source commit that will be flown.", Required = false)]
23+
public string? SourceCommit { get; init; }
2124

2225
[Option("target-repo", HelpText = "Repo (full URI) to flow the commit to (must be under maestro-auth-test/)", Required = true)]
2326
public required string TargetRepository { get; init; }
@@ -28,7 +31,7 @@ internal class FlowCommitOptions : Options
2831
[Option("packages", HelpText = "Name(s) of package(s) to include in the flown build", Required = false)]
2932
public IEnumerable<string> Packages { get; init; } = [];
3033

31-
[Option("realBuildId", HelpText = "Build to take assets from. Shouldn't be combined with packages", Required = false)]
34+
[Option("assets-from-build", HelpText = "A real build id from which to take assets. Shouldn't be provided if using the --packages option", Required = false)]
3235
public int RealBuildId { get; init; }
3336

3437
internal override Operation GetOperation(IServiceProvider sp)

0 commit comments

Comments
 (0)