@@ -11,10 +11,14 @@ namespace Dotnet.Docker;
1111internal partial class FromStagingPipelineCommand : BaseCommand < FromStagingPipelineOptions >
1212{
1313 /// <summary>
14- /// Callback that stages all changes, commits them, pushes them to the
15- /// remote, and creates a pull request.
14+ /// Callback that stages all changes and commits them.
1615 /// </summary>
17- private delegate Task CommitAndCreatePullRequest ( string commitMessage , string prTitle , string prBody ) ;
16+ private delegate Task CommitChanges ( string commitMessage ) ;
17+
18+ /// <summary>
19+ /// Callback that pushes all commits and creates a pull request.
20+ /// </summary>
21+ private delegate Task PushAndCreatePullRequest ( string prTitle , string prBody ) ;
1822
1923 private readonly ILogger < FromStagingPipelineCommand > _logger ;
2024 private readonly IPipelineArtifactProvider _pipelineArtifactProvider ;
@@ -44,17 +48,73 @@ public FromStagingPipelineCommand(
4448
4549 public override async Task < int > ExecuteAsync ( FromStagingPipelineOptions options )
4650 {
51+ var stageContainers = options . GetStageContainerList ( ) ;
52+
53+ if ( stageContainers . Count == 0 )
54+ {
55+ _logger . LogError ( "No stage containers provided." ) ;
56+ return 1 ;
57+ }
58+
59+ _logger . LogInformation (
60+ "Updating dependencies based on {Count} stage container(s): {StageContainers}" ,
61+ stageContainers . Count ,
62+ string . Join ( ", " , stageContainers ) ) ;
63+
4764 // Delegate all git responsibilities to GitRepoContext. Depending on what options were
4865 // passed in, we may or may not want to actually perform git operations. GitRepoContext
4966 // decides what git operations to perform and tells us where to make changes. This keeps
5067 // all the git-related logic in one place.
5168 var gitRepoContext = await _createGitRepoContextAsync ( options ) ;
5269
53- _logger . LogInformation (
54- "Updating dependencies based on stage container {StageContainer}" ,
55- options . StageContainer ) ;
70+ List < string > commitMessages = [ ] ;
71+ List < string > prBodySections = [ ] ;
72+
73+ // Process each stage container, creating a separate commit for each
74+ foreach ( var stageContainer in stageContainers )
75+ {
76+ _logger . LogInformation ( "Processing stage container: {StageContainer}" , stageContainer ) ;
77+
78+ var ( commitMessage , prBodySection , exitCode ) = await ProcessStageContainerAsync (
79+ options ,
80+ stageContainer ,
81+ gitRepoContext ) ;
82+
83+ if ( exitCode != 0 )
84+ {
85+ return exitCode ;
86+ }
87+
88+ // Commit changes for this stage container
89+ await gitRepoContext . CommitChanges ( commitMessage ) ;
90+
91+ commitMessages . Add ( commitMessage ) ;
92+ prBodySections . Add ( prBodySection ) ;
93+ }
5694
57- var stagingPipelineRunId = options . GetStagingPipelineRunId ( ) ;
95+ // Create pull request with all commits
96+ var prTitle = stageContainers . Count == 1
97+ ? $ "[{ options . TargetBranch } ] { commitMessages [ 0 ] } "
98+ : $ "[{ options . TargetBranch } ] Update .NET dependencies from { stageContainers . Count } stage containers";
99+
100+ var prBody = string . Join ( Environment . NewLine + Environment . NewLine , prBodySections ) ;
101+ await gitRepoContext . PushAndCreatePullRequest ( prTitle , prBody ) ;
102+
103+ return 0 ;
104+ }
105+
106+ /// <summary>
107+ /// Processes a single stage container and applies the updates.
108+ /// </summary>
109+ /// <returns>
110+ /// A tuple containing the commit message, PR body section, and exit code.
111+ /// </returns>
112+ private async Task < ( string CommitMessage , string PrBodySection , int ExitCode ) > ProcessStageContainerAsync (
113+ FromStagingPipelineOptions options ,
114+ string stageContainer ,
115+ GitRepoContext gitRepoContext )
116+ {
117+ var stagingPipelineRunId = StagingPipelineOptionsExtensions . GetStagingPipelineRunId ( stageContainer ) ;
58118
59119 // Log staging pipeline tags for diagnostic purposes
60120 var stagingPipelineTags = await _pipelinesService . GetBuildTagsAsync (
@@ -68,15 +128,15 @@ public override async Task<int> ExecuteAsync(FromStagingPipelineOptions options)
68128 {
69129 ArgumentException . ThrowIfNullOrWhiteSpace (
70130 options . StagingStorageAccount ,
71- $ "{ FromStagingPipelineOptions . StagingStorageAccountOption } must be set when using the { FromStagingPipelineOptions . InternalOption } option."
131+ $ "{ FromStagingPipelineOptions . StagingStorageAccountOptionName } must be set when using the { FromStagingPipelineOptions . InternalOption } option."
72132 ) ;
73133
74134 // Release metadata is stored in metadata/ReleaseManifest.json.
75135 // Release assets are stored individually under in assets/shipping/assets/[Sdk|Runtime|aspnetcore|...].
76136 // Full example: https://dotnetstagetest.blob.core.windows.net/stage-2XXXXXX/assets/shipping/assets/Runtime/10.0.0-preview.N.XXXXX.YYY/dotnet-runtime-10.0.0-preview.N.XXXXX.YYY-linux-arm64.tar.gz
77- _buildLabelService . AddBuildTags ( $ "Container - { options . StageContainer } ") ;
137+ _buildLabelService . AddBuildTags ( $ "Container - { stageContainer } ") ;
78138 internalBaseUrl = NormalizeStorageAccountUrl ( options . StagingStorageAccount )
79- + $ "/{ options . StageContainer } /assets/shipping/assets";
139+ + $ "/{ stageContainer } /assets/shipping/assets";
80140 }
81141
82142 var releaseConfig = await _pipelineArtifactProvider . GetReleaseConfigAsync (
@@ -94,7 +154,7 @@ public override async Task<int> ExecuteAsync(FromStagingPipelineOptions options)
94154 _internalVersionsService . RecordInternalStagingBuild (
95155 repoRoot : gitRepoContext . LocalRepoPath ,
96156 dotNetVersion : dotNetVersion ,
97- stageContainer : options . StageContainer ) ;
157+ stageContainer : stageContainer ) ;
98158 }
99159
100160 var productVersions = ( options . Internal , releaseConfig . SdkOnly ) switch
@@ -141,7 +201,7 @@ public override async Task<int> ExecuteAsync(FromStagingPipelineOptions options)
141201 var buildUrl = $ "{ options . AzdoOrganization } /{ options . AzdoProject } /_build/results?buildId={ stagingPipelineRunId } ";
142202 _logger . LogInformation (
143203 "Applying internal build {StageContainer} ({BuildUrl})" ,
144- options . StageContainer , buildUrl ) ;
204+ stageContainer , buildUrl ) ;
145205
146206 _logger . LogInformation (
147207 "Ignore any git-related logging output below, because git "
@@ -163,8 +223,8 @@ public override async Task<int> ExecuteAsync(FromStagingPipelineOptions options)
163223 _logger . LogError (
164224 "Failed to apply stage container {StageContainer}. "
165225 + "Command exited with code {ExitCode}." ,
166- options . StageContainer , exitCode ) ;
167- return exitCode ;
226+ stageContainer , exitCode ) ;
227+ return ( string . Empty , string . Empty , exitCode ) ;
168228 }
169229
170230 var commitMessage = releaseConfig switch
@@ -173,18 +233,18 @@ public override async Task<int> ExecuteAsync(FromStagingPipelineOptions options)
173233 _ => $ "Update .NET { majorMinorVersionString } to { productVersions [ "sdk" ] } SDK / { productVersions [ "runtime" ] } Runtime",
174234 } ;
175235
176- var prTitle = $ "[{ options . TargetBranch } ] { commitMessage } ";
177236 var newVersionsList = productVersions . Select ( kvp => $ "- { kvp . Key . ToUpper ( ) } : { kvp . Value } ") ;
178- var prBody = $ """
179- This pull request updates .NET { majorMinorVersionString } to the following versions:
237+ var prBodySection = $ """
238+ ## .NET { majorMinorVersionString }
239+
240+ This updates .NET { majorMinorVersionString } to the following versions:
180241
181242 { string . Join ( Environment . NewLine , newVersionsList ) }
182243
183- These versions are from .NET staging pipeline run [{ options . StageContainer } ]({ buildUrl } ).
244+ These versions are from .NET staging pipeline run [{ stageContainer } ]({ buildUrl } ).
184245 """ ;
185- await gitRepoContext . CommitAndCreatePullRequest ( commitMessage , prTitle , prBody ) ;
186246
187- return 0 ;
247+ return ( commitMessage , prBodySection , 0 ) ;
188248 }
189249
190250 /// <summary>
@@ -215,13 +275,18 @@ private static string NormalizeStorageAccountUrl(string storageAccount)
215275 /// Holds context about the git repository where changes should be made.
216276 /// </summary>
217277 /// <param name="LocalRepoPath">Root of the repo where all changes should be made.</param>
218- /// <param name="CommitAndCreatePullRequest">Callback that creates a pull request with all changes.</param>
219- private record GitRepoContext ( string LocalRepoPath , CommitAndCreatePullRequest CommitAndCreatePullRequest )
278+ /// <param name="CommitChanges">Callback that commits changes with the given message.</param>
279+ /// <param name="PushAndCreatePullRequest">Callback that pushes all commits and creates a pull request.</param>
280+ private record GitRepoContext (
281+ string LocalRepoPath ,
282+ CommitChanges CommitChanges ,
283+ PushAndCreatePullRequest PushAndCreatePullRequest )
220284 {
221285 /// <summary>
222286 /// Sets up the remote/local git repository based on <paramref name="options"/>.
223287 /// Call this before making any changes, then make changes to <see cref="LocalRepoPath"/>
224- /// and use <see cref="CommitAndCreatePullRequest"/> to create a pull request.
288+ /// and use <see cref="CommitChanges"/> to commit each change individually,
289+ /// then use <see cref="PushAndCreatePullRequest"/> to push all commits and create a pull request.
225290 /// </summary>
226291 /// <remarks>
227292 /// If <see cref="FromStagingPipelineOptions.Mode"/> is <see cref="ChangeMode.Local"/>,
@@ -233,15 +298,21 @@ public static async Task<GitRepoContext> CreateAsync(
233298 FromStagingPipelineOptions options ,
234299 IEnvironmentService environmentService )
235300 {
236- CommitAndCreatePullRequest createPullRequest ;
301+ CommitChanges commitChanges ;
302+ PushAndCreatePullRequest pushAndCreatePullRequest ;
237303 string localRepoPath ;
238304
239305 if ( options . Mode == ChangeMode . Remote )
240306 {
241307 var remoteUrl = options . GetAzdoRepoUrl ( ) ;
242308 var targetBranch = options . TargetBranch ;
243309 var buildId = environmentService . GetBuildId ( ) ?? "" ;
244- var prBranch = options . CreatePrBranchName ( $ "update-deps-int-{ options . StageContainer } ", buildId ) ;
310+ var stageContainerList = options . GetStageContainerList ( ) ;
311+ if ( stageContainerList . Count == 0 )
312+ {
313+ throw new ArgumentException ( "At least one stage container must be provided." ) ;
314+ }
315+ var prBranch = options . CreatePrBranchName ( $ "update-deps-int-{ stageContainerList [ 0 ] } ", buildId ) ;
245316 var committer = options . GetCommitterIdentity ( ) ;
246317
247318 // Clone the repo and configure git identity for commits
@@ -253,10 +324,15 @@ public static async Task<GitRepoContext> CreateAsync(
253324 await git . Local . CreateAndCheckoutLocalBranchAsync ( prBranch ) ;
254325
255326 localRepoPath = git . Local . LocalPath ;
256- createPullRequest = async ( commitMessage , prTitle , prBody ) =>
327+
328+ commitChanges = async ( commitMessage ) =>
257329 {
258330 await git . Local . StageAsync ( "." ) ;
259331 await git . Local . CommitAsync ( commitMessage , committer ) ;
332+ } ;
333+
334+ pushAndCreatePullRequest = async ( prTitle , prBody ) =>
335+ {
260336 await git . PushLocalBranchAsync ( prBranch ) ;
261337 await git . Remote . CreatePullRequestAsync ( new (
262338 Title : prTitle ,
@@ -270,16 +346,22 @@ await git.Remote.CreatePullRequestAsync(new(
270346 {
271347 logger . LogInformation ( "No git operations will be performed in {Mode} mode." , options . Mode ) ;
272348 localRepoPath = options . RepoRoot ;
273- createPullRequest = async ( commitMessage , prTitle , prBody ) =>
349+
350+ commitChanges = async ( commitMessage ) =>
274351 {
275- logger . LogInformation ( "Skipping commit and pull request creation in {Mode} mode." , options . Mode ) ;
352+ logger . LogInformation ( "Skipping commit in {Mode} mode." , options . Mode ) ;
276353 logger . LogInformation ( "Commit message: {CommitMessage}" , commitMessage ) ;
354+ } ;
355+
356+ pushAndCreatePullRequest = async ( prTitle , prBody ) =>
357+ {
358+ logger . LogInformation ( "Skipping push and pull request creation in {Mode} mode." , options . Mode ) ;
277359 logger . LogInformation ( "Pull request title: {PullRequestTitle}" , prTitle ) ;
278360 logger . LogInformation ( "Pull request body:\n {PullRequestBody}" , prBody ) ;
279361 } ;
280362 }
281363
282- return new GitRepoContext ( localRepoPath , createPullRequest ) ;
364+ return new GitRepoContext ( localRepoPath , commitChanges , pushAndCreatePullRequest ) ;
283365 }
284366 }
285367}
0 commit comments