Skip to content
11 changes: 6 additions & 5 deletions eng/pipelines/pipelines/update-dependencies-internal.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
parameters:
# Stage container name (e.g., "stage-1234567") to fetch updates from. This can be
# from the real staging pipeline or the staging test pipeline, but the
# stagingStorageAccount parameter must match which pipeline is used here.
- name: stageContainer
# Comma-delimited list of stage container names (e.g., "stage-1234567,stage-2345678")
# to fetch updates from. This can be from the real staging pipeline or the staging
# test pipeline, but the stagingStorageAccount parameter must match which pipeline
# is used here.
- name: stageContainers
type: string
default: ""
# Staging storage account for .NET release artifacts
Expand Down Expand Up @@ -42,7 +43,7 @@ extends:
inlineScript: >-
dotnet run --project eng/update-dependencies/update-dependencies.csproj --
from-staging-pipeline
${{ parameters.stageContainer }}
"${{ parameters.stageContainers }}"
--mode Remote
--azdo-organization "$(System.CollectionUri)"
--azdo-project "$(System.TeamProject)"
Expand Down
19 changes: 9 additions & 10 deletions eng/pipelines/update-dependencies-internal-official.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
trigger: none
pr: none

parameters:
# Comma-delimited list of stage container names (e.g., "stage-1234567,stage-2345678")
# Each stage container will be processed in sequence.
- name: stageContainers
type: string
default: ""
displayName: Comma-delimited list of stage containers to process

variables:
- template: /eng/docker-tools/templates/variables/dotnet/common.yml@self
# This pipeline expects there to be a variable named "targetBranch" defined
Expand All @@ -9,19 +17,10 @@ variables:
# It is not a parameter since it needs to be permanent but also changed to the
# next release branch every month without modifying the pipeline YAML.

resources:
pipelines:
# All release pipelines are located at: https://dev.azure.com/dnceng/internal/_build?definitionScope=%5Cdotnet%5Crelease
# Stage-DotNet-Prepare-Artifacts: https://dev.azure.com/dnceng/internal/_build?definitionId=1300
# Stage-DotNet-Prepare-Artifacts-Test: https://dev.azure.com/dnceng/internal/_build?definitionId=1286
- pipeline: "dotnet-staging-pipeline"
source: "Stage-DotNet-Prepare-Artifacts"
trigger: true

extends:
template: /eng/pipelines/pipelines/update-dependencies-internal.yml@self
parameters:
stageContainer: "stage-$(resources.pipeline.dotnet-staging-pipeline.runID)"
stageContainers: "${{ parameters.stageContainers }}"
stagingStorageAccount: "dotnetstage"
targetBranch: "$(targetBranch)"
gitServiceConnectionName: "$(updateDepsInt.serviceConnectionName)"
19 changes: 7 additions & 12 deletions eng/pipelines/update-dependencies-internal-unofficial.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,20 @@ parameters:
type: string
default: "nightly"
displayName: Target branch for dependency update pull request.
# Comma-delimited list of stage container names (e.g., "stage-1234567,stage-2345678")
# Each stage container will be processed in sequence.
- name: stageContainers
type: string
default: ""
displayName: Comma-delimited list of stage containers to process

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

resources:
pipelines:
# All release pipelines are located at: https://dev.azure.com/dnceng/internal/_build?definitionScope=%5Cdotnet%5Crelease
# Stage-DotNet-Prepare-Artifacts: https://dev.azure.com/dnceng/internal/_build?definitionId=1300
# Stage-DotNet-Prepare-Artifacts-Test: https://dev.azure.com/dnceng/internal/_build?definitionId=1286
- pipeline: "dotnet-staging-pipeline"
# Although this pipeline is "unofficial", it uses the production staging pipeline as a resource
# since we don't actually need to download any .NET artifacts from it in order to update this
# repo, so there's no risk in using the real versions.
source: "Stage-DotNet-Prepare-Artifacts"

extends:
template: /eng/pipelines/pipelines/update-dependencies-internal.yml@self
parameters:
stageContainer: "stage-$(resources.pipeline.dotnet-staging-pipeline.runID)"
stageContainers: "${{ parameters.stageContainers }}"
stagingStorageAccount: "dotnetstage"
targetBranch: "${{ parameters.targetBranch }}"
gitServiceConnectionName: "$(updateDepsInt-test.serviceConnectionName)"
137 changes: 108 additions & 29 deletions eng/update-dependencies/FromStagingPipelineCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ namespace Dotnet.Docker;
internal partial class FromStagingPipelineCommand : BaseCommand<FromStagingPipelineOptions>
{
/// <summary>
/// Callback that stages all changes, commits them, pushes them to the
/// remote, and creates a pull request.
/// Callback that stages all changes and commits them.
/// </summary>
private delegate Task CommitAndCreatePullRequest(string commitMessage, string prTitle, string prBody);
private delegate Task CommitChanges(string commitMessage);

/// <summary>
/// Callback that pushes all commits and creates a pull request.
/// </summary>
private delegate Task PushAndCreatePullRequest(string prTitle, string prBody);

private readonly ILogger<FromStagingPipelineCommand> _logger;
private readonly IPipelineArtifactProvider _pipelineArtifactProvider;
Expand Down Expand Up @@ -44,17 +48,73 @@ public FromStagingPipelineCommand(

public override async Task<int> ExecuteAsync(FromStagingPipelineOptions options)
{
var stageContainers = options.GetStageContainerList();

if (stageContainers.Count == 0)
{
_logger.LogError("No stage containers provided.");
return 1;
}

_logger.LogInformation(
"Updating dependencies based on {Count} stage container(s): {StageContainers}",
stageContainers.Count,
string.Join(", ", stageContainers));

// Delegate all git responsibilities to GitRepoContext. Depending on what options were
// passed in, we may or may not want to actually perform git operations. GitRepoContext
// decides what git operations to perform and tells us where to make changes. This keeps
// all the git-related logic in one place.
var gitRepoContext = await _createGitRepoContextAsync(options);

_logger.LogInformation(
"Updating dependencies based on stage container {StageContainer}",
options.StageContainer);
List<string> commitMessages = [];
List<string> prBodySections = [];

// Process each stage container, creating a separate commit for each
foreach (var stageContainer in stageContainers)
{
_logger.LogInformation("Processing stage container: {StageContainer}", stageContainer);

var (commitMessage, prBodySection, exitCode) = await ProcessStageContainerAsync(
options,
stageContainer,
gitRepoContext);

if (exitCode != 0)
{
return exitCode;
}

// Commit changes for this stage container
await gitRepoContext.CommitChanges(commitMessage);

commitMessages.Add(commitMessage);
prBodySections.Add(prBodySection);
}

var stagingPipelineRunId = options.GetStagingPipelineRunId();
// Create pull request with all commits
var prTitle = stageContainers.Count == 1
? $"[{options.TargetBranch}] {commitMessages[0]}"
: $"[{options.TargetBranch}] Update .NET dependencies from {stageContainers.Count} stage containers";

var prBody = string.Join(Environment.NewLine + Environment.NewLine, prBodySections);
await gitRepoContext.PushAndCreatePullRequest(prTitle, prBody);

return 0;
}

/// <summary>
/// Processes a single stage container and applies the updates.
/// </summary>
/// <returns>
/// A tuple containing the commit message, PR body section, and exit code.
/// </returns>
private async Task<(string CommitMessage, string PrBodySection, int ExitCode)> ProcessStageContainerAsync(
FromStagingPipelineOptions options,
string stageContainer,
GitRepoContext gitRepoContext)
{
var stagingPipelineRunId = StagingPipelineOptionsExtensions.GetStagingPipelineRunId(stageContainer);

// Log staging pipeline tags for diagnostic purposes
var stagingPipelineTags = await _pipelinesService.GetBuildTagsAsync(
Expand All @@ -68,15 +128,15 @@ public override async Task<int> ExecuteAsync(FromStagingPipelineOptions options)
{
ArgumentException.ThrowIfNullOrWhiteSpace(
options.StagingStorageAccount,
$"{FromStagingPipelineOptions.StagingStorageAccountOption} must be set when using the {FromStagingPipelineOptions.InternalOption} option."
$"{FromStagingPipelineOptions.StagingStorageAccountOptionName} must be set when using the {FromStagingPipelineOptions.InternalOption} option."
);

// Release metadata is stored in metadata/ReleaseManifest.json.
// Release assets are stored individually under in assets/shipping/assets/[Sdk|Runtime|aspnetcore|...].
// 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
_buildLabelService.AddBuildTags($"Container - {options.StageContainer}");
_buildLabelService.AddBuildTags($"Container - {stageContainer}");
internalBaseUrl = NormalizeStorageAccountUrl(options.StagingStorageAccount)
+ $"/{options.StageContainer}/assets/shipping/assets";
+ $"/{stageContainer}/assets/shipping/assets";
}

var releaseConfig = await _pipelineArtifactProvider.GetReleaseConfigAsync(
Expand All @@ -94,7 +154,7 @@ public override async Task<int> ExecuteAsync(FromStagingPipelineOptions options)
_internalVersionsService.RecordInternalStagingBuild(
repoRoot: gitRepoContext.LocalRepoPath,
dotNetVersion: dotNetVersion,
stageContainer: options.StageContainer);
stageContainer: stageContainer);
}

var productVersions = (options.Internal, releaseConfig.SdkOnly) switch
Expand Down Expand Up @@ -141,7 +201,7 @@ public override async Task<int> ExecuteAsync(FromStagingPipelineOptions options)
var buildUrl = $"{options.AzdoOrganization}/{options.AzdoProject}/_build/results?buildId={stagingPipelineRunId}";
_logger.LogInformation(
"Applying internal build {StageContainer} ({BuildUrl})",
options.StageContainer, buildUrl);
stageContainer, buildUrl);

_logger.LogInformation(
"Ignore any git-related logging output below, because git "
Expand All @@ -163,8 +223,8 @@ public override async Task<int> ExecuteAsync(FromStagingPipelineOptions options)
_logger.LogError(
"Failed to apply stage container {StageContainer}. "
+ "Command exited with code {ExitCode}.",
options.StageContainer, exitCode);
return exitCode;
stageContainer, exitCode);
return (string.Empty, string.Empty, exitCode);
}

var commitMessage = releaseConfig switch
Expand All @@ -173,18 +233,18 @@ public override async Task<int> ExecuteAsync(FromStagingPipelineOptions options)
_ => $"Update .NET {majorMinorVersionString} to {productVersions["sdk"]} SDK / {productVersions["runtime"]} Runtime",
};

var prTitle = $"[{options.TargetBranch}] {commitMessage}";
var newVersionsList = productVersions.Select(kvp => $"- {kvp.Key.ToUpper()}: {kvp.Value}");
var prBody = $"""
This pull request updates .NET {majorMinorVersionString} to the following versions:
var prBodySection = $"""
## .NET {majorMinorVersionString}

This updates .NET {majorMinorVersionString} to the following versions:

{string.Join(Environment.NewLine, newVersionsList)}

These versions are from .NET staging pipeline run [{options.StageContainer}]({buildUrl}).
These versions are from .NET staging pipeline run [{stageContainer}]({buildUrl}).
""";
await gitRepoContext.CommitAndCreatePullRequest(commitMessage, prTitle, prBody);

return 0;
return (commitMessage, prBodySection, 0);
}

/// <summary>
Expand Down Expand Up @@ -215,13 +275,18 @@ private static string NormalizeStorageAccountUrl(string storageAccount)
/// Holds context about the git repository where changes should be made.
/// </summary>
/// <param name="LocalRepoPath">Root of the repo where all changes should be made.</param>
/// <param name="CommitAndCreatePullRequest">Callback that creates a pull request with all changes.</param>
private record GitRepoContext(string LocalRepoPath, CommitAndCreatePullRequest CommitAndCreatePullRequest)
/// <param name="CommitChanges">Callback that commits changes with the given message.</param>
/// <param name="PushAndCreatePullRequest">Callback that pushes all commits and creates a pull request.</param>
private record GitRepoContext(
string LocalRepoPath,
CommitChanges CommitChanges,
PushAndCreatePullRequest PushAndCreatePullRequest)
{
/// <summary>
/// Sets up the remote/local git repository based on <paramref name="options"/>.
/// Call this before making any changes, then make changes to <see cref="LocalRepoPath"/>
/// and use <see cref="CommitAndCreatePullRequest"/> to create a pull request.
/// and use <see cref="CommitChanges"/> to commit each change individually,
/// then use <see cref="PushAndCreatePullRequest"/> to push all commits and create a pull request.
/// </summary>
/// <remarks>
/// If <see cref="FromStagingPipelineOptions.Mode"/> is <see cref="ChangeMode.Local"/>,
Expand All @@ -233,15 +298,18 @@ public static async Task<GitRepoContext> CreateAsync(
FromStagingPipelineOptions options,
IEnvironmentService environmentService)
{
CommitAndCreatePullRequest createPullRequest;
CommitChanges commitChanges;
PushAndCreatePullRequest pushAndCreatePullRequest;
string localRepoPath;

if (options.Mode == ChangeMode.Remote)
{
var remoteUrl = options.GetAzdoRepoUrl();
var targetBranch = options.TargetBranch;
var buildId = environmentService.GetBuildId() ?? "";
var prBranch = options.CreatePrBranchName($"update-deps-int-{options.StageContainer}", buildId);
var stageContainerList = options.GetStageContainerList();
var firstStageContainer = stageContainerList.Count > 0 ? stageContainerList[0] : "unknown";
var prBranch = options.CreatePrBranchName($"update-deps-int-{firstStageContainer}", buildId);
var committer = options.GetCommitterIdentity();

// Clone the repo and configure git identity for commits
Expand All @@ -253,10 +321,15 @@ public static async Task<GitRepoContext> CreateAsync(
await git.Local.CreateAndCheckoutLocalBranchAsync(prBranch);

localRepoPath = git.Local.LocalPath;
createPullRequest = async (commitMessage, prTitle, prBody) =>

commitChanges = async (commitMessage) =>
{
await git.Local.StageAsync(".");
await git.Local.CommitAsync(commitMessage, committer);
};

pushAndCreatePullRequest = async (prTitle, prBody) =>
{
await git.PushLocalBranchAsync(prBranch);
await git.Remote.CreatePullRequestAsync(new(
Title: prTitle,
Expand All @@ -270,16 +343,22 @@ await git.Remote.CreatePullRequestAsync(new(
{
logger.LogInformation("No git operations will be performed in {Mode} mode.", options.Mode);
localRepoPath = options.RepoRoot;
createPullRequest = async (commitMessage, prTitle, prBody) =>

commitChanges = async (commitMessage) =>
{
logger.LogInformation("Skipping commit and pull request creation in {Mode} mode.", options.Mode);
logger.LogInformation("Skipping commit in {Mode} mode.", options.Mode);
logger.LogInformation("Commit message: {CommitMessage}", commitMessage);
};

pushAndCreatePullRequest = async (prTitle, prBody) =>
{
logger.LogInformation("Skipping push and pull request creation in {Mode} mode.", options.Mode);
logger.LogInformation("Pull request title: {PullRequestTitle}", prTitle);
logger.LogInformation("Pull request body:\n{PullRequestBody}", prBody);
};
}

return new GitRepoContext(localRepoPath, createPullRequest);
return new GitRepoContext(localRepoPath, commitChanges, pushAndCreatePullRequest);
}
}
}
Loading
Loading