Skip to content

Commit 09cff11

Browse files
Copilotlbussell
andcommitted
Update update-dependencies-internal pipelines to accept comma-delimited stage containers (#7031)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lbussell <36081148+lbussell@users.noreply.github.com> Co-authored-by: Logan Bussell <loganbussell@microsoft.com>
1 parent e72aced commit 09cff11

8 files changed

Lines changed: 192 additions & 78 deletions

eng/pipelines/pipelines/update-dependencies-internal.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
parameters:
2-
# Stage container name (e.g., "stage-1234567") to fetch updates from. This can be
3-
# from the real staging pipeline or the staging test pipeline, but the
4-
# stagingStorageAccount parameter must match which pipeline is used here.
5-
- name: stageContainer
2+
# Comma-delimited list of stage container names (e.g., "stage-1234567,stage-2345678")
3+
# to fetch updates from. This can be from the real staging pipeline or the staging
4+
# test pipeline, but the stagingStorageAccount parameter must match which pipeline
5+
# is used here.
6+
- name: stageContainers
67
type: string
78
default: ""
89
# Staging storage account for .NET release artifacts
@@ -31,6 +32,8 @@ extends:
3132
parameters:
3233
dependencyName: dotnet
3334
updateSteps:
35+
- ${{ if eq(parameters.stageContainers, '') }}:
36+
- "stageContainers parameter must not be empty.": error
3437
- task: AzureCLI@2
3538
displayName: Update .NET
3639
continueOnError: False
@@ -42,7 +45,7 @@ extends:
4245
inlineScript: >-
4346
dotnet run --project eng/update-dependencies/update-dependencies.csproj --
4447
from-staging-pipeline
45-
${{ parameters.stageContainer }}
48+
"${{ parameters.stageContainers }}"
4649
--mode Remote
4750
--azdo-organization "$(System.CollectionUri)"
4851
--azdo-project "$(System.TeamProject)"
Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
trigger: none
22
pr: none
33

4+
parameters:
5+
# Comma-delimited list of stage container names (e.g., "stage-1234567,stage-2345678")
6+
# Each stage container will be processed in sequence.
7+
- name: stageContainers
8+
type: string
9+
default: ""
10+
displayName: Comma-delimited list of stage containers to process
11+
412
variables:
513
- template: /eng/docker-tools/templates/variables/dotnet/common.yml@self
614
# This pipeline expects there to be a variable named "targetBranch" defined
@@ -9,19 +17,10 @@ variables:
917
# It is not a parameter since it needs to be permanent but also changed to the
1018
# next release branch every month without modifying the pipeline YAML.
1119

12-
resources:
13-
pipelines:
14-
# All release pipelines are located at: https://dev.azure.com/dnceng/internal/_build?definitionScope=%5Cdotnet%5Crelease
15-
# Stage-DotNet-Prepare-Artifacts: https://dev.azure.com/dnceng/internal/_build?definitionId=1300
16-
# Stage-DotNet-Prepare-Artifacts-Test: https://dev.azure.com/dnceng/internal/_build?definitionId=1286
17-
- pipeline: "dotnet-staging-pipeline"
18-
source: "Stage-DotNet-Prepare-Artifacts"
19-
trigger: true
20-
2120
extends:
2221
template: /eng/pipelines/pipelines/update-dependencies-internal.yml@self
2322
parameters:
24-
stageContainer: "stage-$(resources.pipeline.dotnet-staging-pipeline.runID)"
23+
stageContainers: "${{ parameters.stageContainers }}"
2524
stagingStorageAccount: "dotnetstage"
2625
targetBranch: "$(targetBranch)"
2726
gitServiceConnectionName: "$(updateDepsInt.serviceConnectionName)"

eng/pipelines/update-dependencies-internal-unofficial.yml

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,20 @@ parameters:
66
type: string
77
default: "nightly"
88
displayName: Target branch for dependency update pull request.
9+
# Comma-delimited list of stage container names (e.g., "stage-1234567,stage-2345678")
10+
# Each stage container will be processed in sequence.
11+
- name: stageContainers
12+
type: string
13+
default: ""
14+
displayName: Comma-delimited list of stage containers to process
915

1016
variables:
1117
- template: /eng/docker-tools/templates/variables/dotnet/common.yml@self
1218

13-
resources:
14-
pipelines:
15-
# All release pipelines are located at: https://dev.azure.com/dnceng/internal/_build?definitionScope=%5Cdotnet%5Crelease
16-
# Stage-DotNet-Prepare-Artifacts: https://dev.azure.com/dnceng/internal/_build?definitionId=1300
17-
# Stage-DotNet-Prepare-Artifacts-Test: https://dev.azure.com/dnceng/internal/_build?definitionId=1286
18-
- pipeline: "dotnet-staging-pipeline"
19-
# Although this pipeline is "unofficial", it uses the production staging pipeline as a resource
20-
# since we don't actually need to download any .NET artifacts from it in order to update this
21-
# repo, so there's no risk in using the real versions.
22-
source: "Stage-DotNet-Prepare-Artifacts"
23-
2419
extends:
2520
template: /eng/pipelines/pipelines/update-dependencies-internal.yml@self
2621
parameters:
27-
stageContainer: "stage-$(resources.pipeline.dotnet-staging-pipeline.runID)"
22+
stageContainers: "${{ parameters.stageContainers }}"
2823
stagingStorageAccount: "dotnetstage"
2924
targetBranch: "${{ parameters.targetBranch }}"
3025
gitServiceConnectionName: "$(updateDepsInt-test.serviceConnectionName)"

eng/update-dependencies/FromStagingPipelineCommand.cs

Lines changed: 111 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@ namespace Dotnet.Docker;
1111
internal 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

Comments
 (0)